权限与隐私最容易出现的审核问题,往往不是"权限申请得太多"这么简单,而是代码、配置、页面文案和上架材料各说各话。module.json5 声明了网络权限,产品说明却写成完全离线;设置页显示"提醒已设置",工程里却没有通知调度;页面提示数据已清除,持久化异常却被静默吞掉。这些细节单独看都不大,合在一起就会让审核人员无法判断应用真正做了什么。
知律当前是一个面向 HarmonyOS 5.0 及以上版本的本地法律学习应用。源码中的题库、收藏、笔记、错题、学习进度和考试记录都可以复核;同时,练习页的语音功能会先创建离线 TTS 引擎,失败后再尝试在线引擎。也正因为这条在线兜底存在,ohos.permission.INTERNET 不能被当成"多余配置",但也不能被扩写成账号、云同步或远程题库能力。
本文基于 D:\huawei\one19-11 的真实源码,重点复核 entry/src/main/module.json5、EntryAbility.ets、Index.ets、HomePage.ets、PracticePage.ets、SettingsPage.ets 与 UserDataManager.ets。包名 com.jiaweikang.one19 是本文草稿回读时使用的唯一标记。全文只描述源码中能确认的行为,对在线 TTS 的具体数据处理范围、系统服务边界和网络实现不做未经官方文档或真机证据支持的推断。

本文会解决五个实际问题:
- 从功能调用反推权限,而不是先堆一张权限表;
- 解释 INTERNET 为什么存在,以及它不代表什么;
- 区分权限拒绝、能力不可用和持久化失败;
- 把本地学习数据、清理入口与隐私材料对齐;
- 找出工程中仍会影响上架审核的一致性风险。
一、先画出四份必须一致的事实
权限审计不应只打开 module.json5 看一眼。真正需要相互校验的是四份事实:
| 事实来源 | 需要回答的问题 | 知律当前证据 |
|---|---|---|
| 产品功能 | 用户能触发什么 | 本地题库、练习、收藏、笔记、错题、统计、TTS |
| 系统 API | 代码实际调用什么 | Preferences、TTS、窗口与路由能力 |
| 模块配置 | 安装包声明什么 | INTERNET,无相机、麦克风、位置等敏感权限 |
| 用户与审核材料 | 对外怎样说明 | 本机存储、网络用途、删除方式、无账号与无云同步 |
只要其中一列变化,其他三列就要重新检查。例如以后加入云端题库,不能只补 HTTP 请求;还要更新隐私政策、网络用途、数据处理说明和离线失败路径。

