d5b11c0943
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
97 lines
10 KiB
Markdown
97 lines
10 KiB
Markdown
# 代码审查报告 — 未提交变更集(出入库 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 error(3 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-fix,non-blocking 但建议尽快处理/确认)
|
||
|
||
### [SHOULD-001] Web 版 mixed content:https 域名入口下 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 脚本。割接前的 tag(deploy 脚本尚未支持 `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 条 SQL(months=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 处不阻塞
|