Admin/Client:采购价格上限按数量计算订单总价 #181

Closed
opened 2026-08-12 14:32:19 +08:00 by ila · 3 comments
Owner

基本信息

  • 类型:缺陷
  • 父级大工单:#96
  • 所属 MVP / 版本:#97 真实采购首单闭环(不支付)
  • 阶段:跨 Admin/Client 采购价格保护契约
  • 当前状态:已验收

要解决什么

数量大于 1 的采购任务把 PDD 采集单价直接保存为 max_price_cent。Client 设置数量后从确认页读取订单总价,却拿订单总价与单价上限比较,导致任务被错误拦截。

生产任务 cg6 已复现:数量 2、采集单价 ¥11.80,Admin 下发 max_price_cent=1180;Client 读取确认页 ¥25.84 后报 PURCHASE_PRICE_EXCEEDED。根因是 Admin、接口文档和 Client 对同一字段分别使用了单价/总价两种语义。

做什么 / 不做什么

  • 做:将 max_price_cent 明确定义为人民币“订单总价上限”。
  • 做:Admin 采购弹窗输入“人民币单价上限”,按固定采购数量即时展示“订单总价上限 = 单价 × 数量”。
  • 做:Admin 后端使用整数分重新计算订单总价上限并校验乘法溢出,不能信任前端展示值。
  • 做:Client 将确认页读取���额��� max_price_cent 明确按订单总价比较,错误信息写明“订单总价”。
  • 做:同步 Admin 文档、Client 接口契约和两端测试。
  • 不做:不放宽价格保护,不自动加入涨价容差,不修改采购数量或 PDD 采集价格。
  • 不做:不重写历史任务金额,不重试 cg6,不触碰付款、验证码、风控和不可逆后核单红线。
  • 不做:不增加数据库字段或迁移。

怎么做

  1. Admin 列表 View 同时生成选定 SKU 的默认单价;模板使用有明确 for 的“人民币单价上限”数字输入框,并用只读 <output aria-live="polite"> 展示数量和计算后的订单总价。
  2. 原生 JavaScript 仅用于即时反馈:弹窗打开及单价输入时用十进制字符串精确计算并刷新总价;提交字段仍只提交人工确认的单价。
  3. Handler 将单价解析为整数分;Service 新增受测的 单价分 × 数量 安全计算,在事务内从最新顺运宝上下文读取数量后生成 MaxPriceCent,防止篡改数量或绕过前端。
  4. Client 保持现有安全比较路径,但把模型注释、错误说明、详情及契约统一为订单总价语义;Admin 仍通过现有 max_price_cent 字段下发,避免引入协议字段兼容问题。
  5. 对数量 1、数量 2、人工修改单价、金额溢出、页面总价联动和 Client ���限错误补充回归测试。

预计修改:

  • admin/templates/syb/list.html
  • admin/static/js/app.js
  • admin/handler/web/others.go
  • admin/service/purchase_workflow.go
  • admin/service/syb.go
  • Admin 对应模板、Handler、Service 测试
  • client/src/purchase_task_service.py
  • Client 对应测试
  • docs/admin/01-requirements.md
  • docs/admin/03-data-model.md
  • docs/admin/04-client-api.md
  • docs/admin/05-ui-specification.md
  • docs/client/04-admin-api-contract.md

验收标准

  • 数量 2、单价上限 ¥11.80 时,弹窗明确显示订单总价上限 ¥23.60。
  • 修改单价后订单总价即时更新,字段具有可见标签和辅助说明,键盘可操作。
  • 创建任务时数据库保存 quantity=2、max_price_cent=2360;数量 1 行为不变。
  • 构造请求不能提交伪造总价或数量;后端按最新数量和单价整数分计算,溢出时拒绝创建。
  • Client 使用确认页订单总价与订单总价上限比较,错误信息不再把单价和总价混淆。
  • Admin 与 Client 契约文档对 max_price_cent 的定义一致。
  • 固定 Go 1.23.0 的 go vet ./...、go build ./...���go test ./... -count=1 通过。
  • Client 相关单元测试和语法检查通过,不执行真实下单。
  • 最新 Admin 部署后服务 active,内网与公网登录页 HTTP 200,启动日志无新增错误。

怎么验证

Set-Location admin
$env:GOTOOLCHAIN='go1.23.0'
go vet ./...
go build ./...
go test ./... -count=1

Set-Location ../client
C:/Python310/python.exe -m unittest discover -s test -p 'test_purchase_task_service.py'
C:/Python310/python.exe -m py_compile src/purchase_task_service.py

模板测试验证数量 2 × ¥11.80 显示 ¥23.60;Service 测试连接独立 _test MySQL 8 数据库验证实际落库金额。