二、module.json5 的真实权限面很小
当前 entry 模块只声明一项权限:
json5
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
源码没有声明相机、麦克风、位置、联系人、短信、日历或媒体读取权限,也没有发现 requestPermissionsFromUser() 一类运行时索权代码。首页和主导航只是组合本地页面、题库数据与学习记录,没有启动时集中申请权限的逻辑。
这里要避免两个相反误判:
- 不能因为只有 INTERNET 就写成"零权限应用";
- 也不能因为存在 INTERNET 就宣称应用具备远程题库、云同步、在线排行或账号服务。
权限只能证明模块被允许使用对应系统能力,不能替代对真实调用路径的说明。
三、INTERNET 对应的是在线 TTS 兜底
练习页导入了 HMS 文本转语音能力:
typescript
import textToSpeech from '@hms.ai.textToSpeech'
初始化时先尝试离线引擎:
typescript
this.ttsEngine = await textToSpeech.createEngine({
language: 'zh-CN',
person: 0,
online: 0
})
只有离线创建失败,才进入第二条路径:
typescript
this.ttsEngine = await textToSpeech.createEngine({
language: 'zh-CN',
person: 0,
online: 1
})
因此,当前源码中最明确的网络意图就是在线 TTS 回退。INTERNET 只服务于在线 TTS 兜底,这是现阶段能够从配置与调用链共同确认的边界;工程中没有普通 HTTP 客户端、Web 组件、账号接口或远程题库下载代码。
上架说明可以写"语音示例优先使用设备离线语音能力,在离线引擎不可用时可能尝试在线语音服务",但是否需要进一步披露文本处理、服务提供方和网络条件,应以对应 TTS Kit 的官方说明及真机抓取结果为准,不能凭经验补写。
四、TTS 不等于录音,不需要麦克风
文本转语音的输入是应用已有的题目文本,输出是合成语音。它不录制用户声音,所以当前没有 ohos.permission.MICROPHONE 是合理的。
这一点直接影响失败文案。如果播放失败,页面不能提示"请允许麦克风权限",因为这会把无关权限引入产品认知,也会诱导开发者错误地增加权限。当前源码使用的文案是:
text
当前设备语音引擎不可用
语音播放失败,请检查系统语音服务
这比"请开启录音权限"准确。更完整的文案还可以补充:文字提示仍可查看,答题流程不受影响。
五、当前没有运行时权限拒绝回调
标题中的"拒绝路径"需要按实际权限类型理解。知律没有请求相机、位置等需要用户在运行时选择允许或拒绝的敏感权限,因此源码中没有真实的授权对话框,也没有 granted、denied、blocked 等授权状态。
当前实际存在的是三类失败:
- 设备没有可用的离线或在线 TTS 引擎;
- TTS 引擎创建成功,但播放回调返回错误;
- Preferences 初始化或写入失败。
它们都不应被叫作"用户拒绝权限"。术语准确很重要,因为排查动作完全不同:权限拒绝要检查授权状态,TTS 不可用要检查系统服务和设备能力,持久化失败则要检查存储初始化与写入结果。
六、语音能力已经有不阻断主流程的降级
用户点击听音题时,toggleAudioPreview() 先打开文字弹窗,再后台尝试语音:
typescript
this.audioDialogText = this.audioText(question)
this.audioDialogStem = question.stem
this.showAudioDialog = true
const ready = await this.ensureTtsEngine()
if (ready && this.ttsEngine) {
this.ttsEngine.speak(this.audioText(question), {
requestId: `${question.id}_${Date.now()}`
})
}
这条顺序设计得很稳。即使引擎创建失败,用户仍然能看到题干或发音提示,继续完成答题。它满足三个审核友好的原则:
- 用户主动点击后才触发语音;
- 失败有可见状态;
- 附加能力失败不阻断核心学习流程。
还可以改进的一点是空 catch。错误日志不应包含完整题目或用户笔记,但可以记录非敏感错误码和阶段,例如 offline_engine_create_failed,帮助区分离线资源缺失与在线服务失败。
七、本地数据并不少,不能写成"不存储数据"
知律不要求登录,也没有云同步,但会处理多类设备内学习数据。UserDataManager 通过 Preferences 保存:
| 键 | 数据内容 | 用户价值 |
|---|---|---|
favoriteRecords |
收藏题目 ID、题库 ID、时间 | 收藏复习 |
noteRecords |
题目笔记文本 | 个性化学习记录 |
wrongRecords |
错题与时间 | 错题复盘 |
bankProgress |
已答数、正确数、章节 | 学习进度 |
chapterProgress |
章节完成统计 | 章节追踪 |
examHistory |
分数、题量、用时 | 模拟考试记录 |
dailyReminderTime |
提醒时间偏好 | 设置显示 |
examDurationSec |
考试时长 | 模拟考试配置 |
autoNextQuestion |
自动下一题开关 | 交互偏好 |
隐私材料应写"学习数据保存在本机",而不是"应用不存储任何数据"。本地存储仍然是数据处理,只是当前没有证据表明这些数据会上传或分享。
八、Preferences 集中封装让审计更容易
EntryAbility.onCreate() 在应用启动时初始化数据层:
typescript
UserDataManager.init(this.context)
随后 UserDataManager 从 Preferences 读取 JSON,再写入 AppStorage:
typescript
UserDataManager.prefs =
preferences.getPreferencesSync(context, { name: UserDataManager.STORE_NAME })
AppStorage.setOrCreate<FavoriteRecord[]>(
'favoriteRecords',
JSON.parse(favStr) as FavoriteRecord[]
)
页面只通过管理器更新数据,避免每个页面直接散落存储调用。这种结构让权限与隐私审计可以沿着一条稳定链路进行:
text
页面操作
-> UserDataManager
-> Preferences
-> AppStorage
-> 页面刷新
不过,存储名仍是 dialect_quiz,而应用已经是知律。这不会直接改变数据范围,却说明项目迁移时留下了旧产品命名。发布前应把这类标识纳入一致性检查,并在需要迁移旧数据时设计兼容读取,不能直接改名导致用户记录丢失。
九、用户笔记是隐私审计的重点
收藏和错题主要是应用生成的结构化记录,笔记则是用户自由输入文本。用户可能在笔记中写入姓名、联系方式、案件线索或其他个人内容,即使应用没有主动要求,也不能把它当成普通统计值。
当前源码把笔记保存在本机,没有上传、分享和云同步代码。更稳的工程边界是:
- 不把笔记正文写入 hilog;
- 不在异常信息中拼接完整笔记;
- 默认不导出、不上传;
- 提供删除入口;
- 未来增加备份、分享或同步时重新做隐私评估;
- 对导出内容提供范围预览和明确的用户触发。
技术文章和商店材料只能说明现有边界,不能把"未来可能支持"写成已经具备的能力。
十、清除全部数据有二次确认
设置页的清理动作不是一次点击立即执行。第一次点击将 pendingClear 设为 true,第二次才调用:
typescript
this.favRecords = UserDataManager.clearFavorites()
this.noteRecords = UserDataManager.clearNotes()
this.wrongRecords = UserDataManager.clearWrong()
this.progressList = UserDataManager.clearProgress()
this.chapterProgressList = UserDataManager.clearChapterProgress()
this.examHistory = UserDataManager.clearExamHistory()
二次确认降低了误删风险,清理范围也覆盖了主要学习记录。需要注意的是,学习设置键没有在"清除所有学习数据"中重置:提醒时间、考试时长和自动下一题仍会保留。
这不一定是错误,但产品说明必须明确"清除学习记录"还是"恢复全部设置"。两个动作的范围不同,按钮文案和审核说明不能混用。
十一、删除成功提示目前缺少落盘证据
persist() 使用同步写入和刷新:
typescript
UserDataManager.prefs.putSync(key, JSON.stringify(value))
UserDataManager.prefs.flushSync()
但异常被空 catch 吞掉:
typescript
try {
// 写入与刷新
} catch (_) {}
清理方法无论写入是否成功都会返回空数组,页面随后显示"所有学习数据已清除"。这会产生一种隐私风险:当前界面看似清空,应用重启后旧数据可能重新出现。
更稳的方案是让持久化层返回结果:
typescript
interface PersistResult {
success: boolean
reason?: string
}
function clearLearningData(): PersistResult {
// 全部键写入成功后才返回 success
return { success: true }
}
页面只有在磁盘写入确认成功后才显示完成;失败时保留明确提示并允许重试。隐私删除的验收标准不是"列表瞬间变空",而是重启后仍为空。
十二、"每日学习提醒"目前只是偏好值
设置页允许选择提醒时间,并调用:
typescript
UserDataManager.saveDailyReminderTime(value)
但是当前源码没有通知发布、提醒代理、定时任务或通知授权逻辑。也就是说,页面只是保存了一个时间字符串,并没有在指定时间真正提醒用户。
因此"每日学习提醒已设置为 09:00"会让用户误以为系统调度已经完成。发布前有两种真实选择:
- 改成"提醒时间偏好",明确当前只保存设置;
- 完成真实提醒实现,再同步权限、授权时机、拒绝路径和隐私说明。
不能为了让文案看起来完整而提前申请通知权限。权限应在功能真实交付后、用户主动开启提醒时按需处理。
十三、HomePage 与 Index 没有隐藏索权
Index.ets 负责手机底部导航和宽屏侧边导航,HomePage.ets 负责本地题库、学习统计和每日一题。两者都没有权限请求、网络调用或 Web 内容加载。
这个事实很适合写进首次启动验收:
- 启动页不应出现批量权限框;
- 首页在飞行模式下应正常展示;
- 本地题库和学习记录不依赖账号;
- 切换 phone、tablet、2in1 布局不应触发额外能力;
- 只有用户主动点击语音时才进入 TTS 路径。
把能力触发点缩到用户动作附近,比在 EntryAbility 中提前初始化所有服务更容易解释和测试。
十四、资源与包描述出现旧产品残留
权限一致性之外,当前工程还有一个更直接的上架材料风险:entry/oh-package.json5 和根 oh-package.json5 的描述仍写着"中国方言题库",而 AppScope 的正式包名是 com.jiaweikang.one19,应用内容和页面品牌是知律。
类似残留还包括 Preferences 存储名 dialect_quiz。这些值未必全部展示给用户,但会让代码审计、依赖包说明和后续维护产生混淆。
发布前至少应对照:
- AppScope 应用名与图标;
- bundleName、versionName、versionCode;
- entry 模块描述与 Ability 标签;
- oh-package 名称、描述、作者;
- 应用内"关于"页面;
- AGC 标题、简介、分类与截图;
- 隐私政策中的产品名和包名。
上架审核看的是完整产品,而不是只看首页截图。
十五、在线 TTS 让"数据保存在本机"需要限定语境
设置页点击"框架"后提示:
text
页面采用 ArkTS 与 ArkUI 构建,数据保存在本机。
对收藏、笔记、错题和学习进度而言,这句话有源码依据。但在线 TTS 会接收应用提供的待合成文本,具体由哪个进程或服务处理、是否经过网络、保存多久,需要依据官方文档和实际服务行为确认。
所以更严谨的表达是:
收藏、笔记、错题、进度和考试记录保存在本机;语音示例优先使用离线引擎,在线语音兜底的处理方式以系统服务说明为准。
这种按数据类别拆分的说明,比一句"全部数据都只在本地"更经得起复核。
十六、第三方与平台 Kit 要分开说明
entry 的包依赖只有本地 librarya 和 libraryb,没有广告、统计、登录或支付 SDK。但 PracticePage 直接使用 @hms.ai.textToSpeech,它仍然属于需要在能力清单中说明的平台服务。
依赖审计应覆盖:
oh-package.json5和锁文件;- ArkTS import;
- HAR/HSP;
- 原生 so;
- SDK 初始化代码;
- 网络域名和 AGC 服务开关;
- 隐私政策中的第三方或平台服务列表。
"没有外部 npm 包"不等于"没有任何平台服务"。对外材料应按真实依赖类型准确分类。
十七、未来新增敏感权限时的状态模型
当前知律不需要运行时敏感权限,因此不应为了通用性硬塞授权框架。若以后增加相机扫描、文件导入或位置能力,可以再引入明确状态:
typescript
type PermissionState =
| 'not_requested'
| 'granted'
| 'denied'
| 'blocked'
| 'unavailable'
页面处理应与用户动作绑定:
| 状态 | 页面行为 |
|---|---|
| not_requested | 在功能入口解释用途 |
| granted | 执行对应能力 |
| denied | 保留降级功能,不重复骚扰 |
| blocked | 提供一次系统设置引导 |
| unavailable | 隐藏入口或说明设备不支持 |
授权失败不能阻断题库、笔记和错题等无权限功能,也不能在启动时连续弹出。
十八、把权限回归检查放进发布流水线
权限与隐私不是写完一次报告就结束。每次新增 Kit、依赖或页面入口,都可能扩大数据与权限面。可以在发布脚本中加入最小扫描:
powershell
rg -n "requestPermissions|ohos.permission" entry/src/main
rg -n "@hms|@kit.NetworkKit|requestPermissionsFromUser" entry/src/main/ets
rg -n "http|Web\\(|fetch\\(|socket|cloud|push" entry/src/main/ets
扫描结果不能代替人工判断,但能发现变更。新增 INTERNET、CAMERA、LOCATION、通知、账号、推送或第三方 SDK 时,应强制更新:
- 权限清单;
- 数据流图;
- 失败与拒绝路径;
- 隐私政策;
- AGC 数据安全与 SDK 字段;
- 真机测试用例。

