于是我把它从「电子书」改成了「数据库」
顺带把六个功能模块和八个坑全拆开讲

上个月把「史鉴」提审了。111 个条目、二十多万字、792 个年份节点、57 张带出处的配图,我自己觉得挺扎实。

驳回意见只有一句话:

应用仅有历史事件人物知识文章罗列,内容丰富度欠佳,从用户实际使用需求与体验来看,目前应用可覆盖的使用场景暂时还比较有限。

翻译一下:你这不是应用,是一本电子书。

这篇不写「鸿蒙真香」,也不写「ArkTS 劝退」。只写一件具体的事:一个人、没有设计稿、没有后端、没有服务器,从空目录到提交上架,功能是怎么一块块长出来的,哪几块是被审核意见逼出来的,以及哪些坑我摔过。

看完你大概能判断:鸿蒙原生开发到底适合做什么,不适合做什么。

111

条目

23万

正文字数

792

年份节点

57

配图

12,093

行 ArkTS

11MB

安装包

01

先定形态,再写第一行代码

历史类应用最容易掉进的坑,是把内容当产品。堆条目、堆字数,最后得到一本翻不到头的电子书。而电子书在应用市场是另一个类目,需要出版资质。

我后来想明白一件事:读者不缺内容,缺的是定位。你告诉他安史之乱讲了什么,他不一定需要;他真正想知道的往往是——755 年发生了什么、往前五年是什么、这条在哪个条目里能看到全貌。

所以产品定义改了一句话:不是「收录多少条」,而是「能不能在三秒内回答一个具体问题」。

这个判断会一路影响后面所有决定:为什么数据要先结构化、为什么时间轴要按年数画宽、为什么宁愿少配图也不配错图。形态定错了,后面每一行代码都在还债。

02

六个功能模块,逐个拆

每个模块按「解决什么问题 → 关键实现 → 踩到的坑」三段说。

应用结构一览

按年份查按关键词查按分类筛
▼

统一条目模型

同一套字段装下时代 / 朝代 / 人物 / 专题

▼
千年时间轴阅读器
分节朗读
趣味答题
60 题

一份数据,三种用法——这是「工具」和「电子书」的分界线

① 数据层:一个模型装下四类内容

时代、朝代、人物、专题,四种东西长得完全不一样。用四个模型,阅读器要写四套;用四个页面,搜索要写四遍。

做法是抽一个 16 字段的统一模型 HistoryArticle:

id / kind / name身份与分类
subtitle / mark / accent副标题、标识字、主题色
facts / readMinutes首屏速览数字、预计阅读时长
lead / question导语、「本章要回答什么」
sections正文 7–9 节,每节带小标题
timeline关键节点,可解析成数值年份
case / evidence / debate微观案例、史料依据、学界争议
quotes / legacy / related原典摘录、遗产影响、延伸阅读

代价是专题和人物要塞进同一套骨架,有几节会显得勉强。收益是阅读器只有一份、搜索只有一份,加一个新条目只需要往数据文件里追一个对象。

坑:人物和朝代都要按时代筛选,但归类信息不该写进正文。所以另建了一张映射表 PERSON_PERIOD。新增人物忘了登记,筛选里就掉进「其他」。

这种隐性约定是纯数据驱动项目最容易烂的地方——它不报错,只是悄悄少一条。

② 索引层:三个入口,一个出口

三个入口都通向同一个阅读器。

按年份 输入 755、前221、公元1644 都认。先查 ±2 年,没结果自动放宽到 ±10 年,还没有就明确告诉你库的覆盖范围(约前 7000 年—20 世纪),不编假结果。

按关键词 标题、标签、速览、正文、年表、案例全部进索引,结果标注命中在哪一部分,而不是丢一堆卡片让你自己找。

按分类 朝代分上古三代、秦汉、魏晋南北朝、隋唐五代、宋辽夏金元、明清、近代七段;人物按「时代 × 身份」双筛,身份分帝王、政治、军事、思想、文学、艺文、科技、探索八类。

整次改造里性价比最高的一步,是把年表从「正文里的一段文字」变成「带数值年份的结构化数据」:

原先 870 条年表散在各条目正文里。写个脚本在构建时解析,得到 792 条 { y, label, title, articleId },覆盖率 98.3%。剩下 15 条是「唐代」「此后」这类相对时间,直接丢弃——不猜。

从这一刻起,年份才是可检索的字段,而不是文章里的一句话。

③ 时间轴:让长度自己说话

23 个政权排在一条横带上,块宽不平均分,按实际统治年数成比例:唐 289 年,秦 15 年,宽度差 19 倍。