风险和回退

  • 风险:历史任务的 max_price_cent 按旧实现保存过单价,新语义只应用于部署后新建任务。控制:部署前确认没有执行中的采购任务;不改历史任务,避免篡改执行记录。
  • 风险:只靠 JavaScript 计算可被绕过。控制:后端只接收单价,并用数据库中的最新数量重新计算总价。
  • 风险:金额乘法溢出。控制:使用整数分并在乘法前检查上界。
  • 风险:新 Admin 与旧 Client 并行。现有 Client 已把确认页金额直接与 max_price_cent 比较,新 Admin 下发总价后执行路径兼容;本任务同步更正文案和测试。
  • 回退:不改数据库结构;回退 Admin 和 Client 对应提交并���新部署上一版。已创建的新任务保留其真实总价上限,不做数据回写。
## 基本信息 - 类型:缺陷 - 父级大工单:#96 - 所属 MVP / 版本:#97 真实采购首单闭环(不支付) - 阶段:跨 Admin/Client 采购价格保护契约 - 当前状态:已验收 ## 要解决什么 数量大于 1 的采购任务把 PDD 采集单价直接保存为 `max_price_cent`。Client 设置数量后从确认页读取订单总价,却拿订单总价与单价上限比较,导致任务被错误拦截。 生产任务 `cg6` 已复现:数量 2、采集单价 ¥11.80,Admin 下发 `max_price_cent=1180`;Client 读取确认页 ¥25.84 后报 `PURCHASE_PRICE_EXCEEDED`。根因是 Admin、接口文档和 Client 对同一字段分别使用了单价/总价两种语义。 ## 做什么 / 不做什么 - 做:将 `max_price_cent` 明确定义为人民币“订单总价上限”。 - 做:Admin 采购弹窗输入“人民币单价上限”,按固定采购数量即时展示“订单总价上限 = 单价 × 数量”。 - 做:Admin 后端使用整数分重新计算订单总价上限并校验乘法溢出,不能信任前端展示值。 - 做:Client 将确认页读取���额��� `max_price_cent` 明确按订单总价比较,错误信息写明“订单总价”。 - 做:同步 Admin 文档、Client 接口契约和两端测试。 - 不做:不放宽价格保护,不自动加入涨价容差,不修改采购数量或 PDD 采集价格。 - 不做:不重写历史任务金额,不重试 `cg6`,不触碰付款、验证码、风控和不可逆后核单红线。 - 不做:不增加数据库字段或迁移。 ## 怎么做 1. Admin 列表 View 同时生成选定 SKU 的默认单价;模板使用有明确 `for` 的“人民币单价上限”数字输入框,并用只读 `<output aria-live="polite">` 展示数量和计算后的订单总价。 2. 原生 JavaScript 仅用于即时反馈:弹窗打开及单价输入时用十进制字符串精确计算并刷新总价;提交字段仍只提交人工确认的单价。 3. Handler 将单价解析为整数分;Service 新增受测的 `单价分 × 数量` 安全计算,在事务内从最新顺运宝上下文读取数量后生成 `MaxPriceCent`,防止篡改数量或绕过前端。 4. Client 保持现有安全比较路径,但把模型注释、错误说明、详情及契约统一为订单总价语义;Admin 仍通过现有 `max_price_cent` 字段下发,避免引入协议字段兼容问题。 5. 对数量 1、数量 2、人工修改单价、金额溢出、页面总价联动和 Client ���限错误补充回归测试。 预计修改: - `admin/templates/syb/list.html` - `admin/static/js/app.js` - `admin/handler/web/others.go` - `admin/service/purchase_workflow.go` - `admin/service/syb.go` - Admin 对应模板、Handler、Service 测试 - `client/src/purchase_task_service.py` - Client 对应测试 - `docs/admin/01-requirements.md` - `docs/admin/03-data-model.md` - `docs/admin/04-client-api.md` - `docs/admin/05-ui-specification.md` - `docs/client/04-admin-api-contract.md` ## 验收标准 - [x] 数量 2、单价上限 ¥11.80 时,弹窗明确显示订单总价上限 ¥23.60。 - [x] 修改单价后订单总价即时更新,字段具有可见标签和辅助说明,键盘可操作。 - [x] 创建任务时数据库保存 `quantity=2`、`max_price_cent=2360`;数量 1 行为不变。 - [x] 构造请求不能提交伪造总价或数量;后端按最新数量和单价整数分计算,溢出时拒绝创建。 - [x] Client 使用确认页订单总价与订单总价上限比较,错误信息不再把单价和总价混淆。 - [x] Admin 与 Client 契约文档对 `max_price_cent` 的定义一致。 - [x] 固定 Go 1.23.0 的 `go vet ./...`、`go build ./...`���`go test ./... -count=1` 通过。 - [x] Client 相关单元测试和语法检查通过,不执行真实下单。 - [x] 最新 Admin 部署后服务 active,内网与公网登录页 HTTP 200,启动日志无新增错误。 ## 怎么验证 ```powershell Set-Location admin $env:GOTOOLCHAIN='go1.23.0' go vet ./... go build ./... go test ./... -count=1 Set-Location ../client C:/Python310/python.exe -m unittest discover -s test -p 'test_purchase_task_service.py' C:/Python310/python.exe -m py_compile src/purchase_task_service.py ``` 模板测试验证数量 2 × ¥11.80 显示 ¥23.60;Service 测试连接独立 `_test` MySQL 8 数据库验证实际落库金额。 ## 风险和回退 - 风险:历史任务的 `max_price_cent` 按旧实现保存过单价,新语义只应用于部署后新建任务。控制:部署前确认没有执行中的采购任务;不改历史任务,避免篡改执行记录。 - 风险:只靠 JavaScript 计算可被绕过。控制:后端只接收单价,并用数据库中的最新数量重新计算总价。 - 风险:金额乘法溢出。控制:使用整数分并在乘法前检查上界。 - 风险:新 Admin 与旧 Client 并行。现有 Client 已把确认页金额直接与 `max_price_cent` 比较,新 Admin 下发总价后执行路径兼容;本任务同步更正文案和测试。 - 回退:不改数据库结构;回退 Admin 和 Client 对应提交并���新部署上一版。已创建的新任务保留其真实总价上限,不做数据回写。
Author
Owner

