App 上架包預檢

1、 iOS 端常見被拒緣由彙總

  1. App 內包含分發下載分發功能(引導用戶下載 App 等功能)
  2. 提供的測試帳號沒法查看實際功能
  3. 經過接口返回布爾值判斷 App 是否升級,但審覈期間該接口不請求
  4. 審覈帳號,任什麼時候候在任何 ip 登陸看到的都是審覈版
  5. 提供的登錄帳號和密碼不對,登錄不上
  6. 運營填寫的營銷關鍵字有問題
  7. 元數據問題,iPhoneX 截圖中 iPhone 殼子是 iPhone7 的,應該是 iPhoneX
  8. 說明隱私權限的做用
  9. 營銷文字,某些能力須要資質。此類功能在審覈期間都關閉
  10. 修改隱私權限相關的文案,作到讓審覈人員看得懂,作到「信達雅」
  11. App 沒法登錄進去,屬於 bug 級別
  12. App 沒有適配 ipad
  13. Privacy - Data Collection and Storage,說明 App 沒有作隱私權限的收集。
  14. 訪問 h5 頁面出現問題。 屬於 bug 級別

2、 App 被拒緣由彙總

從 Android 和 iOS 2端 App 被駁回的一些信息來看,駁回緣由通常劃分爲下面幾類:javascript

  1. 審覈期間,資源和配置都應該調節爲審覈模式
  2. App 包含某些關鍵字
  3. 審覈相關的元數據問題(截圖與實際內容不匹配、機型和截圖不匹配、提供給審覈的帳號和密碼登錄不上)
  4. 使用的隱私權限必須說明,文案描述必須清晰
  5. App 存在 bug (帳號沒法登錄、沒有適配 ipad、訪問 h5 打不開 )
  6. 誘導用戶打開查看更多 App
  7. Android 應用未加固
  8. 應用缺少相關的資質和證書

3、 方案

常見審覈失敗的緣由不少,很大比重一個就是代碼或者文本里面存在一些敏感詞,因此本文的側重點在於關鍵詞掃描。像上架設置的截圖和當前設備不匹配、提供的帳號沒法使用功能 😂 這種狀況打一頓就行了,非主流行爲不在本文範圍內java

3.1 詞雲誰去收集?

每一個公司通常來講都不止一條業務線,因此每一個業務線的 App 狀況和內容也不同,因此敏感詞也是千差萬別。敏感詞收集這個事情,應該由業務線主要負責 App 的開發者來收集,根據平時的上架狀況,蘋果的駁回的郵件來整理。算法

3.2 方案設計

公司自研工具 cli(iOS SDK、iOS App、Android SDK、Android App、RN、Node、React 依賴分析、構建、打包、測試、熱修復、埋點、構建),各個端都是經過「模版」來提供能力。包含若干子項目,每一個子項目就是所謂的 「模版」,每一個模版其實就是一個 Node 工程,一個 npm 模塊,主要負責如下功能:特定項目類型的目錄結構、自定義命令供開發、構建等使用、模版持續更新及 patch 等。npm

因此能夠在打包構建(各個端將項目提交到打包系統,打包系統根據項目語言、平臺調度打包機)的時候,拿到源代碼進行掃描。基於這個現狀,因此方案是「掃描是基於源代碼出發的掃描的」。json

按照 iOS 端 pod install 這個過程,cocoapods 爲咱們預留了鉤子:PreInstallHook.rbPostInstallHook.rb,容許咱們在不一樣的階段爲工程作一些自定義的操做,因此咱們的 iOS 模版設計也參考了這個思想,在打包構建前、構建中、構建後提供了鉤子:prebuildbuildpostbuild。定位好了問題,要作的就是在 prebuild 裏面進行關鍵詞掃描的編碼工做。api

肯定了何時作什麼事情,接下來就要討論怎麼作才合適。數組

3.3 技術方案選擇

字符串匹配算法 KMP 是一開始想到的內容,針對某個 App 進行時機測試,發現50多個敏感詞的狀況下,代碼掃描耗時60秒鐘,以爲很是不理想,看 KMP 算法沒有啥問題,因此換個思路走下去。async

由於模版本質上 Node 項目,因此 Node 下的 glob 模塊正好提供根據正則匹配到合適的文件,也能夠匹配文件裏面的字符串。而後繼續作實驗,數據以下:9個銘感詞語、代碼文件5967個,耗時3.5秒工具

3.4 完整方案

  1. 業務線須要自定義敏感詞雲(由於每條業務線的關鍵詞雲都不同)
  2. 敏感詞須要劃分等級:error、warning。掃描到 error 須要立刻中止構建,並提示「已掃描到你的源碼中存在敏感詞***,可能存在提交審覈失敗的可能,請修改後再次構建」。warning 的狀況不須要立刻中止構建,等任務所有結束後彙總給出提示「已掃描到你的源碼中存在敏感詞***、***...,可能存在提交審覈失敗的可能,請開發者本身確認」
  3. 銘感詞雲的格式 scaner.yml 文件。
  • error: 數組的格式。後面寫須要掃描的關鍵詞,且等級爲 error,表示掃描到 error 則立刻中止構建
  • warning:數組的格式。後面寫須要掃描的關鍵詞,且等級爲 warning,掃描結果不影響構建,最終只是展現出來
  • searchPath:字符串格式。可讓業務線自定義須要進行掃描的路徑。
  • fileType:數組格式。可讓業務線自定義須要掃描的文件類型。默認爲 sh|pch|json|xcconfig|mm|cpp|h|m
  • warningkeywordsScan:布爾值。業務線能夠設置是否須要掃描 warning 級別的關鍵詞。
  • errorKeywordsScan:布爾值。業務線能夠設置是否須要掃描 error 級別的關鍵詞。
