App 上架包预检

作者 | 杭城小刘 
来源 | 掘金

https://juejin.im/post/5e5e671b6fb9a07ca90c3339

一、 iOS 端常见被拒原因汇总

• App 内包含分发下载分发功能(引导用户下载 App 等功能)

• 提供的测试账号无法查看实际功能

• 通过接口返回布尔值判断 App 是否升级,但审核期间该接口不请求

• 审核账号,任何时候在任何 ip 登录看到的都是审核版

• 提供的登陆账号和密码不对,登陆不上

• 运营填写的营销关键字有问题

• 元数据问题,iPhoneX 截图中 iPhone 壳子是 iPhone7 的,应该是 iPhoneX

• 说明隐私权限的作用

• 营销文字,某些能力需要资质。此类功能在审核期间都关闭

• 修改隐私权限相关的文案,做到让审核人员看得懂,做到「信达雅」

• App 无法登陆进去,属于 bug 级别

• App 没有适配 ipad

• Privacy - Data Collection and Storage,说明 App 没有做隐私权限的收集。

• 访问 h5 页面出现问题。属于 bug 级别

二、 App 被拒原因汇总

从 Android 和 iOS 2端 App 被驳回的一些信息来看,驳回原因一般划分为下面几类:

• 审核期间,资源和配置都应该调节为审核模式

• App 包含某些关键字

• 审核相关的元数据问题(截图与实际内容不匹配、机型和截图不匹配、提供给审核的账号和密码登陆不上)

• 使用的隐私权限必须说明,文案描述必须清晰

• App 存在 bug (账号无法登陆、没有适配 ipad、访问 h5 打不开 )

• 诱导用户打开查看更多 App

• Android 应用未加固

• 应用缺乏相关的资质和证书

三、 方案

常见审核失败的原因很多,很大比重一个就是代码或者文本里面存在一些敏感词,所以本文的侧重点在于关键词扫描。像上架设置的截图和当前设备不匹配、提供的账号无法使用功能 ???? 这种情况打一顿就好了,非主流行为不在本文范围内

3.1 词云谁去收集?

每个公司一般来说都不止一条业务线,所以每个业务线的 App 情况和内容也不一样,所以敏感词也是千差万别。敏感词收集这个事情,应该由业务线主要负责 App 的开发者来收集,根据平时的上架情况,苹果的驳回的邮件来整理。

3.2 方案设计

公司自研工具 cli(iOS SDK、iOS App、Android SDK、Android App、RN、Node、React 依赖分析、构建、打包、测试、热修复、埋点、构建),各个端都是通过「模版」来提供能力。包含若干子项目,每个子项目就是所谓的 “模版”,每个模版其实就是一个 Node 工程,一个 npm 模块,主要负责以下功能:特定项目类型的目录结构、自定义命令供开发、构建等使用、模版持续更新及 patch 等。

所以可以在打包构建(各个端将项目提交到打包系统,打包系统根据项目语言、平台调度打包机)的时候,拿到源代码进行扫描。基于这个现状,所以方案是「扫描是基于源代码出发的扫描的」。

按照 iOS 端 pod install 这个过程,cocoapods 为我们预留了钩子:PreInstallHook.rbPostInstallHook.rb,允许我们在不同的阶段为工程做一些自定义的操作,所以我们的 iOS 模版设计也参考了这个思想,在打包构建前、构建中、构建后提供了钩子:prebuildbuildpostbuild。定位好了问题,要做的就是在 prebuild 里面进行关键词扫描的编码工作。

确定了什么时候做什么事情,接下来就要讨论怎么做才合适。

3.3 技术方案选择

字符串匹配算法 KMP 是一开始想到的内容,针对某个 App 进行时机测试,发现50多个敏感词的情况下,代码扫描耗时60秒钟,觉得非常不理想,看 KMP 算法没有啥问题,所以换个思路走下去。

因为模版本质上 Node 项目,所以 Node 下的 glob 模块正好提供根据正则匹配到合适的文件,也可以匹配文件里面的字符串。然后继续做实验,数据如下:9个铭感词语、代码文件5967个,耗时3.5秒

3.4 完整方案

• 业务线需要自定义敏感词云(因为每条业务线的关键词云都不一样)

• 敏感词需要划分等级:error、warning。扫描到 error 需要马上停止构建,并提示「已扫描到你的源码中存在敏感词,可能存在提交审核失败的可能,请修改后再次构建」。warning 的情况不需要马上停止构建,等任务全部结束后汇总给出提示「已扫描到你的源码中存在敏感词、***...,可能存在提交审核失败的可能,请开发者自己确认」

• 敏感词云的格式 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

• iOS 端存在私有 api 的情况,Android 端不存在该问题

私有 api 70111个文件,每个文件假设10个方法,则共70万个 api。所以计划找出 top 100.去扫描匹配,支持业务线是否开启的选项

其实这些问题都是业界标准的做法,肯定需要预留这样的能力,所以自定义规则的格式可以查看上面 yml 文件的各个字段所确定。明确了做什么事,以及做事情的标准,那就可以很快的开展并落地实现。

'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} 参数缺失`)
      }
    }
  }
}

近期精彩内容推荐:  

 程序员这碗青春饭,怎么吃得更久一点?

 顺丰小哥连升3级,国家授予特别奖!

 狠人 Spring Cloud 20000 字总结!

 python实现文件自动归类

在看点这里好文分享给更多人↓↓

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值