实现与验证已完成,进入生产部署:

  • 实现提交:253be7c
  • 归档提交:aa6d812
  • 归档:docs/task/181-采购价格上限按数量计算总价.md
  • Admin:数量 2 的任务按单价上限乘数量写入订单总价上限;整数分计算并检查溢出;模板显示可访问的即时总价。
  • Client:只在最终确认页按订单总价执行上限比较,未放宽价格保护。
  • 验证:临时 MySQL 8 实库目标测试通过;Go 1.23 vet/build/全量测试通过;Client 456 个测试和修改文件语法检查通过;未执行真机下单。

生产部署和健康检查完成后继续回写,工单保持待验收。

实现与验证已完成,进入生产部署: - 实现提交:`253be7c` - 归档提交:`aa6d812` - 归档:`docs/task/181-采购价格上限按数量计算总价.md` - Admin:数量 2 的任务按单价上限乘数量写入订单总价上限;整数分计算并检查溢出;模板显示可访问的即时总价。 - Client:只在最终确认页按订单总价执行上限比较,未放宽价格保护。 - 验证:临时 MySQL 8 实库目标测试通过;Go 1.23 vet/build/全量测试通过;Client 456 个测试和修改文件语法检查通过;未执行真机下单。 生产部署和健康检查完成后继续回写,工单保持待验收。
Author
Owner

生产部署完成,工单状态已更新为“待验收”:

  • 线上版本:aa6d812(功能提交 253be7c)
  • 二进制 SHA-256:294a1efe718110a407b49cfeb2ae3701909872037a19648605dc3d5223074220
  • 发布前再次确认执行中采购任务为 0;没有修改 cg6 或其他历史任务
  • 数据库保持 schema v13,本任务无迁移
  • 旁路启动检查 200;切换后 systemd active,服务器本机登录页 200,公网登录页 200,开发机访问公网 200
  • 新启动日志中的 panic、fatal、迁移失败、数据库连接失败和进程失败计数为 0
  • 发布前备份:/opt/cmautobuy/backups/autobuy-before-aa6d812-20260812T064556Z.sql.gz,已通过 gzip 完整性检查
  • 上一版 354aa6b 保留,可直接回退

请按工单验收:使用数量大于 1 的可采购顺运宝明细打开采购弹窗,确认单价、数量和订单总价显示;如创建业务任务,请使用实际允许的价格上限。实际总价高于计算上限时仍会按安全规则停止。

生产部署完成,工单状态已更新为“待验收”: - 线上版本:`aa6d812`(功能提交 `253be7c`) - 二进制 SHA-256:`294a1efe718110a407b49cfeb2ae3701909872037a19648605dc3d5223074220` - 发布前再次确认执行中采购任务为 0;没有修改 `cg6` 或其他历史任务 - 数据库保持 schema v13,本任务无迁移 - 旁路启动检查 200;切换后 systemd `active`,服务器本机登录页 200,公网登录页 200,开发机访问公网 200 - 新启动日志中的 panic、fatal、迁移失败、数据库连接失败和进程失败计数为 0 - 发布前备份:`/opt/cmautobuy/backups/autobuy-before-aa6d812-20260812T064556Z.sql.gz`,已通过 gzip 完整性检查 - 上一版 `354aa6b` 保留,可直接回退 请按工单验收:使用数量大于 1 的可采购顺运宝明细打开采购弹窗,确认单价、数量和订单总价显示;如创建业务任务,请使用实际允许的价格上限。实际总价高于计算上限时仍会按安全规则停止。
Author
Owner

用户于 2026-08-13 明确验收通过。工单已完成归档,现关闭工单。

用户于 2026-08-13 明确验收通过。工单已完成归档,现关闭工单。
ila closed this issue 2026-08-13 15:56:03 +08:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: chengma/cmautobuy#181