From 14231c62a75789613fbada2c309f0b147f9a2618 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Tue, 29 Sep 2026 10:24:54 +0800 Subject: [PATCH] docs: record batch return use contract and verification #349 --- docs/02-architecture-and-code-map.md | 13 +++++++++++-- docs/03-business-rules-and-glossary.md | 14 ++++++++++++-- docs/08-agent-api-contract.md | 13 +++++++++++-- web/tests/e2e/syb-return-confirm.spec.ts | 3 +++ 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/docs/02-architecture-and-code-map.md b/docs/02-architecture-and-code-map.md index d76ae51..6289c66 100644 --- a/docs/02-architecture-and-code-map.md +++ b/docs/02-architecture-and-code-map.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Architecture-and-Code-Map wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Architecture-and-Code-Map.- -wiki_revision: 0e0ad0b3312c524d90e063167dd2e945fb22b0e9 -synchronized_at: 2026-09-29T01:36:01Z +wiki_revision: 9b4a1a06b8e2f6a8dfdbaef87817d840dfa45389 +synchronized_at: 2026-09-29T02:23:54Z @@ -561,3 +561,12 @@ Web 唯一展示位置为“采集采购 → SYB 同步记录”:列表状态 - Web `syb-products/return-cancel.js` 与列表组件复用单条取消接口;冻结当前页勾选的 SYB 商品 ID 与原匹配 ID,一次确认后串行请求,独立汇总成功、跳过及失败。取消结果不会驱动已有采购或采集任务。 - `returnmatch.Service.Cancel` 在锁定原匹配记录的事务中校验可选状态前提,再释放有效占用指针并写取消审计;事务内审计失败则整体回滚。无额外数据库迁移、权限、批量后台任务或外部写入。 - 元数据加载失败时禁止批量取消;执行时互斥匹配、确认、取消。网络不确定时只读回原匹配,不自动重复写入;登录失效停止后续请求。列表刷新与失败项勾选恢复受查询 generation 约束,不跨页、不覆盖较新的筛选结果。 + +## SYB 当前页批量使用退货(#349) + +实现绑定 4280115(2026-09-29),feat/349-batch-return-use;以 #347 a4c7b5e 为基线,未合并 main、未发布或真实业务验收。 + +- Web syb-products/index.vue 新增“使用退货 N”,紧跟“取消匹配”;return-confirm.js 冻结当前页原 SYB / match 身份,去重后一次确认、串行复用既有 confirm API,结果独立汇总。return-cancel.js 和单条操作接入使用期间互斥,不重构取消逻辑。 +- return-matches.js 的 confirmReturnMatch 增加可选客户端请求选项,仅用于批量汇总时抑制逐项错误弹窗;无新增 HTTP 字段。Server 生产代码、确认锁内 matched 检查、ConfirmedBy/ConfirmedAt、审计和占用指针不变,无迁移或权限变化。 +- 未知写结果只读回原 match,回读确认不宣称为本次成功;登录失效停止余项。刷新保持筛选与页码,失败选择恢复受 generation、当前页及原 matched 身份约束。未调用 Yeeke 发货或修改采购任务。 +- 回归入口:Web tests/unit/syb-return-confirm.test.cjs、tests/e2e/syb-return-confirm.spec.ts;Server returnmatch/confirm_batch_test.go。测试使用合成数据和隔离 SQLite,不代表生产 MySQL 并发或真实退货验证。 diff --git a/docs/03-business-rules-and-glossary.md b/docs/03-business-rules-and-glossary.md index 4aa9fb6..c1c0505 100644 --- a/docs/03-business-rules-and-glossary.md +++ b/docs/03-business-rules-and-glossary.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Business-Rules-and-Glossary wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Business-Rules-and-Glossary.- -wiki_revision: e95c53cec27080e073e6f7a4aaed46d5421dddf5 -synchronized_at: 2026-09-29T01:36:04Z +wiki_revision: 9a7317728132c450c7abf244c3e5eaac7bfb71bc +synchronized_at: 2026-09-29T02:23:57Z @@ -722,3 +722,13 @@ SYB 回填与资料完整性独立:正式SYB任务已有单号且仍关联有 - 一次确认后按原匹配编号逐条取消。并发确认、已经取消等状态冲突跳过;其他错误保留失败。网络异常先回读原记录,不盲目重试,不追随后来重新匹配的新记录;登录失效停止剩余写入。 - 成功后重新计算采购准备状态,并按当前退货状态与同步可用性判断退货池资格,不保证所有商品立即可采购,不改变既有采购或采集任务。保持查询条件刷新;仅保留仍在当前页、仍为原匹配且可取消的失败项勾选,其余失败项保留在结果表。 - 原有单条确认及取消兼容:未携带状态前提的单条取消仍允许取消已确认匹配;本次不新增权限或迁移,不自动创建采购或执行付款。 + +## SYB 当前页批量使用退货(#349) + +实现绑定 4280115(2026-09-29),尚未合并/发布,待验收。 + +- 管理员、采购员及售后沿用既有权限,工具栏为“匹配退货 → 取消匹配 → 使用退货”。仅当前页勾选且处理阶段 return_pending、原有效匹配 matched 的明细计入使用数量;已用退货与未匹配行不参与。 +- 一次确认后逐条标记 confirmed / 已用退货,保留有效占用;不是 Yeeke 发货或重出,不修改已有采购/采集任务,不自动创建采购或支付,不扩大原候选资格。 +- 只操作冻结的原 match ID。并发确认或取消冲突跳过,不追随后来的替代匹配。其他错误失败,各项不互相回滚;登录失效停止后续写入并明确未执行。 +- 网络结果未知先回读原记录:已确认/已取消显示回读状态而不是本次成功;仍为原 matched 可人工重试;回读失败保留不确定结果,不自动重复提交。 +- 完成保持筛选、页码和结果明细。成功项在退货待确认筛选下移出;仅当前页仍为原 matched 的可重试失败项恢复勾选,离页结果不丢失、不跨页操作。操作中互斥匹配、使用、取消及单条操作,无新增审批或权限。 diff --git a/docs/08-agent-api-contract.md b/docs/08-agent-api-contract.md index ec3818a..c10bab3 100644 --- a/docs/08-agent-api-contract.md +++ b/docs/08-agent-api-contract.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Android-Agent-API-Contract wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Android-Agent-API-Contract.- -wiki_revision: fff074b921368be646cda1bf0a434608fc5082c8 -synchronized_at: 2026-09-29T01:36:20Z +wiki_revision: 102d40fb2d960b05684e2a82e85729362070fdb5 +synchronized_at: 2026-09-29T02:24:16Z @@ -1408,3 +1408,12 @@ GET /api/admin/v1/purchase-tasks 的既有 status 参数新增查询专用值 sy 携带前提时,服务端在锁定 `:id` 原记录的事务中检查状态仍为 `matched` 且有效占用指针存在,否则 HTTP 409 / `STATE_CONFLICT`,不取消已确认或已取消的匹配。不存在返回 HTTP 404 / `NOT_FOUND`。成功仍为 `{code:200,data:{item:ReturnMatch}}`,释放占用并记录 CancelledBy / CancelledAt 和审计日志。权限沿用 admin / purchaser / after_sales,没有新增批量接口或权限点。 Web 批量入口冻结原匹配 ID 并顺序调用;HTTP 409 状态冲突跳过,权限/网络错误失败,HTTP 401 或登录失效停止后续操作。写入结果未知时只允许只读回读原记录,不自动重发取消;不以 SYB ID 重新查询替代匹配并取消。 + +## Admin 批量使用退货复用确认接口(#349) + +实现绑定 4280115(2026-09-29),未合并/发布;仅补充管理端调用规则,Android Agent 契约不变。 + +- Web 当前页批量使用复用 POST /api/admin/v1/return-matches/:id/confirm,无新增请求体、批量接口、状态或权限。Server 沿用锁定原记录后仅接受 matched;成功为 {code:200,data:{item:ReturnMatch}},写 confirmed、确认人/时间及审计,保留 active 占用。 +- HTTP 409 / STATE_CONFLICT 跳过,HTTP 404 / NOT_FOUND 或其他处理错误明确失败。沿用 admin / purchaser / after_sales 权限;HTTP 401、业务认证错误及旧拦截器 false 结果均停止余项,不能误计为成功。 +- 写结果不明确时仅 GET /api/admin/v1/return-matches/:id 回读冻结原记录,验证 match 身份;已 confirmed/cancelled 显示回读已处理,仍 matched 才允许手动重试。不得自动重发 POST 或改查新匹配后使用。 +- 无数据库、Server 生产行为、Agent 协议或外部 Yeeke 写入变化。真实业务确认及部署另需授权。 diff --git a/web/tests/e2e/syb-return-confirm.spec.ts b/web/tests/e2e/syb-return-confirm.spec.ts index 7267d73..1d0d9ea 100644 --- a/web/tests/e2e/syb-return-confirm.spec.ts +++ b/web/tests/e2e/syb-return-confirm.spec.ts @@ -64,6 +64,7 @@ test('v2 button order, one confirmation, no write on return, progress and mutual await expect(page.getByText('成功 2 条', { exact: true })).toBeVisible() await expect(page.getByText('跳过 1 条', { exact: true })).toBeVisible() await page.getByRole('button', { name: '返回列表', exact: true }).click() + await expect(page.getByRole('dialog', { name: '批量使用退货结果', exact: true })).toBeHidden() await expect(page.getByText('已选择 0 条', { exact: true })).toBeVisible() }) @@ -78,6 +79,7 @@ for (const mode of ['partial', 'conflict', 'auth', 'readback-confirmed']) { const summary = mode === 'auth' ? '失败 2 条' : mode === 'partial' ? '失败 1 条' : mode === 'conflict' ? '跳过 2 条' : '跳过 3 条' await expect(page.getByText(summary, { exact: true })).toBeVisible() await page.getByRole('button', { name: '返回列表', exact: true }).click() + await expect(page.getByRole('dialog', { name: '批量使用退货结果', exact: true })).toBeHidden() if (mode === 'partial') { await expect(page.getByText('已选择 1 条', { exact: true })).toBeVisible() await expect(page.getByRole('button', { name: /使用退货\s*1/ })).toBeEnabled() @@ -110,5 +112,6 @@ test('pending filter and page size retained; used rows disappear', async ({ page expect(lists.at(-1)?.searchParams.get('processStage')).toBe('return_pending') expect(lists.at(-1)?.searchParams.get('pageSize')).toBe('200') await page.getByRole('button', { name: '返回列表', exact: true }).click() + await expect(page.getByRole('dialog', { name: '批量使用退货结果', exact: true })).toBeHidden() await expect(page.getByText('暂无 SYB 商品明细', { exact: true })).toBeVisible() })