## 背景
用户要同时办三件事:① 上架华为 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:需要登录用户账号,部分环节要扫码/人脸
175 lines
7.8 KiB
Python
175 lines
7.8 KiB
Python
# -*- 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) |