# -*- coding: utf-8 -*- """校验上架材料里的关键数据与真实代码是否一致。 材料一旦填错(包名、版本、权限数、第三方 SDK),轻则驳回重来、 重则包名冲突无法上架。所以每一条都要能从代码里找到出处,而不是"我记得"。 跑法:python3 verify_materials.py """ import io import json import os import re import sys HM = '/home/yongquan/code/FamilyBook_harmonyos' MAT = os.path.dirname(os.path.abspath(__file__)) passed = 0 failed = 0 fails = [] def ok(name, cond, detail=''): global passed, failed if cond: passed += 1 print(' OK %s' % name) else: failed += 1 fails.append(name + ((' — ' + detail) if detail else '')) print(' XX %s%s' % (name, (' — ' + detail) if detail else '')) def read(p): return io.open(p, encoding='utf-8').read() # ---------- 从真实代码取事实 ---------- app_json = read(os.path.join(HM, 'AppScope/app.json5')) # json5:去注释后解析 clean = re.sub(r'/\*.*?\*/', '', app_json, flags=re.S) clean = re.sub(r'//[^\n]*', '', clean) APP = json.loads(clean)['app'] mod_json = read(os.path.join(HM, 'entry/src/main/module.json5')) mclean = re.sub(r'/\*.*?\*/', '', mod_json, flags=re.S) mclean = re.sub(r'//[^\n]*', '', mclean) MOD = json.loads(mclean)['module'] str_json = json.loads(read(os.path.join(HM, 'AppScope/resources/base/element/string.json'))) APP_NAME = [s['value'] for s in str_json['string'] if s['name'] == 'app_name'][0] PERMS = [p['name'] for p in MOD.get('requestPermissions', [])] bundle = APP['bundleName'] ver_name = APP['versionName'] ver_code = APP['versionCode'] print('=== 0. 从代码提取到的事实 ===') print(' bundleName = %s' % bundle) print(' versionName = %s / versionCode = %s' % (ver_name, ver_code)) print(' app_name = %s' % APP_NAME) print(' 权限 (%d 个) = %s' % (len(PERMS), ', '.join(PERMS))) print() # ---------- 材料文本 ---------- struct = json.loads(read(os.path.join(MAT, '商店信息_结构化.json'))) overview = read(os.path.join(MAT, 'README总览.md')) store_md = read(os.path.join(MAT, '商店上架文案.md')) privacy = read(os.path.join(MAT, '隐私政策.md')) terms = read(os.path.join(MAT, '用户服务协议.md')) cert_md = read(os.path.join(MAT, '正式证书与签名切换.md')) beian = read(os.path.join(MAT, 'APP备案材料.md')) print('=== 1. 包名 / 版本 / 名称 与代码一致 ===') ok('商店 JSON 的 bundleName 正确', struct['package']['bundleName'] == bundle, struct['package']['bundleName']) ok('商店 JSON 的 versionName 正确', struct['package']['versionName'] == ver_name, struct['package']['versionName']) ok('商店 JSON 的 versionCode 正确', struct['package']['versionCode'] == ver_code, str(struct['package']['versionCode'])) ok('应用名与包内 string.json 一致', struct['app_name'] == APP_NAME, struct['app_name']) ok('总览里的包名正确', bundle in overview) ok('备案材料里的包名正确', bundle in beian) ok('备案材料的 MD5 示例与实测调试证书一致', '7d0c4b46b7c1c0d23c6e4316e5c996a3' in beian.lower()) ok('备案材料明确标注"调试证书不能用于备案"', '调试证书的,勿用' in beian or '不能用于备案' in beian) print('\n=== 2. 权限清单逐条对得上 ===') declared = [p['permission'] for p in struct['permissions_declaration']] ok('材料声明的权限数 = 代码里的权限数', len(declared) == len(PERMS), '%d vs %d' % (len(declared), len(PERMS))) ok('权限名称集合完全一致', set(declared) == set(PERMS), '材料多: %s / 代码多: %s' % (set(declared) - set(PERMS), set(PERMS) - set(declared))) for p in PERMS: ok('权限 %s 出现在隐私政策里' % p, p in privacy) ok('总览里写的"3 个权限"与实测一致', ('%d 个' % len(PERMS)) in overview or '3 个' in overview, '实测 %d 个' % len(PERMS)) print('\n=== 3. 隐私政策不撒谎:没申请的东西不能说"收集" ===') # 反向:政策里声明"未申请"的权限,代码里就真的不能有 not_collected_claims = { '定位': 'LOCATION', '通讯录': 'READ_CONTACTS', '相机': 'CAMERA', '麦克风': 'MICROPHONE', } for word, perm in not_collected_claims.items(): has_perm = any(perm in p for p in PERMS) claims_not = ('未申请' in privacy and word in privacy) if not has_perm and claims_not: ok('政策说"未申请 %s",代码里确实没有' % word, True) elif has_perm: ok('政策说"未申请 %s",但代码里有 %s' % (word, perm), False, '这是重大问题') else: ok('政策对 %s 的表述可核对' % word, True, '政策里没明确提"未申请",不算错但不严谨') # 设备标识符 ok('政策声明不采集设备标识符,且代码确实没申请', '设备' in privacy and not any('DEVICE_ID' in p or 'OAID' in p for p in PERMS)) print('\n=== 4. 一句话简介字数在限内(≤17 汉字)===') one = struct['one_line_intro']['value'] # 汉字与全角字符按 1 计 cnt = len(one) ok('一句话简介 ≤17', cnt <= 17, '%d 字:%s' % (cnt, one)) for alt in struct['one_line_intro_alternatives']: ok('备选「%s」≤17' % alt['value'], len(alt['value']) <= 17, '%d 字' % len(alt['value'])) ok('应用名 ≤15 汉字', len(APP_NAME) <= 15, '%d 字' % len(APP_NAME)) print('\n=== 5. 材料内部一致性(三处文档说法不能打架)===') ok('总览与商店文案的包名相同', bundle in store_md) ok('总览与证书文档都要求 .app 不是 .hap', 'assembleApp' in cert_md and '.app' in store_md) ok('三份文档都说"无广告/无统计 SDK"', '广告' in privacy and '广告' in store_md and '广告' in beian) ok('备案材料明确个人主体只能选工具类', '工具' in beian and '个人' in beian) ok('证书文档明确指出调试证书不能上架(991/1014)', '991' in cert_md and '1014' in cert_md) ok('证书文档说明 MD5 要去冒号', '去' in cert_md and '冒号' in cert_md) ok('用户协议与隐私政策互相引用', '隐私政策' in terms and '用户服务协议' not in privacy or '本政策' in privacy) ok('两份文档都有生效日期 2026 年 11 月 1 日', '2026 年 11 月 1 日' in privacy and '2026 年 11 月 1 日' in terms) ok('两份文档都有联系方式 i@yangyq.net', 'i@yangyq.net' in privacy and 'i@yangyq.net' in terms) print('\n=== 6. 图标文件规格达标 ===') try: from PIL import Image for name, size, limit_kb in [('app_icon_216.png', 216, 500), ('app_icon_1024.png', 1024, 3072)]: p = os.path.join(MAT, '图标', name) if not os.path.isfile(p): ok('图标 %s 存在' % name, False, '文件不存在') continue im = Image.open(p).convert('RGBA') w, h = im.size c = im.load() corners_solid = all(c[x, y][3] >= 250 for x, y in [(0, 0), (w - 1, 0), (0, h - 1), (w - 1, h - 1)]) kb = os.path.getsize(p) / 1024.0 ok('%s 尺寸 %d×%d' % (name, size, size), (w, h) == (size, size), '%dx%d' % (w, h)) ok('%s 四角不透明(无自带圆角)' % name, corners_solid) ok('%s 大小 %.1fKB ≤ %dKB' % (name, kb, limit_kb), kb <= limit_kb) except ImportError: print(' (没装 PIL,跳过图标检查)') print('\n=== 7. 已知缺口如实记录(不能假装没有)===') ok('备案材料写明"没有家书软著"', '没有' in beian and '软著' in beian) ok('备案材料给出免责函这条过渡路', '免责函' in beian) ok('总览列出"四个必须先解决的问题"', '必须先解决的' in overview or '先解决' in overview) ok('总览明确隐私政策页面目前不存在', '不存在' in overview and '隐私政策' in overview) print('\n=== 结果 ===') print('通过 %d / 失败 %d' % (passed, failed)) if fails: print('失败项:') for f in fails: print(' - ' + f) sys.exit(1 if failed else 0)