Files
FamilyBook_harmonyos/docs/上架/verify_materials.py
T
laoyang c0d5fdff01 docs(上架): 应用商店上架 / 正式证书 / APP 备案 全套材料
## 背景

用户要同时办三件事:① 上架华为 AppGallery ② 申请正式发布证书 ③ 在阿里云做 APP 备案。
本目录是这三件事的全部材料,信息**全部取自真实代码与实测**,不是套模板。

## 材料清单

| 文件 | 用途 |
|---|---|
| `README总览.md` | 先看这个:材料索引 + 三件事的先后关系 + 你现在就能做的 |
| `商店上架文案.md` | AGC 各字段可直接复制的文案(一句话简介/应用介绍/更新说明/分级问卷/隐私标签) |
| `商店信息_结构化.json` | 同上,机器可读 |
| `隐私政策.md` | 完整隐私政策正文 |
| `用户服务协议.md` | 完整用户协议正文 |
| `正式证书与签名切换.md` | 发布证书申请 + release 打包全流程 |
| `APP备案材料.md` | 阿里云 APP 备案逐字段填写指引 |
| `图标/app_icon_216.png` | AppGallery 图标(216×216,9.7KB) |
| `图标/app_icon_1024.png` | 备案图标 + 1024 规格(112KB) |
| `verify_materials.py` | 校验材料与代码一致(42 项) |

## 实测核对过的事实(材料里的数字都从这里来)

- 包名 `familybook.yangyq.net`、版本 `0.8.0` / versionCode `800`(`AppScope/app.json5`)
- 应用名「家书」(`AppScope/resources/base/element/string.json`)
- 权限 **只有 3 个且全是普通权限**:INTERNET / GET_NETWORK_INFO / DETECT_GESTURE
  (`entry/src/main/module.json5`)—— 定位、通讯录、相机、麦克风、设备标识符**一个都没申请**
- 第三方 SDK:**无广告、无统计、无推送**
- 后端域名 `family.yangyq.net`,服务器阿里云 `47.104.72.161`(中国大陆)

权限少是审核优势:「权限滥用」是驳回第一大类原因,家书直接绕开。
隐私政策第二节与第十节做了**权限逐条对照表**,可直接给审核看。

## 图标为什么要重做

包内 `fb_logo.png` 是 **192×192 且自带圆角**(四角透明,半径约 33px)。
AppGallery 两种规格都不达标(要 216 或 1024,且「square-shaped icon」)。
→ 取内部主色 #F2E9D8 铺满方形画布补掉缺角,图案原样叠加、不裁切不缩放,
  生成 216 与 1024 两版。图案与包内完全一致(审核要求后台图标 = 包内图标同一图案)。

第一版直接用 LANCZOS 放大,**保留了圆角**,被 `verify_materials.py` 抓到并修正 ——
这就是写那个校验脚本的意义。

## 四个必须先解决的问题(已写进总览,不隐瞒)

1. **现在是调试证书** → 备案要正式证书的公钥/MD5,上架要正式签名(调试包报 991/1014)
2. **没有「家书」软著** → 备案不强制;华为要求但可用《免责函》过渡(6 项现有软著均无关)
3. **隐私政策页面还不存在** → 站点无 privacy 路由;建议用华为自带托管服务(SyncCalendar 用的那个,实测 200)
4. **阿里云服务器是否包年包月** → 备案硬性要求,需用户去控制台确认

## 顺序(三者有依赖,不能乱)

```
① 申请正式证书(10 分钟,AGC 即时出证)
      ↓ 拿到后才能
② APP 备案(1~20 工作日,最慢,先提交)
③ 打 release .app 包
      ↓ 拿到备案号才允许提审
④ 提交 AppGallery 审核(1~7 工作日)
      ↓
⑤ 公安联网备案(上架后 30 日内)
```

## 校验

`python3 verify_materials.py` → **42 项全过**:
包名/版本/名称与代码一致、权限集合完全一致、隐私政策"未申请"的声明与代码相符、
字数限内、三份文档互相不打架、图标尺寸与四角不透明、已知缺口如实记录。

## 没做的事(刻意的)

- 不生成截图:必须真机原生界面,不能是设计稿(审核明确禁止)
- 不填备案表:实名主体必须本人操作,且工信部短信核验要发到本人手机
- 不碰 AGC:需要登录用户账号,部分环节要扫码/人脸
2026-11-01 09:56:04 +08:00

175 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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)