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

97 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.
# 代码审查报告 — 未提交变更集(出入库 rolling30 + DS 重构 + CI ali 单轨化)
**审查时间**2026-07-03
**变更文件**200 个文件,+10283 行,−9665 行(含 staged 删除 `client/lib/widgets/data_table_card.dart`
**验证结果**`go build ./...` / `go vet ./...` / `go test ./...` 全部通过;`flutter analyze` 0 error3 warning 均在 test/96 info
## 总体评价
变更集质量整体很高。后端 rolling30 窗口边界数学正确、窗口相接不重叠、有测试覆盖,新增查询多租户 `shop_id` 条件齐全;客户端大规模 DS 重构后,我逐一核对了全部 40+ 处确认弹窗的「按钮标签 ↔ onPressed ↔ pop 值」语义,未发现批量正则转换导致的错位;CI ali 单轨化的 `DEPLOY_*` 变量在 workflows / lib-forgejo.sh / deploy-*.sh / backup-db.sh 之间自洽。无阻塞合并的问题;「裸 IP 明文 HTTP」是用户拍板的备案过渡方案,但有两条具体的破坏路径需要确认(见 SHOULD-001/002)。
---
## 必须修复(blocking
无。
---
## 建议改进(should-fixnon-blocking 但建议尽快处理/确认)
### [SHOULD-001] Web 版 mixed contenthttps 域名入口下 API 全被浏览器拦截
**文件**`scripts/ci/compile-client-web.sh:20-21`(同模式见 `compile-android.sh:44``compile-ios.sh:106``compile-macos.sh:33``compile-windows.sh:43``scripts/local_test.sh:33`
**问题**Web 构建的 `BASE_URL` 硬编码为 `http://182.92.213.171:8443``deploy-client.yml:183-184` 注释说明 EC2 仍保留 `jiu.51yanmei.com` 反代桥接到 ali——若任何用户仍从 `https://jiu.51yanmei.com/app` 打开 Web 版,页面是 https 源、XHR 目标是 http,浏览器会以 active mixed content 直接拦截所有 API 请求,登录静默失败(native 桌面/移动端无此策略,不受影响)。
**建议**:三选一——① Web 构建 `BASE_URL` 留空走同源相对路径(`AppConfig` 里 Web 分支取 `Uri.base`),天然兼容 http/https 两个入口;② 确认 EC2 桥已把 `/app` 路径 301 到 `http://182.92.213.171:8443/app`;③ 明确弃用域名 Web 入口并在 baize 台账登记。
### [SHOULD-002] 明文 HTTP 承载登录口令与 JWT(过渡期已知风险,建议登记回切项)
**文件**`scripts/ci/compile-*.sh`(同上)、`client/android/app/src/main/AndroidManifest.xml:11``client/ios/Runner/Info.plist:11``client/macos/Runner/Info.plist:37`
**问题**:备案过渡期所有客户端流量(含 `/auth/login` 的明文密码、后续每个请求的 JWT)走公网明文 HTTP;三端又同时开了全局明文豁免(Android `usesCleartextTraffic="true"`、iOS/macOS `NSAllowsArbitraryLoads=true`)。注释里已写「备案后应移除」,但散落在 6 个 compile 脚本 + 3 个平台清单共 9 处。另注意:iOS 带 `NSAllowsArbitraryLoads=true` 提交 App Store/TestFlight 可能被要求填写豁免理由。
**建议**:用 todo skill 登记一条「备案通过后回切 https」任务,列全 9 处文件清单,避免遗漏;iOS 可考虑收窄为 `NSExceptionDomains` 只豁免该 IP。
### [SHOULD-003] manual.yml 回滚到割接前的旧 tag 会失败
**文件**`.gitea/workflows/manual.yml:22-24, 31-35`
**问题**manual 回滚 `checkout ref: inputs.version` 后执行**旧 tag 里的** deploy 脚本。割接前的 tagdeploy 脚本尚未支持 `DEPLOY_*` 回退写法、直接引用 `${EC2_HOST}`)在 `set -u` 下会因 unbound variable 直接失败——而回滚恰恰最可能选旧版本。
**建议**:要么在 manual.yml env 里同时兼容传一份 `EC2_* = ALI_*`(让旧脚本也指向 ali),要么在 workflow 注释里写明「最早可回滚 tag」的下限。
### [SHOULD-004] DsButton 无 disabled 视觉态,加载中按钮看起来仍可点
**文件**`client/lib/widgets/ds/ds_atoms.dart:47``InkWell(onTap: onPressed)`
**问题**`onPressed: null` 时(如 `about_screen.dart:758` 提交反馈、`stock_in_form_screen.dart` 暂存/提交的 `_submitting ? null : ...`)按钮仅失去点击响应,颜色/边框与可用态完全一致,用户无从感知「提交中」。旧 Material 按钮自带 disabled 置灰,本次批量替换后此反馈丢失。
**建议**:在 `DsButton.build` 里对 `onPressed == null``Opacity(0.5)` 或 fg/bg 换 `t.muted`/`t.bg`(对齐原型 `.btn:disabled` 若有定义;没有则先在原型 atoms.css 登记再实现,遵守设计系统单一真源)。
### [SHOULD-005] 结清成功后不刷新列表/财务数据
**文件**`client/lib/screens/stock_in/stock_in_list_screen.dart:1101-1135``client/lib/screens/stock_out/stock_out_list_screen.dart`(同构 `_confirmSettle`
**问题**`closeByRef` 成功后只弹 snackbar,既不 `reload()` 列表也不 `invalidate` 财务相关 provider。若详情抽屉/行内展示了应收应付状态徽章,会显示过期状态;财务屏的 `financePartnerRowsProvider` 非 autoDispose 时也会陈旧(`finance_screen.dart:105` 有手动 invalidate,但只在财务屏自己的刷新路径里)。
**建议**:结清成功后补 `ref.read(stockXxListProvider.notifier).reload()``ref.invalidate(financePartnerRowsProvider)`
### [SHOULD-006] finance Trend 循环发 2N 条 SQLmonths=24 时 48 条)
**文件**`backend/internal/handler/finance.go:198-215`
**问题**:每个月对 `stock_in_orders`/`stock_out_orders` 各发一条 SUM 查询,`months` 上限 24 → 单请求最多 48 次往返。数据量小时无碍,但属于典型可合并聚合。
**建议**:各表一条 `GROUP BY strftime/DATE_FORMAT(order_date, '%Y-%m')`(或按窗口一条 SQL 取回后内存分桶),把 2N 降到 2。注意保持 MySQL/SQLite 双方言兼容(现有代码用日期串比较正是为此,分桶在 Go 内存里做最稳)。
---
## 细节(nit
### [NIT-001] 出库列表头部计数回退值口径混用
**文件**`client/lib/screens/stock_out/stock_out_list_screen.dart:466`
`'近30天 ${...summary30...monthCount ?? total}'`——summary 未加载时回退到 `total`(当前筛选下的分页总数,非 30 天口径),文案与数值短暂不符。入库侧 `stock_in_list_screen.dart:426` 同构,建议未加载时显示 `—`
### [NIT-002] DsSelect<Warehouse> 按对象同一性判等
**文件**`client/lib/screens/inventory/inventory_check_screen.dart:227`
`o.$1 == value` 依赖实例同一性(`Warehouse` 未重写 `==`)。仓库列表 provider 因换店/网络恢复重建后,`_selectedWarehouse` 指向旧实例,选中标签会回落成「请选择仓库」。建议 `DsSelect<int>``warehouse.id` 做 value,或给模型加 `==`/`hashCode`
### [NIT-003] `_resetPassword` 的 TextEditingController 未 dispose
**文件**`client/lib/screens/settings/users_screen.dart:461`
对话框关闭后 `ctrl` 泄漏(对照 `stock_in_list_screen.dart:1084``_confirmCost` 是有 dispose 的)。低危但建议补齐。
### [NIT-004] finance Summary 的 GROUP BY 依赖 MySQL 函数依赖推导
**文件**`backend/internal/handler/finance.go:233-247`
`SELECT COALESCE(p.name,'')``GROUP BY f.partner_id, f.type`。MySQL 5.7+ 的 `ONLY_FULL_GROUP_BY` 能识别 `p.id = f.partner_id` 的函数依赖所以现在能跑,但把 `p.name` 显式加进 GROUP BY 更防御(不同版本/方言行为差异)。
### [NIT-005] deploy 脚本头注释仍写 EC2 为默认目标
**文件**`scripts/ci/deploy-client.sh:5``deploy-site.sh:4``deploy-server.sh:21``DEPLOY_TARGET:-ec2` 默认值)
割接后 EC2 轨已从 workflows 移除,但脚本注释与 `DEPLOY_TARGET` 默认值仍是 ec2。行为不受影响(workflow 显式传 ali),属文档漂移;下次动这些脚本时顺手更正即可。
### [NIT-006] 确认进价对话框:全部留空点「确认」无任何反馈
**文件**`client/lib/screens/stock_in/stock_in_list_screen.dart:1087`
`ok == true``items` 为空时静默 return,用户不知道为什么没生效。建议 snackbar 提示「未填写任何进价」。
---
## 值得肯定的地方
- **rolling30 窗口边界正确**`summaryBounds``backend/internal/handler/stock_in.go:170-180`)当前窗 `[今-29d, 明)` 与对照窗 `[今-59d, 今-29d)` 各 30 天、首尾相接不重叠;默认分支与原 `monthBounds` 行为完全等价;`stock_in_test.go:167-179` 补了 -40 天单据落对照窗的边界测试 ✅
- **多租户无缺口**:本次触及的全部查询(stock_in/out Summary 的 agg 与 pending 计数、finance Trend 的 `sum`、finance Summary 的 Raw SQL、inventory summary 月初快照)均带 `shop_id = ?``shopID` 一律取自 `middleware.GetShopID(c)`,无一处从请求参数读 ✅
- **批量转换零错位**:逐一核对 40+ 处 `DsButton` 弹窗动作——取消一律 pop(false/null/void)、确认语义键一律 pop(true)`_detailActionGroups` 中标签与 `_confirmDelete/Submit/Approve/Reject/Withdraw/Return/Settle` 的映射全部对应正确 ✅
- **除零防御到位**`_momDelta`inventory_list_screen.dart:474)、`StockSummary.countDeltaPct/amountDeltaPct`stock_summary.dart:27-32)、`DsBarChart``max<=0→1`ds_bar_chart.dart:41)均有保护 ✅
- **口径拆分清晰**`stockXxSummaryProvider`(自然月,财务屏用)与 `stockXxSummary30Provider`rolling30,列表 KPI 用)并存,避免财务屏被列表口径变更波及,注释写明了用户拍板缘由 ✅
- **CI ali 单轨化自洽**`DEPLOY_SSH_KEY/HOST/USER``lib-forgejo.sh:114-115`、三个 deploy 脚本、`backup-db.sh:20``${DEPLOY_*:-$EC2_*}` 回退写法一致;backup/manual/deploy-client/site/server 五个 workflow 均已切 `ALI_*` secrets ✅
- **reload 保留旧数据**inventory_provider.dart:105 `copyWithPrevious`)消除筛选切换白屏;`WheelDatePanel` 起止倒置时自动交换(wheel_date_picker.dart:285-287);stock_out 表单空明细在提交与渲染两处都有守卫 ✅
---
## 结论
- [x] 0 处 blocking 问题,可以合并
- [ ] 建议改进 6 处(SHOULD-001/002 建议在下次发版前确认/登记回切项;003-006 可随后续迭代处理),nit 6 处不阻塞