不需要写一句「秦朝很短」,一眼就看见了。点任一政权,下方立刻列出它的关键节点,再点进条目。

实现上只有两个公式,轴范围固定在 −2100 到 1979:

left = (start - AXIS_MIN) * 100 / (AXIS_MAX - AXIS_MIN)

width = (end - start) * 100 / (AXIS_MAX - AXIS_MIN)

朝代列表页的每张卡片上也嵌了同一条微型轴,标出它在千年里的位置。卡片不再是纯文字列表,而是「能看见时间的卡片」。

④ 阅读器:一份排版、四种内容,还要能听

分节渲染,每节独立标题;四级字号(15/16/18/20,行高 26/28/32/36);朗读用 @kit.CoreSpeechKit,逐段 speak,靠 onComplete 串下一段,当前段高亮;延伸阅读跳到库里真实存在的条目。

坑一:从延伸阅读跳到另一篇,阅读器不归顶。看着像「点了没反应」,其实内容换了、滚动位置还停在上篇底部。给 articleId 加 @Watch,切换时把 scroller 归零。

坑二:同一篇自跳会和返回栈打架,变成「按返回没反应」的死循环。跳转前先判断 id 相同就直接 return。

⑤ 答题:把「读过」变成「记得」

60 题分五类(制度、人物、事件、辨析、文物),三档难度。每轮 Fisher-Yates 洗牌抽 10 题。

答对:14 个金色粒子从卡片中心四散,620ms 淡出,连击徽标跟着缩放;答错:卡片抖一下 [-9, 8, -6, 4, -2, 0],随即给解析,并标明出自哪一条,一键跳回原文。

设计上做了取舍:不做排行榜,不做积分。一个人做的 App 没有账号体系,排行榜就是假的。把反馈做在「当场」——对了有手感,错了立刻能回原文,比攒一个分数有用。

坑:粒子层放在 Stack 里写了 height('100%'),反过来把父容器撑高,整张卡片被拉变形。改成固定尺寸才正常。

题目也不是背题库。很多考的是辨析:长城是秦始皇一次修成的吗、活字印刷为什么没取代雕版、海禁是不是完全闭关。

⑥ 进度与收藏:重启还在

已读进度、收藏状态、字号偏好全部本地持久化。没有账号,不收集任何个人信息。

看着最不起眼,但审核看「使用场景」时,这正是「能长期用的工具」和「一次性浏览」的分界线之一。

03

二十多万字,是怎么堆出来的

答案是:数据即代码。12 个 .ets 数据文件,每个条目是一个对象字面量,直接编进 HAP。

好处:零请求、离线可读、搜索就是数组遍历,快。代价:内容更新要发版本。对工具型应用来说这个交换很划算。

每条统一骨架九段,写完再挂六块附件:

正文九段

导语 → 本章问题 → 国家机器 → 财政赋役 → 经济与技术 → 社会与衣食住行 → 文化观念 → 由盛转衰的机制 → 遗留影响

附件六块

关键节点年表 · 微观案例 · 原典摘录 · 史料依据 · 学界争议 · 遗产影响

不写「皇帝昏庸」,而是把赋税、兵制、土地、继承的结构性压力摊开讲。这是内容质量的底线,也是后面申诉时唯一能拿得出手的东西。

两条生成管线,都可以复跑:

▍ build_timeline_index.py 解析所有条目的年表 → 792 节点 / 23 条带 / 轴范围

▍ build_image_index.py 汇总三个图源 → 57 条图片记录

配图这块值得单独说,因为我在这里犯过最蠢的错。

最早一批图里,秦朝配了一张清代财神像,辽代配了金代器物——因为只用了关键词匹配。图片年代错配,比没有图难看得多:读者第一眼看到,会觉得整个应用都不可信。

后来定了两条必须同时满足的硬约束:

约束说明
词边界匹配馆藏 period 字段必须整词对上,Han 不能命中 Shang
区间重叠 ≥ 50%馆藏断代区间和条目存续期重叠要过半

两条都不过,这条就不配图,退回主题色。宁可没有,也不拿近似图充数。

结果是 57 张图覆盖 45 个条目,剩下 66 个条目就是纯色头图。器物图来自 The Met 开放馆藏(CC0),现场照片与人物画像来自 Wikimedia Commons,每张都写明原件名、馆藏断代和作者。

人物画像要另外处理:Commons 关键词搜索合格率不到一半,会搜出同名运动员、月球环形山、故居门楼、邮票。办法是把候选图渲成一张联络表,逐张人眼看,能用的才入库。

