# apps/client 全量体检(2026-09-03) 扫描范围:428 个 dart 文件 / 187,759 行 / 41 个语种。 工具:`dart analyze` 全量 + 括号配平的自定义扫描脚本 + 逐条 grep 反查。 **未做**:真机运行、动态链路验证、`local_plugins/` 下 25 个原生插件的内部审查。 统计:analyzer 1,222 项(1,049 info + 173 warning)。 --- ## 必须先修 ### 01. ~~32~~ 27 个翻译 key 在全部 41 个语言文件里都不存在 —— 用户可见 ✅ 已修(2026-09-03) > **修复记录**:26 个 key 已补入全部 41 个语言文件(1,066 条);第 27 个 `tips` 改为复用 > 项目里已有的 `tip`(30 处在用),不新增同义重复键。 > 复查结果:代码里生效的 1,151 个 `.tr` key **全部有译文**,各语言文件无重复键, > `dart analyze` 全量 1,222 → 1,222 项、0 error(无新增问题)。 > > **修正初版报告**:初版列的 32 个里有 5 个只出现在**被注释掉的代码**里,界面上并不会显示, > 已从清单剔除:`codeSend`、`clearInput`、`ttsInterruptToggle`、`unsupportedMode`、 > `checkUpdateTooFrequent`。真正生效的是 27 个。 > (`pleaseEnterVerificationCode` 在 `:477` 是生效的,未受此修正影响。)
原始问题描述 GetX 的 `.tr` 找不到 key 时**原样返回 key 本身**,不报错、不回退中文。这 32 处在 **包括简体中文在内的每一种语言**下,界面上直接显示英文标识符。 命中主流程: | key | 位置 | 表现 | |---|---|---| | `pleaseEnterVerificationCode` | `login_controller.dart:477` | 登录页显示 `pleaseEnterVerificationCode` | | `codeSend` | `login_controller.dart:331` | 发送验证码按钮 | | `userDataParseFailed` | `login_controller.dart:718` | 登录失败提示 | | `notCharging` | `home_controller.dart:513` | 首页设备卡片充电状态 | | `finishRecording` / `pauseRecording` / `markFavorite` / `unmarkFavorite` / `enterFileName` | `meeting_record_view.dart` | 会议录音页控制按钮 | | `monthlyTranscriptionDuration` / `industrySpecificSummaryTemplate` / `distinguishDifferentSpeakers` / `importAudioFromAppWeb` / `findMore` / `bindImmediately` / `autopilotTemplateSummary` | `member_bottom_sheet.dart` | 会员权益弹窗 6 条文案 | | `clearInput` | `text_translation_view.dart:84` | 文本翻译清空按钮 | | `ttsInterruptToggle` / `tips` | `agent_view.dart:319`, `agent_controller.dart:1773` | 智能体页 | | `notBound` | `goodsvip_view.dart:363` | VIP 商品页 | | `unsupportedMode` | `translation_controller.dart:343` | 通话翻译 | | `storageBytes` / `storageUploads` | `meeting_stats_view.dart:226,232` | 用量统计 | | `checkUpdateTooFrequent` | `settings_controller.dart:319` | 设置页 | | `scanIdle` / `scanning` / `startScan` / `stopScan` / `discoveredDevices` / `noDevicesFound` / `unnamedDevice` / `disconnect` | `devices_view.dart` | 杰理扫描页(本项目不走,优先级低) | **修法**:先补 `zh_cn` / `en_us`,再批量灌其余 39 语。 ⚠️ 语言文件里单双引号两种写法都存在,补之前两种都要查,否则 const map 会在 build 阶段撞重复键。
**遗留**:防回归的 CI 检查还没加(见第 04 条),现在仍然可能再漏进新的缺失 key。 ### 02. 微信 appSecret 硬编码 + 无条件打印进设备日志 ``` weixinLogin.dart:148 print("_getTokenUrl 数据获取${_appId},${_appSecret}"); weixinLogin.dart:160 print("_getRefreshTokenUrl 数据获取${_appId},${_appSecret}"); login_service.dart:9 appSecret: 'c792a0387c339f7b5c85d439466c67b7' pay_service.dart:41 appSecret: '9ceb39df312b3ba95a229b2aa0d15496' ``` 裸 `print()` **不受 `kReleaseMode` 约束**,正式包照打。(同项目 `appleLogin.dart` 的 print 都套了 `kDebugMode`,这两行漏了。)拿到 appSecret 就能以应用身份换 access_token。 **修法**:先删这两行 print(零成本止血)。appSecret 本身应挪到服务端(客户端只发 code); 一时改不动至少改成后端 `AppConfig.env()` 下发。**已进过 git 的 secret 必须视为已泄漏,去微信开放平台重置。** ### 03. 支付宝已签名订单串被打进日志 `pay_service.dart` 有 **68 处裸 `print()`,全文件零 `kDebugMode` 门禁**。 ``` pay_service.dart:732 print('支付宝支付订单: $orderInfo'); ← 含服务端签好的 sign pay_service.dart:610 print('创建支付订单: $response'); pay_service.dart:836 print('创建支付宝订单: $response'); ``` **修法**:整文件 print 换成项目已有的 `Logger`(自带 `kReleaseMode ? warning : debug` 分级); 涉及订单串/响应体的直接删,不要降级。 --- ## 高 ### 04. 30 个语种各缺 160+ 个正在使用的 key 全项目 key 并集 1,599 个: | 语种 | key 数 | 覆盖率 | |---|---:|---:| | `zh_cn` `en_us`(2 个) | 1,555 | 97% | | `de` `es` `fr` `ja` `ko` `pt` `ru` `zh_hk` `zh_tw`(9 个) | 1,541 | 96% | | `ar` `bg` `cs` `da` `el` … (28 个) | 1,322 | 83% | | `tr_tr` | 1,320 | 83% | | `nl_nl` | 1,213 | 76% | 荷兰语/土耳其语等用户会在自动回连授权弹窗、蓝牙扫描页、设备绑定页看到 `autoConnectConsentTitle` 这样的标识符。 **修法**:把「以 en_us 为基准做 key 差集」做成 CI 检查,仓库已有的 `tools/i18n_audit.py` 加一条覆盖率断言。 ### 05. `AppConfig.getproduct()` 的地雷仍有 6 处调用 ✅ 已修(2026-09-03) > **修复记录**:`appconfig.dart` 的 `getproduct()` 与 `agentsystemPrompt()` 都改成了 > 显式循环 + 返回 null(不引入 `collection` 依赖——项目 pubspec 里没有它)。 > 同时补了两道原本不成立的前置条件: > - **未初始化时返回 null**:`config` 是 `static late`,`user_getappconfig` 还没回来时 > 原实现会抛 `LateInitializationError`,null 契约照样不成立。 > - **空设备名返回 null**:`devicename.contains('')` 恒为 true,原实现会把**产品表里的 > 第一个产品**当成匹配结果返回。调用点本来就都紧接着判 `deviceName != ''`,无一依赖此行为。 > > 6 个调用点的代码**没动**——它们写的 `if (product != null)` 现在才真正开始工作。 > 另外把三处「不要用 getproduct,它会抛」的注释改写成了真实理由(那三个本地 > `_findProduct` 有更宽的双向包含匹配规则,所以保留,见第 10 条)。 > `dart analyze` 对改动文件 0 新增告警。 内部 `firstWhere` **没有 orElse**,查不到抛 `StateError` 而不是返回 null。 代码里已有三处注释写着「不要用这个函数」,但调用点一个没换。 调用方全写成 `DBProduct? p = getproduct(...); if (p != null) {...}`——那个 null 分支 **永远等不到**,异常被外层 try 吞掉后直接走兜底,全程不报错: ``` home_controller.dart:526 getCurrentDeviceType() → 设备类型退化成 unknown home_controller.dart:609 translation_controller.dart:1490 broadcastPeer 恒为 false translation_controller.dart:1584 agent_controller.dart:368 TTS 打断偏好读不到 ble_manager.dart:1055 ``` 同类:`appconfig.dart:35` 的 `agentsystemPrompt()` 也是无 orElse 的 firstWhere。 **修法**:改成 `firstWhereOrNull`(collection 包已在依赖里)。改完那 6 个 `if (p != null)` 才真正开始工作。 ### 06. 耳机配对页整页硬编码中文 `bes_scan_view.dart` 是恒玄耳机唯一配对入口、新用户必经流程,**一句都没走 `.tr`**: ``` :223 Text('添加 AI 耳机($_targetName)') :274 Text('没有找到 $_targetName 耳机') :282 '请先在系统「设置 → 蓝牙」里连接 …' :86 Get.snackbar('连接成功', …) :190 Get.snackbar('连接失败', …) ``` ### 07. 会议转写编辑框每次重建都被重置 `speech_tab.dart:142` 在 `Obx` 回调里 `new TextEditingController(text: item['content'])`。 `Obx` 每次重建都造一个新的:**用户正在改的发言人文字会被打回原文**,旧 controller 一个都没 dispose。 同类(仅泄漏、不影响功能):`profile_view.dart:148`、`profile_meeting_view.dart:12`、 `translation_list_view.dart:347`。 **修法**:`speech_tab` 改 StatefulWidget,controller 按 `item['id']` 缓存在 State 里、dispose 时统一释放。 ### 08. 两端都全局放开了明文 HTTP ``` ios/Runner/Info.plist NSAllowsArbitraryLoads = true android/…/AndroidManifest.xml:73 usesCleartextTraffic="true" android/…/network_security_config.xml base-config 全放开 + certificates src="user" ``` **全站可被降级到明文**(业务后端、支付回调、会议音频上传都在口子里),而实际需要明文的 只有讯飞声音复刻一家:`timbre_controller.dart:107,128,155,200,241` (`opentrain.xfyousheng.com` / `avatar-hci.xfyousheng.com`)。 Apple 对全局 `NSAllowsArbitraryLoads` 会要求提交理由说明。 **修法**:iOS 用 `NSExceptionDomains` 只放 `xfyousheng.com`;Android `base-config` 改 `cleartextTrafficPermitted="false"` 再加 `domain-config`。顺带去掉 `certificates src="user"` (当前配置默认接受用户证书 = 默认接受抓包代理)。 --- ## 中 ### 09. 付费页在卖一个已经关掉的功能 ``` goodsai_view.dart:324 'helpCallAndSMS'.tr ← AI 商品购买页卖点 assistant_directive_service.dart:48 'SEND_message': '本版已移除短信与联系人能力' ``` 用户付费后说「给爸爸发短信」,什么都不会发生。 **修法**:摘掉这张 feature card,或去百炼后台把这两个 tool 也摘掉并恢复端侧实现。两边同时留着是最差组合。 ### 10. 「按设备名找产品」有 5 套各写各的实现 ``` appconfig.dart:53 getproduct() ← 会抛异常那个 home_controller.dart:89 _findProductByName() bes_ota_upgrade_controller.dart:208 _findProduct() ota_core_contropller.dart:429 findProduct() bes_device_auth.dart:269 内联实现 ``` 匹配规则互不相同,改一次规则要改 5 处。 **修法**:收敛成 `AppConfig.findProduct(name)`,规则用 OTA 那版最完整的 (名字互相包含 → 唯一 `devicetype==1` → null)。和第 05 条一起做。 ### 11. eq_settings 是 1,911 行完全不可达的死代码 `app_routes.dart:22` 声明了 `/eq_settings` 常量,但 `app_pages.dart` **没有对应 GetPage**, 全项目**没有任何一处导航过去**。模块内部 UI 标签也全是硬编码中文(音效/均衡器/默认/流行/摇滚…)。 ### 12. 4 个调试页面挂在正式路由上,无任何门禁 `bes_test`、`opus_test`、`speech_test`、`log_export` 都在 `app_pages.dart` 注册为正常 GetPage, 正式包可达。`bes_test` 尤其敏感——能对耳机下发任意私有协议命令,而 `0x5x/0x6x` 段会切换耳机工作模式。 ### 13. 60 处空 catch 把失败原因全吞了 分布在 34 个文件,top:`volcano_tts_api_service.dart`(6)、`translation_controller.dart`(3)、 `translation_detail_controller.dart`(3)、`agent_controller.dart`(3)、`ble_manager.dart`(3)。 **修法**:不必全改,先给**网络与蓝牙**相关的补一行 `Logger.w`——这两类线上最常出问题、最难复现。 ### 14. 核心流程里还有一批中文硬编码 ``` call_recording_controller.dart:470,763 「已标记」「录音已存入语音记录助手」 picture_translation_controller.dart:214,221 agent_controller.dart:1773,2446,2728 导航失败 / 播放提示 settings_controller.dart:278,279,301-306 清除配对设备确认框 calendar_controller.dart:345,528+ ${month}月${day}日 + 中国节假日表 ``` ### 15. 微信登录里残留着教程示例代码 `weixinLogin.dart:193` 的 `postData()` 往 `jsonplaceholder.typicode.com`(公开 API 教程站)发 POST。 全项目无调用。同文件 `fetchData()` 也只剩注释里的调用。删掉即可,顺带解决 `:177/:208` 打印接口响应体的 print。 ### 16. 网络图片零缓存 8 处 `Image.network`,未引 `cached_network_image`。产品图/头像每次重建、滚动都重新走网络。 --- ## 清理项 ### 17. 1,222 项 analyzer 告警 ``` 358 deprecated_member_use ← 升 SDK 时变硬错误,值得先看 198 prefer_const_constructors ← 直接影响重建开销 191 avoid_print ← 即第 02/03 条 53 unused_import 34 unused_element / 23 unused_field / 17 unused_local_variable 14 dead_null_aware_expression(goods_base_controller 占 10 处) 2 must_call_super ``` `dart fix --apply` 能自动清掉约 400 项。deprecated_member_use 建议升 Flutter 前单独排一次。 ### 18. 两处 `onClose` 没调 `super.onClose()` `record_timer_controller.dart:88`、`translation_history_controller.dart:373`。 GetX 的 `ListNotifierMixin.onClose` 标了 `@mustCallSuper`,不调则该 controller 的 worker 不回收。 ### 19. 本地闹钟记录读回时不校验时间格式 `fromDirective` 走 `_normalizeTime`、格式不对返回 null,这条路径是稳的。 但 `assistant_directive.dart:376` 的 `fromJson` 直接 `j['time'] ?? '00:00'` 不校验, 而 `:354/:355` 的 `hour`/`minute` 是裸 `int.parse(time.split(':')[…])`—— 本地存储里有一条格式不对的旧记录,代办页整页渲染就抛。 ### 20. CLAUDE.md 与代码已经对不上的几处 | 位置 | 文档说 | 代码实际 | |---|---|---| | 微信 Universal Link | `https://ymaikj.com/app/` | `https://yimai.games/app/`。代码与 `Runner.entitlements` 的 `applinks:yimai.games` **一致**,不是 bug;但第三条腿(微信开放平台登记 + `yimai.games/.well-known/apple-app-site-association`)需确认已同步,否则微信登录与支付静默失败 | | `version_update_service` | 失败会把游客入口锁死 10 分钟 | 已修:`:156` 成功后才写 `_lastCheckTime`,失败分支显式不写 | | `bes_bluetooth_service.dart:296` | — | 注释写「`AA 20` 耳机状态」,而实现和下方长注释都明确是 `AA 09`。`AA 20` 恰是实测过、完全无回包的那个错误命令 | | Google Play 权限 | — | 已核对:`SEND_SMS` 与 `MANAGE_EXTERNAL_STORAGE` 确实是 XML 注释状态,`GOOGLE_PLAY_RELEASE_CHECKLIST.md` 判断正确。仍生效的受限权限是 `QUERY_ALL_PACKAGES` 和已废弃的 `GET_TASKS`,checklist §3 已覆盖 | --- ## 扫描过程中修正过的判断 - 一度把 `SEND_SMS` / `MANAGE_EXTERNAL_STORAGE` 记成生效权限,逐行核对后确认是 XML 注释,已从清单移除。 - `appleLogin.dart` 的 31 处 print 确认套了 `kDebugMode`,未列入第 02 条。