Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
10 KiB
代码审查报告 — 未提交变更集(出入库 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 按对象同一性判等
文件: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 表单空明细在提交与渲染两处都有守卫 ✅
结论
- 0 处 blocking 问题,可以合并
- 建议改进 6 处(SHOULD-001/002 建议在下次发版前确认/登记回切项;003-006 可随后续迭代处理),nit 6 处不阻塞