error:
 - checkSwitch
warning:
 - loan
 - online
 - ischeck
searchPath:
  ../fixtures
fileType:
 - h
 - m
 - cpp
 - mm
 - js
warningkeywordsScan: true
errorKeywordsScan: true
複製代碼
  1. iOS 端存在私有 api 的狀況,Android 端不存在該問題 私有 api 70111個文件,每一個文件假設10個方法,則共70萬個 api。因此計劃找出 top 100.去掃描匹配,支持業務線是否開啓的選項

其實這些問題都是業界標準的作法,確定須要預留這樣的能力,因此自定義規則的格式能夠查看上面 yml 文件的各個字段所肯定。明確了作什麼事,以及作事情的標準,那就能夠很快的開展並落地實現。post

'use strict'

const { Error, logger } = require('@company/BFF-utils')
const fs = require('fs-extra')
const glob = require('glob')
const YAML = require('yamljs')

module.exports = class PreBuildCommand {
  constructor(ctx) {
    this.ctx = ctx
    this.projectPath = ''
    this.fileNum = 0
    this.isExist = false
    this.errorFiles = []
    this.warningFiles = []
    this.keywordsObject = {}
    this.errorReg = null
    this.warningReg = null
    this.warningkeywordsScan = false
    this.errorKeywordsScan = false
    this.scanFileTypes = ''
  }

  async fetchCodeFiles(dirPath, fileType = 'sh|pch|json|xcconfig|mm|cpp|h|m') {
    return new Promise((resolve, reject) => {
      glob(`**/*.?(${fileType})`, { root: dirPath, cwd: dirPath, realpath: true }, (err, files) => {
        if (err) reject(err)
        resolve(files)
      })
    })
  }

  async scanConfigurationReader(keywordsPath) {
    return new Promise((resolve, reject) => {
      fs.readFile(keywordsPath, 'UTF-8', (err, data) => {
        if (!err) {
          let keywords = YAML.parse(data)
          resolve(keywords)
        } else {
          reject(err)
        }
      })
    })
  }

  async run() {
    const { argv } = this.ctx
    const buildParam = {
      scheme: argv.opts.scheme,
      cert: argv.opts.cert,
      env: argv.opts.env
    }

    // 處理包關鍵詞掃描(敏感詞彙 + 私有 api)
    this.keywordsObject = (await this.scanConfigurationReader(this.ctx.cwd + '/.scaner.yml')) || {}
    this.warningkeywordsScan = this.keywordsObject.warningkeywordsScan || false
    this.errorKeywordsScan = this.keywordsObject.errorKeywordsScan || false
    if (Array.isArray(this.keywordsObject.fileType)) {
      this.scanFileTypes = this.keywordsObject.fileType.join('|')
    }
    if (Array.isArray(this.keywordsObject.error)) {
      this.errorReg = this.keywordsObject.error.join('|')
    }
    if (Array.isArray(this.keywordsObject.warning)) {
      this.warningReg = this.keywordsObject.warning.join('|')
    }

    // 從指定目錄下獲取全部文件
    this.projectPath = this.keywordsObject ? this.keywordsObject.searchPath : this.ctx.cwd
    const files = await this.fetchCodeFiles(this.projectPath, this.scanFileTypes)

    if (this.errorReg && this.errorKeywordsScan) {
      await Promise.all(
        files.map(async file => {
          try {
            const content = await fs.readFile(file, 'utf-8')
            const result = await content.match(new RegExp(`(${this.errorReg})`, 'g'))
            if (result) {
              if (result.length > 0) {
                this.isExist = true
                this.fileNum++
                this.errorFiles.push(
                  `編號: ${this.fileNum}, 所在文件: ${file}, 出現次數: ${result && (result.length || 0)}`
                )
              }
            }
          } catch (error) {
            throw error
          }
        })
      )
    }

    if (this.errorFiles.length > 0) {
      throw new Error(
        `從你的項目中掃描到了 error 級別的敏感詞,建議你修改方法名稱、屬性名、方法註釋、文檔描述。\n敏感詞有 「${ this.errorReg }」\n存在問題的文件有 ${JSON.stringify(this.errorFiles, null, 2)}`
      )
    }

    // warning
    if (this.warningReg && !this.isExist && this.fileNum === 0 && this.warningkeywordsScan) {
      await Promise.all(
        files.map(async file => {
          try {
            const content = await fs.readFile(file, 'utf-8')
            const result = await content.match(new RegExp(`(${this.warningReg})`, 'g'))
            if (result) {
              if (result.length > 0) {
                this.isExist = true
                this.fileNum++
                this.warningFiles.push(
                  `編號: ${this.fileNum}, 所在文件: ${file}, 出現次數: ${result && (result.length || 0)}`
                )
              }
            }
          } catch (error) {
            throw error
          }
        })
      )

      if (this.warningFiles.length > 0) {
        logger.info(
          `從你的項目中掃描到了 warning 級別的敏感詞,建議你修改方法名稱、屬性名、方法註釋、文檔描述。\n敏感詞有 「${ this.warningReg }」。有問題的文件有${JSON.stringify(this.warningFiles, null, 2)}`
        )
      }
    }

    for (const key in buildParam) {
      if (!buildParam[key]) {
        throw new Error(`build: ${key} 參數缺失`)
      }
    }
  }
}
複製代碼
相關文章
相關標籤/搜索