这一步没有捷径。算法能帮你筛掉 60%,剩下的 40% 只能自己看。

04

上架材料:六样东西,少一样都过不去

项目要求容易踩的坑
图标1024 或 216 方形 PNG,≤3MB分级图标要分层素材,不是一张图
截图竖 1080×1920 或横 1920×1080,3–10 张真机 1216×2688 直接缩会变形
名称≤15 汉字,与软著完全一致17 字标题只能放副标题/推荐位
简介一句话 ≤80 字别写成内容清单,要写能干什么
资质软著、备案、隐私政策、隐私权利隐私政策必须是公网可访问的 URL
签名发布证书 + 发布 Profile发布包装不进自己手机,见下一节

一个具体教训:17 字标题。华为 AGC 的「应用名称」上限是 15 个汉字,所以 17 字标题不能填在名称里,只能放副标题或推荐位。

而且中文标题的字数必须用 len() 数——「一百一十一」是 5 个字,我第一版数成 4 个,整条标题就差一位。

截图也不是截完就完事。我的做法是写一个排版脚本:真机截图作为素材,脚本负责按比例缩放、加设备边框、排标题文案,一次同时出竖屏 6 张和横屏 6 张。改文案重跑 40 秒,比在图片软件里手动改 12 张快得多。

05

签名:最容易卡住的一步

DevEco Studio 的自动签名用的是 debug 证书,能装机,不能上架。

换成发布证书之后,hdc install 直接报错:

9568322 · not trusted app source

这不是配置错了,是机制如此:发布签名只信任应用市场这个渠道,不信任本地直装。

所以流程要反过来:

▍ 日常调试用 debug 签名,DevEco 自动生成

▍ 出包前切发布签名,只看 assembleHap 能不能过

▍ 真机验证一律用 debug 包

还有个更隐蔽的:debug 证书的密码是 DevEco 用本机密钥加密的一串字符,拷到另一个工程会直接报 Signature material verification failed。不能复用——换机器、换工程,重新生成一次自动签名就好。

我第一次遇到时以为是证书坏了,折腾了两个小时。

06

被驳回之后,我改了什么

先判断这条意见在说什么。它不是在说条目不够多——继续加文章解决不了这个问题。它说的是应用形态:用户只能在列表里点开文章往下读,没有一件可以「完成」的事。

所以做了五件事:

#动作为什么
1改分类从「图书阅读」改「教育 > 学习工具」。分类决定审核员用什么标准衡量你
2改介绍开头不再写「收录 111 个条目」,改成「能查、能读、能听」
3换首图第 1 张从首页换成时间轴页,第一眼看到查询与条带
4加工具年表抽成结构化数据,加年份检索、千年条带、趣味答题
5写申诉逐条对应:6 项能力、7 类场景、4 个后续版本

一句话总结这段经历:

与其争「我的内容很多」,不如证明「用户能在这里完成一件事」。

这条经验对所有内容型应用大概都通用:审核看的不是你写了多少,而是用户能做什么。

07

ArkTS 踩坑清单,八条

现象原因改法
Path 画的图标在 3x 屏小得看不见commands 按 px 解析,24 单位在 3x 屏只有 8vp换 SVG + fillColor()
筛选标签点了不亮@Builder 按值传参,状态变了不重建抽成 @Component + @Prop
答题页按返回直接退出应用详情层与答题层平级,返回键先关答题Stack 叠层 + onBackPress 里详情优先
粒子层把整张卡片撑变形Stack 内 height('100%') 反向撑开父容器固定 250×150
Alignment.CenterStart 编译报错没这个枚举Alignment.Start
自动点击总点偏hdc 坐标是物理像素,不是逻辑像素uitest dumpLayout 取 bounds
名字被压成一个字和旁边的标签挤在一行name 独占一行 width('100%')
发布包装不进手机9568322 · not trusted app source真机验证用 debug 包

这八条里,前五条都不是「难」,是「反直觉」。文档里不会写,只能自己撞一次。

现在这个应用:111 个条目、23 万字、792 个年份节点、57 张带署名的图、60 道题,HAP 11MB,纯本地,只有一条联网权限——在线语音合成。

第二次提审在等结果。如果又被打回,我大概还会继续改形态,而不是继续堆内容。

一个人做应用,最大的优势是可以随时推翻自己;最大的风险是把力气花在了没人反对的地方。

审核意见难听,但它至少是一份免费的、具体的、来自真实用户视角的反馈。这话说起来有点阿 Q,但确实省了我几个月。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