十九、真机验证矩阵
权限与隐私的验收必须覆盖"配置正确"和"失败可用"两方面:
| 场景 | 预期结果 |
|---|---|
| 首次启动 | 不弹相机、麦克风、位置等无关权限框 |
| 飞行模式进入首页 | 本地题库、统计和导航可用 |
| 飞行模式点击语音 | 离线引擎可用则播放,否则显示文字降级 |
| 在线 TTS 可用 | 仅用户点击语音后触发 |
| TTS 服务异常 | 不崩溃,答题可以继续 |
| 新建笔记后重启 | 笔记仍存在 |
| 清除全部学习数据 | 二次确认后主要学习记录为空 |
| 清除后杀进程重启 | 已删除记录不恢复 |
| 设置提醒时间 | 不能把仅保存偏好宣传为已调度通知 |
| 卸载重装 | 本地学习记录不应自动恢复 |
如果某项无法在当前设备验证,应记录为"未验证",不能写成已经通过。
二十、上架前权限与隐私核对表
-
module.json5只声明实际需要的 INTERNET; - 没有误加麦克风、相机、位置等权限;
- 首次启动不请求无关权限;
- 在线 TTS 的用途、触发时机和说明一致;
- 不把 TTS 错误描述成麦克风权限拒绝;
- 收藏、笔记、错题、进度和考试记录列入本地数据说明;
- 用户笔记不上传、不写日志;
- 清除范围与按钮文案一致;
- 清除结果经过重启验证;
- 每日提醒未实现调度时不宣称系统通知;
- 无账号、云同步、广告和支付时不勾选对应能力;
-
中国方言题库、dialect_quiz等旧命名已评估并清理或迁移; - AppScope、模块描述、关于页、AGC 和隐私政策产品名一致;
- 安装、启动、核心流程、退出和卸载正常;
- 平台服务清单包含实际使用的 TTS Kit。
二十一、常见问题排查
| 症状 | 先查哪里 | 正确处理 |
|---|---|---|
| 审核质疑为何联网 | PracticePage 的 online TTS | 说明用途并核对官方数据处理规则 |
| 页面提示麦克风授权 | 错把 TTS 当录音 | 删除无关权限和误导文案 |
| 飞行模式语音失败 | 设备无离线引擎 | 保留文字提示,不阻断答题 |
| 清空后重启数据回来 | persist() 异常被吞掉 |
返回落盘结果,失败时提示重试 |
| 提醒时间到了没通知 | 只保存 Preferences | 完成真实调度或修改产品文案 |
| 隐私政策写"不存数据" | 忽略本地学习记录 | 按 Preferences 键逐项说明 |
| 材料出现旧产品名 | 项目迁移残留 | 全量核对 AppScope、模块和包描述 |
| 宣称云同步或在线排名 | 没有服务端实现 | 删除未实现能力宣称 |
| 启动时批量索权 | 权限申请位置错误 | 改为用户触发功能时按需申请 |
二十二、总结:权限少不代表说明可以简单
知律当前的权限面并不复杂:模块声明 INTERNET,主要用于离线 TTS 失败后的在线语音兜底;没有相机、麦克风、位置等敏感权限,也没有启动时运行时索权。收藏、笔记、错题、学习进度、章节进度、考试历史和设置通过 Preferences 保存在本机,设置页提供二次确认的学习数据清理入口。
真正需要在上架前继续收紧的,是事实一致性:
- 在线 TTS 与"本机数据"说明要按能力拆分;
- 清理成功必须以真实落盘结果为准;
- 每日提醒当前只保存偏好,不能描述成已调度通知;
- 模块描述和存储名中的旧产品残留要处理;
- 任何新增权限都要同时补齐用途、拒绝、降级、隐私材料和测试。
权限合规不是把 requestPermissions 写得越短越好,而是让每一项声明都能在源码中找到唯一用途,让每一条用户说明都能被配置和运行结果证明。代码、权限、页面与上架材料说同一件事,才是可复核的 HarmonyOS 隐私工程。
本文部分内容由 AI 辅助整理。现有权限、TTS、本地存储、设置与清理行为均依据上述源码复核;关于持久化结果返回、权限状态模型、自动扫描和材料改写的内容属于基于当前实现边界的工程增强建议。