Files
jiu/docs/review/2026-07-03-precommit-review.md
T

122 lines
10 KiB
Markdown
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.
# 代码审查报告 — 提交前复审(auth 双屏重建 + DS 原子扩展 + 备案 https 回切)
**审查时间**2026-07-03(第二轮,接 `docs/review/2026-07-03-uncommitted-review.md`
**变更范围**git diff HEAD 210 个文件(+11354 / 10986+ 约 90 个未跟踪文件(auth_shared / ds_toast / ds_menu / users_screen / golden 基线等)
**已验证事实**(调用方提供):`flutter analyze` 0 error、251 测试全过、`check_ds_code` 0 违规、`go build/vet/test` 全过
## 总体评价
本轮新增改动质量高,无阻塞提交的问题。登录/注册双屏照原型重建后,dispose 路径(Timer/OverlayEntry/FocusNode/Controller)总体齐全;上一轮 6 条 should-fix 中 4 条已修复(https 回切一并解决 SHOULD-001/002manual.yml 兼容变量解决 003DsButton 置灰解决 004),SEC-001/002/003 修复无回归且补了跨租户回归测试。剩余问题集中在登录屏两处小竞态/生命周期缝隙和几条运维/文档收尾项。
---
## 必须修复(blocking
无。
---
## 建议改进(should-fixnon-blocking
### [SHOULD-101] 登录中途导航离开 → catch 分支 setState 打在已 dispose 的 State 上
**文件**`client/lib/screens/auth/login_screen.dart:306-311`
**问题**`_login()``await AuthRepository.login(...)` 期间,「注册新门店 →」「忘记密码?」链接**未随 `_loading` 禁用**,用户可在等待网络时点走(`context.go('/register')`)。此后:
- 失败路径:`on AuthException catch` / `catch` 里的 `setState(() => _errorMessage = ...)``mounted` 守卫(对照 finally 是有的),在已 dispose 的 State 上抛 FlutterError,经全局捕获还会打进错误上报;
- 成功路径:`await ref.read(authStateProvider.notifier).login(user)` 在 dispose 后使用 `ref` 同样会抛,且被泛型 catch 捕到后再次走无守卫的 setState。
**建议**
```dart
} on AuthException catch (e) {
if (!mounted) return;
setState(() => _errorMessage = e.message);
}
```
两个 catch 都补 `mounted` 守卫;或更简单——`_loading` 时把两个 `_Link` 的 onTap 置空。
### [SHOULD-102] 历史下拉:失焦关闭 Timer 在重新聚焦时未取消,会误关刚打开的下拉
**文件**`client/lib/screens/auth/login_screen.dart:81-95, 97-111`
**问题**:失焦分支安排 `_closeShopTimer = Timer(150ms, _closeShop)`;但 `hasFocus` 分支只调 `_closeShop()` 不取消该 Timer。序列「聚焦 → 点外部失焦(Timer 起跑)→ 150ms 内再点回字段(下拉重新打开)→ 旧 Timer 到点」会把刚打开的下拉关掉,且箭头图标状态与实际不符。username 侧同构。
**建议**`hasFocus` 分支先 `_closeShopTimer?.cancel()` / `_closeUsernameTimer?.cancel()` 再开下拉。
### [SHOULD-103] SEC-004 残留:盘点建单仍未校验 warehouse_id / product_id 归属
**文件**`backend/internal/handler/inventory.go:240-271``CreateCheck`
**问题**:本轮 `finance.Create` 已补 `ensureShopRefOpt`finance.go:96-100),stock_in/out 的 Create/Update 也都补了;但审计报告 SEC-004 点名的另一处 `CreateCheck` 依旧直存请求里的 `req.WarehouseID``Items[i].ProductID`,塞他店 id 会留下跨店脏引用。读取侧已按 shop_id 收口(Preload 带条件、system_qty 查询带 shop_id),**无信息泄露**,但脏数据会让该盘点行 system_qty 恒为 0、`Items.Product` 恒为 nil。
**建议**:对齐 stock_in.Create 的写法:
```go
if err := ensureShopRef(h.db, "warehouses", req.WarehouseID, shopID); err != nil { ... }
for i := range req.Items {
if err := ensureShopRef(h.db, "products", req.Items[i].ProductID, shopID); err != nil { ... }
}
```
### [SHOULD-104] 证书续期路径依赖营销站 webroot,8 月底到期前需确认打通
**文件**`deploy/nginx-jiu-ali.conf:107-112`(80 → 443 无条件 301)、注释第 8-9 行
**问题**:证书 2026-08-28 到期(不足两个月)。80 端口整站 `return 301`certbot HTTP-01 会跟随跳转到 443challenge 最终落在 `location /``root /opt/jiu/marketing`。两个脆弱点:① certbot `--webroot` 必须恰好指向 `/opt/jiu/marketing`;② `deploy-site.sh` 若用 `rsync --delete` 同步营销站,续期窗口内可能删掉 challenge 文件。
**建议**:在 80 的 server 块里、`return 301` 之前加一个独立 challenge 位置,让续期与营销站部署解耦:
```nginx
location ^~ /.well-known/acme-challenge/ { root /opt/jiu/acme; }
```
并在 baize 台账登记续期命令用同一 webroot。
### [SHOULD-105] 项目 CLAUDE.md 仍强制使用本轮已删除的 DataTableCard
**文件**`/Users/wangjia/code/jiu/CLAUDE.md`(「表格列头筛选」「响应式 / 移动端适配」两节)
**问题**`client/lib/widgets/data_table_card.dart` 本轮已删(连同 form_dialog / page_scaffold / search_chip / select_product_dialog / tab_state_provider,经全量 grep 确认**零残留引用**),但项目规则仍写「用 `DataTableCard` 时必须同时传 `mobileCards`」「可筛选列使用 `FilterableColumnHeader`」。规则与代码脱节,后续 agent 会照死文档施工。(`ColDef` 仍存活于 `multi_select_dropdown.dart`,该行不受影响。)
**建议**:随本次提交把这两节改写为 DS 重建后的真实套路(ds_atoms / MobileListCard 现状)。
### [SHOULD-106] 测试失败截图混在未跟踪文件里,勿随本次提交入库
**文件**`client/test/failures/tmp_date_menu_{isolatedDiff,maskedDiff,masterImage,testImage}.png`
**问题**golden 失败产物;`client/.gitignore` 只忽略 `test/golden/failures/`,没盖住 `test/failures/``git add -A` 会把 4 张调试图带进仓库(违背「截图放 tmp 不落 repo」约定)。
**建议**`client/.gitignore` 补一行 `test/failures/`,并删除这 4 个文件(用 git clean)。
### [SHOULD-107](上轮 SHOULD-005 收尾)结清后 financePartnerRowsProvider 仍陈旧
**文件**`client/lib/screens/stock_in/stock_in_list_screen.dart:1117-1119`stock_out 同构)
**问题**:本轮已补 `reload()` 刷新列表(好),但 `financePartnerRowsProvider` 是**非 autoDispose** 的 FutureProviderfinance_provider.dart:113),结清后切到财务屏仍显示结清前的应收应付,直到财务屏自己的刷新路径触发。
**建议**closeByRef 成功后追加 `ref.invalidate(financePartnerRowsProvider)`(一行)。
---
## 细节(nit
### [NIT-101] 登录历史下拉定宽 340,窄屏溢出字段甚至屏幕
**文件**`client/lib/screens/auth/login_screen.dart:202`
`context.dialogWidth(340)` 只按屏宽 92% 封顶。360dp 手机上字段实际宽约 248(440 卡 − 24 外距 − 32 内距 ×2),下拉 331 宽从字段左缘起排,右侧超出屏幕约 27px 被裁。建议宽度取锚点字段实宽(LayoutBuilder 或缓存 RenderBox size)。
### [NIT-102] `replaceFirst('AuthException: ', '')` 是死代码
**文件**`client/lib/screens/auth/register_screen.dart:136`
`AuthException.toString()` 直接返回 messageauth_repository.dart:17),该前缀永不出现。无害,删掉即可。
### [NIT-103] `_resetPassword` 的 TextEditingController 仍未 dispose(上轮 NIT-003 未修)
**文件**`client/lib/screens/settings/users_screen.dart:465`
另:该对话框用裸 Material `TextField` + `InputDecoration(hintText)`,与 DS 的 `DsInput` 不一致(check_ds_code 未拦,属风格缝隙)。
### [NIT-104] 授权锁定判定靠英文文案子串匹配
**文件**`client/lib/screens/auth/login_screen.dart:60-64`
`m.contains('license') && (lock|expire)` 与后端 `service.ErrLicenseLocked` 固定文案强耦合,后端一旦本地化/改文案即静默失效退回普通错误条。注释已写明来源,可接受;更稳做法是后端返回错误 code。
### [NIT-105] 注册页「确认密码」与「登录密码」共用一个 `_obscure`
**文件**`client/lib/screens/auth/register_screen.dart:280-297`
点密码框的眼睛会同时明文显示确认框。行为可辩护(对照确认更方便),确认是有意为之即可。
### [NIT-106] 登录/注册页脚年份硬编码「© 2026」
**文件**`client/lib/screens/auth/login_screen.dart:562`
跨年需手改。原型如此则属 1:1 还原,提请知悉。
---
## 值得肯定的地方
- **上轮反馈闭环干净**:https 回切一举解决 SHOULD-001mixed content)与 SHOULD-002——5 个 `compile-*.sh``BASE_URL` 全部回到 `https://jiu.51yanmei.com`Android `usesCleartextTraffic`、iOS/macOS `NSAllowsArbitraryLoads` 明文豁免**全部撤除**(工作树与 HEAD 一致,经 PlistBuddy/grep 逐一确认);`manual.yml``EC2_* = ALI_*` 兼容变量解决旧 tag 回滚炸 unbound variableSHOULD-003);`DsButton` disabled 态 `Opacity(0.55)`SHOULD-004)✅
- **SEC 修复无回归且加了防回归测试**:`ownership.go` 写入侧 `ensureShopRef` + 读取侧全部 `Preload("X", "shop_id = ?", shopID)` 双防线,`TestStockInHandler_CrossTenantRefs` 覆盖「跨店外键 400 + 存量脏引用不带出 PII」两条路径;`main.go` 生产模式空/默认 JWT secret 直接拒启(SEC-002);三个运维 workflow 的 DB 密码改从容器内 `$MYSQL_ROOT_PASSWORD` 取、不再走命令行(SEC-003),`DB_PASSWORD` secret 引用清零 ✅
- **登录屏生命周期总体严谨**`dispose()` 中 Timer cancel、OverlayEntry remove、3 controller + 2 FocusNode 全 dispose`ds_toast` 单例 `_entry.mounted` 守卫 + `_entry == entry` 比对避免关错新 toast;「被顶下线漏提示」的 initState 首帧补读修法正确且注释讲清了根因 ✅
- **`fillStockOutItemSnapshots` 顺带完成了出库明细 product 归属校验**(`shop_id = ? AND id IN ?` + 缺行报错),比逐行 ensureShopRef 还省往返 ✅
- **死文件清理彻底**:6 个删除文件在 lib/ 与 test/ 均无残留引用(analyze 0 error 交叉印证)✅
- **注册契约核对无误**`AuthRepository.register` 六字段与后端 `service.RegisterInput`auth.go:424-432)逐一对应,密码 min=6 与前端校验一致,与原型差异(无编号/兑换券字段、地址必填)在文件头注释里如实登记 ✅
---
## 结论
- [x] 0 处 blocking,可以提交
- [ ] should-fix 7 处:SHOULD-101/102(登录屏两行守卫,建议顺手修掉再提交)、SHOULD-106.gitignore + 删 4 张失败截图,**建议提交前处理**以免脏图入库)、SHOULD-105CLAUDE.md 同步)、SHOULD-103/104/107 可随后续迭代
- [ ] nit 6 处,不阻塞