From ace466ff7f2b1e8b6dec964d848b8d76a8551b21 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Wed, 22 Jul 2026 16:22:58 +0800 Subject: [PATCH 001/118] =?UTF-8?q?docs:=20=E5=AE=8C=E5=96=84=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E9=9C=80=E6=B1=82=E3=80=81=E6=9E=B6=E6=9E=84=E4=B8=8E?= =?UTF-8?q?=E5=8D=8F=E4=BD=9C=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 25 +- ...14\346\224\266\346\240\207\345\207\206.md" | 24 +- ...74\350\257\264\346\230\216\344\271\246.md" | 447 +++++++++++-- ...17\344\275\234\346\265\201\347\250\213.md" | 597 ++++++++++++++++++ ...66\346\236\204\350\256\276\350\256\241.md" | 445 ++++++++++++- 5 files changed, 1443 insertions(+), 95 deletions(-) create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/Git\345\233\242\351\230\237\345\215\217\344\275\234\346\265\201\347\250\213.md" diff --git a/README.md b/README.md index 34626af..23084f2 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,22 @@ > 分工规则详见 `docs/00-项目要求/项目要求.md` 第三节,模块难度权重与考核挂钩。 > 4 人组时,测试与部署职责分摊到各模块负责人,选做功能由认领者负责。 -## 三、技术栈(各组自选,确定后填写) +## 三、技术栈(本组已确认) | 分层 | 技术选型 | 版本 | |------|----------|------| -| 前端 | | | -| 后端 | | | -| 数据库 | | | -| 部署 | | | +| 前端 | Vue 3、TypeScript、Vite、Pinia、Axios | 依赖创建时锁定 | +| 前端测试 | Vitest、Playwright | 依赖创建时锁定 | +| 后端 | .NET 10、ASP.NET Core Web API | .NET 10 | +| 接口契约与文档 | OpenAPI、Swagger | 依赖创建时锁定 | +| 数据访问 | EF Core 10、Npgsql | EF Core 10 | +| 数据库 | PostgreSQL | 镜像创建时锁定 | +| 架构 | 核心领域使用 DDD、Clean Architecture、简化 CQRS | — | +| 事件与可靠性 | 领域事件、RabbitMQ 集成事件、Outbox | — | +| 分布式组件 | Redis、RabbitMQ、Worker Service、Aspire | 依赖创建时锁定 | +| 部署 | Docker Compose、Nginx | 镜像创建时锁定 | +| 对象存储 | S3 Compatible Object Storage;开发环境使用 SeaweedFS | 镜像创建时锁定 | +| 日志与可观测性 | Serilog、OpenTelemetry | 依赖创建时锁定 | ## 四、仓库目录结构 @@ -59,11 +67,14 @@ ## 六、Git 协作规范 -1. 主分支 `main` 只存放可运行代码,禁止直接 push。 -2. 功能开发使用 `feature/功能名` 分支,完成后经组长 Code Review 合并。 +1. `master` 是稳定发布分支,`dev` 是日常集成分支;两个长期分支均禁止直接 push。 +2. 采用短生命周期任务分支:功能开发使用 `feature/<模块>-<任务>`,从最新 `dev` 创建,通过 PR 合入 `dev`,合并后立即删除。 3. 提交信息格式:`: <描述>`,type 取值:`feat` `fix` `refactor` `docs` `test` `chore`。 4. **每人每天至少一次有效提交**,提交记录将作为个人考核依据。 5. **交叉 Code Review**:每人的功能分支由相邻模块负责人审查后方可合并(审查人在合并说明中留名)。 +6. 阶段验收或发布时,由 `dev` 向 `master` 创建发布 PR,通过完整验证和审查后合并。 + +完整流程见 [`docs/02-设计文档/Git团队协作流程.md`](docs/02-设计文档/Git团队协作流程.md)。 ## 七、如何开始 diff --git "a/docs/00-\351\241\271\347\233\256\350\246\201\346\261\202/\351\252\214\346\224\266\346\240\207\345\207\206.md" "b/docs/00-\351\241\271\347\233\256\350\246\201\346\261\202/\351\252\214\346\224\266\346\240\207\345\207\206.md" index 7e81a74..2d8b2d4 100644 --- "a/docs/00-\351\241\271\347\233\256\350\246\201\346\261\202/\351\252\214\346\224\266\346\240\207\345\207\206.md" +++ "b/docs/00-\351\241\271\347\233\256\350\246\201\346\261\202/\351\252\214\346\224\266\346\240\207\345\207\206.md" @@ -30,21 +30,31 @@ | F12 | 后台订单管理 | 发货处理、状态管理正常 | ☐ | | F13 | 后台用户管理 | 禁用用户后无法登录 | ☐ | -### 选做功能(至少 2 项) +### 选做功能(至少 2 项,本组选择 4 项) -| 编号 | 功能名称 | 结果 | -|------|----------|------| -| X01 | | ☐ | -| X02 | | ☐ | +| 编号 | 功能名称 | 验收要点 | 结果 | +|------|----------|----------|------| +| X01 | 商品评价与晒图 | 已完成订单项可提交评分、文字和图片;拦截越权评价与重复评价 | ☐ | +| X02 | 商品收藏、浏览历史 | 可收藏、取消收藏和查看收藏列表;浏览历史按最近时间展示,数据按用户隔离 | ☐ | +| X03 | 站内消息通知 | 订单、支付、发货和售后消息可查看;支持未读数与标记已读,断线后持久化消息仍可查询 | ☐ | +| X04 | 售后流程 | 买家可申请退款/退货,后台可审核并推进状态;拦截越权、超额和重复申请 | ☐ | -### 挑战模块(至少 1 项,现场边界演示/压测 + 原理抽问) +### 挑战模块(至少 1 项,本组选择 7 项;现场边界演示/压测 + 原理抽问) | 编号 | 模块名称 | 验收方式 | 结果 | |------|----------|----------|------| -| C__ | | 边界演示/压测内容: | ☐ | +| C01 | 秒杀与防超卖 | 以 10 件库存模拟 100 并发下单,订单成功数与库存一致且不超卖、不少卖;说明事务、条件更新或锁方案及其取舍 | ☐ | +| C03 | 订单超时自动取消 | 订单未支付满 30 分钟后自动取消并回补库存;演示支付与取消并发竞争,说明扫描/延迟任务、幂等和事务机制 | ☐ | +| C04 | 商品搜索进阶 | 支持中文分词模糊搜索、多条件筛选和排序;同一数据集与 `LIKE/ILIKE` 查询进行性能对比,说明索引与更新机制 | ☐ | +| C06 | 实时消息推送 | 使用 WebSocket/SignalR 推送订单或售后状态;演示断线重连、多标签页和多实例场景,持久化消息可补查 | ☐ | +| C07 | 缓存与性能优化 | 使用 Redis 缓存首页或商品详情;改价、上下架后在约定时限内一致,提供优化前后压测对比 | ☐ | +| C08 | 支付回调幂等与对账 | 重复、乱序回调不造成重复支付或错误状态;每日对账能发现“支付成功但订单未更新”,说明唯一约束、事务及可靠消息机制 | ☐ | +| C10 | 容器化部署与负载均衡 | Docker Compose 一键启动全套系统,Nginx 负载均衡至少 2 个后端实例;演示请求分发、单实例停止后服务可用及登录态有效 | ☐ | > 挑战模块验收要点以《项目要求》中对应编号的"要求与验收要点"为准;只有功能演示、无法通过边界场景验证或讲不清原理的,视为不通过。 +> 本组内部目标为上述 4 项选做功能和 7 项挑战模块全部完成,并逐项留存演示脚本、原始结果和答辩说明;项目总体验收最低要求仍以第五节为准。 + ## 三、非功能验收项 | 编号 | 验收项 | 标准 | diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 25f3362..a29c982 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -1,90 +1,445 @@ # 电子商城需求规格说明书 -> 组别:____ 编写人:____ 编写日期:____ 版本:v1.0 +> 组别:待填写 编写人:罗皓晨 编写日期:2026-07-22 版本:v0.1 +> +> 文档状态:已确认四类用户角色、选做与挑战范围,待补充成员姓名并完成指导教师评审 > 截止:第 1 周周三提交初稿 ## 修订记录 | 版本 | 日期 | 修改人 | 修改说明 | -|------|------|--------|----------| -| v1.0 | | | 初稿 | +|---|---|---|---| +| v0.1 | 2026-07-22 | 罗皓晨 | 形成需求规格初稿,明确四类角色、必做功能、4 项选做、7 项挑战、验收口径和六人后端模块边界 | ## 一、引言 ### 1.1 编写目的 -(说明本文档的读者对象和用途) + +本文档用于统一电子商城项目的业务范围、角色权限、功能流程、异常规则和验收标准,是后续系统架构、数据库、接口、开发、测试和答辩的需求基线。主要读者包括项目组成员、指导教师、测试人员和验收人员。 ### 1.2 项目背景 -(描述项目背景、要解决的问题) -### 1.3 术语定义 +团队实际由 6 人组成,计划在 4 周内完成一个前后端分离、可演示、可部署的 B2C 电子商城。系统包含购物端、商家端和管理端,覆盖用户、商品、购物车、订单、模拟支付、商家履约及平台管理的完整业务闭环。 + +项目采用模块负责制,每名成员独立负责所认领业务模块的数据库、后端接口、前端页面和测试,不按前后端横向分工。 + +### 1.3 范围说明 + +本期必须完成: + +- F01~F13 全部必做验收项。 +- 购物端、商家端和管理端的完整浏览器操作流程。 +- 不少于 30 个商品、3 个分类的演示数据。 +- 自动化测试、部署、过程文档和答辩材料。 + +本期不包含: + +- 真实支付渠道和真实资金结算。 +- 多商户入驻、商户结算和平台抽佣。 +- 后台操作审计日志(谁在什么时间修改了什么);本期仅保留系统运行、排错和链路观测所需的技术日志。 +- 复杂仓储、物流轨迹和电子发票。 +- 原生移动端应用。 + +团队已确认的选做功能: + +- X01 商品评价与晒图。 +- X02 商品收藏、浏览历史。 +- X03 站内消息通知。 +- X04 售后流程(退款/退货申请与后台审核)。 + +团队已确认的挑战模块: + +- C01 秒杀与防超卖。 +- C03 订单超时自动取消。 +- C04 商品搜索进阶。 +- C06 实时消息推送。 +- C07 缓存与性能优化。 +- C08 支付回调幂等与对账。 +- C10 容器化部署与负载均衡。 + +以上 7 项挑战均须准备教师要求的现场边界演示或压测数据及原理讲解;仅有页面或普通功能演示不视为通过。 + +### 1.4 术语定义 | 术语 | 说明 | -|------|------| -| SKU | 例:库存量单位,最小的商品售卖单元 | +|---|---| +| SKU | 库存量单位。本期默认一个商品对应一个库存单位;多规格 SKU 未被本组选择 | +| JWT | 用于 API 身份认证的令牌 | +| RBAC/Policy | 基于角色和策略的接口授权方式 | +| DDD | 领域驱动设计,本项目只在订单、支付、库存等复杂规则中轻量使用 | +| CQRS | 命令与查询职责分离,本项目不要求拆分数据库或微服务 | +| Outbox/Inbox | 保证数据库状态与异步消息最终一致、消费者幂等的技术模式 | +| 幂等 | 同一业务请求重复执行时,不产生重复订单、重复支付或重复状态变更 | +| ProblemDetails | HTTP API 统一错误响应格式 | ## 二、总体描述 ### 2.1 用户角色 | 角色 | 描述 | 主要操作 | -|------|------|----------| -| 游客 | | | -| 会员(买家) | | | -| 管理员 | | | +|---|---|---| +| 游客 | 未登录访问者 | 浏览分类和已上架商品、关键词搜索、查看商品详情、注册、登录 | +| 会员(买家) | 已注册且状态正常的用户 | 维护个人信息和地址、管理购物车、下单、模拟支付、查看和取消自己的订单 | +| 商家(运营人员) | 由管理员开通且状态正常的后台业务用户 | 管理分类和商品、处理订单发货、审核售后申请 | +| 管理员 | 负责平台治理的后台用户 | 管理买家和商家账号状态,监督平台运行 | + +### 2.2 核心业务流程 + +```mermaid +flowchart TB + A[商家维护分类、商品并上架] --> B[游客或买家浏览、搜索商品] + B --> C{是否已登录} + C -- 否 --> D[注册或登录买家账号] + C -- 是 --> E[加入购物车] + D --> E + E --> F[选择商品和收货地址] + F --> G[提交订单并扣减库存] + G --> H[买家模拟支付] + H --> I[商家查看已支付订单] + I --> J[商家发货] + J --> K[买家查看订单并收货] + K --> L[买家确认完成并评价商品] + K --> M{是否申请售后} + M -- 是 --> N[商家审核退款或退货申请] + O[管理员管理买家和商家账号] -. 平台治理 .-> A +``` -### 2.2 业务流程图 -(用 Mermaid 或图片描述核心购物流程:浏览 → 加购 → 下单 → 支付 → 发货 → 完成) +取消流程: ```mermaid flowchart LR - A[浏览商品] --> B[加入购物车] --> C[提交订单] --> D[支付] --> E[商家发货] --> F[确认收货] + A[待支付订单] --> B{买家申请取消} + B --> C[订单变为已取消] + C --> D[回补库存] + D --> E[重复取消不重复回补] ``` ### 2.3 功能模块清单 -| 模块编号 | 模块名称 | 优先级(P0/P1/P2) | 负责人 | -|----------|----------|------------------|--------| -| M01 | 用户模块 | P0 | | -| M02 | 商品模块 | P0 | | -| M03 | 购物车 | P0 | | -| M04 | 订单模块 | P0 | | -| M05 | 支付模块(模拟) | P0 | | -| M06 | 后台管理 | P0 | | -| M07 | (选做功能) | P1 | | +| 模块编号 | 模块名称 | 优先级 | 对应验收项 | 负责人 | +|---|---|---|---|---| +| M00 | 公共基建与集成 | P0 | 项目内部交付,支撑 F01~F13 | 成员 F(建议) | +| M01 | 用户与鉴权 | P0 | F01~F03、F13 | 成员 A(建议) | +| M02 | 分类与商品 | P0 | F04~F06、F11 | 成员 B(建议) | +| M03 | 购物车 | P0 | F07 | 成员 C(建议) | +| M04 | 订单 | P0 | F08、F09、F12 | 成员 D(建议) | +| M05 | 模拟支付 | P0 | F10 | 成员 E(建议) | +| M06 | 商家运营与后台管理 | P0 | F11~F13 | 成员 B/D/A(按商品/订单/账号拆分) | +| M07 | 商品评价与晒图 | P1 | X01 | 成员 B(建议) | +| M08 | 商品收藏与浏览历史 | P1 | X02 | 成员 A(建议) | +| M09 | 站内消息通知 | P1 | X03 | 成员 F(建议) | +| M10 | 售后流程 | P1 | X04 | 成员 E(建议) | +| M11 | 秒杀与防超卖 | P1 | C01 | 成员 C(建议) | +| M12 | 订单超时自动取消 | P1 | C03 | 成员 D(建议) | +| M13 | 商品搜索进阶 | P1 | C04 | 成员 B(建议) | +| M14 | 实时消息推送 | P1 | C06 | 成员 F(建议) | +| M15 | 缓存与性能优化 | P1 | C07 | 成员 F(平台)+ 成员 B(商品规则) | +| M16 | 支付回调幂等与对账 | P1 | C08 | 成员 E(建议) | +| M17 | 容器化部署与负载均衡 | P1 | C10 | 成员 F(建议) | + +优先级说明:P0 为必须首先闭环的项目基础与验收必做;P1 为本组已选择并承诺准备验收证据的选做或挑战。P1 不得反向阻塞 F01~F13 的完成。M00 是本组为六人协作增加的内部 P0 模块,不属于教师发布的 F01~F13,不新增课程验收编号。 ## 三、功能需求详述 -> 每个功能点按以下格式编写,编号规则:模块编号-序号,如 M01-01。 +### M00 公共基建与集成(内部 P0) + +- **描述**:建立统一解决方案骨架、模块注册方式、公共配置和本地运行编排,并将成员 A~E 的业务模块装配为一个可运行的模块化单体。 +- **责任边界**:成员 F 负责组合根、公共技术组件和集成清单;各业务负责人提供本模块的注册入口、数据库迁移和公开应用接口,成员 F 不修改其他模块内部领域规则。 +- **验收要点**:开发环境可启动 API、Worker 及已启用依赖;F01~F13 对应模块均完成注册并能通过公开接口协作;配置不硬编码密钥;模块集成问题有明确责任人和联调记录。 + +### M01-01 用户注册(F01) + +- **描述**:游客使用用户名、密码和可选手机号创建买家账号。 +- **前置条件**:用户未登录;用户名未被注册。 +- **主流程**:校验输入 → 检查用户名唯一性 → 加密密码 → 创建正常状态买家账号 → 返回账号基本信息。 +- **异常流程**:用户名重复、格式非法、密码强度不足或手机号格式错误时拒绝注册并返回明确提示。 +- **验收要点**:重复注册被拦截;数据库中不出现明文密码;非法参数不会创建账号。 + +### M01-02 用户登录与退出(F02) + +- **描述**:用户凭用户名和密码登录,系统返回身份令牌;退出后当前令牌不应继续访问受保护资源。 +- **前置条件**:账号存在且状态正常。 +- **主流程**:验证凭据 → 签发带用户标识、角色和 `jti` 的 JWT → 前端保存登录状态 → 退出时把 `jti` 写入 Redis 失效记录直至令牌自然过期,并清理前端登录状态。 +- **异常流程**:账号不存在、密码错误、账号被禁用或令牌过期时拒绝访问并返回友好错误。 +- **验收要点**:登录态在页面刷新后可恢复;错误密码不泄露账号是否存在;被禁用用户无法登录。 + +### M01-03 个人信息与收货地址(F03) + +- **描述**:买家查看和修改个人信息,并对自己的收货地址进行增删改查。 +- **业务规则**:地址必须属于当前用户;收件人、手机号、省市区和详细地址必填;每个用户最多一个默认地址;删除默认地址后不自动假定其他地址为默认,除非业务实现明确处理。 +- **异常流程**:访问他人地址、地址不存在或字段非法时拒绝操作。 +- **验收要点**:完整完成地址 CRUD;不能越权读取或修改他人地址。 + +### M02-01 商品列表、分类与搜索(F04、F05) + +- **描述**:游客和买家分页浏览已上架商品,可按分类筛选并按关键词模糊搜索。 +- **查询条件**:`page`、`pageSize`、`categoryId`、`keyword`、排序字段和方向。 +- **业务规则**:购物端只返回已上架商品;默认按创建时间倒序;搜索至少匹配商品名称。 +- **异常流程**:分页或排序参数非法时返回参数错误;空结果返回空列表而不是异常。 +- **验收要点**:分页总数正确;分类筛选和关键词搜索有结果;下架商品不出现在购物端列表。 + +### M02-02 商品详情(F06) + +- **描述**:展示商品名称、主图/图片、描述、价格、库存和分类。 +- **业务规则**:购物端只能查看已上架商品;价格与库存以服务端数据为准。 +- **异常流程**:商品不存在或已下架时返回资源不存在或不可售提示。 +- **验收要点**:图片、价格和库存显示正确;刷新后数据与后台修改结果一致。 + +### M03-01 购物车管理(F07) + +- **描述**:买家查看购物车、加入商品、修改数量、删除条目和选择结算商品。 +- **业务规则**:同一用户和商品只保留一个购物车条目;重复加入时累加数量;数量必须大于 0 且不能超过实时库存;金额由服务端按最新商品价格计算。 +- **异常流程**:商品下架、库存不足、数量非法或操作他人购物车时拒绝操作。 +- **验收要点**:增删改数量正确;总金额计算正确;不能通过前端篡改价格。 + +### M04-01 提交订单(F08) + +- **描述**:买家选择购物车条目和收货地址提交订单。 +- **前置条件**:用户已登录;地址属于当前用户;商品已上架且库存充足。 +- **主流程**:校验请求 → 读取商品实时价格和库存 → 原子扣减库存 → 创建订单与订单项快照 → 清理已下单购物车条目 → 返回订单号。 +- **业务规则**:订单项保存商品名称、图片和成交单价快照;订单总额由服务端计算;同一幂等键不得创建多张订单。 +- **异常流程**:库存不足、地址无效、重复提交或任一商品不可售时整单失败,不产生部分订单。 +- **验收要点**:订单与订单项完整;库存扣减正确;失败时事务回滚;重复请求不重复扣库存。 + +### M04-02 订单列表与详情(F09) + +- **描述**:买家分页查看自己的订单,并按状态筛选和查看详情。 +- **业务规则**:买家只能访问自己的订单;详情包含地址快照、订单项快照、金额和状态时间。 +- **验收要点**:列表与详情一致;访问他人订单被拒绝;状态筛选和分页正确。 + +### M04-03 取消订单(F09) + +- **描述**:买家取消自己的待支付订单。 +- **业务规则**:只有待支付订单可取消;取消与库存回补在同一事务中完成;重复取消不得重复回补库存。 +- **异常流程**:已支付、已发货、已完成或已取消订单拒绝取消。 +- **验收要点**:状态流转正确;库存只回补一次;并发取消结果一致。 + +### M05-01 模拟支付(F10) + +- **描述**:买家对自己的待支付订单执行模拟支付,不接入真实支付渠道。 +- **主流程**:创建支付记录 → 模拟成功回调 → 幂等更新支付和订单状态 → 返回支付结果。 +- **业务规则**:只有待支付订单可支付;成功后订单变为已支付;重复回调不得重复记账或改变终态。 +- **异常流程**:订单已取消、已支付、金额不一致或订单不属于当前用户时拒绝支付。 +- **验收要点**:支付后状态正确;支付记录可追踪;重复请求结果幂等。 + +### M06-01 后台分类与商品管理(F11) + +- **描述**:商家维护分类和商品,支持商品增删改查及上下架。 +- **业务规则**:商品名称、价格、库存和分类必填;价格不得为负;库存不得为负;有关联订单的商品不做破坏历史的物理删除,优先下架。 +- **验收要点**:CRUD 和上下架生效;购物端只看到已上架商品;非商家不能访问。 + +### M06-02 后台订单管理(F12) + +- **描述**:商家分页查询订单、查看详情并对已支付订单发货。 +- **业务规则**:只有已支付订单可以发货;发货后状态变为已发货;重复发货不得重复改变状态。 +- **验收要点**:订单查询、状态筛选和发货正常;非法状态流转被拒绝。 + +### M06-03 后台用户管理(F13) + +- **描述**:管理员查看买家和商家账号列表,并禁用或启用账号。 +- **业务规则**:被禁用账号不能重新登录;已签发令牌的失效策略在接口和架构设计中保持一致;管理员不得通过普通接口禁用自己。 +- **验收要点**:用户状态修改生效;禁用后无法登录;非管理员不能操作。 -### M01-01 用户注册 +### M07 商品评价与晒图(X01) -- **描述**: -- **前置条件**: -- **主流程**: - 1. - 2. -- **异常流程**:(如用户名已存在、密码格式错误) -- **验收要点**: +- **描述**:买家对已完成订单中的商品提交评分、文字评价和可选图片,并查看商品公开评价。 +- **业务规则**:评分范围 1~5;评价必须关联当前用户真实订单项;同一订单项只能提交一次;图片类型、大小和数量由实现前契约确定;评价内容不得泄露敏感信息。 +- **异常流程**:未购买、订单未完成、重复评价或图片不合规时拒绝提交。 +- **验收要点**:评价与对应商品正确关联;评分和晒图正常展示;不能评价他人订单或重复评价。 -### M01-02 用户登录 +### M08 商品收藏与浏览历史(X02) -(同上格式,逐个功能点补全…) +- **描述**:买家收藏、取消收藏和分页查看收藏商品;查看商品详情时记录最近浏览历史。 +- **业务规则**:同一用户对同一商品只能存在一条收藏和一条最近浏览记录;再次浏览更新最近时间;下架商品可保留历史,但标记为不可购买。 +- **验收要点**:收藏增删和列表正确;浏览历史按最近时间排序;用户之间数据隔离。 -## 四、非功能需求 +### M09 站内消息通知(X03) + +- **描述**:系统向买家发送订单状态、支付、发货和售后结果等站内通知,支持消息列表、未读数和已读状态。 +- **业务规则**:消息必须关联接收用户和业务资源;用户只能读取和标记自己的消息;持久化消息是事实来源,WebSocket 推送失败不丢消息。 +- **验收要点**:消息生成、列表、未读数和标记已读正确;断线后重新进入仍能看到未读消息。 + +### M10 售后流程(X04) + +- **描述**:买家针对符合条件的订单项申请退款或退货,商家在后台审核并形成售后状态记录。 +- **状态建议**:待审核 → 已同意/已拒绝;退货场景可增加待退货、已退款。真实退款渠道不在本期范围内,退款结果为模拟状态流转。 +- **业务规则**:申请必须关联当前用户订单项;申请金额不能超过该订单项实付金额;同一可售后数量不能重复申请;商家审核必须记录意见。 +- **验收要点**:买家申请、进度查询、后台审核和状态流转完整;越权、超额和重复申请被拦截。 + +## 四、选定挑战模块与验收要求 + +### C01 秒杀与防超卖 + +- 提供限时秒杀活动和秒杀下单入口,未开始、已结束或库存耗尽时拒绝下单。 +- 现场使用压测工具模拟 100 并发抢 10 件库存;成功订单数应为 10,不超卖,在请求充足且无业务失败时不少卖。 +- 库存扣减和订单生成保持一致,失败请求不得产生负库存或孤立订单。 +- 答辩须讲清数据库事务、条件更新/锁方案及为何没有把队列作为唯一正确性保障。 + +### C03 订单超时自动取消 + +- 正式规则为下单 30 分钟未支付自动取消并回补库存;演示环境可配置更短时间,但必须说明与正式参数的对应关系。 +- Worker 使用定时扫描或延迟任务处理,任务可重复执行但不能重复取消或重复回补库存。 +- 现场演示“自动取消瞬间用户恰好支付”的竞争,最终只能出现支付成功或取消成功之一。 +- 答辩须讲清条件更新、事务边界、重试和失败恢复方式。 + +### C04 商品搜索进阶 + +- 支持中文分词模糊搜索、多条件筛选和排序,至少包含分类、价格区间、上下架/可售条件及价格或时间排序。 +- 搜索实现采用搜索适配器;分词与倒排索引的具体实现须在开发前完成技术验证并记录。 +- 准备同一数据规模下与数据库 `LIKE/ILIKE` 查询的性能对比,包括数据量、查询词、并发、平均/百分位耗时和结果正确性。 +- 答辩须能解释分词、索引建立、更新时机和排序逻辑。 + +### C06 实时消息推送 + +- 使用 WebSocket/SignalR 推送订单支付、发货、取消和售后审核等状态变化。 +- 推送与站内消息持久化配合:实时推送失败不影响消息最终可查询。 +- 现场演示断线重连、同一账号多个浏览器标签页接收消息,以及多 API 实例下的消息广播。 +- 答辩须讲清连接身份校验、Redis Backplane/共享通道及断线补偿。 + +### C07 缓存与性能优化 + +- 使用 Redis 优化首页商品数据和商品详情查询。 +- 商品改价、库存或上下架变更后必须执行缓存失效策略,并说明最迟多久可见新值及原因。 +- 准备启用缓存前后的压测对比,记录命中率、吞吐量、平均/百分位耗时和数据库压力。 +- 答辩须讲清 Cache-Aside、TTL、缓存穿透/击穿基本处理及数据库仍是事实来源。 + +### C08 支付回调幂等与对账 + +- 模拟支付回调重复、乱序到达,使用支付流水号/回调标识和订单状态条件保证幂等。 +- 已取消订单不得被迟到的成功回调错误改为已支付;重复成功回调不得重复记账或重复发消息。 +- 每日生成对账结果,至少能识别“支付成功但订单未更新”等差异数据,并提供待处理状态或修复记录。 +- 答辩须讲清唯一约束、事务、Outbox/Inbox 或等价方案及乱序处理规则。 + +### C10 容器化部署与负载均衡 + +- Docker Compose 一键启动前端、Nginx、至少 2 个 API 实例、Worker、PostgreSQL、Redis、RabbitMQ 和最终启用的对象存储。 +- Nginx 对 API 实例负载均衡,现场通过实例标识或日志证明请求落到不同实例。 +- 停止一个 API 实例后,核心查询和已登录访问仍可用。 +- 登录态使用 JWT 和共享 Redis 能力,不依赖单实例内存 Session;答辩须说明多实例可用原因。 + +### 4.1 挑战验收证据 + +每个挑战模块均须保留: + +1. 可重复执行的演示或压测脚本。 +2. 测试数据规模、环境配置、操作步骤和预期结果。 +3. 原始结果或截图、问题记录和最终结论。 +4. 模块负责人对事务、并发、缓存、消息或部署原理的答辩提纲。 + +## 五、业务状态与权限规则 + +### 5.1 商品状态 + +| 状态 | 说明 | 购物端可见 | 可下单 | +|---|---|---|---| +| 草稿 | 尚未上架 | 否 | 否 | +| 已上架 | 正常销售 | 是 | 是,且须有库存 | +| 已下架 | 暂停销售 | 否 | 否 | + +### 5.2 订单状态 + +```mermaid +stateDiagram-v2 + [*] --> PendingPayment: 提交订单 + PendingPayment --> Paid: 支付成功 + PendingPayment --> Cancelled: 买家取消 + Paid --> Shipped: 商家发货 + Shipped --> Completed: 确认完成/演示处理 +``` + +任何未在图中定义的状态跳转均应拒绝。 + +### 5.3 权限矩阵 + +| 功能 | 游客 | 买家 | 商家 | 管理员 | +|---|---:|---:|---:|---:| +| 浏览商品 | ✓ | ✓ | ✓ | ✓ | +| 管理个人信息、地址、购物车和订单 | | ✓ | | | +| 模拟支付自己的订单 | | ✓ | | | +| 评价已完成订单商品、收藏、查看历史和消息 | | ✓ | | | +| 发起和查询自己的售后申请 | | ✓ | | | +| 管理分类和商品 | | | ✓ | | +| 查询订单并发货 | | | ✓ | | +| 审核售后申请 | | | ✓ | | +| 管理买家和商家账号状态 | | | | ✓ | + +## 六、非功能需求 | 类别 | 需求描述 | -|------|----------| -| 性能 | 常规页面加载 < 2s,列表接口分页返回 | -| 安全 | 密码加密存储、登录鉴权、输入校验 | -| 兼容 | Chrome/Edge 最新版 | -| 数据 | 演示数据不少于 30 个商品 | +|---|---| +| 性能 | 常规页面在正常校园网络环境下目标加载时间小于 2 秒;所有列表接口分页;关键接口保留压测结果 | +| 一致性 | 下单扣库存、取消回补库存、支付状态变更必须具备事务或幂等保障,不允许超卖或重复回补 | +| 安全 | 密码使用可靠哈希算法存储;JWT 鉴权;Policy 授权;参数校验;禁止 SQL 拼接;不提交密钥和生产配置 | +| 错误处理 | API 使用标准 HTTP 状态码和 ProblemDetails,包含可追踪错误码与 `traceId`,不返回内部堆栈 | +| 兼容 | Chrome、Edge 最新版正常显示,无明显样式错乱 | +| 数据 | 演示环境预置不少于 30 个商品和 3 个分类,并提供买家、商家、管理员测试账号 | +| 可测试性 | 测试用例不少于 40 条;核心领域规则有单元测试;关键 API 有集成测试;买家购物、商家履约和管理员账号治理流程有 Playwright 测试 | +| 可观测性 | API、数据库、缓存和消息处理的关键链路提供日志、Trace、Metric 和 Health Check;不得记录密码和完整 Token | +| 部署 | 最终系统通过 Docker Compose 部署到浏览器可访问环境;配置通过环境变量或 Secret 注入 | +| 可维护性 | 模块边界、接口、数据库和实际代码保持一致;公共能力集中,禁止跨模块直接修改内部数据 | +| 挑战证据 | C01/C03/C04/C06/C07/C08/C10 均保留可重复脚本、环境参数、原始结果和原理说明,不用单次截图替代完整证据 | + +## 七、界面范围 + +初版原型至少覆盖: + +| 端 | 页面 | +|---|---| +| 购物端 | 登录、注册、首页/商品列表、商品详情、购物车、提交订单、订单列表、订单详情、个人信息、地址管理 | +| 商家端 | 商家登录/入口、商品列表与编辑、分类管理、订单列表与详情、发货、售后审核 | +| 管理端 | 管理员登录/入口、买家账号管理、商家账号管理 | +| 已选选做 | 商品评价与晒图、收藏、浏览历史、站内消息、售后申请、商家售后审核 | +| 挑战演示 | 秒杀活动、进阶搜索、缓存/实例/消息状态辅助展示;压测和部署证据可使用独立脚本与报告 | + +页面需要统一提供加载、空数据、错误、无权限和操作成功/失败反馈。具体线框图待前端原型评审后补充。 + +## 八、六人后端模块分工建议 + +> 本节只划分后端领域与基础设施模块,不定义测试岗位、测试任务或测试交付物。 + +| 成员 | 后端模块边界 | 对应必做 | 主要选做/挑战 | 重点答辩内容 | +|---|---|---|---|---| +| 唐宇昊 | Identity、认证授权、个人资料、地址、买家/商家账号、收藏和浏览历史 | F01~F03、F13 | X02 | 密码哈希、JWT/Policy、角色权限、账号状态和用户数据隔离 | +| 顾欣月 | Catalog、分类、商品、评价、图片和搜索索引 | F04~F06、F11 | X01、C04;协作 C07 的缓存失效规则 | 商品上下架、评价权限、分页搜索、分词和倒排索引 | +| 朱惠惠 | Cart、结算校验、库存条件扣减和 Seckill | F07 | C01 | 用户隔离、后端计价、防超卖事务及锁方案取舍 | +| 韦乾强 | Ordering、订单快照、取消、商家发货和订单超时业务 | F08、F09、F12 | C03 | 订单状态机、库存协作、取消与支付竞争、超时取消和库存回补 | +| 张海洋 | Payment、AfterSales、支付回调、退款状态和对账业务 | F10 | X04、C08 | 支付/退款状态、售后状态机、回调幂等、乱序处理和对账 | +| 罗皓晨 | 项目脚手架、模块装配、公共配置、站内消息、SignalR、Redis、RabbitMQ、Outbox、Worker、Aspire、Docker Compose 和 Nginx | M00 公共基建与集成(内部 P0,支撑 F01~F13) | X03、C06、C07、C10 | 模块集成、消息持久化与推送、缓存基础设施、可靠事件、多实例和负载均衡 | + +协作要求: + +- 每个业务模块和挑战模块只能有一个后端主责人,可列协作人,但实体、事务、接口和事件边界必须清晰。 +- C03 的订单状态规则归成员 D,成员 F 提供 Worker 调度基础设施;C08 的支付和对账规则归成员 E,成员 F 提供 Outbox、RabbitMQ 和后台运行支持。 +- C07 的商品缓存失效时机由成员 B 定义,成员 F 负责 Redis 接入、通用缓存能力和多实例环境。 +- 成员 F 维护解决方案骨架、模块注册、公共配置和联调清单,负责把各模块装配成可运行系统,但不得越过边界直接修改其他成员的领域规则。 +- 测试责任不在本节划分,由测试计划单独确定。 +- 相邻成员交叉 Code Review,PR 记录和 Git 提交必须与最终分工一致。 + +## 九、需求追踪与确认 + +### 9.1 待确认事项 + +1. 班级、组号、组名、六名成员姓名,以及罗皓晨和成员 A~F 的对应关系。 +2. 六人组是否已获得指导教师同意;教师项目要求原文为 4~5 人组。 +3. 用户名、密码、手机号的最终校验规则。 +4. 评价晒图、商品图片的格式、大小、数量限制及 SeaweedFS 启用时间。 +5. 售后可申请订单状态、申请时限和模拟退款最终状态。 +6. C04 的中文分词器和倒排索引最终实现,经小规模技术验证后确认。 +7. 订单“已完成”由买家确认还是由演示流程自动处理。 +8. 用户被禁用后,禁用前已签发令牌是否需要立即失效;退出令牌已确定使用 Redis `jti` 失效记录。 + +### 9.2 已确认范围 -## 五、界面原型 -(可贴手绘/墨刀/Figma 原型图,至少覆盖:首页、商品详情、购物车、下单、订单列表、后台商品管理) +| 类别 | 已确认内容 | +|---|---| +| 选做 | X01 评价晒图、X02 收藏/历史、X03 站内消息、X04 售后流程 | +| 挑战 | C01、C03、C04、C06、C07、C08、C10 | +| 人数 | 实际 6 人,按成员 A~F 设计 | -## 六、需求确认 +### 9.3 需求确认 | 确认人 | 角色 | 日期 | 意见 | -|--------|------|------|------| -| | 指导教师 | | | +|---|---|---|---| +| | 指导教师 | | | +| | 组长 | | | +| | 模块负责人 | | | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/Git\345\233\242\351\230\237\345\215\217\344\275\234\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/Git\345\233\242\351\230\237\345\215\217\344\275\234\346\265\201\347\250\213.md" new file mode 100644 index 0000000..4ad9b80 --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/Git\345\233\242\351\230\237\345\215\217\344\275\234\346\265\201\347\250\213.md" @@ -0,0 +1,597 @@ +# Git 团队协作流程 + +> 适用项目:电子商城(E-Shop)暑期企业级综合项目实战 +> +> 协作模式:`master` 稳定发布分支 + `dev` 集成分支 + 短生命周期任务分支 + PR/MR + CI + 交叉 Code Review +> 命令环境:PowerShell;其他 Shell 中的 Git 命令基本相同。 + +本文是本项目的团队执行规范。若本文与 `docs/00-项目要求/` 冲突,以教师发布的项目要求、验收标准和评分标准为准。 + +## 一、必须遵守的核心规则 + +1. `master` 和 `dev` 是仅有的两个长期分支:`master` 保存稳定发布版本,`dev` 保存已审查的集成版本。 +2. 禁止直接在 `master` 或 `dev` 上开发、Push、Force Push 或改写历史。 +3. 一个任务对应一个短生命周期分支和一个 PR/MR。 +4. 任务分支必须从最新 `dev` 创建,通过 PR/MR 合入 `dev`,合并后立即删除,不得重复使用。 +5. 分支按业务任务划分,不按成员创建长期个人分支。 +6. 每名成员按模块负责制完成数据库、后端接口、前端页面和测试的纵向链路。 +7. 所有任务修改必须经过 CI 和至少一名其他成员的交叉 Code Review 后才能进入 `dev`。 +8. 阶段验收或正式发布时,由 `dev` 向 `master` 创建发布 PR/MR,通过完整验证和审查后合并。 +9. 提交、PR、日报、周报和成员分工必须真实一致。 +10. AI 可以辅助分析、编码和测试,但模块负责人必须阅读、验证并能讲清最终代码。 + +> 当前 Git 仓库已经存在 `master` 和 `dev`,两者目前基于同一初始提交。正式多人开发前,仓库管理员还需确认默认分支为 `master`,并同时配置 `master`、`dev` 的分支保护。 + +## 二、什么是短生命周期任务分支 + +短生命周期分支只为一个明确、可独立验收的任务存在,例如“用户登录”或“商品分页与分类筛选”。 + +本项目约定: + +- 每个分支只包含一个需求点、缺陷或工程任务。 +- 原则上在 1~2 个工作日内完成并创建 PR。 +- 预计超过 2 个工作日的任务,应继续拆分;暂时无法拆分时应尽早创建 Draft PR 暴露风险。 +- 分支不得长期落后于 `dev`,创建 PR 前必须同步最新集成分支。 +- PR 合并或关闭后删除远程分支和本地分支。 +- 禁止使用 `dev-张三`、`dev-lisi` 等长期个人分支。 + +分支不是项目文件夹。创建 `feature/auth-login` 不代表要创建 `feature/auth-login/` 目录;成员仍然修改 `frontend/`、`backend/` 和 `docs/` 中与任务相关的文件。 + +## 三、Git 命名规范 + +### 1. 通用规则 + +- 分支名只使用小写英文字母、数字、正斜杠 `/` 和连字符 `-`。 +- 禁止在分支名中使用中文、空格、下划线或成员姓名。 +- 类型、模块和任务使用英文,多个单词以连字符分隔。 +- 名称应能直接表达修改目的,避免 `update`、`temp`、`new`、`test1` 等模糊词。 +- 本地分支与远程分支保持同名,不另加成员缩写。 +- 分支名建议不超过 60 个字符;任务名称过长时应优先拆小任务。 + +### 2. 分支命名 + +分支名统一使用小写英文和连字符,格式为 `<类型>/<模块>-<任务>`。 + +| 修改目的 | 分支格式 | 示例 | +|---|---|---| +| 新功能 | `feature/*` | `feature/auth-login` | +| 缺陷修复 | `fix/*` | `fix/order-duplicate-submit` | +| 测试 | `test/*` | `test/cart-checkout-e2e` | +| 文档 | `docs/*` | `docs/update-api-contract` | +| 重构 | `refactor/*` | `refactor/order-status-rules` | +| 工程配置 | `chore/*` | `chore/add-docker-compose` | +| CI/CD | `ci/*` | `ci/add-pull-request-checks` | +| 紧急回滚 | `revert/*` | `revert/order-payment` | + +### 3. 模块名称 + +模块名建议统一使用: + +```text +auth 登录鉴权 +user 用户与地址 +catalog 分类与商品 +cart 购物车 +order 订单 +payment 支付 +admin 后台管理 +storage 文件与图片存储 +infra 公共基础设施 +deploy 部署 +docs 文档 +``` + +不要使用含义不清的分支名: + +```text +develop +test +update +my-branch +zhangsan +feature/all +``` + +### 4. Commit 命名 + +Commit 使用 Conventional Commits 风格,模块范围 `()` 可选: + +```text +: <描述> +(): <描述> +``` + +要求: + +- `type` 必须使用 README 规定的类型:`feat`、`fix`、`refactor`、`docs`、`test`、`chore`。CI、依赖和部署配置变更使用 `chore`,通过 `scope` 进一步说明范围。 +- `scope` 优先使用本节规定的模块名称,如 `auth`、`catalog`、`order`。 +- 描述必须说明实际改动,不使用 `update`、`修改代码`、`测试一下` 等模糊内容。 +- 描述可使用简洁中文或英文,但同一个 PR 内应保持一致。 +- 一个 Commit 只表达一个可以独立理解的修改目的。 + +示例: + +```text +feat(auth): add JWT login +fix(order): prevent duplicate cancellation +test(cart): cover checkout amount calculation +docs(api): update payment contract +chore(docker): add PostgreSQL service +chore(ci): add pull request checks +``` + +### 5. PR/MR 命名 + +PR/MR 标题与最终 Commit 使用相同格式: + +```text +feat(auth): add JWT login +``` + +Draft PR 仍使用正式标题,不使用“临时”“未完成”等模糊标题;通过平台的 Draft 状态表达尚未完成。 + +### 6. 发布 Tag 命名 + +发布 Tag 只从 `master` 的已验证 Commit 创建,使用语义化版本: + +```text +v<主版本>.<次版本>.<修订版本> +``` + +示例: + +```text +v0.1.0 第一阶段可演示版本 +v0.2.0 增加新的完整业务功能 +v0.2.1 修复已有版本缺陷 +v1.0.0 最终验收发布版本 +``` + +禁止使用 `final`、`latest`、`new`、`正式版` 等不可排序、不可追踪的 Tag 名称。 + +## 四、任务拆分原则 + +任务必须是可以独立审查、验证和回滚的纵向切片。 + +正确示例: + +```text +feature/auth-register + ├─ 用户表或数据库迁移 + ├─ 注册领域/应用逻辑 + ├─ 注册 API + ├─ 前端注册页面 + ├─ 参数与异常处理 + ├─ 自动化测试 + └─ 接口文档更新 +``` + +错误示例: + +```text +feature/backend-all # 按前后端横向分工 +feature/order-module # 范围过大,包含大量独立需求 +feature/week-two-work # 按时间而不是任务划分 +``` + +同一个任务需要修改公共路由、`Program.cs`、数据库上下文或 Docker 配置时可以修改,但必须在 PR 中说明原因和影响,不得顺手重构无关代码。 + +## 五、开始任务前 + +### 1. 确认任务定义 + +任务至少应明确: + +- 需求编号和负责人。 +- 功能范围与不包含的内容。 +- 正常流程、异常流程和验收条件。 +- 预计修改的数据库、接口、页面和测试。 +- 是否影响公共文件、配置或其他模块。 + +接口发生变化时,必须先更新 `docs/02-设计文档/接口设计.md`,再修改代码。 + +### 2. 检查本地工作区 + +```powershell +git status +``` + +工作区应保持干净。若存在其他任务的修改,先提交到对应分支,或使用带说明的 Stash: + +```powershell +git stash push -u -m "wip: auth login" +``` + +不得带着来源不明的修改开始新任务。 + +### 3. 从最新 `dev` 创建分支 + +```powershell +git fetch origin +git switch dev +git merge --ff-only origin/dev +git switch -c feature/auth-login +``` + +第一次推送会自动创建远程任务分支: + +```powershell +git push -u origin feature/auth-login +``` + +后续推送只需: + +```powershell +git push +``` + +## 六、开发与提交 + +开发过程中先检查修改,再选择性暂存: + +```powershell +git status +git diff +git add 需要提交的文件路径 +git diff --cached +``` + +不得在未检查工作区时机械执行 `git add .`。 + +提交命名遵循本文第三节和 README;模块范围 `()` 可选: + +```text +: <描述> +(): <描述> +``` + +允许的常用类型: + +```text +feat 新功能 +fix 缺陷修复 +refactor 不改变行为的重构 +docs 文档 +test 测试 +chore 构建、依赖、配置和工具 +``` + +示例: + +```text +feat(auth): add JWT login +fix(order): prevent duplicate cancellation +test(cart): cover checkout amount calculation +docs(api): document payment endpoint +chore(docker): add PostgreSQL service +``` + +一个任务分支可以有多个提交,但每个提交必须目的明确、可以理解,不得包含调试文件、生成目录、密码或密钥。禁止使用 `update`、`改一下`、`测试` 等无法说明实际工作的提交信息。 + +## 七、AI 辅助开发规则 + +AI 是开发辅助工具,不是模块责任人。使用 AI 时必须遵守: + +1. AI 修改前先读取 README、适用的 `AGENTS.md`、需求、接口和相关代码。 +2. 一次只处理当前分支对应的一个任务,不得自行扩大范围。 +3. 不允许 AI 修改 `docs/00-项目要求/`。 +4. 不允许 AI 擅自更换技术栈、新增大型依赖或重构无关模块。 +5. AI 给出的构建、测试和验证结果必须来自真实执行,不能把建议命令写成已通过。 +6. 多个 AI 任务并行时必须使用不同任务分支或独立 Worktree;不得同时修改同一工作区。 +7. 提交前由模块负责人逐文件检查 Diff,确认没有敏感信息、无关修改和无法解释的代码。 +8. 模块负责人必须亲自完成浏览器/API 验证,并能在答辩中解释数据库、接口、页面、异常和测试链路。 +9. AI 生成的日报或周报只能基于当天真实提交和验证记录整理,不得补写虚假工作。 + +推荐给 AI 的任务输入至少包含:需求编号、业务规则、修改范围、禁止事项、验收条件和必须运行的验证命令。 + +## 八、创建 PR 前 + +### 1. 完成本地验证 + +项目脚手架建立后,应以仓库中真实存在的脚本为准。按当前技术选型,CI 计划至少覆盖: + +前端: + +```powershell +npm ci +npm run lint +npm run type-check +npm run test +npm run build +``` + +后端: + +```powershell +dotnet restore +dotnet build --configuration Release +dotnet test --configuration Release +``` + +端到端测试和部署配置: + +```powershell +npx playwright test +docker compose config +``` + +在 `package.json`、解决方案和 Compose 文件尚未创建前,不得宣称这些命令已经可用或已通过。后续若脚本名称发生变化,应同步修改本文和 CI。 + +### 2. 同步最新 `dev` + +个人独占的短生命周期分支推荐 Rebase: + +```powershell +git fetch origin +git rebase origin/dev +``` + +已经推送过的个人分支在 Rebase 后使用: + +```powershell +git push --force-with-lease +``` + +禁止使用 `git push --force`。如果分支被多人共享,不要 Rebase;应先协调拆分,确实无法拆分时使用 Merge 同步 `dev`。 + +### 3. 处理冲突 + +冲突必须由理解相关模块的人逐处确认,禁止直接选择“全部保留当前”或“全部接受传入”。解决后继续: + +```powershell +git add 冲突文件路径 +git rebase --continue +``` + +方向不确定时中止,不要强行继续: + +```powershell +git rebase --abort +``` + +## 九、PR/MR 要求 + +日常任务 PR 的源分支必须是任务分支,目标分支必须是 `dev`: + +```text +feature/auth-login → dev +``` + +PR 标题遵循本文第三节,与最终提交使用一致格式,例如: + +```text +feat(auth): add JWT login +``` + +PR 描述必须包含: + +```markdown +## 关联任务 + +- 需求/任务编号: +- 模块负责人: + +## 修改内容 + +- + +## 验收条件 + +- + +## 验证结果 + +| 命令或场景 | 结果 | +|---|---| +| | | + +## 影响范围 + +- 前端: +- 后端: +- 数据库:无 / Migration 名称 +- API 文档:无 / 已更新 + +## 风险与回滚 + +- 风险: +- 回滚方式: + +## 已知问题 + +- 无 / 列出未包含内容 +``` + +未完成但需要提前讨论的任务使用 Draft PR。一个 PR 不得混入无关格式化、其他需求或顺手重构。 + +## 十、交叉 Code Review + +每个 PR 至少由一名其他成员批准,优先由相邻模块负责人审查,并在 PR 中留下明确记录。PR 作者不能作为自己的唯一审批人。 + +审查至少确认: + +1. 是否符合需求编号和验收条件。 +2. 是否实现数据库、接口、页面和测试的完整链路。 +3. 是否破坏接口契约或其他模块。 +4. 是否存在空值、边界、并发、越权或数据一致性问题。 +5. 数据库 Migration 是否安全,是否需要演示数据更新。 +6. 是否包含敏感信息、生成文件或无关修改。 +7. 测试是否覆盖正常和异常流程,验证结果是否真实。 +8. 模块负责人是否能解释关键实现。 + +登录鉴权、权限策略、数据库结构、订单状态、库存扣减、支付、文件上传、Docker 和生产配置应重点审查。 + +## 十一、CI 与长期分支保护 + +仓库管理员至少配置: + +- 禁止直接 Push、Force Push 或删除 `master`、`dev`。 +- 所有修改必须通过 PR/MR。 +- 至少一名其他成员批准。 +- 阻塞性审查意见必须解决。 +- 已配置的前端、后端和必要测试检查必须通过。 +- 合并后自动删除远程任务分支。 + +CI 尚未建立时,不得省略人工构建和测试记录;CI 建立后,失败的 PR 不得以“本地可以运行”为理由绕过检查。 + +## 十二、合并策略、发布与分支清理 + +任务分支合入 `dev` 时统一采用 `Squash and Merge`,使一个任务 PR 对应一个完整、可回滚的业务提交。 + +阶段验收或正式发布时创建发布 PR: + +```text +dev → master +``` + +发布 PR 必须执行完整构建、测试和部署配置验证,并由组长或指定发布负责人审查。`dev → master` 使用普通 Merge Commit,保留两个长期分支的祖先关系;不要对每次发布 PR 反复 Squash,否则后续发布比较和历史追踪容易混乱。 + +任务 PR 合并后同步 `dev`: + +```powershell +git switch dev +git fetch origin +git merge --ff-only origin/dev +``` + +删除已完成的本地分支: + +```powershell +git branch -d feature/auth-login +``` + +任务 PR 使用 Squash Merge 后,Git 可能无法识别传统合并关系。只有在确认 PR 已合并、代码已进入 `dev` 且分支不再需要后,才可执行: + +```powershell +git branch -D feature/auth-login +``` + +远程分支应由平台自动删除;未自动删除时,在确认 PR 已合并后执行: + +```powershell +git push origin --delete feature/auth-login +``` + +下一个任务必须重新从最新 `dev` 创建新分支。 + +## 十三、常见异常处理 + +### 1. 临时切换任务 + +未完成的当前任务使用带说明的 Stash,或在代码已构成完整步骤时正常提交。禁止把当前修改带入另一个任务分支。 + +### 2. 推送被拒绝 + +先检查远程历史: + +```powershell +git fetch origin +git log --oneline --graph --decorate HEAD origin/当前分支名 +``` + +确认是个人独占分支后再 Rebase,并只使用 `--force-with-lease`。不得看到 `non-fast-forward` 就直接强推。 + +### 3. 密钥已提交 + +立即禁用并轮换密钥,再从当前代码删除、检查历史和访问日志。普通删除提交不能清除 Git 历史中的密钥。 + +### 4. 合并后出现严重问题 + +不要改写 `dev` 或 `master` 历史。未发布问题从最新 `dev` 创建 `revert/*` 分支并通过 PR 回滚到 `dev`;已发布问题从最新 `master` 创建 `revert/*` 分支,执行 `git revert` 并通过紧急 PR 回滚到 `master`,随后把修复同步回 `dev`。 + +### 5. 高风险命令 + +`git reset --hard`、`git clean -fd`、`git branch -D` 和远程分支删除均可能导致数据丢失,执行前必须确认精确目标和可恢复性。 + +## 十四、发布规则 + +`dev` 表示代码集成状态,不直接等同于某台开发环境;`master` 表示稳定发布状态。不要继续增加长期 `test`、`staging` 等环境分支。正式发布应从 `master` 的同一个 Commit 构建版本化制品,再依次部署到对应环境。 + +正式发布至少关联: + +- Git Commit SHA 和 Tag。 +- Docker 镜像版本,不能只依赖 `latest`。 +- 数据库 Migration 版本。 +- 发布说明和回滚方案。 +- 对应测试报告。 + +发布 Tag 使用本文第三节规定的 `v<主版本>.<次版本>.<修订版本>` 格式,并且只能标记 `master` 上已经通过发布验证的 Commit。 + +## 十五、每日命令速查 + +开始任务: + +```powershell +git status +git fetch origin +git switch dev +git merge --ff-only origin/dev +git switch -c feature/模块-任务 +``` + +开发提交: + +```powershell +git status +git diff +git add 文件路径 +git diff --cached +git commit -m "feat(module): describe the change" +``` + +首次推送并创建 PR: + +```powershell +git push -u origin feature/模块-任务 +``` + +任务 PR 合并后: + +```powershell +git switch dev +git fetch origin +git merge --ff-only origin/dev +git branch -d feature/模块-任务 +``` + +## 十六、禁止事项 + +- 禁止直接在 `master`、`dev` 开发或 Push。 +- 禁止 Force Push 或删除 `master`、`dev`。 +- 禁止使用长期个人分支或长期开发分支。 +- 禁止一个分支或 PR 混入多个无关任务。 +- 禁止重复使用已合并的任务分支。 +- 禁止多人长期共用一个任务分支。 +- 禁止绕过 CI 或交叉 Code Review。 +- 禁止自己作为自己 PR 的唯一审批人。 +- 禁止提交密码、Token、密钥和生产配置。 +- 禁止提交 `node_modules`、`dist`、`bin`、`obj`、测试报告缓存等生成内容。 +- 禁止无检查执行 `git add .`。 +- 禁止未经验证就宣称功能或测试通过。 +- 禁止 AI 擅自扩大任务范围或代替负责人完成理解与答辩。 + +## 十七、最终流程 + +```text +任务与验收条件确认 + ↓ +同步最新 dev + ↓ +创建短生命周期任务分支 + ↓ +完成数据库 + 后端 + 前端 + 测试 + 文档 + ↓ +本地真实验证 + ↓ +推送并创建 PR/MR + ↓ +CI + 相邻模块负责人交叉 Code Review + ↓ +Squash and Merge 到 dev + ↓ +删除任务分支 + ↓ +阶段验收/发布时 dev → master 发布 PR + ↓ +从最新 dev 开始下一个任务 +``` diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" index 75890df..7fc5f1b 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" @@ -1,59 +1,434 @@ # 系统架构设计 -> 组别:____ 编写人:____ 编写日期:____ 版本:v1.0 +> 组别:待填写 编写人:罗皓晨 编写日期:2026-07-22 版本:v0.1 +> +> 文档状态:已确认主要技术选型、四类角色、选做挑战范围和六人模块边界,待技术验证与全组评审 > 截止:第 1 周周五 ## 修订记录 | 版本 | 日期 | 修改人 | 修改说明 | -|------|------|--------|----------| -| v1.0 | | | 初稿 | +|---|---|---|---| +| v0.1 | 2026-07-22 | 罗皓晨 | 形成系统架构初稿,明确技术选型、分层依赖、模块边界、角色权限、事件与分布式组件及六人后端职责 | -## 一、技术选型及理由 +## 一、架构目标与约束 -| 分层 | 选型 | 版本 | 选型理由 | -|------|------|------|----------| -| 前端框架 | | | | -| UI 组件库 | | | | -| 后端框架 | | | | -| 数据库 | | | | -| 缓存(可选) | | | | -| 部署方式 | | | | +### 1.1 架构目标 -## 二、总体架构图 +- 在 4 周内交付完整、可演示、可部署的 B2C 商城。 +- 支持购物端、商家端、管理端和后台 Worker 的统一开发与部署。 +- 保证订单、库存、支付状态的事务一致性和幂等性。 +- 使用模块化单体控制复杂度,同时保留清晰的业务边界。 +- 支持本地一键编排、健康检查、日志与链路追踪。 +- 完整支撑已选 C01、C03、C04、C06、C07、C08、C10 的边界演示、压测和答辩证据。 +- 为 C10 提供至少两个 API 实例、Nginx 负载均衡和共享登录/消息能力。 -(用 Mermaid 或图片描述,至少体现:浏览器 → 前端应用 → 后端 API → 数据库 的调用关系) +### 1.2 约束 + +- 不拆分业务微服务;核心业务统一部署为一个 ASP.NET Core API。 +- DDD 只用于订单、支付、库存等规则复杂区域,简单 CRUD 不创建多余抽象。 +- 简化 CQRS 只分离命令与查询职责,不拆分读写数据库。 +- Redis 首期用于 JWT 退出失效记录,并承担 C06 SignalR 多实例 Backplane 和 C07 商品缓存。 +- RabbitMQ、Outbox 和 S3 兼容对象存储按对应选做或挑战阶段启用,核心下单正确性仍以 PostgreSQL 事务为准。 +- 对象存储只依赖 S3 兼容协议,开发环境使用 SeaweedFS,其他环境可替换为兼容实现。 +- 团队实际 6 人,与教师发布的 4~5 人基线不一致;架构按六人分工设计,但必须取得指导教师确认。 +- `docs/00-项目要求/` 为教师发布内容,不得修改。 + +### 1.3 已确认功能范围 + +| 类别 | 已确认内容 | +|---|---| +| 选做 | X01 商品评价与晒图、X02 商品收藏/浏览历史、X03 站内消息通知、X04 售后流程 | +| 挑战 | C01 秒杀与防超卖、C03 订单超时自动取消、C04 商品搜索进阶、C06 实时消息推送、C07 缓存与性能优化、C08 支付回调幂等与对账、C10 容器化部署与负载均衡 | + +数据库设计和接口设计目前按用户要求保持原始模板。每个模块进入开发前,必须先由对应负责人把本节范围落实到数据库表、接口清单和详细契约,并通过交叉评审。 + +## 二、技术选型及理由 + +> 精确补丁版本以首次创建脚手架时锁定的依赖清单为准,本文只固定主要版本和技术边界。 + +| 类别 | 选型 | 版本 | 选型理由 | +|---|---|---|---| +| 前端框架 | Vue 3 + TypeScript + Vite | Vue 3.x | 组件化开发、类型检查、构建速度快 | +| 状态与请求 | Pinia + Axios | 待锁定 | 分离页面状态和 HTTP 调用,保持调用方式统一 | +| 前端测试 | Vitest + Playwright | 待锁定 | 分别覆盖单元/组件测试和核心浏览器流程 | +| 后端 | .NET 10 + ASP.NET Core Web API | .NET 10 | 提供 HTTP API、鉴权和基础运行能力 | +| API 契约与文档 | OpenAPI + Swagger UI | 待锁定 | 接口文档先行,以机器可读契约统一前后端开发和联调 | +| 数据访问 | EF Core 10 + Npgsql | EF Core 10 | 支持 PostgreSQL、Migration 和事务 | +| 数据库 | PostgreSQL | 版本待锁定 | 关系模型、事务、约束和统计 SQL 能力适合商城业务 | +| 架构模式 | 核心领域使用 DDD + Clean Architecture + 简化 CQRS | — | 复杂业务规则进入领域层;命令与查询职责分开,但不拆读写库 | +| 领域协作 | 领域事件 | — | 在同一进程内表达并处理领域事实 | +| 集成可靠性 | RabbitMQ 集成事件 + Outbox | — | 事务提交后可靠发布跨进程消息,避免数据库与消息不一致 | +| 缓存/共享通道 | Redis | 版本待锁定 | JWT `jti` 失效记录、C06 多实例消息 Backplane、C07 首页/详情缓存 | +| 后台处理 | .NET Worker Service | .NET 10 | 执行 Outbox 投递、订单超时取消和对账任务 | +| 本地编排 | Aspire | 待锁定 | 声明 API、Worker 和基础设施依赖,提升本地调试效率 | +| 生产部署 | Docker Compose + Nginx | 待锁定 | 满足 C10 一键部署和至少 2 个 API 实例负载均衡验收 | +| 对象存储 | S3 Compatible Object Storage(开发环境:SeaweedFS) | 待锁定 | 业务仅依赖 S3 兼容协议,避免绑定具体存储产品 | +| 日志与可观测性 | Serilog + OpenTelemetry | 待锁定 | Serilog 输出结构化日志;OpenTelemetry 采集 Trace、Metric 并关联日志上下文 | + +UI 组件库尚未最终确定,必须在前端脚手架建立前由全组确认,避免中途更换。 + +## 三、总体架构 ```mermaid flowchart TB - U[浏览器] --> FE[前端应用] - FE -->|HTTP API| BE[后端服务] - BE --> DB[(数据库)] + U[Chrome / Edge] --> N[Nginx] + N --> FE[Vue 3 静态前端] + FE -->|HTTPS / JSON API| N + N --> API1[Mall.Api 实例 1] + N --> API2[Mall.Api 实例 2] + + API1 --> PG[(PostgreSQL)] + API2 --> PG + API1 --> R[(Redis)] + API2 --> R + API1 --> S3[S3 Compatible Object Storage
开发:SeaweedFS] + API2 --> S3 + API1 --> MQ[(RabbitMQ)] + API2 --> MQ + API1 <-->|SignalR Backplane| R + API2 <-->|SignalR Backplane| R + W[Mall.Worker] --> PG + W --> MQ + W --> R + + AH[Mall.AppHost 开发时编排] -. starts .-> API1 + AH -. starts .-> W + AH -. starts .-> PG + AH -. starts .-> R + AH -. starts .-> MQ + AH -. starts .-> S3 ``` -## 三、模块划分 +说明: + +- Aspire AppHost 用于开发时编排,不作为生产运行时。 +- 日常开发允许只运行一个 API 实例;C10 集成验证和验收必须启动第二实例和 Nginx 负载均衡。 +- PostgreSQL 是业务事实来源;Redis 不保存不可恢复的唯一业务事实。 +- RabbitMQ 只承载跨进程集成事件,不用于替代核心下单事务。 +- 站内消息先持久化,再通过 SignalR 实时推送;断线后通过消息列表补偿。 + +## 四、代码结构与依赖 + +### 4.1 仓库结构 + +```text +frontend/ +├─ src/ +│ ├─ api/ +│ ├─ components/ +│ ├─ layouts/ +│ ├─ modules/ +│ ├─ router/ +│ ├─ stores/ +│ ├─ styles/ +│ └─ types/ +└─ tests/ + +backend/ +├─ src/ +│ ├─ Mall.Api/ +│ ├─ Mall.Application/ +│ ├─ Mall.Domain/ +│ ├─ Mall.Infrastructure/ +│ ├─ Mall.Worker/ +│ └─ Mall.AppHost/ +└─ tests/ + ├─ Mall.UnitTests/ + └─ Mall.IntegrationTests/ +``` + +只建立上述共享分层项目,不为每个业务模块再复制一套独立 `.csproj`。 + +### 4.2 后端依赖方向 + +```mermaid +flowchart LR + API[Mall.Api] --> APP[Mall.Application] + WORKER[Mall.Worker] --> APP + APP --> DOMAIN[Mall.Domain] + INFRA[Mall.Infrastructure] --> APP + INFRA --> DOMAIN + API -. composition root .-> INFRA + WORKER -. composition root .-> INFRA +``` + +- `Mall.Domain`:实体、值对象、领域规则、领域事件;不依赖 EF Core、RabbitMQ、HTTP。 +- `Mall.Application`:命令、查询、处理器、DTO、应用接口和事务用例。 +- `Mall.Infrastructure`:EF Core、Npgsql、Redis、RabbitMQ、S3、JWT 等实现。 +- `Mall.Api`:HTTP 端点、认证授权、ProblemDetails、OpenAPI 和依赖装配。 +- `Mall.Worker`:Outbox 投递、RabbitMQ 消费和确有需求的后台任务;消费者通过消息 ID 唯一约束或业务幂等记录防重。 +- `Mall.AppHost`:开发时资源声明和启动依赖。 + +## 五、业务模块划分 + +| 模块 | 职责 | 主要数据 | 可依赖内容 | +|---|---|---|---| +| Identity | 注册、登录、个人资料、地址、用户状态和权限 | 用户、地址 | 公共认证抽象 | +| Catalog | 分类、商品、图片、上下架、库存基础信息 | 分类、商品、商品图片 | 对象存储抽象 | +| Cart | 购物车增删改查和金额预览 | 购物车条目 | Identity、Catalog 的公开应用接口 | +| Ordering | 下单、订单项快照、取消、发货和状态流转 | 订单、订单项 | Identity、Catalog 的公开应用接口 | +| Payment | 模拟支付、支付记录和回调幂等 | 支付记录 | Ordering 的公开应用接口 | +| Review | 商品评分、文字评价和晒图 | 评价、评价图片 | Identity、Catalog、Ordering 的公开应用接口 | +| Engagement | 收藏、浏览历史和站内消息 | 收藏、历史、消息 | Identity、Catalog 的公开应用接口 | +| AfterSales | 退款/退货申请和商家审核 | 售后单、售后状态记录 | Ordering、Payment 的公开应用接口 | +| Seckill | 秒杀活动、资格校验和防超卖下单 | 秒杀活动、秒杀库存/订单关联 | Catalog、Ordering 的公开应用接口 | +| Search | 分词、倒排索引、多条件筛选和排序 | 搜索索引 | Catalog 商品变更事件 | +| Administration | 商家运营入口和管理员账号治理 | 各模块管理查询 | 各模块公开管理用例 | +| Integration | Outbox、集成事件发布与消费 | Outbox 消息 | Application 定义的接口 | + +模块之间通过 Application 用例或明确接口协作,禁止一个模块直接修改另一个模块的内部表。 + +## 六、前端设计 + +### 6.1 模块与页面 + +```text +modules/ +├─ auth/ 登录、注册 +├─ account/ 个人信息、地址 +├─ catalog/ 首页、列表、搜索、详情 +├─ cart/ 购物车 +├─ order/ 下单、订单列表和详情 +├─ payment/ 模拟支付结果 +├─ review/ 评价与晒图 +├─ engagement/ 收藏、历史、站内消息 +├─ after-sales/ 售后申请与进度 +├─ seckill/ 秒杀活动 +├─ merchant/ 商品、分类、订单、发货、售后审核 +└─ admin/ 买家和商家账号管理 +``` + +### 6.2 状态管理 + +- Pinia 只保存跨页面状态:身份信息、购物车摘要和必要的界面偏好。 +- 商品列表、订单列表等服务端数据由页面/API 层按需加载,不把所有响应长期复制到全局 Store。 +- Axios 统一配置 API 基础地址、超时、JWT Header 和错误转换。 +- 401 清理登录状态并跳转登录页;403 显示无权限;业务错误展示 ProblemDetails 的安全提示。 + +### 6.3 路由与权限 + +- 公共路由:登录、注册、商品列表、商品详情。 +- 买家路由:个人信息、地址、购物车、下单、订单。 +- 买家增强路由:评价、收藏、浏览历史、站内消息、售后申请和秒杀活动。 +- 商家路由:后台商品、分类、订单发货和售后审核。 +- 管理员路由:买家和商家账号管理。 +- 前端路由守卫只改善体验,最终权限由后端 Policy 强制执行。 + +## 七、关键业务设计 + +### 7.1 下单事务 + +```mermaid +sequenceDiagram + participant FE as Vue + participant API as Mall.Api + participant DB as PostgreSQL + FE->>API: POST /api/orders + Idempotency-Key + API->>DB: 开启事务 + API->>DB: 校验地址、商品、价格和库存 + API->>DB: 条件扣减库存 + API->>DB: 写订单、订单项快照和 Outbox(启用时) + API->>DB: 删除已结算购物车条目 + API->>DB: 提交事务 + API-->>FE: 201 + 订单号 +``` + +任一步失败时回滚整个事务,不产生部分订单。库存扣减使用数据库条件更新或等价并发控制,不能只依赖前端库存值。 + +### 7.2 支付幂等 + +- 支付请求使用唯一支付流水号和幂等键。 +- 只有待支付订单允许支付。 +- 支付记录和订单状态在同一数据库事务内更新。 +- 重复成功请求返回原成功结果,不重复写入或重复发布事件。 + +### 7.3 领域事件与集成事件 + +- 领域事件在同一进程内表达领域事实,例如 `OrderPaidDomainEvent`。 +- 只有跨进程需求才转换为集成事件,例如 `OrderPaidIntegrationEvent`。 +- 启用 RabbitMQ 时,业务事务同时写入 Outbox;Worker 成功发布后标记已处理。 +- 消费者使用消息 ID 唯一约束或业务幂等记录防止重复处理。 +- 初期只实现一条可验证事件链,不为普通 CRUD 广泛发布事件。 + +### 7.4 图片存储 + +- 业务层依赖 `IObjectStorage` 一类最小接口。 +- Infrastructure 通过 S3 兼容客户端访问对象存储;客户端库在实现阶段锁定,不在架构层绑定具体 SDK。 +- 商品图片和评价晒图共用对象存储抽象,使用不同对象键前缀和权限规则。 +- 数据库只保存对象键、访问 URL、排序和媒体类型,不存图片二进制。 +- 上传接口校验文件类型、大小和数量;对象存储失败时不写入无效商品或评价图片记录。 + +### 7.5 四项选做功能 + +- **评价晒图**:评价必须校验当前用户已完成订单项;评价记录与图片元数据分离,图片存 SeaweedFS。 +- **收藏/历史**:按用户隔离;收藏使用唯一约束防重,浏览历史对同一用户和商品更新最近时间。 +- **站内消息**:消息先落 PostgreSQL,再由 SignalR 推送;已读状态以数据库为准,WebSocket 只负责实时性。 +- **售后流程**:建立独立售后状态机,关联订单项和申请金额;模拟退款状态与真实支付接口隔离。 + +### 7.6 C01 秒杀与防超卖 + +第一版采用 PostgreSQL 事务和条件更新作为正确性边界: + +```text +UPDATE 秒杀库存 +SET available_stock = available_stock - quantity +WHERE activity_id = @id + AND available_stock >= quantity + AND start_at <= now() + AND end_at > now(); +``` + +- 受影响行数为 1 才允许创建秒杀订单。 +- 库存扣减、订单创建和必要 Outbox 写入处于同一事务。 +- Redis 可用于活动热点读取和入口削峰,但不能成为唯一库存事实来源。 +- 压测固定记录并发数、库存、成功/失败数、数据库最终库存和有效订单数,验证不超卖、不少卖。 + +### 7.7 C03 订单超时自动取消 + +- 创建订单时写入 `expires_at = created_at + 30 分钟`。 +- `Mall.Worker` 周期扫描已到期的待支付订单;演示环境只缩短配置值,不改变规则。 +- 多 Worker 使用批量领取/跳过已锁定记录或等价机制,取消时执行带 `PendingPayment` 条件的状态更新。 +- 状态更新和库存回补同事务;支付也必须带待支付状态条件,因此支付与取消竞争只能一方成功。 +- Worker 重试安全,重复扫描不会重复回补库存。 + +### 7.8 C04 商品搜索进阶 + +- Application 只依赖 `IProductSearch`,避免页面和业务代码绑定具体搜索实现。 +- 搜索能力必须包含中文分词、模糊匹配、分类/价格/可售筛选和白名单排序。 +- 实现前先完成小型技术验证,在“应用层分词 + PostgreSQL 倒排索引”和独立搜索引擎之间二选一;选型后更新本文和数据库设计。 +- 商品新增、改名、上下架后通过事务后事件触发索引更新;失败任务由 Worker 重试并可重建索引。 +- 保留同一数据集、查询词和并发参数下与 `LIKE/ILIKE` 的正确性及耗时对比。 + +### 7.9 C06 实时消息推送 + +- ASP.NET Core SignalR 提供 WebSocket 通道,JWT 用于连接身份认证。 +- PostgreSQL 站内消息表保存通知事实;SignalR 推送失败不回滚订单业务,也不丢失可查询消息。 +- Redis Backplane 在两个 API 实例之间传播 Hub 消息,保证用户连接落在不同实例时仍能接收。 +- 前端实现自动重连,重连后查询未读消息补偿;多标签页各自维持连接,但已读状态共享。 + +### 7.10 C07 缓存与性能优化 + +- 首页商品摘要和商品详情使用 Cache-Aside;Key 包含稳定业务版本,设置有限 TTL。 +- 读取未命中时查询 PostgreSQL 并回填 Redis;数据库始终为事实来源。 +- 商品改价、库存或上下架事务提交后删除相关缓存,并通过重试/事务后事件处理删除失败。 +- 为降低更新与回填竞争导致的旧值窗口,可结合短 TTL 和延迟二次失效;文档须说明理论最迟生效时间。 +- 压测报告对比缓存启用前后 P50/P95、吞吐量、命中率和数据库查询次数。 + +### 7.11 C08 支付回调幂等与对账 + +- 回调包含全局唯一回调 ID、支付流水号、订单号、结果和时间;回调 ID/流水号建立唯一约束。 +- 订单状态机拒绝迟到或逆序更新;已取消订单不会因迟到成功回调直接变为已支付,而是进入对账差异。 +- 支付记录、订单状态和 Outbox 在同一事务处理;重复回调读取并返回已处理结果。 +- `Mall.Worker` 生成每日对账批次,对比支付记录和订单状态,输出匹配、差异和处理状态。 +- 验收脚本随机重复并打乱回调顺序,验证最终状态和对账差异。 + +### 7.12 C10 容器化部署与负载均衡 + +- Docker Compose 定义 Nginx、Vue 静态站点、2 个 Mall.Api、Mall.Worker、PostgreSQL、Redis、RabbitMQ 和 SeaweedFS。 +- 两个 API 镜像和配置一致,不使用本地内存 Session;JWT 验签配置一致,失效记录和 SignalR Backplane 共享 Redis。 +- Nginx 负责 API 负载均衡和 WebSocket Upgrade;Health Check 不通过的实例不应继续接收新请求。 +- 镜像使用 Commit SHA/版本 Tag,不只使用 `latest`;Secret 通过环境变量或受控文件注入。 +- 验收演示包含请求分布证明、停止一个 API 实例后的可用性和登录态连续性。 + +## 八、安全设计 + +- 密码采用 ASP.NET Core PasswordHasher 或等价可靠算法,不自行实现加密。 +- JWT 包含用户 ID、角色、`jti` 和过期时间,不包含密码或敏感资料;退出时把 `jti` 写入 Redis 至令牌自然过期。 +- Policy 至少包括 `BuyerOnly`、`MerchantOnly`、`AdminOnly`。 +- 所有资源查询同时校验资源归属,防止水平越权。 +- EF Core 参数化查询,禁止拼接 SQL;手写统计 SQL 也必须参数化。 +- Secret 通过环境变量或 Secret 文件注入,仓库只提交 `.env.example`。 +- 日志和 Trace 不记录密码、完整 Token、数据库连接密码和敏感请求体。 + +## 九、错误处理与 API 约定 + +- 实行接口文档先行:端点开发前先确定 OpenAPI 契约,评审通过后再实现;开发和联调环境通过 Swagger UI 查看与调试接口。 +- 成功响应使用统一业务包装结构;创建资源返回 201,删除成功返回 204 或统一成功响应,最终以接口文档为准。 +- 失败响应使用 `application/problem+json`,包含 HTTP 状态、稳定业务错误码和 `traceId`。 +- 参数错误 400、未认证 401、无权限 403、资源不存在 404、状态冲突 409、服务器错误 500。 +- 全局异常处理不得向客户端返回堆栈、SQL 或内部路径。 + +## 十、可观测性与健康检查 + +### 10.1 遥测 + +- Log:Serilog 输出结构化日志,携带 `traceId`、用户 ID(允许时)、模块和业务标识。 +- Trace:OpenTelemetry 采集 HTTP、EF Core、Redis、RabbitMQ 关键调用链路。 +- Metric:OpenTelemetry 采集请求耗时和错误率、订单创建结果、消息积压和消费失败。 +- Serilog 是应用日志入口,OpenTelemetry 负责遥测标准化、上下文关联和导出,不以 Trace/Metric 替代日志记录。 +- 本期只建设运行诊断日志与遥测,不实现“谁在什么时间修改了什么”的后台操作审计功能,不新增审计日志表、管理页面或审计查询接口。 + +### 10.2 健康端点 + +| 端点 | 用途 | 检查内容 | +|---|---|---| +| `/health/live` | 存活检查 | 进程能够响应 | +| `/health/ready` | 就绪检查 | PostgreSQL 及当前启用的关键依赖可用 | + +Redis、RabbitMQ 或对象存储未被当前阶段启用时,不应错误地阻塞 API 就绪状态。 + +## 十一、环境与部署 + +| 环境 | 用途 | 规划 | +|---|---|---| +| 本地开发 | 单人开发和调试 | Aspire 启动 API、Worker 和所需基础设施;前端由 Vite 启动 | +| 集成验证 | `dev` 分支集成 | Docker Compose 单实例优先,执行接口和 Playwright 测试 | +| 演示/发布 | `master` 稳定版本 | Docker Compose + Nginx;C10 必须启动至少 2 个 API 实例 | + +`dev` 和 `master` 表示代码成熟度,不等同于具体服务器环境。发布使用同一 Commit 构建的版本化镜像。 + +## 十二、测试策略 + +| 测试层级 | 重点 | +|---|---| +| Domain 单元测试 | 订单状态、金额、库存、支付幂等等纯业务规则 | +| Application 单元测试 | 命令/查询的校验与协作行为 | +| API 集成测试 | PostgreSQL 下的真实映射、事务、权限和 ProblemDetails | +| 前端 Vitest | Store、API 转换、核心组件状态 | +| Playwright | 买家浏览加购下单支付、商家发货与售后、管理员账号治理三条主流程 | +| 选做流程 | 评价晒图、收藏/历史、消息已读、售后申请审核的正常与越权场景 | +| 挑战验证 | C01/C03/C04/C06/C07/C08 的并发、边界、乱序、断线和性能脚本 | +| 部署验证 | C10 Compose、Health Check、负载均衡、WebSocket 和单实例故障演示 | + +## 十三、代码规范 -### 3.1 前端模块结构 -(页面/路由划分、公共组件、状态管理方案) +- C# 类型和公开成员使用 PascalCase,局部变量和参数使用 camelCase,异步方法以 `Async` 结尾。 +- TypeScript 变量和函数使用 camelCase,Vue 组件使用 PascalCase。 +- 数据库表、字段、索引使用 snake_case。 +- API 路径使用小写复数名词和连字符,不在路径中使用动词式 RPC 命名,状态动作除外。 +- 一个任务一个短生命周期分支,具体遵循 [Git 团队协作流程](Git团队协作流程.md)。 +- 禁止创建无业务价值的通用仓储、基类、事件或映射层。 -### 3.2 后端模块结构 -(分层结构:控制层 / 服务层 / 数据访问层,各业务模块划分) +## 十四、六人架构职责 -## 四、关键设计决策 +| 成员 | 架构责任边界 | 对应必做 | 主要选做/挑战 | 必须与谁联调 | +|---|---|---|---|---| +| 成员 A(姓名待填) | Identity、认证授权、资料地址、账号状态、收藏和浏览历史 | F01~F03、F13 | X02 | F 的平台能力、B 的商品数据 | +| 成员 B(姓名待填) | Catalog、Review、Search、图片和商品缓存失效规则 | F04~F06、F11 | X01、C04;协作 C07 | C 的库存入口、F 的缓存平台 | +| 成员 C(姓名待填) | Cart、库存条件扣减、Seckill 和防超卖事务 | F07 | C01 | B 的商品、D 的订单创建 | +| 成员 D(姓名待填) | Ordering、订单快照、取消、商家履约和超时规则 | F08、F09、F12 | C03 | C 的库存、E 的支付状态、F 的 Worker | +| 成员 E(姓名待填) | Payment、AfterSales、支付回调、退款状态和对账 | F10 | X04、C08 | D 的订单状态、F 的可靠消息 | +| 成员 F(姓名待填) | 解决方案骨架、模块装配、公共配置、Messaging、SignalR、Redis、RabbitMQ、Outbox、Worker、AppHost、Compose/Nginx | M00 公共基建与集成(内部 P0,支撑 F01~F13) | X03、C06、C07、C10 | 全员 | -| 决策点 | 方案 | 备选方案 | 选择理由 | -|--------|------|----------|----------| -| 登录鉴权方式 | 例:Token | Session | | -| 图片存储方式 | | | | -| 支付模拟方案 | | | | +本节只定义后端架构边界,不分配测试岗位或测试任务;相关责任由测试计划单独确定。M00 负责组合根与模块集成,不拥有其他模块的领域规则和业务数据。罗皓晨对应的成员编号待团队确认。 -## 五、环境规划 +## 十五、分阶段实施 -| 环境 | 用途 | 地址/说明 | -|------|------|-----------| -| 开发环境 | 本地开发 | | -| 演示/生产环境 | 验收演示 | | +| 阶段 | 必须完成 | 暂不阻塞核心闭环 | +|---|---|---| +| 第一阶段 | Vue、API、PostgreSQL、EF Core、JWT、Redis、ProblemDetails、核心模块骨架、六人接口边界 | RabbitMQ、SeaweedFS、多实例 | +| 第二阶段 | F01~F13 完整闭环;X01~X04 的数据和 API 契约 | 不影响核心闭环的挑战 UI | +| 第三阶段 | X01~X04 完整页面;C01/C03/C08;自动化测试、Worker、Outbox | C04/C06/C07/C10 联合验收 | +| 第四阶段 | C04/C06/C07/C10、全量回归、压测/边界证据、部署和答辩 | 未被标框选择的新功能 | -## 六、代码规范约定 +## 十六、待确认事项 -(命名规范、目录规范、前后端各自的 Lint/格式化工具约定) +1. UI 组件库及其版本。 +2. PostgreSQL、Redis、RabbitMQ、SeaweedFS、Aspire 的精确版本。 +3. 六人组是否取得指导教师同意,以及罗皓晨与成员 A~F 的角色映射。 +4. C04 分词器与倒排索引最终实现;技术验证后必须更新本文、数据库和接口设计。 +5. RabbitMQ 首条集成事件及其消费者业务价值,建议从订单状态通知开始。 +6. SeaweedFS 启用时间,以及商品图和评价晒图的限制。 +7. 售后状态、模拟退款规则和订单完成机制。 +8. 演示服务器地址、HTTPS 和域名方案。 -- Gitee From 5af255cd132cf4b8339a8127b2518df64ee248a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A1=BE=E6=AC=A3=E6=9C=88?= Date: Wed, 22 Jul 2026 16:52:52 +0800 Subject: [PATCH 002/118] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E6=97=A5=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...2-\351\241\276\346\254\243\346\234\210.md" | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 "reports/daily/20260722-\351\241\276\346\254\243\346\234\210.md" diff --git "a/reports/daily/20260722-\351\241\276\346\254\243\346\234\210.md" "b/reports/daily/20260722-\351\241\276\346\254\243\346\234\210.md" new file mode 100644 index 0000000..d10b94c --- /dev/null +++ "b/reports/daily/20260722-\351\241\276\346\254\243\346\234\210.md" @@ -0,0 +1,33 @@ +# 日报 - 顾欣月 - 2026-07-22 + + + +## 今日完成 + +1. **通读项目要求文档**:阅读 `docs/00-项目要求/` 下全部 3 份文档(项目要求、验收标准、评分标准),明确该项目需求和要求。 +2. **梳理 4 周关键节点**: + - 第 1 周:需求与设计 + - 第 2 周:核心功能开发 + - 第 3 周:购物车/订单/支付 + 选做与挑战模块 + - 第 4 周:测试、部署、答辩 +3. **参与完成《需求规格说明书》初稿**:基于 `docs/01-需求文档/需求规格说明书.md` 模板,完成初稿 +4. **参加小组分工会议**:按"模块负责制"认领模块包,结合难度权重细分为6 人分工表,敲定技术栈。 + + + +## 遇到的问题 + +暂无 + + + +## 明日计划 + +1. 完善《需求规格说明书》,本周完全确定。 +1. 按照自己负责的模块做计划,应该从大框架做起。 + + + +## 今日工时 + +约 6小时(上午 8:00–11:20 通读文档 + 梳理节点;下午 14:30–17:30 编写需求文档 + 起草分工方案) \ No newline at end of file -- Gitee From fb3e6562dce5ba2366bc3f8fca814fd2545ca606 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Wed, 22 Jul 2026 17:01:18 +0800 Subject: [PATCH 003/118] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E7=BD=97?= =?UTF-8?q?=E7=9A=93=E6=99=A8=202026-07-22=20=E6=97=A5=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...2-\347\275\227\347\232\223\346\231\250.md" | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 "reports/daily/20260722-\347\275\227\347\232\223\346\231\250.md" diff --git "a/reports/daily/20260722-\347\275\227\347\232\223\346\231\250.md" "b/reports/daily/20260722-\347\275\227\347\232\223\346\231\250.md" new file mode 100644 index 0000000..2222f78 --- /dev/null +++ "b/reports/daily/20260722-\347\275\227\347\232\223\346\231\250.md" @@ -0,0 +1,24 @@ +# 日报 - 罗皓晨 - 2026-07-22 + +## 今日完成 + +1. 完成电子商城需求规格说明书 v0.1,明确游客、买家、商家和管理员四类角色,补充核心购物与订单取消流程,并细化 F01~F13 必做功能、4 项选做功能和 7 项挑战模块的业务规则、异常流程及验收要点。 +2. 完善系统架构设计,确定 Vue 3 + TypeScript 前端、.NET 10 Web API、EF Core 10 + PostgreSQL 后端技术路线,以及 Redis、RabbitMQ、Outbox、Worker、Aspire、Docker Compose 和 Nginx 等组件的职责边界;补充模块化单体结构、关键业务设计、安全、测试和部署方案。 +3. 新增 Git 团队协作流程文档,明确 `master`、`dev` 和短生命周期任务分支的用途,补充分支创建、提交、同步、PR、交叉 Code Review、合并及发布流程,并同步更新根 README 中的技术栈与协作规范。 +4. 完善验收标准,确定本组 4 项选做功能和 7 项挑战模块的具体验收口径。以上内容已通过提交 `ace466f`(`docs: 完善项目需求、架构与协作规范`)提交,并通过合并记录 `c39e0cc` 进入 `dev` 分支。 + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式/求助对象 | +|----------|----------|-------------------| +| 原始需求和设计文档大部分为占位模板,功能边界、角色权限、架构职责和验收方式不够明确 | 已解决 | 对照项目要求和验收标准完成需求与架构初稿,并将功能点、异常规则和验收口径写入对应文档 | +| 六名成员与架构文档中成员 A~F 的对应关系,以及部分基础设施和搜索方案的精确版本尚未确认 | 未解决 | 明日与组长、组员及指导教师确认人员分工、组件版本和 C04 搜索技术方案 | + +## 明日计划 + +1. 与团队确认六人分工,将成员 A~F 替换为实际姓名,明确本人负责模块及其数据库、接口、页面和联调边界。 +2. 根据需求规格和系统架构继续补充数据库设计与接口设计,优先确定核心实体、表关系、状态流转和 F01~F13 的 API 契约。 + +## 今日工时 + +约 2 小时 -- Gitee From 1d24b689ad8d7fee03856084843e757de76b820d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=B5=B7=E6=B4=8B?= <2040454617@qq.com> Date: Wed, 22 Jul 2026 17:55:06 +0800 Subject: [PATCH 004/118] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E6=97=A5=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...2-\345\274\240\346\265\267\346\264\213.md" | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 "reports/daily/20260722-\345\274\240\346\265\267\346\264\213.md" diff --git "a/reports/daily/20260722-\345\274\240\346\265\267\346\264\213.md" "b/reports/daily/20260722-\345\274\240\346\265\267\346\264\213.md" new file mode 100644 index 0000000..2098a24 --- /dev/null +++ "b/reports/daily/20260722-\345\274\240\346\265\267\346\264\213.md" @@ -0,0 +1,34 @@ +# 日报 - 张海洋 - 2026-07-22 + +## 今日完成 + +1. 通读 [`docs/02-设计文档/Git团队协作流程.md`](../docs/02-设计文档/Git团队协作流程.md) 全文,掌握本组协作规范的整体框架与全部细节。 +2. 理解并记录核心规则:仓库仅保留 `master`(稳定发布)和 `dev`(日常集成)两个长期分支,二者均禁止直接 Push、Force Push 或改写历史;所有任务通过短生命周期任务分支 + PR 合入 `dev`,合并后立即删除。 +3. 掌握分支命名规范:格式 `<类型>/<模块>-<任务>`,例如 `feature/auth-login`、`fix/order-duplicate-submit`;模块名统一使用 `auth`、`user`、`catalog`、`cart`、`order`、`payment`、`admin`、`storage`、`infra`、`deploy`、`docs`;明确禁止 `develop`、`test`、`update`、个人姓名等含义不清的分支名。 +4. 掌握提交信息规范:遵循 Conventional Commits(`feat`/`fix`/`refactor`/`docs`/`test`/`chore`),可选 `()` 限定模块,描述必须说明实际改动,禁止使用 "update"、"改一下"、"测试" 等模糊内容,一个 Commit 只表达一个独立修改目的。 +5. 整理任务拆分原则:每个分支只包含一个需求点或缺陷,原则上 1~2 个工作日内完成并开 PR;超过 2 天应继续拆分,无法拆分时尽早创建 Draft PR 暴露风险;分支不是文件夹,仍在 `frontend/`、`backend/`、`docs/` 下修改。 +6. 梳理工前流程:开发前先 `git status` 确认工作区干净,必要时使用 `git stash push -u -m "wip: …"`;从最新 `dev` 创建分支,命令为 `git fetch origin` → `git switch dev` → `git merge --ff-only origin/dev` → `git switch -c feature/<模块>-<任务>`,首次推送用 `git push -u origin `。 +7. 记住提交与推送流程:`git status` → `git diff` → 选择性 `git add` → `git diff --cached` 复核 → `git commit`;严禁未检查就执行 `git add .`;提交中不得包含调试文件、生成目录、密码或密钥。 +8. 学习 AI 辅助开发红线:AI 只处理当前分支对应的一个任务,不得自行扩大范围;不允许 AI 修改 `docs/00-项目要求/`,不允许擅自更换技术栈;AI 给出的构建/测试结果必须来自真实执行;模块负责人必须亲自验证并能在答辩中解释实现细节。 +9. 学习 PR 创建与合并要求:开 PR 前需本地完成 `npm ci / lint / type-check / test / build`、`dotnet restore / build / test`、`npx playwright test`、`docker compose config` 等验证;同步最新 `dev` 时个人独占分支用 Rebase + `git push --force-with-lease`,共享分支用 Merge;PR 描述必须含关联任务、修改内容、验收条件、验证结果、影响范围、风险与回滚、已知问题;任务 PR 使用 Squash and Merge,发布 PR 使用普通 Merge Commit。 +10. 学习交叉 Code Review 流程:每个 PR 至少一名其他成员批准,相邻模块负责人优先;PR 作者不能作为自己的唯一审批人;审查覆盖需求匹配度、链路完整性、接口契约、边界与并发、Migration 安全性、敏感信息、测试覆盖与验证真实性;登录鉴权、订单/支付、库存扣减、Docker 配置属重点审查项。 +11. 学习 CI 与分支保护要点:仓库管理员须配置禁止直推 `master/dev`、强制 PR、至少一名批准、阻塞意见必解决、检查全部通过、合并后自动删除远程任务分支;CI 尚未建立时不得省略人工构建与测试记录。 +12. 学习合并后清理与回滚:任务合并后立即同步 `dev` 并删除本地与远程分支;未发布问题从 `dev` 创建 `revert/*` 紧急回滚,已发布问题从 `master` 创建;高风险命令 `git reset --hard`、`git clean -fd`、`git branch -D`、远程分支删除执行前必须确认目标与可恢复性。 + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式/求助对象 | +|----------|----------|-------------------| +| README 第六节 Git 协作规范与 `Git团队协作流程.md` 存在内容重叠,需要区分"对外宣称的简要规则"和"对内执行的完整流程" | 已解决 | 将 README 作为对外速览,正文详细规范以 `Git团队协作流程.md` 为准,并已记录"冲突时以教师发布的项目要求、验收标准和评分标准为准"这条优先级 | +| 分支命名中模块范围(如 `auth`、`catalog`、`order`)与本组需求文档中模块编号(M01~M17)不是一一对应,担心后续创建分支时混淆 | 未解决 | 明日与组长、罗皓晨对照确认分支 `scope` 与需求模块编号 M01~M17 的映射表,避免命名漂移 | +| 当分支需要修改公共路由、`Program.cs`、数据库上下文或 Docker 配置时能否顺手重构无关代码,文档只是说"必须说明原因",缺少具体示例 | 未解决 | 明日拉组长或罗皓晨一起看一两个真实场景,明确"允许的最小改动范围"边界 | + +## 明日计划 + +1. 与组长、罗皓晨对齐分工表,把需求文档中"成员 A~F"替换为实际姓名,确认本人负责模块对应的分支 `scope` 与需求编号。 +2. 在本地按 `Git团队协作流程.md` 第五章演练一遍"从最新 `dev` 拉取 → 创建 `feature/<模块>-<任务>` → 提交 → 推送 → 开 PR"的标准动作,验证命令环境为 PowerShell 是否与文档示例一致。 +3. 与负责 M01 用户与鉴权或 M02 分类与商品的组员结对,约定交叉 Code Review 的节奏和审查清单中重点项的关注方式。 + +## 今日工时 + +约 3 小时(上午 9:30–11:00 通读全文并标记关键规则;下午 14:00–15:30 整理分支命名、提交规范、PR 与回滚要点;晚上 20:30–21:00 复盘并记录问题与明日行动项) \ No newline at end of file -- Gitee From 689b9ad571560dd15115a6555d761a84cfc5ae27 Mon Sep 17 00:00:00 2001 From: FISH SOUP Date: Wed, 22 Jul 2026 18:09:44 +0800 Subject: [PATCH 005/118] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E5=94=90?= =?UTF-8?q?=E5=AE=87=E6=98=8A=202026-07-22=20=E6=97=A5=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...2-\345\224\220\345\256\207\346\230\212.md" | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 "reports/daily/20260722-\345\224\220\345\256\207\346\230\212.md" diff --git "a/reports/daily/20260722-\345\224\220\345\256\207\346\230\212.md" "b/reports/daily/20260722-\345\224\220\345\256\207\346\230\212.md" new file mode 100644 index 0000000..184e430 --- /dev/null +++ "b/reports/daily/20260722-\345\224\220\345\256\207\346\230\212.md" @@ -0,0 +1,26 @@ +# 日报 - 唐宇昊 - 2026-07-22 + +## 今日完成 + +1. **排查并解决本地 Git 工作目录锁问题**:本地仓库 `.git/index.lock` 反复残留且 git 报告 `unable to unlink` 警告;确认无残留 git 进程、ACL 权限正常后删除孤儿锁文件,并通过 `git status` 验证工作目录恢复正常。 +2. **复核 Gitee HTTPS 认证链路**:远程仓库为 `https://gitee.com/grade24-fullstack-class1/eshop-class1-group7.git`(HTTPS),确认 `credential.helper=manager` + `credential.https://gitee.com.provider=generic` 已生效;通过 `cmdkey /list`、`git credential-manager get` 和 `git ls-remote` 三步验证凭据可用、网络可达。 +3. **修复远程拉取失败的根因**:补全 Git 作者信息为“唐宇昊 ``”,并确认 `GIT_TERMINAL_PROMPT=0` 下 `git fetch origin` 能成功更新远程分支 `dev`(本地 `dev` 已与 `origin/dev` 对齐)。 +4. **梳理 daily git report 任务流程**:阅读 `reports/daily/README.md` 中的提交要求和模板,并对照组内同学(罗皓晨、顾欣月)已提交的日报样例,确认本周日报产出节奏与命名规范。 + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式/求助对象 | +|----------|----------|-------------------| +| 本地仓库 `.git/index.lock` 多次残留,git 操作报 `unable to unlink`,疑似环境权限异常 | 已定位根因 | 实际为孤儿锁(非权限问题),当前用户对 `.git` 有完全控制权;删除后 `git status` 与 `git fetch` 均恢复正常;后续如再次出现,先确认无 git 进程后再删除 | +| `git fetch` 报 `could not read Username for 'https://gitee.com': No such device or address` | 已解决 | Windows 凭据管理器中已存在 `target=git:https://gitee.com` 条目(用户名 `tang19168273484@qq.com`),但 bash 非交互环境下不会弹出 GUI 提示框,需在执行 fetch/pull/push 前设置 `GIT_TERMINAL_PROMPT=0` 让其直接走缓存凭证,或使用 Git Credential Manager GUI 登录 | +| 远程 `dev` 是否有最新变化此前无法确认(fetch 失败导致本地落后风险) | 已解决 | 重新 fetch 后确认本地 `dev` 与 `origin/dev` 同步,已看到远程分支 `docs/daily-reports` 的最新提交 `1d24b68` | + +## 明日计划 + +1. 按 Cowork 计划继续推进 daily git report 任务:每日 18:00 前在 `reports/daily/` 提交当天的 `YYYYMMDD-唐宇昊.md`,并走“提交 → push → 发起 PR 合并到 dev”的标准流程。 +2. 把今天梳理出的两条经验(孤儿锁清理、`GIT_TERMINAL_PROMPT=0` + cached credential)写入 `docs/02-设计文档/Git团队协作流程.md`,作为本组 Git 环境排障小贴士。 +3. 等待组内六人分工确认后,开始负责模块的接口与页面设计。 + +## 今日工时 + +约 2 小时 \ No newline at end of file -- Gitee From e0ec5a36690dfc788b32f9321d2d754ad335f911 Mon Sep 17 00:00:00 2001 From: FISH SOUP Date: Wed, 22 Jul 2026 18:11:35 +0800 Subject: [PATCH 006/118] =?UTF-8?q?Revert=20"docs:=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=94=90=E5=AE=87=E6=98=8A=202026-07-22=20=E6=97=A5=E6=8A=A5"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 689b9ad571560dd15115a6555d761a84cfc5ae27. --- ...2-\345\224\220\345\256\207\346\230\212.md" | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 "reports/daily/20260722-\345\224\220\345\256\207\346\230\212.md" diff --git "a/reports/daily/20260722-\345\224\220\345\256\207\346\230\212.md" "b/reports/daily/20260722-\345\224\220\345\256\207\346\230\212.md" deleted file mode 100644 index 184e430..0000000 --- "a/reports/daily/20260722-\345\224\220\345\256\207\346\230\212.md" +++ /dev/null @@ -1,26 +0,0 @@ -# 日报 - 唐宇昊 - 2026-07-22 - -## 今日完成 - -1. **排查并解决本地 Git 工作目录锁问题**:本地仓库 `.git/index.lock` 反复残留且 git 报告 `unable to unlink` 警告;确认无残留 git 进程、ACL 权限正常后删除孤儿锁文件,并通过 `git status` 验证工作目录恢复正常。 -2. **复核 Gitee HTTPS 认证链路**:远程仓库为 `https://gitee.com/grade24-fullstack-class1/eshop-class1-group7.git`(HTTPS),确认 `credential.helper=manager` + `credential.https://gitee.com.provider=generic` 已生效;通过 `cmdkey /list`、`git credential-manager get` 和 `git ls-remote` 三步验证凭据可用、网络可达。 -3. **修复远程拉取失败的根因**:补全 Git 作者信息为“唐宇昊 ``”,并确认 `GIT_TERMINAL_PROMPT=0` 下 `git fetch origin` 能成功更新远程分支 `dev`(本地 `dev` 已与 `origin/dev` 对齐)。 -4. **梳理 daily git report 任务流程**:阅读 `reports/daily/README.md` 中的提交要求和模板,并对照组内同学(罗皓晨、顾欣月)已提交的日报样例,确认本周日报产出节奏与命名规范。 - -## 遇到的问题 - -| 问题描述 | 解决状态 | 解决方式/求助对象 | -|----------|----------|-------------------| -| 本地仓库 `.git/index.lock` 多次残留,git 操作报 `unable to unlink`,疑似环境权限异常 | 已定位根因 | 实际为孤儿锁(非权限问题),当前用户对 `.git` 有完全控制权;删除后 `git status` 与 `git fetch` 均恢复正常;后续如再次出现,先确认无 git 进程后再删除 | -| `git fetch` 报 `could not read Username for 'https://gitee.com': No such device or address` | 已解决 | Windows 凭据管理器中已存在 `target=git:https://gitee.com` 条目(用户名 `tang19168273484@qq.com`),但 bash 非交互环境下不会弹出 GUI 提示框,需在执行 fetch/pull/push 前设置 `GIT_TERMINAL_PROMPT=0` 让其直接走缓存凭证,或使用 Git Credential Manager GUI 登录 | -| 远程 `dev` 是否有最新变化此前无法确认(fetch 失败导致本地落后风险) | 已解决 | 重新 fetch 后确认本地 `dev` 与 `origin/dev` 同步,已看到远程分支 `docs/daily-reports` 的最新提交 `1d24b68` | - -## 明日计划 - -1. 按 Cowork 计划继续推进 daily git report 任务:每日 18:00 前在 `reports/daily/` 提交当天的 `YYYYMMDD-唐宇昊.md`,并走“提交 → push → 发起 PR 合并到 dev”的标准流程。 -2. 把今天梳理出的两条经验(孤儿锁清理、`GIT_TERMINAL_PROMPT=0` + cached credential)写入 `docs/02-设计文档/Git团队协作流程.md`,作为本组 Git 环境排障小贴士。 -3. 等待组内六人分工确认后,开始负责模块的接口与页面设计。 - -## 今日工时 - -约 2 小时 \ No newline at end of file -- Gitee From 6cca10095b0a4ce5d9bcbf18a31f78c919330446 Mon Sep 17 00:00:00 2001 From: FISH SOUP Date: Wed, 22 Jul 2026 18:12:18 +0800 Subject: [PATCH 007/118] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E5=94=90?= =?UTF-8?q?=E5=AE=87=E6=98=8A=202026-07-22=20=E6=97=A5=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...2-\345\224\220\345\256\207\346\230\212.md" | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 "reports/daily/20260722-\345\224\220\345\256\207\346\230\212.md" diff --git "a/reports/daily/20260722-\345\224\220\345\256\207\346\230\212.md" "b/reports/daily/20260722-\345\224\220\345\256\207\346\230\212.md" new file mode 100644 index 0000000..75cd141 --- /dev/null +++ "b/reports/daily/20260722-\345\224\220\345\256\207\346\230\212.md" @@ -0,0 +1,28 @@ +# 日报 - 唐宇昊 - 2026-07-22 + +## 今日完成 + +1. **排查并解决本地 Git 工作目录锁问题**:本地仓库 `.git/index.lock` 反复残留且 git 报告 `unable to unlink` 警告;确认无残留 git 进程、ACL 权限正常后清理孤儿锁文件,并通过 `git status` 验证工作目录恢复正常。 +2. **复核 Gitee HTTPS 认证链路**:远程仓库为 `https://gitee.com/grade24-fullstack-class1/eshop-class1-group7.git`(HTTPS),确认 `credential.helper=manager` + `credential.https://gitee.com.provider=generic` 已生效;通过 `cmdkey /list`、`git credential-manager get` 和 `git ls-remote` 三步验证凭据可用、网络可达。 +3. **修复远程拉取失败的根因**:补全 Git 作者信息为“唐宇昊 ``”,并确认 `GIT_TERMINAL_PROMPT=0` 下 `git fetch origin` 能成功更新远程分支(本地已与 `origin/dev` 对齐)。 +4. **梳理 daily git report 任务流程**:阅读 `reports/daily/README.md` 中的提交要求和模板,并对照组内同学(罗皓晨、顾欣月)已提交的日报样例,确认本周日报产出节奏与命名规范。 +5. **首次提交日报**:基于规范生成 `reports/daily/20260722-唐宇昊.md`,并按团队约定推送至 `docs/daily-reports` 分支(已 `revert` 误推 `dev` 的提交,恢复 `dev` 原始状态)。 + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式/求助对象 | +|----------|----------|-------------------| +| 本地仓库 `.git/index.lock` 多次残留,git 操作报 `unable to unlink`,疑似环境权限异常 | 已定位根因 | 实际为孤儿锁(非权限问题),当前用户对 `.git` 有完全控制权;删除后 `git status` 与 `git fetch` 均恢复正常;后续如再次出现,先确认无 git 进程后再清理 | +| `git fetch` 报 `could not read Username for 'https://gitee.com': No such device or address` | 已解决 | Windows 凭据管理器中已存在 `target=git:https://gitee.com` 条目(用户名 `tang19168273484@qq.com`),但 bash 非交互环境下不会弹出 GUI 提示框,需在执行 fetch/pull/push 前设置 `GIT_TERMINAL_PROMPT=0` 让其直接走缓存凭证,或使用 Git Credential Manager GUI 登录 | +| 远程 `dev` 是否有最新变化此前无法确认(fetch 失败导致本地落后风险) | 已解决 | 重新 fetch 后确认本地 `dev` 与 `origin/dev` 同步,已看到远程分支 `docs/daily-reports` 的最新提交 `1d24b68` | +| 首次推送时误把日报 commit 推到了 `dev` 分支 | 已解决 | 用 `git revert` 生成反向提交推到 `dev`(保留历史、无强推),随后在 `docs/daily-reports` 分支重新生成并提交;后续推送前先确认 `git status` 中分支名符合团队约定 | + +## 明日计划 + +1. 按 Cowork 计划继续推进 daily git report 任务:每日 18:00 前在 `reports/daily/` 提交当天的 `YYYYMMDD-唐宇昊.md`,并走“提交 → push 到 `docs/daily-reports` → 发起 PR 合并到 dev”的标准流程。 +2. 把今天梳理出的经验(孤儿锁处理、`GIT_TERMINAL_PROMPT=0` + cached credential)写入 `docs/02-设计文档/Git团队协作流程.md`,作为本组 Git 环境排障小贴士。 +3. 等待组内六人分工确认后,开始负责模块的接口与页面设计。 + +## 今日工时 + +约 2 小时 \ No newline at end of file -- Gitee From 1148ec0b78cad9377b935c89900ce6fa41b5eb83 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Thu, 23 Jul 2026 07:59:19 +0800 Subject: [PATCH 008/118] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E5=88=86?= =?UTF-8?q?=E6=94=AF=E5=91=BD=E5=90=8D=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- ...17\344\275\234\346\265\201\347\250\213.md" | 54 ++++++++++--------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 23084f2..7be456e 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ ## 六、Git 协作规范 1. `master` 是稳定发布分支,`dev` 是日常集成分支;两个长期分支均禁止直接 push。 -2. 采用短生命周期任务分支:功能开发使用 `feature/<模块>-<任务>`,从最新 `dev` 创建,通过 PR 合入 `dev`,合并后立即删除。 +2. 采用短生命周期任务分支:格式为 `<类型>/<模块>-<任务>-<姓名拼音首字母>`,例如 `feature/auth-login-lhc`;从最新 `dev` 创建,通过 PR 合入 `dev`,合并后立即删除。 3. 提交信息格式:`: <描述>`,type 取值:`feat` `fix` `refactor` `docs` `test` `chore`。 4. **每人每天至少一次有效提交**,提交记录将作为个人考核依据。 5. **交叉 Code Review**:每人的功能分支由相邻模块负责人审查后方可合并(审查人在合并说明中留名)。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/Git\345\233\242\351\230\237\345\215\217\344\275\234\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/Git\345\233\242\351\230\237\345\215\217\344\275\234\346\265\201\347\250\213.md" index 4ad9b80..7a22cd2 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/Git\345\233\242\351\230\237\345\215\217\344\275\234\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/Git\345\233\242\351\230\237\345\215\217\344\275\234\346\265\201\347\250\213.md" @@ -13,7 +13,7 @@ 2. 禁止直接在 `master` 或 `dev` 上开发、Push、Force Push 或改写历史。 3. 一个任务对应一个短生命周期分支和一个 PR/MR。 4. 任务分支必须从最新 `dev` 创建,通过 PR/MR 合入 `dev`,合并后立即删除,不得重复使用。 -5. 分支按业务任务划分,不按成员创建长期个人分支。 +5. 分支按业务任务划分,并在末尾添加负责人姓名拼音首字母用于区分;姓名后缀不代表可以创建长期个人分支。 6. 每名成员按模块负责制完成数据库、后端接口、前端页面和测试的纵向链路。 7. 所有任务修改必须经过 CI 和至少一名其他成员的交叉 Code Review 后才能进入 `dev`。 8. 阶段验收或正式发布时,由 `dev` 向 `master` 创建发布 PR/MR,通过完整验证和审查后合并。 @@ -33,35 +33,36 @@ - 预计超过 2 个工作日的任务,应继续拆分;暂时无法拆分时应尽早创建 Draft PR 暴露风险。 - 分支不得长期落后于 `dev`,创建 PR 前必须同步最新集成分支。 - PR 合并或关闭后删除远程分支和本地分支。 -- 禁止使用 `dev-张三`、`dev-lisi` 等长期个人分支。 +- 禁止使用 `dev-lhc`、`dev-tyh` 等长期个人分支;姓名拼音首字母只能作为短生命周期任务分支的末尾标识。 -分支不是项目文件夹。创建 `feature/auth-login` 不代表要创建 `feature/auth-login/` 目录;成员仍然修改 `frontend/`、`backend/` 和 `docs/` 中与任务相关的文件。 +分支不是项目文件夹。创建 `feature/auth-login-lhc` 不代表要创建同名目录;成员仍然修改 `frontend/`、`backend/` 和 `docs/` 中与任务相关的文件。 ## 三、Git 命名规范 ### 1. 通用规则 - 分支名只使用小写英文字母、数字、正斜杠 `/` 和连字符 `-`。 -- 禁止在分支名中使用中文、空格、下划线或成员姓名。 +- 禁止在分支名中使用中文、空格或下划线;负责人姓名必须使用拼音首字母小写形式。 - 类型、模块和任务使用英文,多个单词以连字符分隔。 +- 姓名后缀按中文姓名顺序取每个汉字的拼音首字母,并统一使用小写,不添加声调、空格或分隔符,例如罗皓晨使用 `lhc`。 - 名称应能直接表达修改目的,避免 `update`、`temp`、`new`、`test1` 等模糊词。 -- 本地分支与远程分支保持同名,不另加成员缩写。 -- 分支名建议不超过 60 个字符;任务名称过长时应优先拆小任务。 +- 本地分支与远程分支保持同名,不在推送时临时增删姓名后缀。 +- 分支名建议不超过 80 个字符;任务名称过长时应优先拆小任务。 ### 2. 分支命名 -分支名统一使用小写英文和连字符,格式为 `<类型>/<模块>-<任务>`。 +除 `master`、`dev` 外,任务分支统一使用格式 `<类型>/<模块>-<任务>-<姓名拼音首字母>`。 | 修改目的 | 分支格式 | 示例 | |---|---|---| -| 新功能 | `feature/*` | `feature/auth-login` | -| 缺陷修复 | `fix/*` | `fix/order-duplicate-submit` | -| 测试 | `test/*` | `test/cart-checkout-e2e` | -| 文档 | `docs/*` | `docs/update-api-contract` | -| 重构 | `refactor/*` | `refactor/order-status-rules` | -| 工程配置 | `chore/*` | `chore/add-docker-compose` | -| CI/CD | `ci/*` | `ci/add-pull-request-checks` | -| 紧急回滚 | `revert/*` | `revert/order-payment` | +| 新功能 | `feature/<模块>-<任务>-<姓名拼音首字母>` | `feature/auth-login-lhc` | +| 缺陷修复 | `fix/<模块>-<任务>-<姓名拼音首字母>` | `fix/order-duplicate-submit-lhc` | +| 测试 | `test/<模块>-<任务>-<姓名拼音首字母>` | `test/cart-checkout-e2e-lhc` | +| 文档 | `docs/<模块>-<任务>-<姓名拼音首字母>` | `docs/update-project-docs-lhc` | +| 重构 | `refactor/<模块>-<任务>-<姓名拼音首字母>` | `refactor/order-status-rules-lhc` | +| 工程配置 | `chore/<模块>-<任务>-<姓名拼音首字母>` | `chore/deploy-add-docker-compose-lhc` | +| CI/CD | `ci/<模块>-<任务>-<姓名拼音首字母>` | `ci/infra-add-pr-checks-lhc` | +| 紧急回滚 | `revert/<模块>-<任务>-<姓名拼音首字母>` | `revert/order-payment-lhc` | ### 3. 模块名称 @@ -90,6 +91,7 @@ update my-branch zhangsan feature/all +feature/auth-login-lh ``` ### 4. Commit 命名 @@ -156,7 +158,7 @@ v1.0.0 最终验收发布版本 正确示例: ```text -feature/auth-register +feature/auth-register-lhc ├─ 用户表或数据库迁移 ├─ 注册领域/应用逻辑 ├─ 注册 API @@ -210,13 +212,13 @@ git stash push -u -m "wip: auth login" git fetch origin git switch dev git merge --ff-only origin/dev -git switch -c feature/auth-login +git switch -c feature/auth-login-lhc ``` 第一次推送会自动创建远程任务分支: ```powershell -git push -u origin feature/auth-login +git push -u origin feature/auth-login-lhc ``` 后续推送只需: @@ -326,7 +328,7 @@ git fetch origin git rebase origin/dev ``` -已经推送过的个人分支在 Rebase 后使用: +已经推送过的任务分支在 Rebase 后使用: ```powershell git push --force-with-lease @@ -354,7 +356,7 @@ git rebase --abort 日常任务 PR 的源分支必须是任务分支,目标分支必须是 `dev`: ```text -feature/auth-login → dev +feature/auth-login-lhc → dev ``` PR 标题遵循本文第三节,与最终提交使用一致格式,例如: @@ -457,19 +459,19 @@ git merge --ff-only origin/dev 删除已完成的本地分支: ```powershell -git branch -d feature/auth-login +git branch -d feature/auth-login-lhc ``` 任务 PR 使用 Squash Merge 后,Git 可能无法识别传统合并关系。只有在确认 PR 已合并、代码已进入 `dev` 且分支不再需要后,才可执行: ```powershell -git branch -D feature/auth-login +git branch -D feature/auth-login-lhc ``` 远程分支应由平台自动删除;未自动删除时,在确认 PR 已合并后执行: ```powershell -git push origin --delete feature/auth-login +git push origin --delete feature/auth-login-lhc ``` 下一个任务必须重新从最新 `dev` 创建新分支。 @@ -526,7 +528,7 @@ git status git fetch origin git switch dev git merge --ff-only origin/dev -git switch -c feature/模块-任务 +git switch -c feature/模块-任务-姓名拼音首字母 ``` 开发提交: @@ -542,7 +544,7 @@ git commit -m "feat(module): describe the change" 首次推送并创建 PR: ```powershell -git push -u origin feature/模块-任务 +git push -u origin feature/模块-任务-姓名拼音首字母 ``` 任务 PR 合并后: @@ -551,7 +553,7 @@ git push -u origin feature/模块-任务 git switch dev git fetch origin git merge --ff-only origin/dev -git branch -d feature/模块-任务 +git branch -d feature/模块-任务-姓名拼音首字母 ``` ## 十六、禁止事项 -- Gitee From f59903d2d964f375e5bb93aaa6ffd4a89eb1bf8f Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Thu, 23 Jul 2026 08:21:44 +0800 Subject: [PATCH 009/118] =?UTF-8?q?docs:=20=E6=A0=87=E6=B3=A8=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=A8=A1=E5=9D=97=E8=B4=9F=E8=B4=A3=E4=BA=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index a29c982..0045667 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -141,13 +141,13 @@ flowchart LR ## 三、功能需求详述 -### M00 公共基建与集成(内部 P0) +### M00 公共基建与集成(内部 P0)— 罗皓晨 - **描述**:建立统一解决方案骨架、模块注册方式、公共配置和本地运行编排,并将成员 A~E 的业务模块装配为一个可运行的模块化单体。 - **责任边界**:成员 F 负责组合根、公共技术组件和集成清单;各业务负责人提供本模块的注册入口、数据库迁移和公开应用接口,成员 F 不修改其他模块内部领域规则。 - **验收要点**:开发环境可启动 API、Worker 及已启用依赖;F01~F13 对应模块均完成注册并能通过公开接口协作;配置不硬编码密钥;模块集成问题有明确责任人和联调记录。 -### M01-01 用户注册(F01) +### M01-01 用户注册(F01)— 唐宇昊 - **描述**:游客使用用户名、密码和可选手机号创建买家账号。 - **前置条件**:用户未登录;用户名未被注册。 @@ -155,7 +155,7 @@ flowchart LR - **异常流程**:用户名重复、格式非法、密码强度不足或手机号格式错误时拒绝注册并返回明确提示。 - **验收要点**:重复注册被拦截;数据库中不出现明文密码;非法参数不会创建账号。 -### M01-02 用户登录与退出(F02) +### M01-02 用户登录与退出(F02)— 唐宇昊 - **描述**:用户凭用户名和密码登录,系统返回身份令牌;退出后当前令牌不应继续访问受保护资源。 - **前置条件**:账号存在且状态正常。 @@ -163,14 +163,14 @@ flowchart LR - **异常流程**:账号不存在、密码错误、账号被禁用或令牌过期时拒绝访问并返回友好错误。 - **验收要点**:登录态在页面刷新后可恢复;错误密码不泄露账号是否存在;被禁用用户无法登录。 -### M01-03 个人信息与收货地址(F03) +### M01-03 个人信息与收货地址(F03)— 唐宇昊 - **描述**:买家查看和修改个人信息,并对自己的收货地址进行增删改查。 - **业务规则**:地址必须属于当前用户;收件人、手机号、省市区和详细地址必填;每个用户最多一个默认地址;删除默认地址后不自动假定其他地址为默认,除非业务实现明确处理。 - **异常流程**:访问他人地址、地址不存在或字段非法时拒绝操作。 - **验收要点**:完整完成地址 CRUD;不能越权读取或修改他人地址。 -### M02-01 商品列表、分类与搜索(F04、F05) +### M02-01 商品列表、分类与搜索(F04、F05)— 顾欣月 - **描述**:游客和买家分页浏览已上架商品,可按分类筛选并按关键词模糊搜索。 - **查询条件**:`page`、`pageSize`、`categoryId`、`keyword`、排序字段和方向。 @@ -178,21 +178,21 @@ flowchart LR - **异常流程**:分页或排序参数非法时返回参数错误;空结果返回空列表而不是异常。 - **验收要点**:分页总数正确;分类筛选和关键词搜索有结果;下架商品不出现在购物端列表。 -### M02-02 商品详情(F06) +### M02-02 商品详情(F06)— 顾欣月 - **描述**:展示商品名称、主图/图片、描述、价格、库存和分类。 - **业务规则**:购物端只能查看已上架商品;价格与库存以服务端数据为准。 - **异常流程**:商品不存在或已下架时返回资源不存在或不可售提示。 - **验收要点**:图片、价格和库存显示正确;刷新后数据与后台修改结果一致。 -### M03-01 购物车管理(F07) +### M03-01 购物车管理(F07)— 朱惠惠 - **描述**:买家查看购物车、加入商品、修改数量、删除条目和选择结算商品。 - **业务规则**:同一用户和商品只保留一个购物车条目;重复加入时累加数量;数量必须大于 0 且不能超过实时库存;金额由服务端按最新商品价格计算。 - **异常流程**:商品下架、库存不足、数量非法或操作他人购物车时拒绝操作。 - **验收要点**:增删改数量正确;总金额计算正确;不能通过前端篡改价格。 -### M04-01 提交订单(F08) +### M04-01 提交订单(F08)— 韦乾强 - **描述**:买家选择购物车条目和收货地址提交订单。 - **前置条件**:用户已登录;地址属于当前用户;商品已上架且库存充足。 @@ -201,20 +201,20 @@ flowchart LR - **异常流程**:库存不足、地址无效、重复提交或任一商品不可售时整单失败,不产生部分订单。 - **验收要点**:订单与订单项完整;库存扣减正确;失败时事务回滚;重复请求不重复扣库存。 -### M04-02 订单列表与详情(F09) +### M04-02 订单列表与详情(F09)— 韦乾强 - **描述**:买家分页查看自己的订单,并按状态筛选和查看详情。 - **业务规则**:买家只能访问自己的订单;详情包含地址快照、订单项快照、金额和状态时间。 - **验收要点**:列表与详情一致;访问他人订单被拒绝;状态筛选和分页正确。 -### M04-03 取消订单(F09) +### M04-03 取消订单(F09)— 韦乾强 - **描述**:买家取消自己的待支付订单。 - **业务规则**:只有待支付订单可取消;取消与库存回补在同一事务中完成;重复取消不得重复回补库存。 - **异常流程**:已支付、已发货、已完成或已取消订单拒绝取消。 - **验收要点**:状态流转正确;库存只回补一次;并发取消结果一致。 -### M05-01 模拟支付(F10) +### M05-01 模拟支付(F10)— 张海洋 - **描述**:买家对自己的待支付订单执行模拟支付,不接入真实支付渠道。 - **主流程**:创建支付记录 → 模拟成功回调 → 幂等更新支付和订单状态 → 返回支付结果。 @@ -222,44 +222,44 @@ flowchart LR - **异常流程**:订单已取消、已支付、金额不一致或订单不属于当前用户时拒绝支付。 - **验收要点**:支付后状态正确;支付记录可追踪;重复请求结果幂等。 -### M06-01 后台分类与商品管理(F11) +### M06-01 后台分类与商品管理(F11)— 顾欣月 - **描述**:商家维护分类和商品,支持商品增删改查及上下架。 - **业务规则**:商品名称、价格、库存和分类必填;价格不得为负;库存不得为负;有关联订单的商品不做破坏历史的物理删除,优先下架。 - **验收要点**:CRUD 和上下架生效;购物端只看到已上架商品;非商家不能访问。 -### M06-02 后台订单管理(F12) +### M06-02 后台订单管理(F12)— 韦乾强 - **描述**:商家分页查询订单、查看详情并对已支付订单发货。 - **业务规则**:只有已支付订单可以发货;发货后状态变为已发货;重复发货不得重复改变状态。 - **验收要点**:订单查询、状态筛选和发货正常;非法状态流转被拒绝。 -### M06-03 后台用户管理(F13) +### M06-03 后台用户管理(F13)— 唐宇昊 - **描述**:管理员查看买家和商家账号列表,并禁用或启用账号。 - **业务规则**:被禁用账号不能重新登录;已签发令牌的失效策略在接口和架构设计中保持一致;管理员不得通过普通接口禁用自己。 - **验收要点**:用户状态修改生效;禁用后无法登录;非管理员不能操作。 -### M07 商品评价与晒图(X01) +### M07 商品评价与晒图(X01)— 顾欣月 - **描述**:买家对已完成订单中的商品提交评分、文字评价和可选图片,并查看商品公开评价。 - **业务规则**:评分范围 1~5;评价必须关联当前用户真实订单项;同一订单项只能提交一次;图片类型、大小和数量由实现前契约确定;评价内容不得泄露敏感信息。 - **异常流程**:未购买、订单未完成、重复评价或图片不合规时拒绝提交。 - **验收要点**:评价与对应商品正确关联;评分和晒图正常展示;不能评价他人订单或重复评价。 -### M08 商品收藏与浏览历史(X02) +### M08 商品收藏与浏览历史(X02)— 唐宇昊 - **描述**:买家收藏、取消收藏和分页查看收藏商品;查看商品详情时记录最近浏览历史。 - **业务规则**:同一用户对同一商品只能存在一条收藏和一条最近浏览记录;再次浏览更新最近时间;下架商品可保留历史,但标记为不可购买。 - **验收要点**:收藏增删和列表正确;浏览历史按最近时间排序;用户之间数据隔离。 -### M09 站内消息通知(X03) +### M09 站内消息通知(X03)— 罗皓晨 - **描述**:系统向买家发送订单状态、支付、发货和售后结果等站内通知,支持消息列表、未读数和已读状态。 - **业务规则**:消息必须关联接收用户和业务资源;用户只能读取和标记自己的消息;持久化消息是事实来源,WebSocket 推送失败不丢消息。 - **验收要点**:消息生成、列表、未读数和标记已读正确;断线后重新进入仍能看到未读消息。 -### M10 售后流程(X04) +### M10 售后流程(X04)— 张海洋 - **描述**:买家针对符合条件的订单项申请退款或退货,商家在后台审核并形成售后状态记录。 - **状态建议**:待审核 → 已同意/已拒绝;退货场景可增加待退货、已退款。真实退款渠道不在本期范围内,退款结果为模拟状态流转。 @@ -268,49 +268,49 @@ flowchart LR ## 四、选定挑战模块与验收要求 -### C01 秒杀与防超卖 +### C01 秒杀与防超卖 — 朱惠惠 - 提供限时秒杀活动和秒杀下单入口,未开始、已结束或库存耗尽时拒绝下单。 - 现场使用压测工具模拟 100 并发抢 10 件库存;成功订单数应为 10,不超卖,在请求充足且无业务失败时不少卖。 - 库存扣减和订单生成保持一致,失败请求不得产生负库存或孤立订单。 - 答辩须讲清数据库事务、条件更新/锁方案及为何没有把队列作为唯一正确性保障。 -### C03 订单超时自动取消 +### C03 订单超时自动取消 — 韦乾强 - 正式规则为下单 30 分钟未支付自动取消并回补库存;演示环境可配置更短时间,但必须说明与正式参数的对应关系。 - Worker 使用定时扫描或延迟任务处理,任务可重复执行但不能重复取消或重复回补库存。 - 现场演示“自动取消瞬间用户恰好支付”的竞争,最终只能出现支付成功或取消成功之一。 - 答辩须讲清条件更新、事务边界、重试和失败恢复方式。 -### C04 商品搜索进阶 +### C04 商品搜索进阶 — 顾欣月 - 支持中文分词模糊搜索、多条件筛选和排序,至少包含分类、价格区间、上下架/可售条件及价格或时间排序。 - 搜索实现采用搜索适配器;分词与倒排索引的具体实现须在开发前完成技术验证并记录。 - 准备同一数据规模下与数据库 `LIKE/ILIKE` 查询的性能对比,包括数据量、查询词、并发、平均/百分位耗时和结果正确性。 - 答辩须能解释分词、索引建立、更新时机和排序逻辑。 -### C06 实时消息推送 +### C06 实时消息推送 — 罗皓晨 - 使用 WebSocket/SignalR 推送订单支付、发货、取消和售后审核等状态变化。 - 推送与站内消息持久化配合:实时推送失败不影响消息最终可查询。 - 现场演示断线重连、同一账号多个浏览器标签页接收消息,以及多 API 实例下的消息广播。 - 答辩须讲清连接身份校验、Redis Backplane/共享通道及断线补偿。 -### C07 缓存与性能优化 +### C07 缓存与性能优化 — 罗皓晨 - 使用 Redis 优化首页商品数据和商品详情查询。 - 商品改价、库存或上下架变更后必须执行缓存失效策略,并说明最迟多久可见新值及原因。 - 准备启用缓存前后的压测对比,记录命中率、吞吐量、平均/百分位耗时和数据库压力。 - 答辩须讲清 Cache-Aside、TTL、缓存穿透/击穿基本处理及数据库仍是事实来源。 -### C08 支付回调幂等与对账 +### C08 支付回调幂等与对账 — 张海洋 - 模拟支付回调重复、乱序到达,使用支付流水号/回调标识和订单状态条件保证幂等。 - 已取消订单不得被迟到的成功回调错误改为已支付;重复成功回调不得重复记账或重复发消息。 - 每日生成对账结果,至少能识别“支付成功但订单未更新”等差异数据,并提供待处理状态或修复记录。 - 答辩须讲清唯一约束、事务、Outbox/Inbox 或等价方案及乱序处理规则。 -### C10 容器化部署与负载均衡 +### C10 容器化部署与负载均衡 — 罗皓晨 - Docker Compose 一键启动前端、Nginx、至少 2 个 API 实例、Worker、PostgreSQL、Redis、RabbitMQ 和最终启用的对象存储。 - Nginx 对 API 实例负载均衡,现场通过实例标识或日志证明请求落到不同实例。 -- Gitee From e5ef05b34ab7e2b63925937d126df73fdf1e0ea1 Mon Sep 17 00:00:00 2001 From: FISH SOUP Date: Thu, 23 Jul 2026 08:52:11 +0800 Subject: [PATCH 010/118] =?UTF-8?q?docs(auth):=20=E6=98=8E=E7=A1=AE?= =?UTF-8?q?=E6=89=8B=E6=9C=BA=E5=8F=B7+=E5=AF=86=E7=A0=81=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E4=B8=8E=E7=B3=BB=E7=BB=9F=E7=94=9F=E6=88=90=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=90=8D=E9=BB=98=E8=AE=A4=E5=A4=B4=E5=83=8F=E8=A7=84?= =?UTF-8?q?=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 0045667..13c90a5 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -149,26 +149,51 @@ flowchart LR ### M01-01 用户注册(F01)— 唐宇昊 -- **描述**:游客使用用户名、密码和可选手机号创建买家账号。 -- **前置条件**:用户未登录;用户名未被注册。 -- **主流程**:校验输入 → 检查用户名唯一性 → 加密密码 → 创建正常状态买家账号 → 返回账号基本信息。 -- **异常流程**:用户名重复、格式非法、密码强度不足或手机号格式错误时拒绝注册并返回明确提示。 -- **验收要点**:重复注册被拦截;数据库中不出现明文密码;非法参数不会创建账号。 +- **描述**:游客使用**手机号**和**密码**创建买家账号。账号创建成功后系统自动生成一个内部用户名用于页面展示,并设置默认头像;用户后续可在个人中心查看并自助重置一次用户名。 +- **前置条件**:用户未登录;该手机号在国内号段合法可用且未被注册。 +- **主流程**: + 1. 校验手机号格式(11 位、国内号段)和密码强度(≥ 8 位,至少包含字母与数字)。 + 2. 检查手机号在本平台未被注册。 + 3. 使用可靠哈希算法加密密码,原文不入库、不写日志。 + 4. 生成一个**系统随机用户名**:8 位小写字母+数字组合,全局唯一,冲突时自动重试,最多重试 5 次。 + 5. 设置默认头像 URL(项目静态资源固定默认头像,如 `/static/avatars/default.png`)。 + 6. 创建正常状态买家账号,初始昵称等同于用户名。 + 7. 返回账号基本信息,包括新生成的用户名与默认头像地址,用户可据此辨识自己的身份。 +- **异常流程**:手机号格式错误、密码强度不足、手机号已注册、用户名随机生成连续冲突时拒绝注册并返回明确提示。 +- **校验规则确认**:本期手机号仅作为账号标识和登录凭据,**不再作为用户名登录**;用户名只用于页面展示和站内提及,登录页只暴露手机号输入框。 +- **验收要点**: + - 注册页只显示"手机号+密码"两个输入项,没有用户名输入框。 + - 注册成功后页面与账号设置立即显示"你的用户名是 xxx"。 + - 数据库中不出现明文密码,手机号作为账号主键具有唯一约束。 + - 默认头像在购物端、个人中心、订单列表等所有用到头像的地方都生效。 + - 用户名 1 次自助重置入口可见、可验证。 ### M01-02 用户登录与退出(F02)— 唐宇昊 -- **描述**:用户凭用户名和密码登录,系统返回身份令牌;退出后当前令牌不应继续访问受保护资源。 +- **描述**:用户凭**手机号**和**密码**登录,系统返回身份令牌;退出后当前令牌不应继续访问受保护资源。登录页只呈现手机号和密码两个字段,不暴露用户名输入;登录成功后界面展示系统生成的用户名以便用户认得自己的账号。 - **前置条件**:账号存在且状态正常。 -- **主流程**:验证凭据 → 签发带用户标识、角色和 `jti` 的 JWT → 前端保存登录状态 → 退出时把 `jti` 写入 Redis 失效记录直至令牌自然过期,并清理前端登录状态。 -- **异常流程**:账号不存在、密码错误、账号被禁用或令牌过期时拒绝访问并返回友好错误。 -- **验收要点**:登录态在页面刷新后可恢复;错误密码不泄露账号是否存在;被禁用用户无法登录。 +- **主流程**: + 1. 校验手机号格式是否合法。 + 2. 通过手机号定位账号,校验密码哈希。 + 3. 签发带用户标识、角色和 `jti` 的 JWT,前端保存登录态。 + 4. 登录成功后页面展示用户名、默认头像、最近收货地址等关键信息,用户可据此辨识"我登进来了"。 + 5. 退出时把 `jti` 写入 Redis 失效记录直至令牌自然过期,并清理前端登录状态。 +- **异常流程**:账号不存在、密码错误、账号被禁用或令牌过期时拒绝访问并返回友好错误。错误信息区分"账号或密码错误"与"账号被禁用"两种文案,不泄露账号是否存在。 +- **验收要点**:登录态在页面刷新后可恢复;不同设备登录会保留同一用户名展示;错误密码不泄露账号是否存在;被禁用用户无法登录;登录页绝不出现"用户名"输入框。 ### M01-03 个人信息与收货地址(F03)— 唐宇昊 -- **描述**:买家查看和修改个人信息,并对自己的收货地址进行增删改查。 -- **业务规则**:地址必须属于当前用户;收件人、手机号、省市区和详细地址必填;每个用户最多一个默认地址;删除默认地址后不自动假定其他地址为默认,除非业务实现明确处理。 -- **异常流程**:访问他人地址、地址不存在或字段非法时拒绝操作。 -- **验收要点**:完整完成地址 CRUD;不能越权读取或修改他人地址。 +- **描述**:买家查看和修改个人信息(包括手机号、用户名、头像与默认地址等)以及对收货地址的增删改查。手机号作为唯一账号标识,变更前需验证原密码或当前会话;用户名支持 1 次自助重置,重置后沿用同一默认头像。 +- **业务规则**: + - 用户名仅作展示用途,长度 3-20 位,全站唯一,字母/数字/下划线,**每位用户整个生命周期最多自助重置 1 次**(重置窗口可通过身份强校验再次开启,例如原密码 + 短信)。 + - 手机号变更需校验原密码;变更后立即需要再次登录才允许下单、支付等敏感动作。 + - 默认头像 URL 在没有上传自定义头像前始终使用 `/static/avatars/default.png`;更换头像在本期内不开放。 + - 地址必须属于当前用户;收件人、手机号、省市区和详细地址必填;每个用户最多一个默认地址;删除默认地址后不自动假定其他地址为默认,除非业务实现明确处理。 +- **异常流程**:访问他人地址、地址不存在、字段非法、改名次数已用完或未通过强校验时拒绝操作。 +- **验收要点**: + - 完整完成地址 CRUD;不能越权读取或修改他人地址。 + - 个人页面顶部清晰展示"用户名 + 默认头像 + 手机号(部分掩码 138****8888)"。 + - 用户名自助重置可体验 1 次,第二次入口出现明确说明文案。 ### M02-01 商品列表、分类与搜索(F04、F05)— 顾欣月 -- Gitee From 18fdce28615a84d43d900e21b1047e8c4aafede4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=83=A0=E6=83=A0?= <2205590672@qq.com> Date: Thu, 23 Jul 2026 08:59:07 +0800 Subject: [PATCH 011/118] =?UTF-8?q?docs:=E6=B7=BB=E5=8A=A020260722-?= =?UTF-8?q?=E6=9C=B1=E6=83=A0=E6=83=A0=E6=97=A5=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...2-\346\234\261\346\203\240\346\203\240.md" | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 "reports/daily/20260722-\346\234\261\346\203\240\346\203\240.md" diff --git "a/reports/daily/20260722-\346\234\261\346\203\240\346\203\240.md" "b/reports/daily/20260722-\346\234\261\346\203\240\346\203\240.md" new file mode 100644 index 0000000..822e601 --- /dev/null +++ "b/reports/daily/20260722-\346\234\261\346\203\240\346\203\240.md" @@ -0,0 +1,20 @@ +# 日报 - 朱惠惠 - 2026-07-22 + +## 今日完成 + +1. 参加项目小组会议,同步当前进度并确认下一阶段分工。 +2. 完成新项目分工确认。 + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式/求助对象 | +|----------|----------|-------------------| +| 今日无阻塞项 | — | — | + +## 明日计划 + +1. 完善M03-01的需求详述文档 + +## 今日工时 + +约 6 小时 \ No newline at end of file -- Gitee From 9f4b02c8417596b7d473670e1d0804f6055c35f0 Mon Sep 17 00:00:00 2001 From: FISH SOUP Date: Thu, 23 Jul 2026 09:10:59 +0800 Subject: [PATCH 012/118] =?UTF-8?q?docs(auth):=20=E8=A1=A5=E5=85=A8=20M06-?= =?UTF-8?q?03=20=E4=B8=8E=20M08=20=E7=94=A8=E6=88=B7=E4=B8=8E=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=A4=B4=E5=83=8F=E7=9B=B8=E5=85=B3=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 13c90a5..7cf1174 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -261,9 +261,20 @@ flowchart LR ### M06-03 后台用户管理(F13)— 唐宇昊 -- **描述**:管理员查看买家和商家账号列表,并禁用或启用账号。 -- **业务规则**:被禁用账号不能重新登录;已签发令牌的失效策略在接口和架构设计中保持一致;管理员不得通过普通接口禁用自己。 -- **验收要点**:用户状态修改生效;禁用后无法登录;非管理员不能操作。 +- **描述**:管理员在后台查看买家和商家账号列表,对账号进行**禁用 / 启用**操作。列表页必须同时展示手机号(部分掩码,如 `138****8888`)、系统生成的用户名、默认头像、注册时间、最近活跃时间和账号状态,管理员可按手机号、用户名、状态、注册时间筛选。 +- **业务规则**: + - 列表页展示项固定包括:用户 ID、手机号(掩码)、用户名、头像、状态、注册时间、最近活跃时间。 + - 手机号仅用于后台运营识别,**完整手机号默认掩码**(管理员可临时点开查看,写入审计日志"管理员 X 于 Y 时间查看了用户手机号")。 + - 禁用 = 把 `users.status` 置为 `Disabled`,同时把该用户所有已签发但未失效的 JWT 的 `jti` 写入 Redis 失效记录,**立即使已签发令牌失效**。 + - 启用 = 把 `users.status` 置为 `Active`,并清空该用户历史的 `jti` 失效记录(用户下次登录重新签发令牌即可)。 + - 同一账号的禁用 / 启用操作必须有明确审计条目:操作管理员 ID、时间、原状态 → 新状态。 + - 管理员不得通过普通接口禁用自己;可由更高一级管理员或紧急接口恢复。 +- **异常流程**:非管理员访问、操作自己、参数非法或目标账号不存在时拒绝操作并返回明确错误。 +- **验收要点**: + - 列表页正确展示掩码手机号 + 用户名 + 默认头像。 + - 禁用账号同时立即踢出已登录会话(前端收到 401 自动跳转登录页)。 + - 启用账号后该用户能重新登录,旧 token 不能再用。 + - 禁用 / 启用操作可在审计日志中追溯。 ### M07 商品评价与晒图(X01)— 顾欣月 @@ -274,9 +285,20 @@ flowchart LR ### M08 商品收藏与浏览历史(X02)— 唐宇昊 -- **描述**:买家收藏、取消收藏和分页查看收藏商品;查看商品详情时记录最近浏览历史。 -- **业务规则**:同一用户对同一商品只能存在一条收藏和一条最近浏览记录;再次浏览更新最近时间;下架商品可保留历史,但标记为不可购买。 -- **验收要点**:收藏增删和列表正确;浏览历史按最近时间排序;用户之间数据隔离。 +- **描述**:买家对商品进行收藏、取消收藏并分页查看收藏列表;查看商品详情时记录最近浏览历史。收藏与浏览列表都按"最近活动"倒序展示,并展示商品的当前价格、当前状态与默认头像(收藏人 / 浏览人),方便买家快速回忆"我当时收藏的什么"。 +- **业务规则**: + - 同一用户对同一商品只存在一条收藏记录,重复点击"收藏"不会增加条目,只更新时间戳。 + - 收藏动作完成后**列表必须按最近收藏时间倒序**,并展示该项的关键信息:商品名 / 当前售价 / 主图 / 商品当前上下架状态。 + - 同一用户对同一商品只保留一条最近浏览记录,再次浏览只更新时间戳,不增加条目。 + - 浏览历史按最近浏览时间倒序,最近浏览过的商品显示在列表最上面,最多保留最近 N 条(默认 50,可由个人中心开启 / 关闭;首次注册即默认开启)。 + - 已下架商品**仍保留在收藏和浏览历史中**,但显示"已下架,不可购买"占位状态;展示仍复用用户当前默认头像 URL,避免收藏页出现头像空缺。 + - 收藏和浏览历史严格按当前用户隔离,跨账号看不见对方数据。 +- **异常流程**:商品不存在、参数非法、越权访问他人收藏 / 历史时拒绝操作。 +- **验收要点**: + - 收藏增删正确、列表倒序、最近时间显示明确。 + - 浏览历史按最近时间排序、可在个人中心清空、可一键关闭记录。 + - 下架商品保留记录但明确标记不可购买;展示头像统一从 `users.avatar_url` 取默认 URL。 + - 用户间数据 100% 隔离,单元 / 集成测试覆盖幂等与边界。 ### M09 站内消息通知(X03)— 罗皓晨 -- Gitee From 24b98cc34daa36c6e6818a01aa7b60406e601c03 Mon Sep 17 00:00:00 2001 From: FISH SOUP Date: Thu, 23 Jul 2026 09:28:27 +0800 Subject: [PATCH 013/118] =?UTF-8?q?docs(auth):=20=E5=B7=B2=E5=AE=8C?= =?UTF-8?q?=E5=96=84=20M01-01/02/03=E3=80=81M06-03=E3=80=81M08=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E7=9A=84=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 165 +++++++++++++++--- 1 file changed, 144 insertions(+), 21 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 7cf1174..0cddd44 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -149,6 +149,12 @@ flowchart LR ### M01-01 用户注册(F01)— 唐宇昊 +#### 1. 挑战目标与部署范围 + +本模块要求游客使用**手机号**和**密码**创建买家账号,注册完成后系统为用户**自动生成一个内部用户名**用于页面展示,并设置默认头像;用户后续可在个人中心查看并自助重置一次用户名。本期不要求用户自行取名,登录页只暴露手机号输入框。 + +#### 2. 功能需求 + - **描述**:游客使用**手机号**和**密码**创建买家账号。账号创建成功后系统自动生成一个内部用户名用于页面展示,并设置默认头像;用户后续可在个人中心查看并自助重置一次用户名。 - **前置条件**:用户未登录;该手机号在国内号段合法可用且未被注册。 - **主流程**: @@ -161,15 +167,36 @@ flowchart LR 7. 返回账号基本信息,包括新生成的用户名与默认头像地址,用户可据此辨识自己的身份。 - **异常流程**:手机号格式错误、密码强度不足、手机号已注册、用户名随机生成连续冲突时拒绝注册并返回明确提示。 - **校验规则确认**:本期手机号仅作为账号标识和登录凭据,**不再作为用户名登录**;用户名只用于页面展示和站内提及,登录页只暴露手机号输入框。 -- **验收要点**: - - 注册页只显示"手机号+密码"两个输入项,没有用户名输入框。 - - 注册成功后页面与账号设置立即显示"你的用户名是 xxx"。 - - 数据库中不出现明文密码,手机号作为账号主键具有唯一约束。 - - 默认头像在购物端、个人中心、订单列表等所有用到头像的地方都生效。 - - 用户名 1 次自助重置入口可见、可验证。 + +#### 3. 现场验收脚本 + +| 步骤 | 操作 | 预期证据 | +|---|---|---| +| 1 | 打开注册页 | 只显示"手机号+密码"两个输入项,没有用户名输入框。 | +| 2 | 输入合法手机号与符合强度的密码并提交 | 提示"注册成功"并立即展示"你的用户名是 xxx"。 | +| 3 | 进入个人中心 / 账号设置 | 顶部展示该用户名、默认头像 URL 和部分掩码手机号。 | +| 4 | 检查数据库 | 不出现明文密码;`users.phone` 具有唯一约束;`username` 字段为 8 位小写字母+数字。 | +| 5 | 在购物端、个人中心、订单列表等多处浏览头像 | 均显示同一默认头像 `/static/avatars/default.png`。 | +| 6 | 进入"修改用户名"入口 | 可正常重置一次;再次进入时出现明确说明文案(次数已用完)。 | +| 7 | 故意制造冲突(弱密码 / 已注册手机号 / 非法号段) | 分别返回对应友好错误提示,不泄露系统内部细节。 | + +#### 4. 验收证据与答辩要求 + +- 保存注册流程的成功 / 失败截图或录屏,覆盖正常路径与三类异常。 +- 保存数据库中 `users` 表的字段证据:`password_hash` 非明文、`phone` 唯一约束、`username` 长度格式。 +- 保存默认头像在多端一致展示的截图。 +- 能解释为什么本期选择"系统随机用户名 + 1 次自助重置",而不是允许用户自取名。 +- 能解释手机号为何仅作账号标识和登录凭据、用户名为何只用于展示与提及。 +- 能解释密码哈希算法选型、随机用户名冲突重试上限和默认头像静态资源的服务边界。 ### M01-02 用户登录与退出(F02)— 唐宇昊 +#### 1. 挑战目标与部署范围 + +本模块要求用户凭**手机号**和**密码**登录,系统签发可被任一 API 实例验证的 JWT;退出后当前令牌立即失效且不再能访问受保护资源。登录页只暴露手机号和密码两个字段,登录成功后界面展示系统生成的用户名、默认头像与最近收货地址,让用户**一眼认出这是自己的账号**。 + +#### 2. 功能需求 + - **描述**:用户凭**手机号**和**密码**登录,系统返回身份令牌;退出后当前令牌不应继续访问受保护资源。登录页只呈现手机号和密码两个字段,不暴露用户名输入;登录成功后界面展示系统生成的用户名以便用户认得自己的账号。 - **前置条件**:账号存在且状态正常。 - **主流程**: @@ -179,10 +206,37 @@ flowchart LR 4. 登录成功后页面展示用户名、默认头像、最近收货地址等关键信息,用户可据此辨识"我登进来了"。 5. 退出时把 `jti` 写入 Redis 失效记录直至令牌自然过期,并清理前端登录状态。 - **异常流程**:账号不存在、密码错误、账号被禁用或令牌过期时拒绝访问并返回友好错误。错误信息区分"账号或密码错误"与"账号被禁用"两种文案,不泄露账号是否存在。 -- **验收要点**:登录态在页面刷新后可恢复;不同设备登录会保留同一用户名展示;错误密码不泄露账号是否存在;被禁用用户无法登录;登录页绝不出现"用户名"输入框。 + +#### 3. 现场验收脚本 + +| 步骤 | 操作 | 预期证据 | +|---|---|---| +| 1 | 打开登录页 | 只显示"手机号+密码"两个输入框,绝不出现"用户名"输入框。 | +| 2 | 用注册账号登录 | 右上角立即展示用户名、默认头像和最近收货地址,用户能确认"我登进来了"。 | +| 3 | 刷新页面 | 登录态恢复,仍展示同一用户名和头像。 | +| 4 | 在不同设备分别登录同一账号 | 两端都能展示同一用户名与默认头像。 | +| 5 | 输入错误密码连续登录 | 返回统一文案"账号或密码错误",不区分手机号错或密码错、不泄露账号是否存在。 | +| 6 | 用被禁用账号登录 | 返回明确文案"账号已停用,请联系管理员",允许用户自查原因。 | +| 7 | 登录后点击"退出" | 前端清空登录状态;再访问受保护接口返回 401,被立即带回登录页。 | +| 8 | 检查 Redis | 退出时该账号最新 `jti` 出现在失效记录中,原令牌自然过期后被清理。 | + +#### 4. 验收证据与答辩要求 + +- 保存登录 / 退出 / 刷新 / 错误密码 / 被禁用 / 多设备 的截图或录屏。 +- 保存 JWT 关键 Claims(用户标识、角色、`jti`)的解码示例与签名密钥配置位置。 +- 保存 Redis 中 `jti` 失效记录的键名、TTL 与写入时机的证据。 +- 能解释为何退出使用 Redis `jti` 失效而非服务端短令牌或前端 Cookie 清除,以及为什么这种做法可由两个 API 实例共同验证。 +- 能解释错误文案为何统一处理、如何不泄露账号存在性、被禁用用户的文案为何独立。 +- 能解释登录态连续性如何与 C10 多实例负载均衡协作。 ### M01-03 个人信息与收货地址(F03)— 唐宇昊 +#### 1. 挑战目标与部署范围 + +本模块允许买家查看和修改个人信息(包括手机号、用户名、头像与默认地址等)以及对收货地址的增删改查。手机号作为唯一账号标识,变更前需验证原密码或当前会话;用户名支持 1 次自助重置,重置后沿用同一默认头像。所有改动必须让用户**清楚知道自己在改什么、改完后会触发什么副作用**。 + +#### 2. 功能需求 + - **描述**:买家查看和修改个人信息(包括手机号、用户名、头像与默认地址等)以及对收货地址的增删改查。手机号作为唯一账号标识,变更前需验证原密码或当前会话;用户名支持 1 次自助重置,重置后沿用同一默认头像。 - **业务规则**: - 用户名仅作展示用途,长度 3-20 位,全站唯一,字母/数字/下划线,**每位用户整个生命周期最多自助重置 1 次**(重置窗口可通过身份强校验再次开启,例如原密码 + 短信)。 @@ -190,10 +244,28 @@ flowchart LR - 默认头像 URL 在没有上传自定义头像前始终使用 `/static/avatars/default.png`;更换头像在本期内不开放。 - 地址必须属于当前用户;收件人、手机号、省市区和详细地址必填;每个用户最多一个默认地址;删除默认地址后不自动假定其他地址为默认,除非业务实现明确处理。 - **异常流程**:访问他人地址、地址不存在、字段非法、改名次数已用完或未通过强校验时拒绝操作。 -- **验收要点**: - - 完整完成地址 CRUD;不能越权读取或修改他人地址。 - - 个人页面顶部清晰展示"用户名 + 默认头像 + 手机号(部分掩码 138****8888)"。 - - 用户名自助重置可体验 1 次,第二次入口出现明确说明文案。 + +#### 3. 现场验收脚本 + +| 步骤 | 操作 | 预期证据 | +|---|---|---| +| 1 | 进入个人中心 | 顶部清晰展示"用户名 + 默认头像 + 手机号(部分掩码 138****8888)"。 | +| 2 | 修改用户名(首次) | 提交成功,新用户名在顶部立即生效;剩余重置次数由 1 变为 0。 | +| 3 | 再次进入"修改用户名"入口 | 出现明确说明文案(次数已用完,需走身份强校验后再开启),而不是直接报错。 | +| 4 | 修改手机号 | 必须先输入原密码;提交成功后立刻被登出,重新登录后才允许下单 / 支付。 | +| 5 | 新增 / 编辑 / 删除地址 | 增删改查完整生效;地址收件人、手机号、省市区、详细地址均必填。 | +| 6 | 设置默认地址 / 取消默认地址 | 同一时间最多 1 个默认地址;删除默认地址后,下单流程不会被静默指定其他地址。 | +| 7 | 尝试访问他人地址 / 用他人 ID 改地址 | 返回 403 或资源不存在,不泄露地址是否存在。 | +| 8 | 未登录访问个人中心 / 地址管理 | 自动跳转登录页,登录后回到原页。 | + +#### 4. 验收证据与答辩要求 + +- 保存个人信息修改流程的截图与日志,覆盖用户名、手机号、地址 CRUD 与默认地址切换。 +- 保存"第二次改名入口"出现友好说明文案的截图与文案样例。 +- 保存改名 / 手机号变更的次数审计记录与权限校验证据。 +- 能解释"1 次自助重置"的设计原因、重置窗口再次开启的强校验路径,以及为什么本期不开放自定义头像。 +- 能解释手机号变更后强制重新登录的原因(防旧手机号继续操作敏感动作)。 +- 能解释默认地址删除后为什么不静默假定其他地址为默认,以及下单流程的兜底体验。 ### M02-01 商品列表、分类与搜索(F04、F05)— 顾欣月 @@ -261,6 +333,12 @@ flowchart LR ### M06-03 后台用户管理(F13)— 唐宇昊 +#### 1. 挑战目标与部署范围 + +本模块由管理员在后台查看买家和商家账号列表,并对账号进行**禁用 / 启用**操作。禁用必须**立即**让该账号的所有已签发令牌失效,被禁用户再访问页面会被礼貌请回登录页并看到明确说明,而不是莫名空白或报错。完整手机号默认掩码展示,临时点开必须留痕。 + +#### 2. 功能需求 + - **描述**:管理员在后台查看买家和商家账号列表,对账号进行**禁用 / 启用**操作。列表页必须同时展示手机号(部分掩码,如 `138****8888`)、系统生成的用户名、默认头像、注册时间、最近活跃时间和账号状态,管理员可按手机号、用户名、状态、注册时间筛选。 - **业务规则**: - 列表页展示项固定包括:用户 ID、手机号(掩码)、用户名、头像、状态、注册时间、最近活跃时间。 @@ -270,11 +348,29 @@ flowchart LR - 同一账号的禁用 / 启用操作必须有明确审计条目:操作管理员 ID、时间、原状态 → 新状态。 - 管理员不得通过普通接口禁用自己;可由更高一级管理员或紧急接口恢复。 - **异常流程**:非管理员访问、操作自己、参数非法或目标账号不存在时拒绝操作并返回明确错误。 -- **验收要点**: - - 列表页正确展示掩码手机号 + 用户名 + 默认头像。 - - 禁用账号同时立即踢出已登录会话(前端收到 401 自动跳转登录页)。 - - 启用账号后该用户能重新登录,旧 token 不能再用。 - - 禁用 / 启用操作可在审计日志中追溯。 + +#### 3. 现场验收脚本 + +| 步骤 | 操作 | 预期证据 | +|---|---|---| +| 1 | 以管理员身份进入后台用户管理 | 列表展示掩码手机号 + 用户名 + 默认头像 + 状态 + 注册时间 + 最近活跃时间,可按手机号/用户名/状态/注册时间筛选。 | +| 2 | 点开任一行查看完整手机号 | 弹窗明示"查看完整手机号将写入审计日志";确认后可见完整号码,并在审计日志中新增一条记录。 | +| 3 | 选中正常账号点击"禁用" | 数据库 `users.status` 置为 `Disabled`;Redis 中该账号全部有效 `jti` 进入失效记录。 | +| 4 | 在另一设备登录该被禁账号 | 登录请求被拒绝;前端收到明确"账号已停用,请联系管理员"提示,被带至登录页。 | +| 5 | 在被禁用户原登录设备刷新受保护页面 | 收到 401 后被自动带回登录页;不出现"看似登着但其实没登"的半登录状态。 | +| 6 | 管理员点击"启用" | `users.status` 恢复 `Active`;该用户的 `jti` 失效记录被清空;审计日志新增原状态→新状态条目。 | +| 7 | 被禁用户重新登录 | 登录成功并获取新 `jti`;旧 token 已失效,不能再使用。 | +| 8 | 管理员对自己的账号点"禁用" | 按钮不可点或拒绝执行,并明示"不能禁用自己的账号",避免管理员把自己锁在外面。 | +| 9 | 非管理员访问后台用户管理接口 | 返回 403 或路由不可见。 | + +#### 4. 验收证据与答辩要求 + +- 保存列表页、禁用 / 启用操作、点开完整手机号、自助禁用自己等关键截图或录屏。 +- 保存 Redis 中 `jti` 失效记录的键名、TTL 与禁用/启用时的写入 / 清理证据。 +- 保存审计日志中"查看完整手机号""禁用/启用"两类条目的样例。 +- 能解释禁用为何必须立即踢出已登录会话、为何用 Redis `jti` 失效而非缩短令牌有效期。 +- 能解释为何默认掩码手机号、点开需留痕、管理员不可禁用自己的设计原因。 +- 能解释启用时清空 `jti` 失效记录与"用户下次登录重新签发令牌"的衔接关系。 ### M07 商品评价与晒图(X01)— 顾欣月 @@ -285,6 +381,12 @@ flowchart LR ### M08 商品收藏与浏览历史(X02)— 唐宇昊 +#### 1. 挑战目标与部署范围 + +本模块允许买家对商品进行收藏、取消收藏并分页查看收藏列表;查看商品详情时记录最近浏览历史。收藏与浏览列表都按"最近活动"倒序展示,**即便商品下架也保留记录**以便用户回想起当时为什么加购,但必须以明确的占位状态告诉用户"不可购买",避免出现"点了购买才发现买不了"的尴尬。展示头像统一从 `users.avatar_url` 取默认 URL,避免各页面头像不一致。 + +#### 2. 功能需求 + - **描述**:买家对商品进行收藏、取消收藏并分页查看收藏列表;查看商品详情时记录最近浏览历史。收藏与浏览列表都按"最近活动"倒序展示,并展示商品的当前价格、当前状态与默认头像(收藏人 / 浏览人),方便买家快速回忆"我当时收藏的什么"。 - **业务规则**: - 同一用户对同一商品只存在一条收藏记录,重复点击"收藏"不会增加条目,只更新时间戳。 @@ -294,11 +396,32 @@ flowchart LR - 已下架商品**仍保留在收藏和浏览历史中**,但显示"已下架,不可购买"占位状态;展示仍复用用户当前默认头像 URL,避免收藏页出现头像空缺。 - 收藏和浏览历史严格按当前用户隔离,跨账号看不见对方数据。 - **异常流程**:商品不存在、参数非法、越权访问他人收藏 / 历史时拒绝操作。 -- **验收要点**: - - 收藏增删正确、列表倒序、最近时间显示明确。 - - 浏览历史按最近时间排序、可在个人中心清空、可一键关闭记录。 - - 下架商品保留记录但明确标记不可购买;展示头像统一从 `users.avatar_url` 取默认 URL。 - - 用户间数据 100% 隔离,单元 / 集成测试覆盖幂等与边界。 + +#### 3. 现场验收脚本 + +| 步骤 | 操作 | 预期证据 | +|---|---|---| +| 1 | 进入商品详情并点击"收藏" | 收藏成功;列表多一条记录,置顶展示商品名、当前售价、主图、当前上下架状态。 | +| 2 | 对同一商品重复点击"收藏" | 不会新增条目,只更新时间戳;列表顺序保持。 | +| 3 | 取消收藏 | 该条目从列表移除;列表顺序保持倒序。 | +| 4 | 浏览多件商品后查看浏览历史 | 按最近浏览时间倒序展示,最近浏览过的商品在最上面。 | +| 5 | 同一商品重复浏览 | 只更新时间戳,不增加条目。 | +| 6 | 浏览历史条目数超过 50 | 最早的一条被自动挤掉,仍保留最近 50 条;用户不会感知数据丢失。 | +| 7 | 在个人中心关闭"记录浏览历史" | 后续浏览不再写入历史;重新开启后正常记录。 | +| 8 | 收藏 / 浏览的商品被商家下架 | 列表保留记录,显示"已下架,不可购买"占位,"购买"按钮隐藏或置灰。 | +| 9 | 切换 / 登录不同账号 | 收藏与浏览历史 100% 隔离,跨账号互不可见。 | +| 10 | 用他人 ID 越权访问收藏 / 历史 | 返回 403 或资源不存在。 | +| 11 | 在收藏页 / 浏览历史页查看头像 | 与个人中心、订单页等位置一致读取 `users.avatar_url`,不会出现头像空缺或不一致。 | + +#### 4. 验收证据与答辩要求 + +- 保存收藏增删、重复收藏、列表倒序、最近时间展示的截图或录屏。 +- 保存浏览历史排序、上限 50 条的滚动行为、关闭 / 开启记录开关的证据。 +- 保存下架商品在收藏和浏览历史中的占位状态截图,明确"不可购买"提示。 +- 保存跨账号数据隔离的单元 / 集成测试用例与执行结果。 +- 能解释为什么下架商品仍保留记录,以及占位状态如何避免误导用户去尝试购买。 +- 能解释 50 条上限的滚动淘汰策略、为什么首次注册即默认开启记录、为什么统一从 `users.avatar_url` 读取默认头像。 +- 能解释收藏 / 浏览历史如何在多实例 API 下保持用户隔离,与 C10 多实例负载均衡的协作方式。 ### M09 站内消息通知(X03)— 罗皓晨 -- Gitee From 7e904a17a201f2556422148361eaf6ddd58da061 Mon Sep 17 00:00:00 2001 From: FISH SOUP Date: Thu, 23 Jul 2026 09:38:27 +0800 Subject: [PATCH 014/118] =?UTF-8?q?docs(auth):=20=E8=A1=A5=E9=BD=90=20M01-?= =?UTF-8?q?01/02/03=E3=80=81M06-03=E3=80=81M08=20=E5=9B=9B=E8=BA=AB?= =?UTF-8?q?=E4=BB=BD=E5=B7=AE=E5=BC=82=E5=8C=96=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - M01-01 注册只产出买家,不开放商家自助注册、不接受外部 role 注入 - M01-02 登录页四身份共用,按账号 role 路由到买家/商家/管理端 - M01-03 个人中心与地址管理只对买家开放,商家/管理员调用返回 403 - M06-03 后台用户管理仅覆盖 buyer/seller,不管理管理员且不改 role - M08 收藏与浏览历史只在买家侧可用,严格按用户隔离含跨角色 --- ...74\350\257\264\346\230\216\344\271\246.md" | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 0cddd44..716f279 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -153,6 +153,8 @@ flowchart LR 本模块要求游客使用**手机号**和**密码**创建买家账号,注册完成后系统为用户**自动生成一个内部用户名**用于页面展示,并设置默认头像;用户后续可在个人中心查看并自助重置一次用户名。本期不要求用户自行取名,登录页只暴露手机号输入框。 +- **身份边界**:注册流程只产出**买家**角色账号,**本期不开放商家自助注册**(商家账号由管理员在 M06-03 后台开通);**管理员账号不开放在线注册**,由更高一级途径(后台种子 / 初始化脚本 / 紧急接口)开通。游客注册完成后从"未登录"切换为"买家",角色字段 `users.role = buyer` 在数据库层固定,接口不接受外部传入。 + #### 2. 功能需求 - **描述**:游客使用**手机号**和**密码**创建买家账号。账号创建成功后系统自动生成一个内部用户名用于页面展示,并设置默认头像;用户后续可在个人中心查看并自助重置一次用户名。 @@ -179,6 +181,7 @@ flowchart LR | 5 | 在购物端、个人中心、订单列表等多处浏览头像 | 均显示同一默认头像 `/static/avatars/default.png`。 | | 6 | 进入"修改用户名"入口 | 可正常重置一次;再次进入时出现明确说明文案(次数已用完)。 | | 7 | 故意制造冲突(弱密码 / 已注册手机号 / 非法号段) | 分别返回对应友好错误提示,不泄露系统内部细节。 | +| 8 | 在注册请求中尝试注入 `role=seller` 或 `role=admin` | 注册接口忽略 / 拒绝外部 `role` 字段,账号 `users.role` 仍固定为 `buyer`。 | #### 4. 验收证据与答辩要求 @@ -188,6 +191,7 @@ flowchart LR - 能解释为什么本期选择"系统随机用户名 + 1 次自助重置",而不是允许用户自取名。 - 能解释手机号为何仅作账号标识和登录凭据、用户名为何只用于展示与提及。 - 能解释密码哈希算法选型、随机用户名冲突重试上限和默认头像静态资源的服务边界。 +- 能解释为何本期只允许游客注册成买家、不开放商家自助注册、管理员账号为何不通过在线注册开通,以及如何防御外部注入 `role` 字段的越权请求。 ### M01-02 用户登录与退出(F02)— 唐宇昊 @@ -195,6 +199,13 @@ flowchart LR 本模块要求用户凭**手机号**和**密码**登录,系统签发可被任一 API 实例验证的 JWT;退出后当前令牌立即失效且不再能访问受保护资源。登录页只暴露手机号和密码两个字段,登录成功后界面展示系统生成的用户名、默认头像与最近收货地址,让用户**一眼认出这是自己的账号**。 +- **身份边界**:登录入口对**四种身份(游客 / 买家 / 商家 / 管理员)共用同一登录页和登录接口**,但登录成功后的落地路由由账号 `role` 决定: + - 游客(首次登录后)→ 买家端。 + - 买家(`buyer`)→ 买家端。 + - 商家(`seller`)→ 商家工作台。 + - 管理员(`admin`)→ 管理端。 + - 登录接口**不强制按入口来源**校验角色(如商家账号从买家端入口登录不会被拒绝,而是按角色路由到商家工作台),但**会用角色校验拦截后续越权请求**(如买家账号请求商家接口返回 403)。 + #### 2. 功能需求 - **描述**:用户凭**手机号**和**密码**登录,系统返回身份令牌;退出后当前令牌不应继续访问受保护资源。登录页只呈现手机号和密码两个字段,不暴露用户名输入;登录成功后界面展示系统生成的用户名以便用户认得自己的账号。 @@ -219,6 +230,8 @@ flowchart LR | 6 | 用被禁用账号登录 | 返回明确文案"账号已停用,请联系管理员",允许用户自查原因。 | | 7 | 登录后点击"退出" | 前端清空登录状态;再访问受保护接口返回 401,被立即带回登录页。 | | 8 | 检查 Redis | 退出时该账号最新 `jti` 出现在失效记录中,原令牌自然过期后被清理。 | +| 9 | 用同一商家账号从买家端入口登录 | 登录成功,并被路由到商家工作台;不会出现"密码错"或"无权限"的误判。 | +| 10 | 买家账号请求商家/管理端接口 | 返回 403,明确告知角色不足,但不泄露对方接口存在性。 | #### 4. 验收证据与答辩要求 @@ -228,6 +241,7 @@ flowchart LR - 能解释为何退出使用 Redis `jti` 失效而非服务端短令牌或前端 Cookie 清除,以及为什么这种做法可由两个 API 实例共同验证。 - 能解释错误文案为何统一处理、如何不泄露账号存在性、被禁用用户的文案为何独立。 - 能解释登录态连续性如何与 C10 多实例负载均衡协作。 +- 能解释为何登录页对四种身份共用、按账号 `role` 路由而非强制按端登录,以及如何用角色校验拦截"买家账号请求商家/管理端"的越权请求。 ### M01-03 个人信息与收货地址(F03)— 唐宇昊 @@ -235,6 +249,8 @@ flowchart LR 本模块允许买家查看和修改个人信息(包括手机号、用户名、头像与默认地址等)以及对收货地址的增删改查。手机号作为唯一账号标识,变更前需验证原密码或当前会话;用户名支持 1 次自助重置,重置后沿用同一默认头像。所有改动必须让用户**清楚知道自己在改什么、改完后会触发什么副作用**。 +- **身份边界**:本模块的"个人中心 + 地址管理"**只对买家(`role=buyer`)开放**。商家账号有自己的商家资料维护(不在 F03 范围),管理员账号不开个人中心。游客(未登录)访问个人中心或地址管理会被重定向到登录页;商家/管理员已登录身份调用买家侧个人中心 / 地址接口返回 403,不泄露字段是否存在。 + #### 2. 功能需求 - **描述**:买家查看和修改个人信息(包括手机号、用户名、头像与默认地址等)以及对收货地址的增删改查。手机号作为唯一账号标识,变更前需验证原密码或当前会话;用户名支持 1 次自助重置,重置后沿用同一默认头像。 @@ -257,6 +273,8 @@ flowchart LR | 6 | 设置默认地址 / 取消默认地址 | 同一时间最多 1 个默认地址;删除默认地址后,下单流程不会被静默指定其他地址。 | | 7 | 尝试访问他人地址 / 用他人 ID 改地址 | 返回 403 或资源不存在,不泄露地址是否存在。 | | 8 | 未登录访问个人中心 / 地址管理 | 自动跳转登录页,登录后回到原页。 | +| 9 | 用商家或管理员已登录身份调用买家个人中心 / 地址接口 | 返回 403,不返回买家字段;不被 401 错误诱导重新登录。 | +| 10 | 商家账号尝试走"个人中心 → 改名入口" | 入口不在商家端导航里出现,接口也不接受该角色调用。 | #### 4. 验收证据与答辩要求 @@ -266,6 +284,7 @@ flowchart LR - 能解释"1 次自助重置"的设计原因、重置窗口再次开启的强校验路径,以及为什么本期不开放自定义头像。 - 能解释手机号变更后强制重新登录的原因(防旧手机号继续操作敏感动作)。 - 能解释默认地址删除后为什么不静默假定其他地址为默认,以及下单流程的兜底体验。 +- 能解释为何"个人中心 + 地址管理 + 1 次改名"规则只对买家生效,商家 / 管理员账号在 F03 中没有对应入口,越权调用应被 403 拦截而非 401。 ### M02-01 商品列表、分类与搜索(F04、F05)— 顾欣月 @@ -337,6 +356,8 @@ flowchart LR 本模块由管理员在后台查看买家和商家账号列表,并对账号进行**禁用 / 启用**操作。禁用必须**立即**让该账号的所有已签发令牌失效,被禁用户再访问页面会被礼貌请回登录页并看到明确说明,而不是莫名空白或报错。完整手机号默认掩码展示,临时点开必须留痕。 +- **身份边界**:列表与禁用 / 启用操作**只针对 `role ∈ {buyer, seller}` 两类账号**;`role=admin` 不在被管理范围(管理员账号由更高级管理员或紧急接口维护,超出 F13)。禁用 / 启用行为对买家和商家一致:均通过 `users.status = Disabled / Active` + Redis `jti` 失效记录踢出已登录会话。`users.role` 字段不允许通过本模块接口修改——"提权"或"改角色"超出 F13 范围,由其他管理流程负责。 + #### 2. 功能需求 - **描述**:管理员在后台查看买家和商家账号列表,对账号进行**禁用 / 启用**操作。列表页必须同时展示手机号(部分掩码,如 `138****8888`)、系统生成的用户名、默认头像、注册时间、最近活跃时间和账号状态,管理员可按手机号、用户名、状态、注册时间筛选。 @@ -362,6 +383,8 @@ flowchart LR | 7 | 被禁用户重新登录 | 登录成功并获取新 `jti`;旧 token 已失效,不能再使用。 | | 8 | 管理员对自己的账号点"禁用" | 按钮不可点或拒绝执行,并明示"不能禁用自己的账号",避免管理员把自己锁在外面。 | | 9 | 非管理员访问后台用户管理接口 | 返回 403 或路由不可见。 | +| 10 | 检查后台用户管理列表结果 | 仅返回 `role ∈ {buyer, seller}` 账号;管理员账号不出现在列表中。 | +| 11 | 尝试通过普通接口修改账号 `role` 字段(如 PUT 改 `role=seller`) | 接口不存在或被拒绝;`users.role` 只能由对应管理流程变更,本模块不提供。 | #### 4. 验收证据与答辩要求 @@ -371,6 +394,7 @@ flowchart LR - 能解释禁用为何必须立即踢出已登录会话、为何用 Redis `jti` 失效而非缩短令牌有效期。 - 能解释为何默认掩码手机号、点开需留痕、管理员不可禁用自己的设计原因。 - 能解释启用时清空 `jti` 失效记录与"用户下次登录重新签发令牌"的衔接关系。 +- 能解释为何管理员账号不在被管理列表、买家与商家禁用行为为何一致、以及为何本模块不提供"修改 `role` 字段"的接口而把提权与改角色留给其他管理流程。 ### M07 商品评价与晒图(X01)— 顾欣月 @@ -385,6 +409,8 @@ flowchart LR 本模块允许买家对商品进行收藏、取消收藏并分页查看收藏列表;查看商品详情时记录最近浏览历史。收藏与浏览列表都按"最近活动"倒序展示,**即便商品下架也保留记录**以便用户回想起当时为什么加购,但必须以明确的占位状态告诉用户"不可购买",避免出现"点了购买才发现买不了"的尴尬。展示头像统一从 `users.avatar_url` 取默认 URL,避免各页面头像不一致。 +- **身份边界**:收藏与浏览历史**只对买家(`role=buyer`)开放**。商家 / 管理员调用相关接口应被拒绝(403),购物端的收藏 / 历史导航对这两类角色不显示;游客(未登录)尝试触发收藏或浏览历史接口被拦截并引导登录。数据隔离**严格按当前用户**,含跨角色:商家账号即使登录到购物端,也看不到也不可访问任何买家账号的收藏或浏览历史。 + #### 2. 功能需求 - **描述**:买家对商品进行收藏、取消收藏并分页查看收藏列表;查看商品详情时记录最近浏览历史。收藏与浏览列表都按"最近活动"倒序展示,并展示商品的当前价格、当前状态与默认头像(收藏人 / 浏览人),方便买家快速回忆"我当时收藏的什么"。 @@ -412,6 +438,8 @@ flowchart LR | 9 | 切换 / 登录不同账号 | 收藏与浏览历史 100% 隔离,跨账号互不可见。 | | 10 | 用他人 ID 越权访问收藏 / 历史 | 返回 403 或资源不存在。 | | 11 | 在收藏页 / 浏览历史页查看头像 | 与个人中心、订单页等位置一致读取 `users.avatar_url`,不会出现头像空缺或不一致。 | +| 12 | 用商家 / 管理员已登录身份调用收藏或浏览历史接口 | 返回 403;前端购物端不展示"收藏 / 浏览历史"导航项。 | +| 13 | 游客(未登录)点击商品详情页"收藏"或访问浏览历史列表 | 收藏动作被拒绝,提示并引导登录;浏览历史接口返回 401 转登录。 | #### 4. 验收证据与答辩要求 @@ -422,6 +450,7 @@ flowchart LR - 能解释为什么下架商品仍保留记录,以及占位状态如何避免误导用户去尝试购买。 - 能解释 50 条上限的滚动淘汰策略、为什么首次注册即默认开启记录、为什么统一从 `users.avatar_url` 读取默认头像。 - 能解释收藏 / 浏览历史如何在多实例 API 下保持用户隔离,与 C10 多实例负载均衡的协作方式。 +- 能解释为何收藏与浏览历史只在买家侧可用、跨账号(含跨角色)的隔离为什么是关键安全规则,以及商家 / 管理员调用为何统一返回 403 而非隐藏入口。 ### M09 站内消息通知(X03)— 罗皓晨 -- Gitee From db840e4d24af1af6ff0ef65950e8a307e664b7bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=B5=B7=E6=B4=8B?= <2040454617@qq.com> Date: Thu, 23 Jul 2026 09:43:14 +0800 Subject: [PATCH 015/118] =?UTF-8?q?docs(payment):=20=E7=BB=86=E5=8C=96?= =?UTF-8?q?=E6=94=AF=E4=BB=98/=E5=94=AE=E5=90=8E/=E5=AF=B9=E8=B4=A6?= =?UTF-8?q?=E9=9C=80=E6=B1=82=E8=AF=A6=E8=BF=B0=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=B0=8F=E9=87=91=E5=BA=93=E5=85=85=E5=80=BC=E6=94=AF=E4=BB=98?= =?UTF-8?q?=EF=BC=88=E5=8D=95=E7=AC=94=E2=89=A41=E4=B8=87=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 85 ++++++++++++++++--- 1 file changed, 72 insertions(+), 13 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 0045667..c05f8a7 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -10,6 +10,7 @@ | 版本 | 日期 | 修改人 | 修改说明 | |---|---|---|---| | v0.1 | 2026-07-22 | 罗皓晨 | 形成需求规格初稿,明确四类角色、必做功能、4 项选做、7 项挑战、验收口径和六人后端模块边界 | +| v0.2 | 2026-07-23 | 张海洋 | 细化 M05-01 模拟支付、M10 售后流程、C08 支付回调幂等与对账三个模块功能需求详述,补充四类用户身份差异处理、用户体验目标、主流程、异常友好提示与验收要点;M05-01 引入内置钱包(小金库)自填金额充值支付,单笔充值上限 10000 元 | ## 一、引言 @@ -216,11 +217,44 @@ flowchart LR ### M05-01 模拟支付(F10)— 张海洋 -- **描述**:买家对自己的待支付订单执行模拟支付,不接入真实支付渠道。 -- **主流程**:创建支付记录 → 模拟成功回调 → 幂等更新支付和订单状态 → 返回支付结果。 -- **业务规则**:只有待支付订单可支付;成功后订单变为已支付;重复回调不得重复记账或改变终态。 -- **异常流程**:订单已取消、已支付、金额不一致或订单不属于当前用户时拒绝支付。 -- **验收要点**:支付后状态正确;支付记录可追踪;重复请求结果幂等。 +- **描述**:买家使用平台**内置钱包("小金库")余额**对属于自己的待支付订单完成模拟支付。本期不接入真实支付渠道:买家先向小金库充值(模拟充值、自填金额),支付时从钱包余额扣款。核心目标是让买家像使用真实余额支付一样:**余额随处可见、充值即时到账、付款一步扣款、异常有明确出路**,全程不出现"点了没反应""是否重复扣款说不清"的困惑。 +- **内置钱包与充值(小金库)**: + - 每个买家拥有一个钱包账户,记录当前余额(初始为 0);余额以服务端为事实来源,前端不可篡改。 + - **充值**:买家自行填写充值金额发起充值;本期为模拟充值、无真实资金流入,充值成功即时到账并增加余额。 + - **单笔充值上限**:单笔充值金额必须大于 0 且**不超过 10000 元**,金额精度为两位小数;超过上限或金额非法一律拒绝并提示。 + - **充值幂等**:充值请求携带幂等键,重复提交或网络重试不重复到账。 + - **充值记录**:保存充值时间、金额、结果,供买家查询。 +- **涉及用户身份与权限(四类身份差异处理)**: + - **游客**:无钱包、无充值与支付入口;未登录调用相关接口返回未授权(401)并引导登录,不泄露任何订单信息。 + - **会员(买家)**:唯一拥有钱包并可充值/支付的角色,且只能操作**本人钱包**、支付**本人名下**的待支付订单;操作他人钱包或订单一律拒绝(403)。 + - **商家(运营人员)**:不涉及买家钱包与支付,后台只对"已支付"订单执行发货(见 M06-02/F12)。 + - **管理员**:负责平台治理,不介入买家钱包与订单支付,无相关入口。 +- **统一入口**:支付入口收敛为下单成功页"立即支付"、订单列表待支付订单"去支付"、订单详情页"付款"三处,均跳转同一收银台,金额与状态一致;余额不足时收银台提供"去充值"一键入口。 +- **用户体验目标(核心)**: + - **信息透明**:收银台展示订单号、应付金额、**当前钱包余额**与"距超时自动取消剩余时间"倒计时,付款前对"付多少、余额够不够、还剩多久"没有疑问。 + - **不被卡住**:余额不足时不是报错了事,而是明确提示"还差 X 元"并一键跳转充值,充值后无缝回到收银台继续支付。 + - **反馈明确**:充值与支付提交后都进入加载态并给出确定结果(成功 / 失败 / 处理中),杜绝点击后无反应。 + - **不怕手抖**:重复点击、网络抖动、返回后重进都不会重复充值或重复扣款。 + - **知道下一步**:支付成功后自动回到订单详情并置顶"已支付"状态与后续说明(等待商家发货)。 +- **前置条件**:买家已登录;订单存在且属于当前用户;订单处于待支付(PendingPayment)状态且未超时;钱包余额 ≥ 应付金额(不足时先充值)。 +- **主流程**:进入收银台展示订单摘要与钱包余额 →(余额不足则先充值,单笔 ≤ 10000)→ 买家确认并提交支付(携带幂等键)→ 服务端在同一数据库事务内校验余额充足、从钱包扣款、创建支付记录(`payment`)并生成唯一支付流水号、幂等更新订单状态(PendingPayment → Paid)→ 写入 Outbox 供发货与站内消息等下游消费 → 返回支付结果 → 前端展示成功并跳转订单详情。 +- **业务规则**: + - 充值:单笔金额 > 0 且 ≤ 10000 元;金额非法(负数、零、超两位小数、超上限)拒绝;模拟充值成功即时增加余额,幂等不重复到账。 + - 支付:只有待支付订单可支付;已支付、已取消、已发货、已完成订单一律拒绝并给出对应原因。 + - 支付金额以服务端订单实付金额为准,从钱包余额扣款;余额不足时拒绝并引导充值,**余额不得为负**。 + - 支付金额与前端传入金额校验一致,禁止前端篡改金额或余额。 + - 以幂等键 + 支付流水号保证支付幂等:重复回调或重复提交只扣款一次、只记一次账、订单终态不被二次改写,并返回首次成功结果。 + - 支付与"超时自动取消"(C03)竞争时,两者均带待支付状态条件更新,最终只能一方成功——要么支付成功、要么已取消,绝不出现"又支付又取消"的矛盾状态。 + - 钱包扣款、支付记录与订单状态在同一事务内更新以保证一致;支付记录保存订单号、流水号、金额、结果与时间,供买家查询与后台对账(C08)复用。 + - 支付成功后通过站内消息(X03)通知买家。 +- **异常流程与友好提示**: + - 充值超限(> 10000)或金额非法:明确提示单笔上限 10000 元及正确格式,不产生任何到账。 + - 余额不足:提示"余额不足,还差 X 元"并一键跳转充值,不让买家卡在支付页。 + - 订单已取消 / 已超时:提示"订单已取消,无法支付",引导买家重新下单,不返回后端堆栈。 + - 订单已支付:提示"该订单已完成支付"并直接展示当前状态,消除买家"是否重复扣款"的顾虑。 + - 金额不一致或订单/钱包不属于当前用户:拒绝操作,返回明确、可追踪(含 `traceId`)的错误,不泄露他人信息。 + - 支付处理中 / 回调延迟:展示"处理中"占位态并允许安全刷新查询结果,不诱导买家反复点击。 +- **验收要点**:单笔充值上限 10000 元生效、超限被拦截;充值即时到账且幂等不重复;支付从钱包正确扣款、余额不足被拦截并可充值后继续;支付后订单状态正确流转为已支付;支付记录完整可查;重复充值/支付结果幂等、不重复扣款记账;四类身份权限边界正确(游客无入口、买家只能操作本人钱包与订单、商家/管理员无相关权限);买家全程有明确反馈。 ### M06-01 后台分类与商品管理(F11)— 顾欣月 @@ -261,10 +295,31 @@ flowchart LR ### M10 售后流程(X04)— 张海洋 -- **描述**:买家针对符合条件的订单项申请退款或退货,商家在后台审核并形成售后状态记录。 -- **状态建议**:待审核 → 已同意/已拒绝;退货场景可增加待退货、已退款。真实退款渠道不在本期范围内,退款结果为模拟状态流转。 -- **业务规则**:申请必须关联当前用户订单项;申请金额不能超过该订单项实付金额;同一可售后数量不能重复申请;商家审核必须记录意见。 -- **验收要点**:买家申请、进度查询、后台审核和状态流转完整;越权、超额和重复申请被拦截。 +- **描述**:买家针对符合条件的订单项发起退款或退货申请,商家在后台审核并形成清晰、可追踪的售后状态记录。这是一个典型的**"买家发起 + 商家审核"双角色流程**:目标是让买家"**申请简单、进度看得见、结果有交代**",让商家"**审核有据、操作不出错**"。真实退款渠道不在本期范围,退款为模拟处理并原路退回买家小金库余额(见 M05-01)。 +- **涉及用户身份与权限(四类身份差异处理)**: + - **游客**:无售后入口;未登录访问售后接口返回未授权(401),需登录为买家后才能操作。 + - **会员(买家)**:可对**本人订单项**发起退款/退货申请、查看本人售后进度、在未审核前撤销申请(若实现);不能查看或操作他人售后,也不能审核。 + - **商家(运营人员)**:在后台售后审核列表处理申请——同意 / 拒绝、填写审核意见、推进退货与退款状态;在售后审核权限内处理平台售后申请,不能代替买家发起申请,也不能篡改买家申请内容。 + - **管理员**:负责平台治理与账号管理,默认不直接审批售后;如需监督按最终实现约定提供只读查看,不参与业务审批。 +- **买家侧体验目标**: + - **可申请一目了然**:清楚看到"哪些订单项可申请、最多可申请多少金额/数量、当前进度到哪一步",避免反复试错。 + - **提交前就拦错**:表单对退款/退货类型、原因、金额即时校验,提交前就拦截超额、重复申请。 + - **进度不用猜**:每次状态变化(提交、同意、拒绝、退款完成)都配合站内消息(X03)通知买家,无需反复刷新猜结果。 +- **商家侧体验目标**: + - 审核界面完整呈现订单项、实付金额、申请金额与买家理由,一屏看懂无需来回查单。 + - 强制填写审核意见,减少误判与来回沟通;审核操作幂等,重复点击不重复改状态。 +- **售后状态机(建议)**:待审核 → 已同意 / 已拒绝;退货场景可增加 待退货 → 已退款。状态只能按图流转,任何未定义跳转(如"已拒绝"直接变"已退款")一律拒绝。 +- **业务规则**: + - 申请必须关联当前用户本人的订单项;买家越权申请他人订单被拒绝(403)。 + - 申请金额不得超过该订单项实付金额;同一可售后数量不得重复申请(已在售后流程中的数量不可再次发起)。 + - 售后可申请的订单状态、申请时限以需求确认结论为准(见 9.1 第 5 条),实现前锁定并写入接口设计。 + - 商家审核必须记录意见;同意/拒绝为终态动作,重复审核不重复改变状态(幂等);商家只在售后审核权限内操作,不得发起或篡改买家申请内容。 + - 退款为模拟处理:退款完成后将退款金额**原路退回买家小金库余额**(模拟入账、幂等,不重复退款),只更新售后状态与必要的对账标记,不误改订单核心状态。 +- **异常流程与友好提示**: + - 订单项不可售后 / 超出时限 / 已全部申请:明确提示原因并禁用申请入口,而不是提交后才报错。 + - 超额或重复申请:表单即时校验并提示可申请上限,指引买家修正。 + - 商家审核冲突(重复点击、并发审核):带状态条件更新,仅一次生效,其余返回已处理结果。 +- **验收要点**:买家申请、进度查询、商家后台审核与状态流转链路完整;四类身份权限边界正确(游客无入口、买家不能审核、商家不能替买家申请、管理员不参与审批);越权、超额、重复申请被拦截;退款通过后金额原路退回买家小金库余额且幂等不重复退款;每次状态变化有站内消息通知;商家审核意见被记录且可追溯。 ## 四、选定挑战模块与验收要求 @@ -305,10 +360,14 @@ flowchart LR ### C08 支付回调幂等与对账 — 张海洋 -- 模拟支付回调重复、乱序到达,使用支付流水号/回调标识和订单状态条件保证幂等。 -- 已取消订单不得被迟到的成功回调错误改为已支付;重复成功回调不得重复记账或重复发消息。 -- 每日生成对账结果,至少能识别“支付成功但订单未更新”等差异数据,并提供待处理状态或修复记录。 -- 答辩须讲清唯一约束、事务、Outbox/Inbox 或等价方案及乱序处理规则。 +- **目标**:在模拟支付回调可能重复、乱序、延迟到达的真实场景下,保证买家始终看到"**正确且稳定**"的订单与支付状态,平台每天能对账发现并处理差异。从用户体验看:买家不会因回调重复而被重复扣款或重复通知,也不会因回调乱序而看到"忽已支付忽未支付"的状态跳变;从平台看:"钱到了单没更新"这类问题必须被自动发现并有处理入口。 +- 模拟支付回调重复、乱序到达,使用支付流水号/回调标识和订单状态条件保证幂等;回调携带全局唯一回调 ID、支付流水号、订单号、结果与时间,回调 ID / 流水号建立唯一约束。 +- 已取消订单不得被迟到的成功回调错误改为已支付——此类迟到成功回调进入对账差异而非直接改状态;重复成功回调不得重复记账、不得重复发送站内消息或事件。 +- 支付记录、订单状态与 Outbox 在同一事务处理;重复回调读取并返回已处理结果(幂等应答)。 +- 每日由 `Mall.Worker` 生成对账批次,对比支付记录与订单状态,输出匹配、差异(如"支付成功但订单未更新""订单已支付但缺支付流水")和处理状态,并提供待处理状态或修复记录,保证差异可追踪、可闭环。 +- 面向不同身份的可见性(四类身份差异处理):**买家**只看到最终一致的稳定订单/支付状态,绝不暴露回调重复、乱序等内部过程;**商家**据以发货的"已支付"状态必须真实可靠,不因迟到回调误判;**管理员/运营**可见每日对账差异并有处理入口,负责差异闭环;**游客**不涉及。 +- 答辩须讲清唯一约束、事务、Outbox/Inbox 或等价方案、乱序与迟到回调处理规则,以及对账差异的识别与修复流程。 +- 验收证据(挑战模块,见 4.1):保留可重复执行的回调重放/乱序脚本、测试数据规模与环境参数、原始结果与对账差异清单、最终结论及答辩提纲,不用单次截图替代完整证据。 ### C10 容器化部署与负载均衡 — 罗皓晨 -- Gitee From 88929d1d68059864c44063388aff7f347623b56a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=B5=B7=E6=B4=8B?= <2040454617@qq.com> Date: Thu, 23 Jul 2026 09:50:06 +0800 Subject: [PATCH 016/118] =?UTF-8?q?docs(payment):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E9=9C=80=E6=B1=82=E8=A7=84=E6=A0=BC=E8=AF=B4=E6=98=8E=E4=B9=A6?= =?UTF-8?q?=20v0.2=20=E4=BF=AE=E8=AE=A2=E8=AE=B0=E5=BD=95=E6=9D=A1?= =?UTF-8?q?=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...0\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" | 1 - 1 file changed, 1 deletion(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index c05f8a7..801542f 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -10,7 +10,6 @@ | 版本 | 日期 | 修改人 | 修改说明 | |---|---|---|---| | v0.1 | 2026-07-22 | 罗皓晨 | 形成需求规格初稿,明确四类角色、必做功能、4 项选做、7 项挑战、验收口径和六人后端模块边界 | -| v0.2 | 2026-07-23 | 张海洋 | 细化 M05-01 模拟支付、M10 售后流程、C08 支付回调幂等与对账三个模块功能需求详述,补充四类用户身份差异处理、用户体验目标、主流程、异常友好提示与验收要点;M05-01 引入内置钱包(小金库)自填金额充值支付,单笔充值上限 10000 元 | ## 一、引言 -- Gitee From dd6b23f6ab1b3b3f21cc2fc8f89aec4c5bdad174 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Thu, 23 Jul 2026 09:57:49 +0800 Subject: [PATCH 017/118] =?UTF-8?q?docs:=20=E5=AE=8C=E5=96=84=E7=BD=97?= =?UTF-8?q?=E7=9A=93=E6=99=A8=E6=A8=A1=E5=9D=97=E9=9C=80=E6=B1=82=E4=B8=8E?= =?UTF-8?q?=E8=BA=AB=E4=BB=BD=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 438 +++++++++++++++++- 1 file changed, 416 insertions(+), 22 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 716f279..5b02c5c 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -143,9 +143,82 @@ flowchart LR ### M00 公共基建与集成(内部 P0)— 罗皓晨 -- **描述**:建立统一解决方案骨架、模块注册方式、公共配置和本地运行编排,并将成员 A~E 的业务模块装配为一个可运行的模块化单体。 -- **责任边界**:成员 F 负责组合根、公共技术组件和集成清单;各业务负责人提供本模块的注册入口、数据库迁移和公开应用接口,成员 F 不修改其他模块内部领域规则。 -- **验收要点**:开发环境可启动 API、Worker 及已启用依赖;F01~F13 对应模块均完成注册并能通过公开接口协作;配置不硬编码密钥;模块集成问题有明确责任人和联调记录。 +#### 1. 模块定位与目标 + +M00 是六人协作的内部公共基建模块,不新增课程功能编号,也不替代 F01~F13 的业务模块。模块目标是建立可供全组共同使用的工程骨架、运行环境和集成约定,使各成员提交的模块能够以一致方式注册、配置、启动、观测和联调,最终组合成一个可运行、可测试、可部署的模块化单体。 + +主要使用者包括全体开发成员、联调人员和部署人员。普通买家、商家和管理员不会直接操作 M00,但其访问的所有页面和 API 都依赖本模块提供的基础运行能力。 + +**用户体验目标**:公共基建最终必须转化为一致、可预期的使用体验。用户进入任何页面时都应看到统一的加载、空数据、成功、失败和无权限反馈;普通故障不应导致白屏、重复提交或操作结果不明确;错误提示使用用户能理解的语言,并保留可供排查的 `traceId`,不直接展示内部异常。 + +**身份处理矩阵:** + +| 身份 | 是否涉及 | 公共处理要求 | +|---|---|---| +| 游客 | 是 | 可访问首页、分类、商品列表和详情等公开资源;访问登录后功能时引导登录,并保留安全的原访问目标;不得获得任何会员、商家或管理员数据。 | +| 会员(买家) | 是 | 登录后进入购物端,统一识别买家身份并执行 `BuyerOnly` 等策略;登录过期时提示重新登录,禁止访问商家端和管理端受保护能力。 | +| 商家(运营人员) | 是 | 登录后进入商家端,只能访问商品、订单、发货和售后等已授权经营能力;身份有效但权限不足时返回明确无权限反馈,不伪装成操作成功。 | +| 管理员 | 是 | 登录后进入管理端,只能执行账号治理和已明确的平台管理功能;管理员身份不自动获得查看任意用户私人订单、地址或站内消息的权限。 | + +四类身份共用同一套认证、ProblemDetails、加载与错误反馈基础能力,但路由入口、Policy、可见菜单和数据范围必须按身份区分。M00 只提供统一接入和拦截机制,具体业务授权条件仍由对应模块负责人定义。 + +#### 2. 责任范围 + +| 编号 | 能力 | 详细要求 | +|---|---|---| +| M00-FR01 | 解决方案骨架 | 建立前端、API、Worker、模块项目、测试项目和 Aspire AppHost 的基础目录与启动入口;项目依赖方向符合 Clean Architecture,业务模块之间不得直接引用对方内部实现。 | +| M00-FR02 | 模块注册 | 约定每个业务模块公开统一的服务注册和端点映射入口;API 启动时能够装配已启用模块,未启用模块不得留下无法解析的依赖。 | +| M00-FR03 | 公共 Web API 能力 | 提供统一的 JSON 配置、参数校验、ProblemDetails、OpenAPI/Swagger、JWT 认证和 Policy 授权接入位置;具体业务权限规则仍由对应模块负责人定义。 | +| M00-FR04 | 数据库集成 | 提供 PostgreSQL、EF Core 10 和 Npgsql 的公共接入方式、连接配置与 Migration 执行约定;每个模块维护自身实体映射和迁移内容,不允许 M00 直接修改其他模块表结构。 | +| M00-FR05 | 可靠事件基础设施 | 提供领域事件到集成事件的公共约定,以及 RabbitMQ、Outbox、消费者幂等和失败重试的基础接入能力;业务模块负责定义事件发生条件、业务字段和消费后的领域行为。 | +| M00-FR06 | 后台任务 | 提供 `Mall.Worker` 的运行入口、任务注册、取消令牌、重试和健康检查基础能力;订单超时、对账等业务任务由对应业务负责人提供处理逻辑。 | +| M00-FR07 | 缓存与共享能力 | 提供 Redis 连接、统一序列化、Key 命名约定、健康检查和多实例共享能力;具体缓存对象、失效时机和业务正确性由缓存使用方共同确认。 | +| M00-FR08 | 对象存储接入 | 提供面向 S3 Compatible Object Storage 的最小公共接口和开发环境 SeaweedFS 配置;商品与评价模块负责文件归属、数量、格式和权限规则。 | +| M00-FR09 | 本地与容器编排 | 开发环境使用 Aspire 编排 API、Worker 和所需依赖;集成及演示环境提供 Docker Compose 和 Nginx 配置,支持同一版本启动和关闭。 | +| M00-FR10 | 可观测性 | 使用 Serilog 输出结构化运行日志,使用 OpenTelemetry 采集 HTTP、数据库、Redis 和 RabbitMQ 的 Trace/Metric,并提供存活与就绪健康检查;不得记录密码、完整 Token 或连接密码。 | +| M00-FR11 | 配置管理 | 区分开发、集成和演示配置;仓库只保留安全默认值与 `.env.example`,密钥、密码和生产连接信息通过环境变量或受控 Secret 注入。 | +| M00-FR12 | 集成清单 | 维护模块负责人、注册入口、数据库迁移、公开接口、事件、外部依赖、联调对象和验证结果清单,确保集成问题能够追踪到具体模块和负责人。 | +| M00-FR13 | 统一交互反馈 | 为全站约定加载、空数据、成功、失败、无权限、登录过期和网络中断的统一表现;提交类操作必须防止用户连续点击造成重复请求,并明确告知操作正在处理、已经成功或需要重试。 | +| M00-FR14 | 身份一致性 | 前端路由、菜单、API Policy 和资源查询必须使用一致身份语义;同一账号在前后端不得被识别为不同角色,跨身份访问必须在服务端再次校验,不能只依赖隐藏菜单。 | + +#### 3. 主流程 + +1. 各模块负责人先依据 OpenAPI 和模块边界完成本模块的注册入口、端点、数据库迁移及必要事件定义。 +2. M00 将模块注册到统一 API/Worker 组合根,并检查依赖方向、配置项和启动顺序。 +3. 开发人员通过 Aspire 启动当前任务需要的 API、Worker、PostgreSQL、Redis、RabbitMQ 和对象存储;未使用的可选依赖可以不启动。 +4. API 启动后暴露 Swagger UI、`/health/live` 和 `/health/ready`,开发人员据此确认应用和当前启用的关键依赖可用。 +5. 联调时按照“接口文档先行”原则调用公开接口或集成事件,不允许通过跨模块 DbContext、内部仓储或直接改表完成协作。 +6. 合入 `dev` 前记录实际运行命令、配置要求、Migration 状态、联调结果和剩余问题,由至少一名其他成员交叉审查。 + +#### 4. 业务与工程规则 + +- M00 只拥有组合根、公共技术组件和集成约定,不拥有用户、商品、购物车、订单、支付和售后的领域规则或业务数据。 +- 公共组件必须解决两个及以上模块的确定需求;仅被一个模块使用的逻辑优先留在该模块,避免提前抽象。 +- PostgreSQL 是业务事实来源;Redis、RabbitMQ 和进程内存不得保存无法从事实数据恢复的唯一业务状态。 +- RabbitMQ 只用于跨进程集成事件,不替代下单、扣库存、支付等核心数据库事务。 +- API 和 Worker 必须支持优雅停止;后台任务收到停止信号后不再领取新任务,并安全完成或释放当前任务。 +- 可选依赖仅在对应功能启用时进入就绪检查;未启用的 Redis、RabbitMQ 或对象存储不得导致基础 API 永久不就绪。 +- 所有公共约定必须提供最小使用示例或说明,但不得为了未来可能出现的需求增加复杂基类、通用仓储或无业务价值的事件层。 + +#### 5. 异常与降级要求 + +- 必需配置缺失、连接字符串非法或模块注册失败时,应用应在启动阶段明确失败并指出配置键或模块名称,不得带着半初始化状态继续运行。 +- PostgreSQL 不可用时,就绪检查失败,依赖数据库的业务请求返回可追踪的服务异常,不返回内部堆栈。 +- Redis 暂时不可用时,允许不依赖分布式锁或强制共享状态的查询回退到数据库;具体能否降级由使用方需求决定,不得静默返回旧数据冒充成功。 +- RabbitMQ 暂时不可用时,已写入 Outbox 的事件保留待投递状态,由 Worker 重试;业务事务不得因为投递进程瞬时失败而丢失已提交事实。 +- 单个业务模块集成失败时,应明确阻塞模块、负责人、复现步骤和日志 `traceId`,不得由 M00 越权修改其领域规则规避问题。 + +#### 6. 交付物与验收标准 + +- 新成员按照 README 和示例配置,能够在不修改源代码中的密钥或地址的情况下启动开发环境。 +- API、Worker 及当前启用的依赖能够启动;Swagger UI、存活检查和就绪检查返回符合预期的状态。 +- F01~F13 的模块均有明确注册入口,并能通过公开 API、应用接口或集成事件完成规定协作。 +- 任取一条跨模块链路,能够通过结构化日志和 `traceId` 定位请求、数据库访问及消息处理过程。 +- 关闭 RabbitMQ 后产生的待发布事件不会丢失;恢复 RabbitMQ 后 Worker 能够继续投递且消费者不会重复产生业务结果。 +- 配置文件、Git 历史和日志中不出现真实密码、完整 JWT、Secret 或生产连接信息。 +- 集成清单至少记录模块、负责人、依赖、迁移、接口/事件、验证命令、验证结果和未解决问题。 +- 随机检查至少三个不同业务页面,其加载、空状态、错误、无权限和成功反馈风格一致;网络请求较慢时页面不会白屏,提交过程中不会因重复点击产生多次业务操作。 +- 分别使用游客、会员、商家和管理员访问公开页面及各端受保护页面:公开能力按预期可用,合法身份进入对应页面,跨身份访问被服务端拒绝且不会泄露目标数据。 ### M01-01 用户注册(F01)— 唐宇昊 @@ -454,9 +527,95 @@ flowchart LR ### M09 站内消息通知(X03)— 罗皓晨 -- **描述**:系统向买家发送订单状态、支付、发货和售后结果等站内通知,支持消息列表、未读数和已读状态。 -- **业务规则**:消息必须关联接收用户和业务资源;用户只能读取和标记自己的消息;持久化消息是事实来源,WebSocket 推送失败不丢消息。 -- **验收要点**:消息生成、列表、未读数和标记已读正确;断线后重新进入仍能看到未读消息。 +#### 1. 功能目标与范围 + +M09 为已登录用户提供可持久化的站内通知,覆盖订单创建/取消/支付/发货、售后申请/审核等已确认业务结果。站内消息列表是通知事实来源,C06 实时推送只是到达速度优化;用户断网、关闭浏览器或推送失败时,消息仍必须可在重新登录后查询。 + +本期不实现用户自由聊天、群聊、客服工单、短信、邮件、营销群发和复杂消息模板后台。业务模块只提交“已经发生的业务事实”,M09 负责生成、保存、查询和标记通知,不反向改变订单、支付或售后状态。 + +**用户体验目标**:用户应当在需要时快速找到与自己有关的消息,而不是被大量弹窗打断。消息标题要直接说明“发生了什么”,正文要说明“影响是什么”和“下一步能做什么”;未读角标准确,列表清晰可筛选,已读操作立即反馈。实时通知使用轻提示,不遮挡当前购物、下单或支付操作;重要事实始终可以回到消息中心再次查看。 + +#### 2. 身份处理与触发来源 + +**身份处理矩阵:** + +| 身份 | 是否涉及 | 接收内容 | 消息入口与操作 | +|---|---|---|---| +| 游客 | 否 | 游客没有稳定用户身份,本期不生成个人站内消息,也不建立未读数。 | 不显示个人消息中心;需要查看订单等个人信息时先登录。 | +| 会员(买家) | 是 | 订单创建/取消、支付结果、商家发货、售后提交和审核结果等与本人业务直接相关的通知。 | 从购物端消息入口查看;点击后只能进入本人的订单、支付或售后详情,可标记单条/全部已读。 | +| 商家(运营人员) | 是 | 新的已支付待发货订单、买家售后申请及其他需要商家处理的经营通知。 | 从商家端消息入口查看;点击后进入商家角色有权处理的订单或售后页面,不得进入买家私人页面或管理端。 | +| 管理员 | 否(本期) | 当前已选 X03 只覆盖订单、支付、发货和售后通知,不为管理员新增泛化系统告警或用户私人消息查看能力。 | 管理端本期不建设独立消息中心;未来若增加平台治理通知,必须另行定义事件、权限和验收。 | + +**触发来源与职责:** + +| 来源 | 职责 | +|---|---| +| 订单模块 | 在订单创建、取消、支付状态确认和发货事务成功后提供事件事实、业务 ID、买家 ID,以及确需处理时的商家接收标识。 | +| 支付模块 | 提供已确认且经过幂等处理的支付结果,不以尚未落库或处理中状态生成成功通知。 | +| 售后模块 | 在买家提交申请和商家完成审核后,分别生成面向商家和买家的事件事实。 | +| Messaging 模块 | 校验事件、接收身份和数据范围,按不同身份选择文案与跳转目标,完成去重、持久化、未读状态和实时推送衔接。 | + +消息归属最终以接收用户 ID 和业务数据范围为准,角色只用于选择消息模板、入口和可执行动作。禁止仅按“全部买家”或“全部商家”广播包含订单、支付或售后信息的私人通知。 + +本项目中的“商家”是同一 B2C 平台内的运营账号,不新增多商户入驻、租户隔离或商户结算模型。商家通知事件必须明确接收账号或受控接收范围,避免所有运营账号收到与其工作无关的重复提醒;具体接收规则在接口设计前由订单、售后和 Identity 负责人共同确认。 + +#### 3. 功能需求 + +| 编号 | 功能 | 详细要求 | +|---|---|---| +| X03-FR01 | 生成消息 | 接收已确认的业务事件后生成站内消息,至少记录接收用户、消息类型、标题、摘要/正文、关联业务类型、关联业务 ID、创建时间和已读状态。 | +| X03-FR02 | 事件去重 | 同一业务事件重复投递时不得为同一接收人重复生成相同消息;去重依据必须稳定,并能在进程重启后继续生效。 | +| X03-FR03 | 消息列表 | 用户按创建时间倒序分页查看自己的消息;支持按全部、未读和消息类型筛选,不允许一次返回无上限数据。 | +| X03-FR04 | 消息详情 | 用户查看消息详情时,系统校验消息归属;关联业务仍存在且用户有权限时可返回安全的前端跳转信息,不直接暴露内部路由或敏感字段。 | +| X03-FR05 | 未读数量 | 返回当前用户未读消息总数;新增消息后增加,首次成功标记已读后减少,重复标记不得重复减少。 | +| X03-FR06 | 单条已读 | 用户可将自己的一条未读消息标记为已读,并记录首次已读时间;已读消息再次操作返回幂等成功。 | +| X03-FR07 | 全部已读 | 用户可将本人当前未读消息批量标记为已读;操作只影响当前用户,不影响并发到达且不在本次更新范围内的新消息。 | +| X03-FR08 | 实时通知衔接 | 消息持久化成功后向 C06 提交推送任务;推送载荷只包含展示所需的最小数据,前端仍可通过消息查询接口获取完整事实。 | +| X03-FR09 | 可追踪性 | 消息生成失败、重复事件、推送触发和已读操作应记录消息 ID、业务类型、业务 ID、接收用户标识和 `traceId`,不得记录完整 Token 或敏感业务内容。 | +| X03-FR10 | 消息中心交互 | 全站提供容易发现但不过度突出的消息入口和未读角标;列表提供加载占位、空状态、失败重试和分页加载反馈;标记已读后角标和列表状态立即更新,失败时恢复原状态并提示用户重试。 | +| X03-FR11 | 身份化内容与跳转 | 同一业务事实可按接收身份生成不同标题、正文和操作入口,例如支付成功对买家提示等待发货,对商家提示处理待发货订单;跳转前必须重新校验当前身份和资源权限。 | + +#### 4. 主流程 + +1. 订单、支付或售后模块完成自身事务,形成包含事件 ID、业务标识、事件类型、发生时间和接收人的事件事实。 +2. Messaging 消费事件并检查必填字段、事件类型和接收用户;无效事件进入失败记录并告警,不生成半完整消息。 +3. 系统以事件 ID、接收用户和消息类型进行幂等判断,未处理过时生成消息并保存为未读。 +4. 数据库提交成功后触发实时推送;推送成功与否不改变数据库中的消息事实和未读状态。 +5. 用户进入消息中心,分页查询消息和未读数;点击消息后查看详情并按需要标记为已读。 +6. 用户断线重连或重新登录时重新查询未读消息,补偿离线期间未收到的实时通知。 + +#### 5. 业务规则与权限 + +- 消息归属以接收用户 ID 为准,所有列表、详情和写操作都必须同时过滤当前用户,禁止仅凭消息 ID 查询。 +- 消息创建后不得因为关联业务标题、商品名称或用户昵称变化而改变历史通知含义;需要展示历史信息时保存必要快照。 +- 消息类型使用受控枚举,不允许客户端任意传入类型创建系统消息。 +- `createdAt` 由服务端生成并统一使用 UTC 存储;前端负责按用户时区展示。 +- 未读状态以 PostgreSQL 为准,Redis 或前端角标只可作为缓存,不得成为唯一事实来源。 +- 业务事件必须在原业务事务成功后才能生成通知;被回滚的订单、支付或售后操作不得产生成功通知。 +- 默认不提供物理删除消息能力,避免破坏验收追踪;后续如需清理历史数据,应单独定义保留期和归档规则。 + +#### 6. 异常与边界场景 + +- 查询不存在或属于他人的消息时返回资源不存在或无权限,不泄露该消息是否存在及其接收人信息。 +- 重复事件只返回已有处理结果,不重复新增消息、增加未读数或触发重复业务通知。 +- RabbitMQ 暂时不可用时,事件由 Outbox 保留并在恢复后补发;重复补发仍受幂等约束。 +- SignalR/Redis 不可用时,消息仍正常落库并可查询,实时推送失败可记录并告警,但不得回滚原业务事务。 +- 用户在“全部已读”操作过程中收到新消息时,新消息应保持未读,避免把用户尚未看到的消息误标已读。 +- 关联订单或售后记录已不存在、已归档或当前用户无查看权限时,消息正文仍可查看,但前端不得提供无效或越权跳转。 + +#### 7. 验收标准 + +- 分别触发订单创建/取消、支付成功、发货和售后审核事件,正确用户能够看到类型、内容和关联对象正确的消息。 +- 消息列表分页、类型筛选和未读筛选结果正确,排序稳定且不出现其他用户数据。 +- 单条已读、重复已读和全部已读均满足幂等要求,未读数与数据库实际未读记录一致。 +- 将同一事件重复投递至少两次,只生成一条对应消息。 +- 关闭浏览器或断开实时连接后触发消息,重新登录仍可从列表查询并保持未读。 +- 使用另一用户身份直接请求消息详情或已读接口时被拒绝,且响应不泄露目标消息内容。 +- 保存消息成功但实时推送失败时,业务操作保持成功,消息仍能通过查询接口补偿。 +- 消息标题、摘要和时间在常用屏幕宽度下清晰可读;连续产生多条通知时不会用多个阻塞弹窗打断用户,用户可从统一消息入口集中查看。 +- 列表加载、空数据、请求失败、已读成功和已读失败均有明确界面反馈;失败后可直接重试,不要求刷新整个页面或重新登录。 +- 用同一笔订单验证身份化通知:买家收到面向买家的状态说明和购物端跳转,商家只在需要处理时收到经营通知和商家端跳转;游客和管理员不产生该业务消息。 +- 使用另一个买家和另一个商家直接请求消息、订单或售后跳转目标时必须被拒绝,且不能从响应中判断他人的私人消息内容。 ### M10 售后流程(X04)— 张海洋 @@ -490,17 +649,167 @@ flowchart LR ### C06 实时消息推送 — 罗皓晨 -- 使用 WebSocket/SignalR 推送订单支付、发货、取消和售后审核等状态变化。 -- 推送与站内消息持久化配合:实时推送失败不影响消息最终可查询。 -- 现场演示断线重连、同一账号多个浏览器标签页接收消息,以及多 API 实例下的消息广播。 -- 答辩须讲清连接身份校验、Redis Backplane/共享通道及断线补偿。 +#### 1. 挑战目标与边界 + +C06 在 M09 持久化消息之上提供低延迟实时到达能力。系统使用 ASP.NET Core SignalR 建立 WebSocket 通道,在订单支付、发货、取消和售后审核等消息成功落库后,将最小通知载荷推送给目标用户。实时推送不是业务事务的一部分,不承担消息永久保存,也不能作为订单或支付状态的唯一来源。 + +本挑战只实现业务状态通知,不实现在线客服对话、群聊、历史聊天同步、已送达回执和端到端加密。 + +**用户体验目标**:连接正常时,用户无需手动刷新即可及时得知关键状态变化;连接中断时,系统应优先静默重连,不频繁弹错或打断当前操作。只有持续断线影响实时性时才显示简短、可理解的状态提示,并告知用户消息仍可在消息中心查看。重复推送不得重复弹出相同提示。 + +**身份处理矩阵:** + +| 身份 | 是否建立实时连接 | 推送处理 | +|---|---|---| +| 游客 | 否 | 本期没有个人消息,不建立 M09/C06 用户连接;普通商品浏览不依赖 SignalR。 | +| 会员(买家) | 是 | 按认证用户 ID 接收本人订单、支付、发货和售后结果通知,点击后进入购物端已授权页面。 | +| 商家(运营人员) | 是 | 按认证用户及经营数据范围接收待发货、售后待处理等通知,点击后进入商家端已授权页面。 | +| 管理员 | 否(本期) | 当前没有管理员站内通知需求,不订阅买家或商家消息通道;未来新增时必须单独定义管理员事件。 | + +#### 2. 功能需求 + +| 编号 | 功能 | 详细要求 | +|---|---|---| +| C06-FR01 | 连接鉴权 | 客户端建立 SignalR 连接时携带有效 JWT;服务端从认证上下文获取用户 ID,不接受客户端自行声明接收用户。过期、伪造、被禁用或已失效的令牌不得建立有效连接。 | +| C06-FR02 | 用户定向推送 | 服务端按认证用户标识向目标用户的全部在线连接推送,不向无关用户或公共广播组泄露业务通知。 | +| C06-FR03 | 多标签页 | 同一账号在同一浏览器或不同浏览器打开多个标签页时,每个有效连接都能收到通知;任一标签页标记已读后,共享的数据库未读状态保持一致。 | +| C06-FR04 | 自动重连 | 前端在非主动退出导致的连接中断后按照有限退避策略自动重连,并展示连接状态;重连失败不能阻塞页面其他功能。 | +| C06-FR05 | 断线补偿 | 初次连接和重连成功后,前端重新查询 M09 未读数或最近消息,不假设断线期间的推送能够重放。 | +| C06-FR06 | 多实例广播 | 两个 Mall.Api 实例使用 Redis Backplane/共享通道传播 Hub 消息;无论用户连接落在哪个实例、事件由哪个实例触发,都能收到通知。 | +| C06-FR07 | 最小载荷 | 推送至少包含消息 ID、类型、标题/摘要、关联业务类型与 ID、创建时间;不发送完整订单、支付信息、地址、Token 或其他敏感字段。 | +| C06-FR08 | 失败隔离 | SignalR 或 Redis 推送失败不得回滚已提交的订单、支付、售后事务和 M09 消息;失败需留下可关联的日志和指标。 | +| C06-FR09 | 连接生命周期 | 用户主动退出后关闭当前连接;服务端清理断开的连接状态,不依赖单个 API 实例内存保存跨实例唯一在线状态。 | +| C06-FR10 | 非打扰式反馈 | 实时消息采用轻量提示和未读角标,不使用必须立即关闭的连续模态弹窗;相同消息 ID 只展示一次。短暂断线静默重连,持续断线才显示连接状态,重连成功后自动恢复提示并刷新未读数。 | +| C06-FR11 | 身份化路由 | Hub 连接和用户通道必须来自服务端认证结果;买家与商家可以复用技术通道,但接收组、消息模板和跳转目标按身份隔离,客户端不得通过修改参数订阅其他身份或其他用户。 | + +#### 3. 时序要求 + +1. 业务模块提交事务并形成事件。 +2. M09 消费事件、幂等生成站内消息并提交数据库事务。 +3. 消息提交成功后调用实时推送能力,按接收用户 ID 发送最小载荷。 +4. Redis Backplane 将通知传播到持有该用户连接的 API 实例。 +5. 前端收到通知后更新角标、展示轻提示,并可按消息 ID 查询详情;客户端不得仅凭推送载荷自行修改订单最终状态。 +6. 若步骤 3~5 任一步失败,用户在重连或打开消息中心时通过 M09 查询补偿。 + +#### 4. 安全、异常与边界规则 + +- Hub 方法和连接组操作均基于服务端认证身份,不提供“传入任意 userId 即可订阅”的接口。 +- WebSocket 握手、普通 API 和 Nginx 转发使用一致的 JWT 验签配置;Token 出现在连接参数时不得被日志完整记录。 +- 网络抖动造成重复连接或重复推送时,前端以消息 ID 去重展示;数据库未读数不得因重复推送增加。 +- Redis Backplane 短暂不可用时允许实时能力降级,但 M09 查询必须保持可用;恢复后不要求重放所有推送,因为持久化列表负责补偿。 +- 单个 API 实例停止后,连接到该实例的客户端应进入重连流程并切换到可用实例;系统不承诺连接完全无中断,但必须保证消息事实不丢失。 +- 前端页面不可见或浏览器节流时,不以客户端收到时间作为业务发生时间,统一展示服务端消息创建时间。 + +#### 5. 现场验收场景 + +| 场景 | 操作 | 预期结果 | +|---|---|---| +| 正常推送 | 买家在线时由商家完成发货 | 买家在无需刷新页面的情况下收到发货通知,消息中心存在同一消息。 | +| 断线补偿 | 断开网络后触发订单状态消息,再恢复网络 | 客户端自动重连;即使实时提示未重放,未读数和消息列表也能查到该消息。 | +| 多标签页 | 同一账号打开至少两个标签页并触发一条消息 | 两个标签页均收到通知;任一标签页标记已读后,刷新另一标签页可看到一致已读状态。 | +| 用户隔离 | 用户 A、B 同时在线,只触发 A 的订单消息 | 只有 A 的连接收到通知,B 的消息列表和未读数不变化。 | +| 身份隔离 | 买家、被指定的商家运营账号、未被指定的商家运营账号和管理员同时在线,触发一笔订单状态变化 | 只向事件明确指定的买家或商家运营账号推送;其他在线账号不收到该私人订单通知。 | +| 多实例 | 两个 API 实例运行,连接落到实例 1,事件由实例 2 触发 | 通过 Redis Backplane 成功送达,并能用实例标识、Trace 或日志证明跨实例路径。 | +| 单实例故障 | 保持客户端在线并停止其当前连接所在 API 实例 | 客户端重连到存活实例;重新查询后消息和未读状态完整。 | +| 推送依赖故障 | 暂停 Redis/实时推送后触发消息 | 原业务和消息落库成功;恢复后用户通过列表补查,不出现消息丢失。 | +| 非打扰体验 | 连续触发多条消息并制造一次短暂断线 | 当前表单或操作不被中断;相同消息不重复弹出;短暂断线自动恢复,用户仍能从消息中心查看全部消息。 | + +#### 6. 验收证据与答辩要求 + +- 保留多标签页、断线重连、用户隔离、多 API 实例和单实例停止的可重复操作脚本或步骤。 +- 保存每次演示的 API 实例标识、连接/重连时间、消息 ID、`traceId`、关键日志和最终数据库查询结果。 +- 能说明 WebSocket 与 HTTP 的差异、SignalR 的作用、JWT 如何认证连接、为什么不能相信客户端传入的用户 ID。 +- 能说明 Redis Backplane 解决的是跨实例连接路由而非消息持久化,以及 M09 如何补偿断线和推送失败。 ### C07 缓存与性能优化 — 罗皓晨 -- 使用 Redis 优化首页商品数据和商品详情查询。 -- 商品改价、库存或上下架变更后必须执行缓存失效策略,并说明最迟多久可见新值及原因。 -- 准备启用缓存前后的压测对比,记录命中率、吞吐量、平均/百分位耗时和数据库压力。 -- 答辩须讲清 Cache-Aside、TTL、缓存穿透/击穿基本处理及数据库仍是事实来源。 +#### 1. 挑战目标与协作边界 + +C07 使用 Redis 优化首页商品摘要和商品详情两个高频只读场景,在不改变接口结果和权限规则的前提下降低 PostgreSQL 重复查询压力,并通过可重复压测证明效果。罗皓晨负责 Redis 接入、通用缓存能力、多实例一致使用和压测环境;顾欣月负责确认商品改价、库存、上下架及商品内容变更后哪些缓存必须失效。数据库始终是事实来源,缓存不得保存唯一业务事实。 + +本期不缓存购物车、订单、支付和售后写操作,不使用缓存替代库存事务,也不建设通用分布式缓存框架或多级缓存平台。 + +**用户体验目标**:用户打开首页和商品详情时应明显感到响应快速、内容稳定,不因缓存而看到长期错误的价格、库存或上下架状态。缓存故障时页面可以稍慢,但不能直接白屏;请求较慢时提供加载占位,查询失败时保留页面结构并给出重试入口。任何性能优化都不能以牺牲数据正确性和操作可理解性为代价。 + +**身份处理矩阵:** + +| 身份 | 是否涉及 | 缓存处理要求 | +|---|---|---| +| 游客 | 是 | 读取已上架商品的公开首页摘要和详情缓存,不得看到草稿、下架商品、商家字段或任何用户个性化数据。 | +| 会员(买家) | 是 | 浏览阶段可复用与游客一致的公开商品缓存;收藏状态、购物车数量等个人字段不得混入公共缓存;下单时重新从数据库校验价格、库存和可售状态。 | +| 商家(运营人员) | 是 | 商品新增、改价、改库存、上下架或修改内容成功后触发相关缓存失效;商家管理查询以授权后的事实数据为准,不直接使用可能隐藏管理字段的公开缓存。 | +| 管理员 | 否 | 当前 C07 只优化首页和商品详情,不为管理员账号治理页面增加缓存,避免扩大范围。 | + +#### 2. 功能需求 + +| 编号 | 功能 | 详细要求 | +|---|---|---| +| C07-FR01 | 缓存对象 | 至少缓存首页商品摘要和商品详情;缓存内容只包含接口返回所需且允许公开的数据,不缓存管理员字段、连接信息或用户敏感数据。 | +| C07-FR02 | Cache-Aside 读取 | 查询先读取 Redis;命中时返回缓存值,未命中时查询 PostgreSQL、生成与原接口一致的响应并写入有限 TTL 的缓存。 | +| C07-FR03 | Key 隔离 | Key 必须包含环境、模块、资源类型、资源 ID 或稳定查询标识及必要版本信息,避免不同环境、不同查询条件和不同数据结构互相污染。 | +| C07-FR04 | 写后失效 | 商品改价、库存调整、上下架、名称/图片/描述变更的数据库事务提交后,删除受影响的详情和首页缓存;事务回滚时不得提前删除并生成错误的新值。 | +| C07-FR05 | 最终一致窗口 | 明确每类缓存 TTL、主动失效时机和删除失败后的重试方式;文档和验收报告必须给出理论最迟生效时间,不能只描述“最终会一致”。 | +| C07-FR06 | 空值保护 | 对不存在或不可售商品的重复查询采用短时空值、受控校验或等价方式降低缓存穿透;空值有效期必须短于正常数据且不能掩盖新上架商品。 | +| C07-FR07 | 热点保护 | 同一热点 Key 并发失效时使用受控的请求合并、短期互斥或等价策略减少数据库瞬时冲击;等待失败的请求必须有超时和回退,不得无限阻塞。 | +| C07-FR08 | 故障降级 | Redis 不可用时,允许首页和商品详情回退到 PostgreSQL 并记录降级指标;不得返回无法判断新旧的缓存副本冒充数据库结果。 | +| C07-FR09 | 多实例一致使用 | 两个 API 实例共享同一 Redis 和 Key 约定;任一实例完成商品变更后,其他实例后续读取应遵守同一失效结果。 | +| C07-FR10 | 可观测性 | 记录命中、未命中、写入、失效、错误和降级次数,以及缓存读取耗时;日志携带资源标识和 `traceId`,但不记录完整缓存值中的敏感信息。 | +| C07-FR11 | 用户感知性能 | 首页和商品详情请求期间展示与页面结构一致的加载占位,避免内容突然跳动;请求失败提供就地重试。Redis 降级到数据库时不向用户暴露技术错误,只有数据库查询也失败时才展示统一错误反馈。 | +| C07-FR12 | 身份与数据隔离 | 公共商品缓存只能存放游客和买家均可见的数据;个人字段、商家管理字段和管理员字段使用独立查询且默认不进入本期缓存,防止不同身份共享 Key 导致越权泄露。 | + +#### 3. 读取与更新流程 + +**缓存读取:** + +1. 服务端完成参数、资源范围和公开可见性校验,构造稳定缓存 Key。 +2. 查询 Redis;命中且反序列化成功时返回缓存响应,并记录命中指标。 +3. 未命中时查询 PostgreSQL;资源不存在时按约定写入短期空值或直接返回不存在。 +4. 查询成功后以有限 TTL 写入 Redis;缓存写入失败只影响性能,不改变本次数据库查询结果。 + +**商品变更:** + +1. 商品模块在 PostgreSQL 事务内完成改价、库存、上下架或内容更新。 +2. 事务成功后触发缓存失效;删除商品详情 Key,并删除或版本化受影响的首页列表 Key。 +3. 删除失败时记录待重试信息;在重试完成前由较短 TTL 限制旧值最长存在时间。 +4. 下一次读取未命中后从 PostgreSQL 回填新值,所有 API 实例共享更新结果。 + +#### 4. 一致性与边界规则 + +- 价格、库存和上下架状态以 PostgreSQL 当前值为准;下单流程必须重新校验数据库,不能相信首页或详情缓存中的库存和价格。 +- 主动失效与有限 TTL 必须同时存在:主动失效缩短正常更新窗口,TTL 负责约束删除失败或漏删后的最长旧值时间。 +- 如果采用延迟二次失效,其目的仅是缩短“并发旧查询回填旧值”的窗口,不能替代事务提交后的首次失效和 TTL。 +- 首页存在分类、排序、分页等多种组合时,只缓存已明确纳入验收的固定首页摘要,不为任意查询参数生成无限数量 Key。 +- 缓存数据结构发生不兼容变化时通过版本化 Key 或受控清理处理,不直接尝试把旧结构反序列化为新结构。 +- 缓存 Key 不得包含用户密码、完整 Token、手机号或收货地址;公开商品缓存不得混入当前登录用户的个性化字段。 + +#### 5. 故障和边界验证 + +- Redis 完全不可用:接口回退数据库并保持结果正确,健康状态和日志能够反映缓存降级。 +- 缓存中存在损坏或旧版本数据:视为未命中并删除异常 Key,不向客户端返回反序列化异常或错误结构。 +- 商品刚下架时发生并发读取:事务提交后的失效流程启动,购物端最终不再展示商品;即使命中旧详情,下单仍通过数据库校验拒绝不可售商品。 +- 热点 Key 同时过期:数据库请求量受到控制,等待请求不会无限阻塞;保护机制失败时仍以正确响应或可解释错误结束。 +- 多实例环境中由实例 1 修改商品、实例 2 查询:实例 2 在约定一致性窗口内读取到新值。 +- 游客和会员读取同一公开商品时可共享公开缓存,但会员个人字段由独立接口返回;商家修改商品后,游客和会员均在一致性窗口内看到新值,商家管理页始终显示其有权查看的完整字段。 + +#### 6. 压测方案与通过标准 + +| 项目 | 要求 | +|---|---| +| 对比环境 | 缓存关闭与缓存开启使用同一 Commit、同一数据库快照、同一机器资源、同一请求脚本和相同并发参数。 | +| 数据准备 | 至少满足课程要求的 30 个商品和 3 个分类;为首页与详情准备固定可重复的热点访问集合。 | +| 测试阶段 | 每组测试包含预热、稳定采样和冷缓存场景;预热数据不混入正式统计。 | +| 指标 | 记录请求总数、成功率、吞吐量、平均耗时、P50、P95、P99、缓存命中率、Redis 错误数和 PostgreSQL 查询次数。 | +| 正确性 | 开关缓存时响应业务字段一致;改价、库存和上下架均在约定时限内生效;测试期间无跨环境 Key 污染。 | +| 性能结果 | 在读多写少的稳定热点场景中,缓存开启后 P95 应低于无缓存基线,吞吐量不低于基线,PostgreSQL 查询次数明显下降;若未改善,必须保留原始结果并解释瓶颈,不能选择性删除失败数据。 | +| 稳定性 | 压测期间无未处理异常;Redis 故障测试能够降级,恢复后可继续产生正常命中。 | +| 用户体验 | 首页和详情加载过程无白屏;缓存命中与数据库回退的页面结构、字段和操作方式一致;改价或下架后用户不会在约定一致性窗口之外继续看到旧值。 | + +#### 7. 验收证据与答辩要求 + +- 保存压测脚本、数据初始化方式、软硬件环境、Commit SHA、配置、测试时间、原始输出和汇总表。 +- 保存商品改价、库存变化、上下架、多实例读取、Redis 故障和热点 Key 过期的操作步骤与结果。 +- 能解释 Cache-Aside 的读写流程、数据库为何仍是事实来源、TTL 与主动失效的分工,以及旧值窗口的来源和理论上限。 +- 能区分缓存穿透、击穿和雪崩,并说明本项目实际处理了哪些场景、没有实现哪些高级方案及原因。 ### C08 支付回调幂等与对账 — 张海洋 @@ -511,10 +820,94 @@ flowchart LR ### C10 容器化部署与负载均衡 — 罗皓晨 -- Docker Compose 一键启动前端、Nginx、至少 2 个 API 实例、Worker、PostgreSQL、Redis、RabbitMQ 和最终启用的对象存储。 -- Nginx 对 API 实例负载均衡,现场通过实例标识或日志证明请求落到不同实例。 -- 停止一个 API 实例后,核心查询和已登录访问仍可用。 -- 登录态使用 JWT 和共享 Redis 能力,不依赖单实例内存 Session;答辩须说明多实例可用原因。 +#### 1. 挑战目标与部署范围 + +C10 要求使用 Docker Compose 从同一版本一次性启动可演示的完整系统,并由 Nginx 将 API 请求负载均衡到至少两个相同版本的 Mall.Api 实例。系统应证明请求能够分发、单个 API 实例停止后核心服务仍可用、已登录用户身份保持有效,并能够支持 C06 WebSocket/SignalR 连接。 + +演示环境至少包含 Nginx、Vue 前端、2 个 Mall.Api、Mall.Worker、PostgreSQL、Redis、RabbitMQ 和开发/演示采用的 SeaweedFS。Aspire 仅用于本地开发编排,不作为 C10 生产式演示入口。 + +**用户体验目标**:用户始终通过同一个网址访问系统,不需要理解后端有几个实例。单个 API 实例故障时,已经打开的页面可以短暂重试但不应退出登录或丢失已提交结果;服务恢复后用户无需手工切换地址。系统确实无法继续服务时,应显示友好错误和重试入口,而不是 Nginx 默认错误页、白屏或无限加载。 + +**身份处理矩阵:** + +| 身份 | 是否涉及 | 多实例与故障处理要求 | +|---|---|---| +| 游客 | 是 | 通过统一地址访问公开页面,请求可落到任一健康 API;实例切换不应影响商品浏览,受保护页面仍要求登录。 | +| 会员(买家) | 是 | JWT 在任一 API 实例均可验证;实例切换后购物、订单、支付和消息权限保持一致,不因负载均衡被退出登录或访问到他人数据。 | +| 商家(运营人员) | 是 | JWT 和 `MerchantOnly` 策略在两个实例结果一致;实例切换后仍只访问商家角色及当前账号有权处理的商品、订单和售后数据。 | +| 管理员 | 是 | JWT 和 `AdminOnly` 策略在两个实例结果一致;实例切换不扩大管理权限,也不允许查看未授权的用户私人业务数据。 | + +#### 2. 功能需求 + +| 编号 | 功能 | 详细要求 | +|---|---|---| +| C10-FR01 | 一键编排 | 提供明确的一条 Docker Compose 启动命令和一条停止命令;首次启动时按依赖关系完成网络、持久卷和服务创建,不要求人工进入容器修改配置。 | +| C10-FR02 | 镜像一致 | 两个 API 实例使用同一 Commit SHA/版本 Tag 构建的同一镜像和等价配置,仅实例标识等运行信息不同;不得用 `latest` 作为唯一可追踪版本。 | +| C10-FR03 | 双实例 API | Compose 默认启动至少两个 API 实例,二者均能独立处理无状态 HTTP 请求并访问共享 PostgreSQL、Redis、RabbitMQ 和对象存储。 | +| C10-FR04 | Nginx 入口 | 浏览器只通过 Nginx 暴露的统一入口访问前端、API 和 WebSocket;后端容器端口默认不直接暴露给公网。 | +| C10-FR05 | 负载均衡 | Nginx 将 API 请求分发到两个健康实例,并正确转发客户端 IP、协议、Host、请求 ID 及 WebSocket Upgrade 所需请求头。 | +| C10-FR06 | 健康检查 | API 提供 `/health/live` 和 `/health/ready`;Nginx/Compose 能识别不可用实例,停止或未就绪实例不应持续接收新请求。 | +| C10-FR07 | 登录态共享 | 身份使用由两个实例共同验证的 JWT;必要的 `jti` 失效记录和共享状态存放 Redis,不依赖单实例内存 Session,因此请求切换实例后仍可鉴权。 | +| C10-FR08 | 实时连接 | Nginx 支持 SignalR WebSocket Upgrade;两个 API 通过 Redis Backplane 共享实时消息通道,单实例停止后客户端可以重连到存活实例。 | +| C10-FR09 | Worker 单独运行 | `Mall.Worker` 使用独立容器运行 Outbox、超时取消或对账任务,不随某个 API 实例停止;同一任务的并发与幂等规则由对应业务模块保证。 | +| C10-FR10 | 数据持久化 | PostgreSQL、Redis(需要保留的运行数据)、RabbitMQ 和 SeaweedFS 使用明确持久卷;重建应用容器不得删除数据库和对象文件。 | +| C10-FR11 | 配置与 Secret | 环境差异通过环境变量或受控文件注入;仓库提供 `.env.example`,不提交真实密码、Token、证书私钥或生产连接信息。 | +| C10-FR12 | 日志与追踪 | 每个容器日志包含服务名和实例标识;API 响应或日志可用于证明请求落点,关键请求能够用 `traceId` 跨 Nginx、API、数据库和消息处理追踪。 | +| C10-FR13 | 连续访问体验 | 前端对短暂网络或实例切换提供有限次数自动重试,仅对安全的查询请求自动重试;提交类请求不得盲目重放。持续失败时展示统一维护/服务不可用页面、可理解提示和手动重试入口。 | +| C10-FR14 | 跨实例身份一致性 | 两个 API 实例必须使用一致的 JWT、Policy 和账号状态校验配置;对同一 Token、同一资源和同一请求应给出一致授权结果,禁止因实例差异出现偶发越权或错误拒绝。 | + +#### 3. 启动与运行流程 + +1. 部署人员准备 Docker/Compose、复制示例环境变量并填写演示环境值,确认端口和持久卷目录可用。 +2. 使用版本化镜像或从指定 Commit 构建前端、API、Worker 和 Nginx 镜像。 +3. Compose 先创建网络和持久卷,再启动 PostgreSQL、Redis、RabbitMQ、SeaweedFS 等依赖。 +4. 依赖达到可用状态后启动两个 API、Worker 和前端/Nginx;数据库迁移采用明确且只执行一次的受控步骤,不允许两个 API 无约束并发迁移。 +5. 部署人员通过统一入口检查首页、Swagger(若演示环境开放)、存活端点、就绪端点和一条核心业务 API。 +6. 使用实例标识端点、响应头或结构化日志连续发送请求,确认两个 API 实例均收到流量。 +7. 停止其中一个 API 实例,继续执行商品查询和已登录访问,确认 Nginx 将新请求转发到存活实例。 +8. 恢复被停止实例,确认其就绪后重新参与请求处理,且数据库、消息和对象数据未丢失。 + +#### 4. 网络、安全与配置规则 + +- 只公开演示必需端口;PostgreSQL、Redis、RabbitMQ 管理端和 SeaweedFS 管理端默认限制在 Compose 网络或受控管理网络。 +- 两个 API 使用一致的 JWT Issuer、Audience 和签名配置;若签名配置不同,请求切换实例将导致登录态失效,验收视为失败。 +- Nginx 不负责保存用户 Session;登录连续性来自可由任一 API 验证的 JWT 和 Redis 中的共享失效/通道数据。 +- 容器不得依赖开发机绝对路径、IDE 启动配置或人工复制 DLL 才能运行。 +- 健康检查不得只验证进程端口打开;就绪状态至少反映 PostgreSQL 和当前阶段必需依赖是否可用。 +- 日志不得输出环境变量中的密码、完整 JWT 或连接字符串;演示截图和报告同样需要脱敏。 +- 数据卷删除属于破坏性运维操作,不包含在日常停止和重启命令中;清空演示数据必须使用单独、明确并经过确认的步骤。 + +#### 5. 故障与恢复场景 + +- **单 API 停止**:Nginx 将新请求转发到存活实例;短暂失败应受连接重试/健康摘除控制,已登录用户无需重新登录。 +- **API 恢复**:恢复实例通过就绪检查后重新加入服务,版本和配置与存活实例一致。 +- **Worker 停止**:普通同步查询仍可使用;Outbox 或后台任务保留待处理数据,Worker 恢复后继续处理且不重复产生业务结果。 +- **Redis 停止**:依赖 C06/C07 的实时和缓存能力允许降级,健康状态和日志清晰;使用 Redis 的 Token 失效策略必须按安全设计处理,不能静默绕过失效校验。 +- **RabbitMQ 停止**:业务事务与 Outbox 事实保留,恢复后继续投递;不得因容器重启丢失已持久化消息。 +- **数据库停止**:API 就绪检查失败,数据库业务不可继续伪装为成功;数据库恢复后实例能够重新就绪。 +- **Nginx 停止**:统一入口不可用,应能通过容器状态和日志快速定位;Nginx 恢复后无需重建业务数据容器。 + +#### 6. 现场验收脚本 + +| 步骤 | 操作 | 预期证据 | +|---|---|---| +| 1 | 从停止状态执行 Compose 启动命令 | 所有必需容器启动,两个 API 和 Worker 状态清晰,持久卷创建成功。 | +| 2 | 通过 Nginx 打开前端并完成登录 | 页面可访问,登录成功,后续请求只使用统一入口。 | +| 3 | 连续请求实例识别/健康或普通查询接口 | 响应头或日志显示请求至少到达两个 API 实例。 | +| 4 | 保持登录状态并停止 API 实例 1 | 商品查询、消息查询等核心请求继续成功,Token 在实例 2 验证有效。 | +| 5 | 触发一条实时消息并观察 WebSocket | SignalR 经 Nginx 正常连接;跨实例消息能够送达,断线后可重连补查。 | +| 6 | 恢复 API 实例 1 | 实例通过就绪检查后重新收到请求,无需重新构建数据库或登录。 | +| 7 | 重启应用容器但保留数据卷 | 用户、商品、消息和对象文件仍存在,证明数据未存于临时容器层。 | +| 8 | 模拟短暂后端不可用和持续不可用 | 短暂故障恢复后查询可继续且登录态保留;持续故障显示友好页面和重试入口,不出现无限加载或浏览器原始错误。 | +| 9 | 分别以游客、会员、商家和管理员连续请求并切换 API 实例 | 各身份在两个实例上的菜单入口、接口授权和数据范围一致;跨身份请求始终被拒绝,合法用户不会因实例切换退出登录。 | + +#### 7. 验收证据与答辩要求 + +- 保存 Compose 配置、`.env.example`、镜像 Tag/Commit SHA、容器清单、网络/卷说明和完整启动命令。 +- 保存两实例请求分布、健康检查、停止/恢复实例、登录态连续性、WebSocket 转发和数据持久化的原始日志或录屏。 +- 能解释 Nginx 反向代理与负载均衡、健康检查、WebSocket Upgrade、容器网络和持久卷的作用。 +- 能解释 JWT 为什么可在多实例验证、Redis 保存哪些共享能力、为什么应用不能依赖单实例内存 Session。 +- 能说明 Aspire 与 Docker Compose 的使用边界,以及为什么演示部署必须使用同一版本镜像和受控 Secret。 ### 4.1 挑战验收证据 @@ -555,7 +948,8 @@ stateDiagram-v2 | 浏览商品 | ✓ | ✓ | ✓ | ✓ | | 管理个人信息、地址、购物车和订单 | | ✓ | | | | 模拟支付自己的订单 | | ✓ | | | -| 评价已完成订单商品、收藏、查看历史和消息 | | ✓ | | | +| 评价已完成订单商品、收藏、查看历史 | | ✓ | | | +| 查看和处理本人站内消息 | | ✓ | ✓ | | | 发起和查询自己的售后申请 | | ✓ | | | | 管理分类和商品 | | | ✓ | | | 查询订单并发货 | | | ✓ | | @@ -584,10 +978,10 @@ stateDiagram-v2 | 端 | 页面 | |---|---| -| 购物端 | 登录、注册、首页/商品列表、商品详情、购物车、提交订单、订单列表、订单详情、个人信息、地址管理 | -| 商家端 | 商家登录/入口、商品列表与编辑、分类管理、订单列表与详情、发货、售后审核 | +| 购物端 | 登录、注册、首页/商品列表、商品详情、购物车、提交订单、订单列表、订单详情、个人信息、地址管理、买家消息中心 | +| 商家端 | 商家登录/入口、商品列表与编辑、分类管理、订单列表与详情、发货、售后审核、商家消息中心 | | 管理端 | 管理员登录/入口、买家账号管理、商家账号管理 | -| 已选选做 | 商品评价与晒图、收藏、浏览历史、站内消息、售后申请、商家售后审核 | +| 已选选做 | 商品评价与晒图、收藏、浏览历史、买家/商家站内消息、售后申请、商家售后审核 | | 挑战演示 | 秒杀活动、进阶搜索、缓存/实例/消息状态辅助展示;压测和部署证据可使用独立脚本与报告 | 页面需要统一提供加载、空数据、错误、无权限和操作成功/失败反馈。具体线框图待前端原型评审后补充。 -- Gitee From d2e3ca8f5cbeb42683b154850a893580eadf230f Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Thu, 23 Jul 2026 10:14:03 +0800 Subject: [PATCH 018/118] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E8=A7=84=E5=88=99=E6=89=81=E5=B9=B3=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 45 +++ eshop-project-rules-upload/AGENTS.md | 289 ++++++++++++++++++ eshop-project-rules-upload/README.md | 49 +++ .../eshop-align-docs.SKILL.md | 70 +++++ .../eshop-align-docs.openai.yaml | 4 + .../eshop-deliver-feature.SKILL.md | 63 ++++ .../eshop-deliver-feature.openai.yaml | 4 + .../eshop-fix-bug.SKILL.md | 61 ++++ .../eshop-fix-bug.openai.yaml | 4 + .../eshop-manage-git.SKILL.md | 49 +++ .../eshop-manage-git.openai.yaml | 4 + .../eshop-project-workflow.SKILL.md | 65 ++++ .../eshop-project-workflow.openai.yaml | 4 + .../eshop-verify-acceptance.SKILL.md | 71 +++++ .../eshop-verify-acceptance.openai.yaml | 4 + .../validate_project_skills.py | 152 +++++++++ 16 files changed, 938 insertions(+) create mode 100644 .gitignore create mode 100644 eshop-project-rules-upload/AGENTS.md create mode 100644 eshop-project-rules-upload/README.md create mode 100644 eshop-project-rules-upload/eshop-align-docs.SKILL.md create mode 100644 eshop-project-rules-upload/eshop-align-docs.openai.yaml create mode 100644 eshop-project-rules-upload/eshop-deliver-feature.SKILL.md create mode 100644 eshop-project-rules-upload/eshop-deliver-feature.openai.yaml create mode 100644 eshop-project-rules-upload/eshop-fix-bug.SKILL.md create mode 100644 eshop-project-rules-upload/eshop-fix-bug.openai.yaml create mode 100644 eshop-project-rules-upload/eshop-manage-git.SKILL.md create mode 100644 eshop-project-rules-upload/eshop-manage-git.openai.yaml create mode 100644 eshop-project-rules-upload/eshop-project-workflow.SKILL.md create mode 100644 eshop-project-rules-upload/eshop-project-workflow.openai.yaml create mode 100644 eshop-project-rules-upload/eshop-verify-acceptance.SKILL.md create mode 100644 eshop-project-rules-upload/eshop-verify-acceptance.openai.yaml create mode 100644 eshop-project-rules-upload/validate_project_skills.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73f8b25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Windows and macOS +Thumbs.db +Desktop.ini +.DS_Store + +# Editors and local workspace +.vscode/ +.idea/ +*.code-workspace +*.swp +*.swo + +# Python tooling +.venv/ +venv/ +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +coverage.xml +htmlcov/ + +# Logs and temporary files +*.log +*.tmp +*.temp +*.bak +*.orig + +# Local secrets and environment files +.env +.env.* +!.env.example + +# Local archives +*.zip +*.7z +*.tar +*.tar.gz + +# Personal repository-local Codex rules +/.agents/ +/AGENTS.md diff --git a/eshop-project-rules-upload/AGENTS.md b/eshop-project-rules-upload/AGENTS.md new file mode 100644 index 0000000..8526fb1 --- /dev/null +++ b/eshop-project-rules-upload/AGENTS.md @@ -0,0 +1,289 @@ +禁止使用全局 skill 和全局 agents。 + +本项目只使用仓库内的项目级规则:根目录 `AGENTS.md`、目标文件路径上更近的模块级 `AGENTS.md`,以及 `.agents/skills/` 下的项目级 skill。 + +# E-Shop 项目级 AGENTS + +## 一、核心规则 + +1. 在回答项目问题、制定计划、修改代码、修改文档或创建文件前,必须先完整阅读根目录 `README.md`,然后使用中文回答。 +2. 每次任务必须先使用项目级入口 Skill:`.agents/skills/eshop-project-workflow/SKILL.md`,再按任务主要目标选择一个专项 Skill。 +3. 不得读取、引用或使用用户目录、系统目录、其他仓库中的全局 skill 或全局 AGENTS 作为本项目规则。 +4. `docs/00-项目要求/` 是教师发布的只读基线,不得修改其中任何文件。 +5. 优先完成当前明确需求,采用符合现有结构的最小可行方案,不得过度设计。 +6. 只修改与当前任务直接相关的文件,不得顺手重构、格式化或清理无关内容。 +7. 不得覆盖、删除、暂存或提交工作区中不属于当前任务的已有改动。 + +如果 `README.md` 不存在,必须明确说明: + +```text +未找到 README.md。 +``` + +然后先检查项目结构,再继续处理。即使项目级 skill 缺失,也不得改用全局 skill;应明确说明缺失情况,并继续遵守本文件中可执行的规则。 + +## 二、规则作用范围与优先级 + +- 根目录 `AGENTS.md` 作用于整个仓库。 +- 模块级 `AGENTS.md` 只作用于其所在目录及子目录。 +- 修改某个文件前,必须从仓库根目录沿目标路径检查所有 `AGENTS.md`。 +- 同一事项发生冲突时,模块级 `AGENTS.md` 优先于根目录 `AGENTS.md`。 +- 模块级规则不得推翻教师要求、项目核心目标、模块负责制、长期分支保护和禁止使用全局规则等硬约束。 +- 教师基线与其他项目文档冲突时,以 `docs/00-项目要求/` 中的项目要求、验收标准和评分标准为准。 +- 用户的新要求会改变任务范围时,先指出影响;不得自行扩大到其他成员模块或其他任务。 + +## 三、每次任务的必读内容 + +开始任何项目任务时,按顺序完成以下预检: + +1. 完整阅读 `README.md`。 +2. 完整阅读根目录 `AGENTS.md`。 +3. 完整阅读 `.agents/skills/eshop-project-workflow/SKILL.md`。 +4. 检查当前分支、远程基线、工作区改动和未跟踪文件,至少确认 `git status --short --branch` 与 `git diff --name-only`。 +5. 检查目标目录及其父目录中是否存在模块级 `AGENTS.md`;存在时必须完整阅读。 +6. 根据任务类型继续读取第四节列出的教师基线、需求、设计、实现和测试文件;读取与当前任务相关的完整章节,不为简单任务加载无关内容,也不得只看单个文件就推断完整链路。 + +已经在当前连续任务中完整读取且内容未发生变化的文件可以不重复输出,但在修改前仍须确认其状态没有变化。 + +### 项目级 Skill 路由 + +| 任务主要目标 | 必读专项 Skill | +|---|---| +| 新增功能、改变预期行为、调整 API/数据库/页面 | `.agents/skills/eshop-deliver-feature/SKILL.md` | +| Bug、报错、白屏、测试失败或行为回归 | `.agents/skills/eshop-fix-bug/SKILL.md` | +| 文档、日报、周报、会议、总结或一致性审查 | `.agents/skills/eshop-align-docs/SKILL.md` | +| 构建、测试、烟测、验收、压测或发布就绪判断 | `.agents/skills/eshop-verify-acceptance/SKILL.md` | +| 分支、暂存、提交、推送、PR/MR、合并或回滚 | `.agents/skills/eshop-manage-git/SKILL.md` | + +- 默认使用“入口 Skill + 1 个专项 Skill”,任务确实跨越两个主要目标时最多叠加第二个专项 Skill,不得每次加载全部 Skill。 +- 功能和缺陷任务的普通针对性验证留在原专项 Skill;完整验收才叠加测试验收 Skill。 +- 只有用户明确要求 Git 操作时才叠加 Git Skill;测试发现缺陷后,只有用户要求修复才切换缺陷 Skill。 +- 不得使用全局 Skill,也不得使用 `.agents/skills/` 之外未列入本路由的个人规则。 + +## 四、按任务类型读取文件 + +### 1. 需求、范围与验收 + +涉及功能范围、角色、权限、业务规则、模块归属或验收结论时,读取: + +- `docs/00-项目要求/项目要求.md` 中与当前任务相关的完整章节; +- `docs/01-需求文档/需求规格说明书.md` 中与当前模块相关的完整章节; +- `docs/00-项目要求/验收标准.md` 中对应的 F、X、C、N 或 D 编号; +- 涉及验收、计划、报告或成绩判断时,再读取 `docs/00-项目要求/评分标准.md` 中相关评分与扣分规则。 + +不得把计划项、占位内容或尚未实现的设计描述成已完成功能。 + +### 2. 架构、技术选型与公共能力 + +涉及目录结构、依赖方向、公共组件、认证授权、事件、缓存、对象存储、可观测性或部署边界时,读取: + +- `docs/02-设计文档/系统架构设计.md`; +- 相关项目文件、入口文件、依赖清单和配置文件; +- 当前能力涉及的需求与验收章节。 + +不得脱离现有架构引入新框架、新基础设施或新分层。 + +### 3. 数据库、实体、Migration 与种子数据 + +涉及表、字段、索引、关系、状态、事务、Migration 或演示数据时,读取: + +- `docs/02-设计文档/数据库设计.md`; +- 对应需求与接口章节; +- 现有实体、映射、DbContext、Migration、初始化或种子数据文件; +- 相关测试。 + +不得随意删除字段、破坏历史数据、绕过模块边界直接改表,或假设 Migration 可以无风险执行。 + +### 4. Web API、DTO 与模块间协作 + +涉及接口、请求响应、错误码、分页、鉴权或模块间调用时,读取: + +- `docs/02-设计文档/接口设计.md`; +- 对应需求、数据库设计和验收标准; +- 现有 Controller/Endpoint、DTO、应用服务、领域逻辑和测试; +- OpenAPI/Swagger 契约文件(存在时)。 + +必须遵守“接口文档先行”:模块间接口需要变化时,先确认并更新接口契约,再修改实现和调用方。不得通过跨模块 DbContext、内部仓储或直接改表代替公开接口。 + +### 5. 前端页面与交互 + +涉及前端时,读取: + +- `frontend/` 路径上的模块级 `AGENTS.md`(存在时); +- `frontend/package.json` 及实际使用的锁文件; +- 相关路由、页面、组件、状态管理、API 客户端、类型和测试; +- 对应需求、接口与验收章节。 + +复用现有组件和视觉语言;不得私自更换 UI 框架、状态管理方案或构建工具。 + +### 6. 后端功能与后台任务 + +涉及后端时,读取: + +- `backend/` 路径上的模块级 `AGENTS.md`(存在时); +- 解决方案、项目文件、程序入口和组合根; +- 当前模块的 API、Application、Domain、Infrastructure 和测试链路; +- 对应需求、接口、数据库与验收章节。 + +遵守现有依赖方向和模块边界;不得把单模块逻辑提前抽象为全局基础设施。 + +### 7. 测试、缺陷与验收证据 + +涉及测试或完成度判断时,读取: + +- `docs/03-测试文档/测试计划.md`; +- `docs/03-测试文档/测试报告.md`; +- 对应验收标准; +- 现有测试项目、测试配置、脚本和 CI 配置。 + +只记录真实执行的命令、环境、输入和结果。未运行的测试不得写成通过,模板中的占位数据不得写成真实结果。 + +### 8. Git、分支、提交与 PR/MR + +涉及 Git 操作时,完整阅读 `docs/02-设计文档/Git团队协作流程.md`,并遵守: + +- `master` 是稳定发布分支,`dev` 是日常集成分支,二者均禁止直接开发和直接 Push; +- 普通任务从最新 `dev` 创建短生命周期分支; +- 分支格式为 `<类型>/<模块>-<任务>-<姓名拼音首字母>`; +- 一个任务只使用一个分支和一个 PR/MR,不混入无关改动; +- 合入 `dev` 前需要真实验证、CI 和至少一名其他成员交叉 Code Review; +- 未经用户明确要求,不提交、不推送、不创建 PR/MR; +- 工作区不干净时先识别改动归属,不强制切换、不擅自 stash、不丢弃修改。 +- 暂存时使用明确文件路径并先检查差异,禁止无检查执行 `git add .`; +- `reset --hard`、`clean -fd`、强制删除、Rebase 和强推属于高风险操作,必须先确认精确目标与可恢复性;长期分支禁止强推,个人任务分支确需强推时只使用 `--force-with-lease`; +- 多个会修改文件的并行任务必须使用不同任务分支和独立 Worktree,禁止同时修改同一工作区或同一批文件。 + +### 9. 日报、周报、会议与总结 + +涉及过程文档时,读取: + +- 日报:`reports/daily/README.md`、当天本人提交记录和工作区证据; +- 周报:`reports/weekly/README.md`、本周成员提交记录及真实产出; +- 会议纪要:`docs/04-会议记录/会议纪要模板.md` 和会议事实; +- 总结答辩:`docs/05-总结答辩/项目总结报告.md`、需求、设计、测试和实际实现证据。 + +日报、周报和总结必须真实、具体,不得批量补写,不得用计划或他人成果冒充本人完成内容。 + +### 10. 配置、部署与 CI + +涉及环境、容器、发布或 CI 时,读取: + +- README 与系统架构中的环境、部署章节; +- 实际存在的 Compose、Dockerfile、Nginx、AppHost、环境变量示例和 CI 文件; +- 对应测试报告、Migration 与回滚说明。 + +不得提交真实密码、Token、密钥、生产连接信息或仅适用于个人电脑的绝对路径。 + +## 五、模块负责制与协作边界 + +1. 本项目按业务模块纵向负责,不按前端、后端横向分工。 +2. 功能任务应覆盖该模块所需的数据库、后端接口、前端页面、测试和文档链路;如用户只要求其中一部分,必须明确剩余链路,不得宣称整个模块完成。 +3. 不得批量代写其他成员模块,不得擅自更改模块负责人、功能编号或验收边界。 +4. 公共能力只处理两个及以上模块已经确认的共同需求;单模块逻辑优先留在所属模块。 +5. 跨模块协作使用公开 API、应用接口或集成事件,不直接依赖其他模块内部实现。 +6. 变更接口、数据库或共享配置时,必须说明影响模块、兼容性和联调要求。 +7. 保持现有模块化单体边界,不擅自拆分微服务;DDD、CQRS 和事件只用于架构文档已确认的复杂领域,简单 CRUD 不增加重型模式。 +8. PostgreSQL 是业务事实来源;Redis、RabbitMQ、缓存和进程内存不得保存无法恢复的唯一业务事实。 + +## 六、修改前要求 + +修改任何文件前必须: + +1. 明确用户目标、当前模块、需求编号和验收条件。 +2. 完成第三、四节要求的文件读取。 +3. 检查现有相似实现、目录结构、命名和代码风格。 +4. 检查 Git 分支与工作区,识别并保护无关改动。 +5. 判断功能应放在哪个目录、哪一层、哪个文件。 +6. 选择最小且安全的文件集合。 +7. 用中文简要说明准备修改什么以及为什么。 + +如果相关链路没有理解清楚,不得直接修改。 + +## 七、修改时要求 + +- 只修改当前任务直接需要的文件。 +- 遵循现有目录、命名、代码和文档风格。 +- 不删除已有功能,不改变未被当前需求要求改变的接口行为。 +- 不进行全仓格式化、无关重构、随意移动或重命名。 +- 不引入新架构、新框架或新依赖,除非需求明确且现有方案无法满足。 +- 不创建无必要的接口、抽象类、基类、通用仓储或额外分层。 +- 不为未来可能出现的需求提前增加扩展点。 +- 不修改 `docs/00-项目要求/`。 +- 不暴露或硬编码敏感信息。 +- 密码使用可靠哈希,数据库查询参数化,权限和资源归属必须由服务端校验,不能只依赖前端隐藏入口。 +- 不生成并提交 `node_modules`、`dist`、`bin`、`obj`、缓存、日志和临时文件。 +- 发现任务外问题时记录并说明,不顺手扩大修改范围。 + +## 八、构建、测试与检查 + +1. 只使用仓库中真实存在的构建、测试、格式化和检查命令。 +2. 不确定命令时,先检查 README、`package.json`、解决方案/项目文件、Makefile、脚本目录和 CI 配置。 +3. 先运行与改动最相关的检查,再根据风险运行更完整的构建或测试。 +4. 文档或规则修改至少检查 `git diff --check`、实际差异、文件编码、路径和链接。 +5. 接口、权限、订单、库存、支付、并发、Migration、上传、Docker 和生产配置属于高风险变更,必须增加针对性验证。 +6. 不得伪造命令、输出、测试数量、覆盖率或运行环境。 +7. 修改项目级 Skill 后,必须运行 `python -X utf8 .agents/skills/eshop-project-workflow/scripts/validate_project_skills.py`。 +8. 当前环境可用官方 Skill 校验器时,再对每个已修改 Skill 运行 `quick_validate.py`,但不得依赖团队成员电脑上的全局 Skill 内容作为项目规则。 + +### Python 工具环境 + +- Python 只用于仓库自动化、检查和辅助脚本,不改变 Vue 3、.NET 10、PostgreSQL 的产品技术栈。 +- 本项目工具当前在 Python 3.14.6 上验证;运行仓库脚本时使用 `python -X utf8`,保证 Windows 中文路径与输出编码一致。 +- 仓库工具优先使用 Python 标准库。确需第三方包时,必须先说明用途,并提交依赖清单和可复现安装方式;不得把个人全局 `site-packages` 当作团队隐式依赖。 +- 不提交 `.venv/`、`__pycache__/`、`.pytest_cache/`、`.mypy_cache/`、`.ruff_cache/`、覆盖率文件或其他 Python 临时产物。 + +如果没有找到可用命令,必须明确说明: + +```text +未找到明确的构建或测试命令,因此没有运行。 +``` + +## 九、修改后与最终回复要求 + +修改后必须重新检查工作区,区分本次改动与原有改动,并用中文说明: + +### 1. 修改了什么 + +说明本次实际完成的内容和关键取舍。 + +### 2. 修改了哪些文件 + +列出本次新增、修改或删除的全部文件。没有修改时明确写“未修改文件”。 + +### 3. 是否已阅读 README.md + +明确写“已阅读 README.md。”或“未找到 README.md。” + +### 4. 是否读取了模块级 AGENTS.md + +列出已读取的相关模块级文件;不存在时明确写“未发现相关模块级 AGENTS.md。” + +### 5. 是否运行了构建或测试命令 + +列出真实运行的命令及结果;未运行时说明原因。 + +### 6. 为什么没有过度设计 + +说明方案如何复用现有结构、控制文件数量、避免无关抽象或依赖。 + +### 7. 剩余风险 + +说明未验证功能、未覆盖边界、兼容性影响和环境限制;没有明显风险时写“暂无明显剩余风险。” + +不得只回复“完成了”或夸大完成度。 + +## 十、模块级 AGENTS.md 编写要求 + +只有模块确有独立规则时才创建模块级 `AGENTS.md`,内容只写该模块特有的目录、边界、构建、测试或风格要求,不复制本文件全文。 + +模块级规则应简短、可执行,并明确适用范围。不得为了形式完整而给每个目录创建空泛或重复的 `AGENTS.md`。 + +## 十一、防止过度设计 + +本项目优先级: + +```text +能跑 > 清晰 > 易维护 > 可扩展 +``` + +当前需求没要求的功能不提前做,当前规模用不到的架构不提前引入,现有结构能解决的问题不新建体系,局部修改能解决的问题不重写模块。 diff --git a/eshop-project-rules-upload/README.md b/eshop-project-rules-upload/README.md new file mode 100644 index 0000000..fbb5ba7 --- /dev/null +++ b/eshop-project-rules-upload/README.md @@ -0,0 +1,49 @@ +# E-Shop 项目级 AGENTS 与 Skill 扁平上传包 + +本目录用于单独上传 Git。所有文件都位于同一层,没有嵌套子目录。 + +## 文件说明 + +- `AGENTS.md`:项目全局规则。 +- `eshop-*.SKILL.md`:六个项目级 Skill 的主文件。 +- `eshop-*.openai.yaml`:各 Skill 对应的界面元数据。 +- `validate_project_skills.py`:原项目目录结构的自动校验脚本。 +- `.gitignore`:忽略本地环境、缓存、日志、临时文件和敏感配置。 + +## 恢复到项目中的目录结构 + +扁平文件适合上传和集中审阅,但 Codex 不会把 `eshop-*.SKILL.md` 直接识别为可用 Skill。需要使用时,按下面的规则恢复: + +```text +AGENTS.md +.agents/ +└─ skills/ + └─ / + ├─ SKILL.md + └─ agents/ + └─ openai.yaml +``` + +其中: + +```text +eshop-project-workflow.SKILL.md +→ .agents/skills/eshop-project-workflow/SKILL.md + +eshop-project-workflow.openai.yaml +→ .agents/skills/eshop-project-workflow/agents/openai.yaml +``` + +其他五个 Skill 按相同规则恢复。校验脚本恢复到: + +```text +.agents/skills/eshop-project-workflow/scripts/validate_project_skills.py +``` + +恢复后运行: + +```powershell +python -X utf8 .agents/skills/eshop-project-workflow/scripts/validate_project_skills.py +``` + +不要修改 `SKILL.md` frontmatter 中的 `name`,它必须与恢复后的 Skill 目录名一致。 diff --git a/eshop-project-rules-upload/eshop-align-docs.SKILL.md b/eshop-project-rules-upload/eshop-align-docs.SKILL.md new file mode 100644 index 0000000..c6cf613 --- /dev/null +++ b/eshop-project-rules-upload/eshop-align-docs.SKILL.md @@ -0,0 +1,70 @@ +--- +name: eshop-align-docs +description: 维护 E-Shop 教师要求、需求、架构、数据库、接口、实现、测试与过程材料之间的一致性。编写、修改或审查 README、需求/设计/测试文档、日报、周报、会议纪要、总结答辩、项目说明或完成度表述时使用。 +--- + +# E-Shop 文档一致性 + +## 先使用总入口 + +- 先执行 `$eshop-project-workflow` 的规则读取、工作区预检和范围确认。 +- 文档是主要交付物时使用本 Skill;功能开发中只同步一个直接相关接口文档时仍由功能 Skill 负责。 +- 不修改代码来迁就文档,除非用户明确扩大任务范围。 + +## 确定事实来源 + +按以下优先级核对内容: + +1. `docs/00-项目要求/` 中教师发布的项目要求、验收标准和评分标准;该目录只读。 +2. 实际存在的代码、配置、数据库、运行结果、测试结果和 Git 证据。 +3. 已确认的需求规格、架构、数据库、接口和测试计划。 +4. 模板、占位内容和未来计划只能作为待办,不能证明已经实现。 + +教师基线与其他文档冲突时以教师基线为准;设计文档与真实实现冲突时必须明确指出差异,不能静默选择更好看的说法。 + +## 按文档类型读取 + +| 文档类型 | 必读事实来源 | +|---|---| +| 需求与范围 | 项目要求、验收编号、需求对应模块、角色与负责人 | +| 架构/数据库/API | 需求、实际目录/依赖、实体/Migration、OpenAPI 和调用方 | +| 测试计划/报告 | 验收标准、实际测试项目、命令、环境、原始结果和缺陷闭环 | +| 日报 | `reports/daily/README.md`、本人当天 Git 提交、工作区和真实验证证据 | +| 周报 | `reports/weekly/README.md`、本周成员提交与实际交付 | +| 会议纪要 | 模板、真实参会人、决策、负责人、截止时间和未决项 | +| 总结/答辩 | 需求、设计、实现、测试报告、部署和可演示证据 | + +## 建立一致性追踪 + +逐项核对: + +```text +F/X/C/N/D 编号 → 负责人 → 需求与边界 → 数据设计 → API 契约 +→ 实际实现 → 权限与状态 → 测试场景 → 验证证据 → 当前状态 +``` + +使用明确状态: + +- `已验证`:有真实命令、场景或运行证据; +- `已实现未验证`:存在实现但未完成本轮验证; +- `部分实现`:仅完成纵向链路的一部分; +- `计划中`:只有需求或设计; +- `缺失`:要求存在但没有对应资产。 + +不得使用“已完成”“全部通过”“可部署”等词替代尚未获得的证据。 + +## 聚焦修改 + +- 只修改目标文档及保持直接一致所必需的关联文档。 +- 不批量重写教师文件,不改变已确认项目方向和成员归属。 +- 保留原模板结构和仓库命名风格,不为排版引入无关生成工具。 +- 日报、周报和总结只写真实工作,不补造时间、测试数量、提交或他人成果。 +- 量化数字必须能追溯到当前文件、命令或原始结果。 + +## 验证文档 + +检查 Markdown 标题、表格、编号、路径、相对链接、UTF-8 编码、术语和负责人一致性,并运行 `git diff --check`。若仓库存在文档检查脚本,再运行真实脚本并记录结果。 + +## 交付结果 + +列出修改的文档、使用的事实来源、修正的一致性问题、仍为计划/部分/缺失的内容和未验证风险。 diff --git a/eshop-project-rules-upload/eshop-align-docs.openai.yaml b/eshop-project-rules-upload/eshop-align-docs.openai.yaml new file mode 100644 index 0000000..dcf55dc --- /dev/null +++ b/eshop-project-rules-upload/eshop-align-docs.openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "E-Shop 文档一致性" + short_description: "基于需求、实现和验证证据维护 E-Shop 项目文档一致性" + default_prompt: "使用 $eshop-align-docs 检查并维护这项 E-Shop 文档的一致性。" diff --git a/eshop-project-rules-upload/eshop-deliver-feature.SKILL.md b/eshop-project-rules-upload/eshop-deliver-feature.SKILL.md new file mode 100644 index 0000000..8fb31ad --- /dev/null +++ b/eshop-project-rules-upload/eshop-deliver-feature.SKILL.md @@ -0,0 +1,63 @@ +--- +name: eshop-deliver-feature +description: 按业务模块纵向交付 E-Shop 新功能或预期行为变更,覆盖需求编号、数据库、API/OpenAPI、后端、前端、测试和必要文档。新增或修改 F01-F13、X01-X04、挑战模块、M00 公共能力、页面、DTO、数据表、Migration 或跨模块公开契约时使用。 +--- + +# E-Shop 功能纵向交付 + +## 先使用总入口 + +- 先执行 `$eshop-project-workflow` 的规则读取、工作区预检和范围确认。 +- 已有预期行为损坏时改用 `$eshop-fix-bug`;主目标是完整验收时使用 `$eshop-verify-acceptance`。 +- 仅在用户明确要求分支、提交、推送或 PR 时叠加 `$eshop-manage-git`。 + +## 建立任务卡 + +1. 从 `docs/00-项目要求/项目要求.md`、`验收标准.md` 和 `docs/01-需求文档/需求规格说明书.md` 确认需求编号、负责人、角色、业务规则和验收条件。 +2. 需求编号、功能名称、负责人或验收项相互冲突时停止实施并请求确认,不自行合并、重编号或替换负责人。 +3. 写明本次包含项、明确排除项、依赖模块和受影响用户流程。 +4. 检查相关实现是否真实存在;不存在时先说明脚手架或契约缺口,不虚构代码结构。 +5. M00 或公共脚手架缺失时将其记录为前置依赖;除非当前任务明确属于 M00,不由单个业务功能任务顺手搭建全仓或代写公共负责人工作。 +6. 区分计划、已实现、已验证和缺失状态。 + +## 建立纵向影响清单 + +| 层面 | 必查内容 | +|---|---| +| 需求 | 对应 F/X/C/M 编号、角色、权限、主流程、异常和验收条件 | +| 数据 | `数据库设计.md`、实体、映射、约束、索引、Migration、Seed 和历史兼容性 | +| 契约 | `接口设计.md`、OpenAPI、DTO、错误码、分页、鉴权和调用方 | +| 后端 | API/Endpoint、Application、Domain、Infrastructure、事务、事件和后台任务 | +| 前端 | 路由、页面、组件、Store、API 客户端、类型、加载/空态/错误反馈 | +| 质量 | 单元测试、集成测试、前端测试、必要场景验证和文档同步 | + +只读取和当前功能相关的完整章节与代码链路。纵向清单用于逐层检查影响,不代表每个功能必须机械修改所有层;只修改真正受影响的层,并说明未涉及层为何无需变更。 + +## 按安全顺序实施 + +1. 先固定需求与验收边界。 +2. 需要改变模块间接口时先更新接口设计或 OpenAPI 契约,再改实现与调用方。 +3. 需要持久化变更时同步实体、映射、Migration、约束、兼容性、Seed 和测试。 +4. 后端实现服务端参数校验、Policy、资源归属、事务、一致性、幂等和错误处理。 +5. 前端实现真实 API 调用、类型、角色入口和加载、空数据、成功、失败、无权限反馈。 +6. 通过公开 API、应用接口或集成事件协作,不跨模块直接使用内部 DbContext、仓储或表。 +7. 只为已经确认的共同需求建设公共能力;单模块逻辑留在本模块。 + +简单 CRUD 保持简单。只有架构文档已确认的复杂规则才使用 DDD、CQRS、Outbox、缓存或消息等机制。 + +## 验证纵向闭环 + +1. 从 README、依赖文件、项目文件、脚本和 CI 中发现真实命令。 +2. 先运行最接近改动的单元、类型、契约或组件测试。 +3. 再按风险运行前端构建、后端构建、集成测试、API/数据库检查和浏览器主流程。 +4. 权限、库存、订单、支付、并发、Migration、上传和部署变更增加边界验证。 +5. 仓库缺少脚手架或命令时明确说明,不能把计划命令写成已通过。 + +## 交付结果 + +按“需求 → 数据 → 契约 → 后端 → 前端 → 测试 → 文档”列出实际覆盖情况,并明确: + +- 已完成和已验证的纵向链路; +- 未完成、未验证或由其他负责人承担的链路; +- 接口、数据库和跨模块兼容性影响; +- 实际命令、结果、证据和剩余风险。 diff --git a/eshop-project-rules-upload/eshop-deliver-feature.openai.yaml b/eshop-project-rules-upload/eshop-deliver-feature.openai.yaml new file mode 100644 index 0000000..d78ef78 --- /dev/null +++ b/eshop-project-rules-upload/eshop-deliver-feature.openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "E-Shop 功能纵向交付" + short_description: "按需求编号完成数据库、接口、后端、前端与测试纵向交付" + default_prompt: "使用 $eshop-deliver-feature 按模块完成这项 E-Shop 功能的纵向交付。" diff --git a/eshop-project-rules-upload/eshop-fix-bug.SKILL.md b/eshop-project-rules-upload/eshop-fix-bug.SKILL.md new file mode 100644 index 0000000..f708f19 --- /dev/null +++ b/eshop-project-rules-upload/eshop-fix-bug.SKILL.md @@ -0,0 +1,61 @@ +--- +name: eshop-fix-bug +description: 先确认、复现并定位 E-Shop 已有行为中的缺陷,再实施最小根因修复和回归验证。用户报告 Bug、异常、白屏、接口错误、截图问题、测试失败、权限/数据/状态流转异常或行为回归时使用;只要求诊断时保持只读。 +--- + +# E-Shop 缺陷修复 + +## 先使用总入口 + +- 先执行 `$eshop-project-workflow` 的规则读取、工作区预检和范围确认。 +- 如果需要改变原本预期的业务规则或公开契约,转用 `$eshop-deliver-feature` 重新确认需求。 +- 只有用户要求完整验收或 Git 操作时,才叠加对应专项 Skill。 + +## 确认问题成立 + +1. 从需求、接口、验收标准、现有测试或明确产品行为中确定预期结果。 +2. 记录实际结果、复现步骤、账号角色、数据前置条件、浏览器/服务版本和环境。 +3. 优先使用失败测试、日志、浏览器操作、API 响应或数据库只读查询复现。 +4. 截图驱动的前端问题要检查真实页面入口和交互,不只看静态 DOM 或代码。 +5. 无法复现时继续收集证据,不进行猜测性修改。 + +用户只要求诊断、审查或原因说明时,到定位和证据结论为止,不修改文件。 + +## 定位首个错误点 + +沿实际调用链检查: + +```text +页面/交互 → API 客户端 → Controller/Endpoint → Application/Domain → 数据库/缓存/消息/外部依赖 +``` + +将原因分类为: + +- 实现缺陷; +- 数据或种子问题; +- 配置或环境问题; +- 契约或文档不一致; +- 使用方式或权限前置条件不满足。 + +检查相关模块级 `AGENTS.md`、需求与验收编号、接口/数据库设计、完整代码链路、现有测试和必要 Git 历史。不要因为症状出现在前端就默认根因也在前端。 + +## 实施最小修复 + +1. 修复已确认的根因,不只遮住可见症状。 +2. 保留未被当前缺陷影响的接口、状态流转和用户行为。 +3. 不借修 Bug 进行大范围重构、依赖升级或架构替换。 +4. 不用吞异常、硬编码数据、关闭校验、扩大权限或跳过事务来伪造成功。 +5. 在最接近根因的层增加或更新回归测试;无法自动化时记录可重复的手工步骤。 +6. 若根因属于其他成员模块,说明证据和影响边界,不越权批量改写该模块。 + +## 验证修复 + +- 重新执行原始复现步骤,记录修复前后差异。 +- 运行新增回归测试和最相关的既有测试。 +- 检查相邻正常流程、异常流程、权限边界和数据一致性没有回归。 +- UI 问题使用真实浏览器路径验证;API/数据库问题保留状态码、响应和只读查询证据。 +- 只报告实际执行的命令;环境受限或测试不存在时明确说明。 + +## 交付结果 + +说明问题是否确认、根因位置、最小修改、复现与回归证据、未覆盖边界和剩余风险。若未修复,明确停在哪一步以及还缺什么信息。 diff --git a/eshop-project-rules-upload/eshop-fix-bug.openai.yaml b/eshop-project-rules-upload/eshop-fix-bug.openai.yaml new file mode 100644 index 0000000..4ecbe62 --- /dev/null +++ b/eshop-project-rules-upload/eshop-fix-bug.openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "E-Shop 缺陷修复" + short_description: "先复现并定位 E-Shop 缺陷,再做最小修复和回归验证" + default_prompt: "使用 $eshop-fix-bug 复现、定位并最小修复这个 E-Shop 问题。" diff --git a/eshop-project-rules-upload/eshop-manage-git.SKILL.md b/eshop-project-rules-upload/eshop-manage-git.SKILL.md new file mode 100644 index 0000000..7a2b223 --- /dev/null +++ b/eshop-project-rules-upload/eshop-manage-git.SKILL.md @@ -0,0 +1,49 @@ +--- +name: eshop-manage-git +description: 安全处理 E-Shop 多人协作中的分支、工作区、暂存、提交、推送、PR/MR、Review、Rebase、冲突、回滚、Tag、Worktree 和发布操作。用户要求任何 Git 状态判断或实际 Git 变更时使用。 +--- + +# E-Shop Git 协作 + +## 读取协作规则 + +1. 先执行 `$eshop-project-workflow` 的规则读取、工作区预检和任务范围确认。 +2. 完整阅读 README 的 Git 章节和 `docs/02-设计文档/Git团队协作流程.md`。 +3. 检查当前分支、上游、远程、提交基线、工作区、未跟踪文件、暂存区和实际差异。 +4. 识别每项改动的任务与归属;不得覆盖、暂存、提交、Stash 或清理他人的改动。 +5. `origin/dev` 等远程跟踪引用只是上次获取的本地快照。创建基于最新远端的分支或推送前,应在允许联网时获取远端状态;无法刷新时明确标为“远端实时状态未验证”。 + +## 管理分支与并行任务 + +- `master` 是稳定发布分支,`dev` 是日常集成分支,二者禁止直接开发和直接 Push。 +- 普通任务从最新 `dev` 创建短生命周期分支,格式为 `<类型>/<模块>-<任务>-<姓名拼音首字母>`。 +- 一个任务对应一个分支和一个 PR/MR,不混入无关功能、文档或格式化。 +- 多个会写文件的并行任务使用独立分支和 Worktree,不在同一工作区并发修改。 +- 干净工作区是开始新任务的推荐前提;工作区已经混有其他任务改动时,先确认归属并用精确路径隔离,不强制切换,也不擅自 Stash、清理或还原。 + +## 暂存、提交与推送 + +1. 暂存前检查 `git diff` 和未跟踪文件,只使用明确文件路径,不执行未经检查的 `git add .`。 +2. 暂存后检查 `git diff --cached`,确认没有混入无关文件、敏感信息、生成文件或本地配置。 +3. 提交信息遵守仓库约定,准确表达类型、模块和单一目的,不伪造验证结论。 +4. 推送前重新确认分支、上游、提交范围和真实验证结果。 +5. 未经用户明确要求,不执行暂存、提交、推送或创建 PR/MR。 + +## PR/MR、合并与发布 + +- 普通任务 PR/MR 目标为 `dev`,正文说明需求编号、负责人、改动、验证、兼容性、风险和回滚方式。 +- 合入 `dev` 前保留 CI 和至少一名其他成员交叉 Review;不得自行声称已获 Review。 +- 任务分支合入 `dev` 按团队流程使用 Squash;发布由 `dev` 合入 `master` 时保留 Merge Commit。 +- 发布、Tag、回滚和长期分支同步前,确认目标提交、版本、依赖、Migration 和可恢复路径。 + +## 处理冲突和高风险操作 + +- 冲突处理前先读取双方差异和相关文件规则;不以简单选择一方覆盖另一方。 +- Rebase 后逐提交核对差异并重新验证,不因为历史整洁而牺牲可追溯性或他人工作。 +- `reset --hard`、`clean -fd`、强制删除分支、删除远程分支和强推属于高风险操作,必须先确认精确目标、影响和恢复办法。 +- `master`、`dev` 禁止强推;个人任务分支确需更新远程历史时只能在明确授权后使用 `--force-with-lease`。 +- 优先使用可恢复操作;发现目标不清楚时停止并请求确认。 + +## 交付 Git 结果 + +最终报告实际执行的命令、当前分支、基线、工作区状态、提交或 PR 标识、未包含的原有改动和剩余风险。只报告成功完成的 Git 操作,不把计划中的提交、推送、Review 或合并描述成已发生。 diff --git a/eshop-project-rules-upload/eshop-manage-git.openai.yaml b/eshop-project-rules-upload/eshop-manage-git.openai.yaml new file mode 100644 index 0000000..b877db2 --- /dev/null +++ b/eshop-project-rules-upload/eshop-manage-git.openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "E-Shop Git 协作" + short_description: "安全处理 E-Shop 分支、工作区、暂存、提交、推送、PR 与合并" + default_prompt: "使用 $eshop-manage-git 安全完成这项 E-Shop Git 协作操作。" diff --git a/eshop-project-rules-upload/eshop-project-workflow.SKILL.md b/eshop-project-rules-upload/eshop-project-workflow.SKILL.md new file mode 100644 index 0000000..c802a93 --- /dev/null +++ b/eshop-project-rules-upload/eshop-project-workflow.SKILL.md @@ -0,0 +1,65 @@ +--- +name: eshop-project-workflow +description: 作为 E-Shop 仓库所有任务的项目级总入口,统一完成规则读取、Git 与工作区预检、任务边界确认、专项 Skill 路由和最终交付检查。凡在本仓库进行项目理解、计划、修改、验证、文档或 Git 操作时都使用,并根据主要目标再选择一个专项 Skill。 +--- + +# E-Shop 项目工作流 + +## 读取项目规则 + +1. 完整阅读仓库根目录 `README.md` 和 `AGENTS.md`。 +2. 检查目标路径上的模块级 `AGENTS.md`;存在时完整阅读。 +3. 只使用 `.agents/skills/` 中的项目级 Skill,不把个人电脑上的全局规则作为项目依据。 +4. 从仓库根目录解析相对路径,不假设 `frontend/`、`backend/`、测试、部署或 CI 文件已经存在。 +5. 始终使用中文说明计划、修改、验证结果和风险。 + +## 完成统一预检 + +1. 检查 `git status --short --branch`、`git diff --name-only` 和未跟踪文件。 +2. 识别当前分支基线、任务负责人、业务模块、需求编号、验收编号和允许修改范围。 +3. 区分本任务改动与工作区原有改动;不得覆盖、暂存、提交或清理无关改动。 +4. 根据根 `AGENTS.md` 的读取矩阵,只读取与当前任务有关的教师基线、需求、设计、实现和测试章节。 +5. 若关键文件或实现不存在,明确说明当前阶段,不虚构目录、命令或完成度。 + +## 选择专项 Skill + +| 主要目标 | 使用 Skill | +|---|---| +| 新增功能、改变预期行为、调整 API/数据库/页面 | `$eshop-deliver-feature` | +| Bug、报错、白屏、测试失败、截图问题或行为回归 | `$eshop-fix-bug` | +| 文档、日报、周报、会议纪要、总结或一致性审查 | `$eshop-align-docs` | +| 构建、测试、烟测、验收、压测或发布就绪判断 | `$eshop-verify-acceptance` | +| 创建/切换分支、暂存、提交、推送、PR、合并或回滚 | `$eshop-manage-git` | + +默认使用本入口加一个主要专项 Skill。只有任务确实跨越两个主要目标时才叠加第二个专项 Skill;不要每次加载全部 Skill。 + +路由边界: + +- 功能任务中的普通针对性验证仍由功能 Skill 完成;完整验收才叠加验收 Skill。 +- 功能或缺陷任务只有在用户明确要求 Git 操作时才叠加 Git Skill。 +- 测试发现失败时先报告;只有用户要求修复才转入缺陷 Skill。 +- 已有预期行为损坏使用缺陷 Skill;需要改变预期行为使用功能 Skill。 +- 用户只要求诊断、审查或说明时保持只读,不自动实施修复。 + +## 控制实施范围 + +- 先寻找现有相似实现、命名、测试和文档结构。 +- 只选择完成当前目标所需的最小文件集合,不顺手处理范围外问题。 +- 保持模块纵向负责和公开协作边界,不代写其他成员模块。 +- 不修改 `docs/00-项目要求/`,不更换技术栈,不进行全仓格式化或大范围重构。 +- 不提交密码、Token、密钥、生产配置、个人绝对路径或生成目录。 + +开始编辑前,用一句简短中文说明准备修改哪些文件以及原因。 + +## 完成统一收尾 + +1. 运行专项 Skill 要求且仓库真实存在的验证命令。 +2. 检查 `git diff --check`、实际差异、当前状态和无关改动是否保持原样。 +3. 只报告真实执行的命令与结果,不把计划、模板或未验证行为描述成完成。 +4. 最终回复覆盖:修改内容、文件清单、README、模块级 AGENTS、验证命令、不过度设计说明和剩余风险。 + +修改项目 Skill 后运行: + +```powershell +python -X utf8 .agents/skills/eshop-project-workflow/scripts/validate_project_skills.py +``` diff --git a/eshop-project-rules-upload/eshop-project-workflow.openai.yaml b/eshop-project-rules-upload/eshop-project-workflow.openai.yaml new file mode 100644 index 0000000..f8dbad2 --- /dev/null +++ b/eshop-project-rules-upload/eshop-project-workflow.openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "E-Shop 项目协作入口" + short_description: "统一预检并路由 E-Shop 功能、缺陷、文档、验收与 Git 任务" + default_prompt: "使用 $eshop-project-workflow 预检并路由这项 E-Shop 仓库任务。" diff --git a/eshop-project-rules-upload/eshop-verify-acceptance.SKILL.md b/eshop-project-rules-upload/eshop-verify-acceptance.SKILL.md new file mode 100644 index 0000000..bcd17ff --- /dev/null +++ b/eshop-project-rules-upload/eshop-verify-acceptance.SKILL.md @@ -0,0 +1,71 @@ +--- +name: eshop-verify-acceptance +description: 为 E-Shop 构建、测试、烟测、验收、压测和发布就绪判断提供真实、可复现的证据。运行前端或后端构建测试、API/数据库/浏览器/Docker 全链路验证、F/X/C/N/D 编号验收、挑战模块边界验证或更新测试报告时使用。 +--- + +# E-Shop 测试与验收 + +## 先做静态门禁 + +1. 先执行 `$eshop-project-workflow` 的规则读取、工作区预检和任务范围确认。 +2. 核对 `docs/00-项目要求/` 教师基线是否完整、是否相对初始基线出现未经确认的改动。 +3. 检查真实实现、解决方案或项目清单、依赖文件、测试入口、部署入口和可发现命令是否存在。 +4. 全项目验收先按 F、X、C、N、D 分组给出门禁结论;只有具备运行条件的分组或场景,再展开七字段验收矩阵。 +5. 如果没有实现或测试入口,记录为 `阻塞(无实现入口)` 或 `阻塞(无测试入口)`,不要运行虚构命令,也不要把未运行误写成业务失败。 + +## 确认验证边界 + +1. 先读 `docs/00-项目要求/验收标准.md`、项目要求和相关评分项,确定编号与交付门槛。 +2. 再按未通过门禁或准备运行的分组,读取对应需求、设计、实现和测试章节;不为零实现门禁一次性加载全部材料。 +3. 读取 `docs/03-测试文档/测试计划.md`、`docs/03-测试文档/测试报告.md`,以及实际测试项目、脚本、CI、依赖和配置。 +4. 从 README、项目清单、脚本和 CI 中发现真实命令;不得把架构文档中的计划命令当作已存在命令。 +5. 本 Skill 负责验证和报告,不默认修改实现。发现失败时先记录证据;用户明确要求修复后再使用 `$eshop-fix-bug`。 + +## 建立验收矩阵 + +每个场景至少记录: + +| 字段 | 内容 | +|---|---| +| 编号 | 对应 F、X、C、N、D 或缺陷编号 | +| 场景 | 可复现的业务或技术场景 | +| 前置条件 | 角色、数据、服务、浏览器和依赖状态 | +| 输入与步骤 | 实际使用的参数和操作 | +| 预期 | 来自需求、接口或验收标准的结果 | +| 实际 | 真实输出、状态码、页面状态或数据库状态 | +| 证据 | 命令、日志、截图、响应或查询结果 | + +没有编号的技术检查也要说明其验证目的和覆盖范围。 + +## 按风险执行验证 + +1. 先检查配置、静态分析、格式、类型和编译问题。 +2. 再运行与改动最相关的前端、后端或模块测试。 +3. 涉及 API 时验证正常、参数错误、未认证、无权限、资源不存在和重复请求等边界。 +4. 涉及数据库时核对 Migration、约束、事务、种子数据、持久化结果和回滚风险。 +5. 涉及页面时使用真实浏览器验证主流程、空状态、加载状态、错误反馈和目标浏览器兼容性。 +6. 涉及库存、订单、支付、权限、并发、消息、缓存、上传或挑战模块时,执行对应的竞争、幂等、资源归属、恢复和降级场景。 +7. 涉及部署时验证 Compose、环境变量、健康检查、Migration、日志和回滚说明;不得使用或暴露生产密钥。 + +先执行最小相关集合,再根据失败影响面和交付风险扩大范围。不要为了数量运行与任务无关的检查。 + +## 保护环境与测试数据 + +- 明确环境、服务版本、数据库实例和测试账号,不对生产环境做破坏性验证。 +- 会改变数据的测试应使用可识别、可清理的测试数据,并记录清理方式。 +- Migration、重置、批量删除、并发和压测前先确认目标与可恢复性;高风险操作未经授权不得执行。 +- 不通过降低断言、跳过测试、关闭权限或吞掉错误来制造通过结果。 + +## 判定并报告结果 + +统一使用以下状态: + +- `通过`:已按记录步骤执行,实际结果满足预期。 +- `失败`:已执行,存在可复现差异。 +- `部分通过`:只验证了部分场景或环境。 +- `阻塞`:缺少实现入口、测试入口、依赖、权限、数据或外部条件;必须附原因标签。 +- `未运行`:存在可运行入口但本次未执行;必须说明原因,不得写成通过。 + +远程 PR 审批、分支保护、远程 CI、线上地址或部署平台状态无法从本地仓库确认时,标为 `阻塞(缺少外部证据)`,并说明需要的链接、导出记录或只读访问权限。 + +最终说明实际命令、环境、通过与失败项、证据位置、未覆盖边界和剩余风险。更新测试报告时只写本次真实结果,不用模板占位生成虚假数量、覆盖率或结论。 diff --git a/eshop-project-rules-upload/eshop-verify-acceptance.openai.yaml b/eshop-project-rules-upload/eshop-verify-acceptance.openai.yaml new file mode 100644 index 0000000..5615117 --- /dev/null +++ b/eshop-project-rules-upload/eshop-verify-acceptance.openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "E-Shop 测试与验收" + short_description: "按验收编号执行构建、测试、接口、数据库、浏览器和场景验证" + default_prompt: "使用 $eshop-verify-acceptance 验证这项 E-Shop 交付是否达到验收要求。" diff --git a/eshop-project-rules-upload/validate_project_skills.py b/eshop-project-rules-upload/validate_project_skills.py new file mode 100644 index 0000000..2b3abf8 --- /dev/null +++ b/eshop-project-rules-upload/validate_project_skills.py @@ -0,0 +1,152 @@ +"""Validate the repository-local E-Shop skill suite without third-party packages.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +REQUIRED_SKILLS = { + "eshop-project-workflow", + "eshop-deliver-feature", + "eshop-fix-bug", + "eshop-align-docs", + "eshop-verify-acceptance", + "eshop-manage-git", +} +NAME_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") + + +def read_text(path: Path) -> str: + return path.read_text(encoding="utf-8-sig").replace("\r\n", "\n") + + +def parse_frontmatter(path: Path) -> dict[str, str]: + text = read_text(path) + lines = text.splitlines() + if not lines or lines[0] != "---": + raise ValueError("SKILL.md 必须以 YAML frontmatter 开头") + + try: + closing_index = lines.index("---", 1) + except ValueError as exc: + raise ValueError("SKILL.md 缺少 frontmatter 结束标记") from exc + + metadata: dict[str, str] = {} + for line in lines[1:closing_index]: + if not line.strip(): + continue + if ":" not in line: + raise ValueError(f"无法解析 frontmatter 行:{line}") + key, value = line.split(":", 1) + metadata[key.strip()] = value.strip().strip('"\'') + return metadata + + +def quoted_yaml_value(text: str, field: str) -> str | None: + pattern = re.compile( + rf"^\s*{re.escape(field)}:\s*([\"'])(.*?)\1\s*$", + re.MULTILINE, + ) + match = pattern.search(text) + return match.group(2) if match else None + + +def validate_skill(skill_dir: Path) -> list[str]: + errors: list[str] = [] + skill_file = skill_dir / "SKILL.md" + metadata_file = skill_dir / "agents" / "openai.yaml" + + if not skill_file.is_file(): + return ["缺少 SKILL.md"] + + try: + metadata = parse_frontmatter(skill_file) + except (OSError, UnicodeError, ValueError) as exc: + return [str(exc)] + + name = metadata.get("name", "") + description = metadata.get("description", "") + if name != skill_dir.name: + errors.append(f"name 必须与目录名一致:{skill_dir.name}") + if not NAME_PATTERN.fullmatch(name) or len(name) > 64: + errors.append("name 必须是最长 64 字符的小写字母、数字和单连字符") + if not description or len(description) > 1024 or "<" in description or ">" in description: + errors.append("description 必须为 1-1024 字符且不能包含尖括号") + + skill_text = read_text(skill_file) + if "TODO" in skill_text.upper(): + errors.append("SKILL.md 仍包含 TODO") + + if not metadata_file.is_file(): + errors.append("缺少 agents/openai.yaml") + return errors + + try: + metadata_text = read_text(metadata_file) + except (OSError, UnicodeError) as exc: + errors.append(f"无法读取 agents/openai.yaml:{exc}") + return errors + + display_name = quoted_yaml_value(metadata_text, "display_name") + short_description = quoted_yaml_value(metadata_text, "short_description") + default_prompt = quoted_yaml_value(metadata_text, "default_prompt") + if not display_name: + errors.append("openai.yaml 缺少带引号的 display_name") + if not short_description or not 25 <= len(short_description) <= 64: + errors.append("short_description 必须是 25-64 个字符") + if not default_prompt or f"${name}" not in default_prompt: + errors.append(f"default_prompt 必须显式包含 ${name}") + return errors + + +def main() -> int: + repo_root = Path(__file__).resolve().parents[4] + skills_root = repo_root / ".agents" / "skills" + root_agents = repo_root / "AGENTS.md" + errors: list[str] = [] + + actual_skills = {path.name for path in skills_root.iterdir() if path.is_dir()} + missing = REQUIRED_SKILLS - actual_skills + unexpected = actual_skills - REQUIRED_SKILLS + if missing: + errors.append(f"缺少项目 Skill:{', '.join(sorted(missing))}") + if unexpected: + errors.append(f"存在未纳入路由的 Skill:{', '.join(sorted(unexpected))}") + + if not root_agents.is_file(): + errors.append("仓库根目录缺少 AGENTS.md") + agents_text = "" + else: + agents_text = read_text(root_agents) + agent_lines = agents_text.splitlines() + first_line = agent_lines[0] if agent_lines else "" + if first_line != "禁止使用全局 skill 和全局 agents。": + errors.append("AGENTS.md 第一行必须禁止使用全局 skill 和全局 agents") + + for skill_name in sorted(REQUIRED_SKILLS): + skill_dir = skills_root / skill_name + if skill_dir.is_dir(): + skill_errors = validate_skill(skill_dir) + if skill_errors: + errors.extend(f"{skill_name}: {error}" for error in skill_errors) + else: + print(f"[OK] {skill_name}") + + expected_path = f".agents/skills/{skill_name}/SKILL.md" + if expected_path not in agents_text: + errors.append(f"AGENTS.md 未引用 {expected_path}") + + if errors: + for error in errors: + print(f"[ERROR] {error}", file=sys.stderr) + print(f"校验失败:{len(errors)} 个问题。", file=sys.stderr) + return 1 + + print(f"项目级 Skill 校验通过:{len(REQUIRED_SKILLS)} 个。") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) -- Gitee From d840acb98c7a9bb477b5bc82224db19be48e895c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A1=BE=E6=AC=A3=E6=9C=88?= Date: Thu, 23 Jul 2026 09:42:32 +0800 Subject: [PATCH 019/118] =?UTF-8?q?docs(catalog):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E9=A1=BE=E6=AC=A3=E6=9C=88=E8=B4=9F=E8=B4=A3=E6=A8=A1=E5=9D=97?= =?UTF-8?q?(M02-01/02-02/06-01/07/C04)=E5=8A=9F=E8=83=BD=E9=9C=80=E6=B1=82?= =?UTF-8?q?=E8=AF=A6=E8=BF=B0v3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 M09 / C10 模板重写为 7 段式结构: - 覆盖 F04/F05/F06/F11 必做验收项 - 覆盖 X01 选做与 C04 挑战 - 每个模块:功能目标与范围 → 身份处理与触发来源 → FR 表 → 主流程 → 业务规则与权限 → 异常与边界场景 → 验收标准 - 4 类用户身份(游客/买家/商家/管理员)在每个模块都有专属矩阵与叙事化验收 - 77 条 FR 编号、50+ 条禁止项、66+ 条验收项 - 跨模块协作、安全性、可观测性、演示数据、国际化、数据生命周期等专题集中 --- ...00\346\261\202\350\257\246\350\277\260.md" | 1273 +++++++++++++++++ 1 file changed, 1273 insertions(+) create mode 100644 "docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\241\276\346\254\243\346\234\210-\346\250\241\345\235\227\345\212\237\350\203\275\351\234\200\346\261\202\350\257\246\350\277\260.md" diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\241\276\346\254\243\346\234\210-\346\250\241\345\235\227\345\212\237\350\203\275\351\234\200\346\261\202\350\257\246\350\277\260.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\241\276\346\254\243\346\234\210-\346\250\241\345\235\227\345\212\237\350\203\275\351\234\200\346\261\202\350\257\246\350\277\260.md" new file mode 100644 index 0000000..5d93d4d --- /dev/null +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\241\276\346\254\243\346\234\210-\346\250\241\345\235\227\345\212\237\350\203\275\351\234\200\346\261\202\350\257\246\350\277\260.md" @@ -0,0 +1,1273 @@ +# 顾欣月负责模块 · 功能需求详述 + +> 文档版本:v3.0 编写人:顾欣月 编写日期:2026-07-23 状态:v3 重写(按 M09 / C10 模板,待评审) +> +> 对应需求基线:`docs/01-需求文档/需求规格说明书.md`(罗皓晨 v0.1,2026-07-22) +> +> 模块范围:**M02-01 商品列表、分类与搜索(F04、F05)**、**M02-02 商品详情(F06)**、**M06-01 后台分类与商品管理(F11)**、**M07 商品评价与晒图(X01)**、**C04 商品搜索进阶** +> +> 模板参考:`需求规格说明书.md` §四 "C10 容器化部署" 与罗皓晨个人 M09 章节。每个模块按 7 段式:**功能目标与范围 → 身份处理与触发来源 → 功能需求(FR 表)→ 主流程 → 业务规则与权限 → 异常与边界场景 → 验收标准**。 +> +> 协作边界:协作成员 F(罗皓晨)实现 Redis 缓存基础设施与缓存失效触发点;本模块负责商品域缓存失效规则的定义(详见 §1.4 与 §8)。 + +--- + +## 修订记录 + +| 版本 | 日期 | 修改人 | 修改说明 | +|---|---|---|---| +| v1.0 | 2026-07-23 | 顾欣月 | 初稿。5 个模块的场景、规则、交互、接口契约与验收 | +| v2.0 | 2026-07-23 | 顾欣月 | 按 C10 模板增强:FR 表 / 4 类身份 / 故障降级 / 现场验收 / 跨模块专题 | +| v2.1 | 2026-07-23 | 顾欣月 | + 主流程 / 禁止项 / 触发来源 / 叙事化验收 | +| v3.0 | 2026-07-23 | 顾欣月 | **整体重写**:①统一按 M09/C10 7 段式结构(目标/身份/FR/主流程/规则/异常/验收);②内容更叙事化、更精炼;③合并 v2.1 的"主流程+禁止项"到模块"主流程"与"业务规则与权限"两节;④保留 v2.1 的 4 类身份差异处理;⑤保留跨模块专题(§8-§12);⑥移除 v2.2 候选内容(SQL DDL/状态机/时序图/OpenAPI/缓存Key/错误码/限流/性能/兼容/种子/测试脚本/迁移/监控/答辩/FAQ/CR/CI/前后端清单)作为待评审增量 | + +--- + +## 〇、阅读说明与全文约定 + +- **章节顺序**:每个模块严格按 7 段式编排,便于评审与答辩快速对照。评审时若时间紧张,至少看 1)每章的"功能需求"小节、2)"主流程"、3)"验收标准"。 +- **命名约定**:本模块文档中的"商品"特指由商家发布、状态为"已上架"的销售品;后台未上架或已下架的商品仅在管理端可见,购物端不可见。 +- **4 类用户身份**:本项目存在 4 类身份(**游客 / 买家 / 商家 / 管理员**),每章"身份处理"小节统一按四列矩阵给出。 +- **FR 编号规则**:`<模块>-FR`,如 `M02-01-FR01`、`C04-FR03`;同一编号在代码、测试、答辩中口径一致。 +- **图例**:所有 ASCII 框图只描述结构;最终视觉稿、组件命名以 `docs/02-设计文档/接口设计.md` 为准。 + +--- + +## 一、模块总览 + +### 1.1 业务目标 + +- 让 **游客和买家** 在 3 次点击内找到自己想买的商品,或在 30 秒内发现可能想要的商品。 +- 让 **商家** 在 5 分钟内完成一个新商品的发布,并能在 1 次操作内调整价格、库存或上下架。 +- 让 **买家** 在确认收货后能在 30 秒内提交一条评价(含晒图),并在商品详情页清晰看到其他买家的真实反馈。 +- 在 **C04 商品搜索进阶** 的支撑下,让"我想买……但我打不出准确商品名"这种模糊意图也能被高效召回。 + +### 1.2 模块依赖关系 + +``` + ┌──────────────────────────────┐ + │ M01 用户与鉴权(唐宇昊) │ ← 提供 buyerId、JWT、Policy + └──────────────┬───────────────┘ + │ 当前用户、权限 + ▼ +┌──────────────────┐ ┌──────────────────────────────┐ ┌──────────────────┐ +│ M03 购物车 │◀──│ M02-01 商品列表/分类/搜索 │──▶│ M02-02 商品详情 │ +│ (朱惠惠) │ │ (顾欣月,§2) │ │ (顾欣月,§3) │ +└──────────────────┘ └──────────────┬───────────────┘ └────────┬─────────┘ + │ 分类、商品数据、上下架 │ 评价列表(公开) + ▼ ▼ + ┌──────────────────────────┐ ┌──────────────────────┐ + │ M06-01 后台分类与商品管理 │ │ M07 商品评价与晒图 │ + │ (顾欣月,§4) │ │ (顾欣月,§5) │ + └──────────────┬───────────┘ └──────────┬───────────┘ + │ 触发缓存失效 │ 调用订单状态 + ▼ ▼ + ┌──────────────────────┐ ┌──────────────────────┐ + │ M15 缓存与性能优化 │ │ M04 订单(韦乾强) │ + │ (罗皓晨 + 本人协作) │ │ 需已完成订单项校验 │ + └──────────────────────┘ └──────────────────────┘ + +C04 商品搜索进阶(§6)独立升级 M02-01 的搜索能力,复用 M02-01 的搜索适配器接口。 +``` + +### 1.3 角色与权限(模块内基线) + +| 操作 | 游客 | 买家 | 商家 | 管理员 | +|---|:-:|:-:|:-:|:-:| +| 浏览商品列表、分类、详情 | ✓ | ✓ | ✓ | ✓ | +| 关键词搜索(含 C04 进阶) | ✓ | ✓ | ✓ | ✓ | +| 查看公开评价与评分汇总 | ✓ | ✓ | ✓ | ✓ | +| 提交评价(含晒图) | | ✓ | | | +| 追评 | | ✓ | | | +| 进入商家后台 `/admin/*` | | | ✓ | ✓(只读) | +| 商家后台:分类 CRUD | | | ✓ | ✗ | +| 商家后台:商品 CRUD、上下架 | | | ✓ | ✗ | +| 商家后台:评价管理(回复/隐藏) | | | ✓ | ✗ | +| 管理员:直接操作商品/评价/分类 | | | | ✗ | + +**核心约束**:本模块对**管理员仅放读不放写**;任何后台写接口在 Policy 层校验 `Role=Merchant`。 + +### 1.4 缓存失效 SLA(与 M15 协作的硬约定) + +| 事件 | 触发位置 | 失效范围 | 最迟可见时间 | +|---|---|---|---| +| 商品价格、库存、上下架、名称、主图变更 | M06-01 写接口提交事务后 | 详情缓存 + 列表缓存 + 搜索索引 | ≤ 2 秒 | +| 商品删除(软删) | M06-01 写接口 | 详情 + 列表 + 索引(标记删除) | ≤ 2 秒 | +| 分类新增/重命名/停用 | M06-01 分类写接口 | 分类树缓存 + 首页分类卡 | ≤ 5 秒 | +| 评价新增 | M07 写接口 | 评分汇总缓存 + 最新评价区块 | ≤ 5 秒 | +| 评价隐藏 | M07 写接口 | 同上 | ≤ 5 秒 | + +### 1.5 9 条 UX 原则("用得舒服"的具体定义) + +| # | 原则 | 落地位置 | +|---|---|---| +| U1 | 少即是多:一屏说清不拆两屏 | §2.3 / §3.3 布局 | +| U2 | 状态可见:加载/空/错/成功都明示 | §2.6 / §3.6 / §5.6 异常与边界 | +| U3 | 容错第一:破坏性操作二次确认 | §4.5 商品删除/上下架二次确认 | +| U4 | 所见即所得:改价最迟 2s 可见 | §1.4 缓存失效 SLA | +| U5 | 一次输入多次复用 | §4.4 Stepper 草稿自动保存 | +| U6 | 检索透明:结果可解释可二次过滤 | §2.3 / §6.6 关键词高亮与"为什么命中" | +| U7 | 反馈即时:写操作乐观 UI | §4.5 / §5.4 提交反馈 | +| U8 | 移动友好:360px 不横向滚动 | §2.3 / §3.3 / §4.4 响应式 | +| U9 | 可访问性:色弱友好 + ARIA | §3.5 库存颜色 + 文字 | + +--- + +## 二、M02-01 商品列表、分类与搜索(F04、F05) + +### 2.1 功能目标与范围 + +M02-01 是购物端的**入口**。游客和买家分页浏览已上架商品,可按分类筛选、按关键词模糊搜索,并叠加价格区间、库存与排序条件。本期不实现商品对比、收藏夹置顶、个性化推荐、搜索历史云同步、商品问答等扩展功能。 + +**体验目标**:用户从首页到详情不超过 3 次点击;模糊关键词(拼写错误、长尾词、类目意图词)能被高效召回;结果可解释("为什么是这个结果"),并允许二次过滤;不出现"幽灵商品"(已下架却仍在结果中)。 + +### 2.2 身份处理与触发来源 + +#### 2.2.1 身份处理矩阵 + +| 身份 | 是否涉及 | 接收内容 | 入口与操作 | +|---|:-:|---|---| +| **游客** | ✓ | 浏览、搜索、详情查看 | 顶栏搜索框/分类胶囊/首页;不可加购,弹登录引导 | +| **买家** | ✓ | 同上 + 加入购物车 + 收藏 | 购物端全功能;列表卡片"加入购物车"可用 | +| **商家** | ✓ | 购物端浏览(用于核对竞品) + 后台全权 | 顶栏入口切换为"后台";列表"加入购物车"禁用 | +| **管理员** | ✓ | 购物端浏览 + 后台只读监督 | 同商家,但后台写按钮全部 disable | + +#### 2.2.2 触发来源与职责 + +| 来源 | 职责 | +|---|---| +| M01 用户与鉴权 | 提供 `UserId` / `Role` / JWT;按身份决定 UI 行为 | +| M06-01 后台写接口 | 提交事务后触发缓存失效(§1.4 SLA) | +| C04 搜索进阶 | 同事务内更新倒排索引(§6) | +| 本模块 | 不写任何业务状态;只读取 + 缓存协调 | + +### 2.3 功能需求(FR 表) + +| 编号 | 功能 | 详细要求 | 适用身份 | 优先级 | 关联验收 | +|---|---|---|---|:-:|---| +| M02-01-FR01 | 分类树渲染 | 一级 + 二级;仅 `Status=Active`;面包屑可点击 | 全部 4 类 | P0 | F04-1 | +| M02-01-FR02 | 分类筛选 | `categoryId` 单选;切换保留其他筛选 | 全部 4 类 | P0 | F04-2 | +| M02-01-FR03 | 关键词搜索 | 子串匹配(默认)/ 分词(C04);trim + 长度限制 + 高亮 | 全部 4 类 | P0 | F05-1/F05-2 | +| M02-01-FR04 | 价格区间筛选 | 数字输入 + 快捷档位;上限 ≥ 下限 | 全部 4 类 | P0 | F05-3 | +| M02-01-FR05 | 仅看有货筛选 | `stock > 0` | 全部 4 类 | P1 | F04-2 | +| M02-01-FR06 | 排序 | 综合/价格↑↓/销量↓/上架时间;切换重置到第 1 页 | 全部 4 类 | P0 | F05-4 | +| M02-01-FR07 | 分页 | 默认 12/页,最大 48;URL 同步 `page` | 全部 4 类 | P0 | F04-3/F04-4 | +| M02-01-FR08 | 列表只返回已上架 | 草稿/已下架/已软删绝不出现在购物端 | 全部 4 类 | P0 | F04-5 | +| M02-01-FR09 | 搜索建议 | 输入 ≥1 字符 300ms 防抖;≤8 条 | 全部 4 类 | P1 | F05-1 | +| M02-01-FR10 | 列表加入购物车 | 游客→登录引导(登录后自动回放);买家→M03;商家/管理员→引导登录 | 全部 4 类 | P0 | F04-2 | +| M02-01-FR11 | 直链可还原 | 任意筛选/排序/分页条件由 URL 还原 | 全部 4 类 | P0 | F04-4 | +| M02-01-FR12 | 搜索适配器可切换 | `ISearchAdapter` 隔离 Like / Inverted | 内部 | P1 | C04 | +| M02-01-FR13 | 角色化日志 | 列表查询记录 `userId/role/keyword/total/costMs` | 全部 4 类 | P2 | D04 | + +### 2.4 主流程 + +``` +1. 用户打开购物端首页 / 进入商品列表 / 在顶栏搜索框输入关键词回车 +2. 前端解析 URL 参数(page / categoryId / keyword / sort / priceMin / priceMax / inStockOnly),构造请求 +3. 前端调 GET /api/products?...;请求头携带 X-Trace-Id 与(已登录时)JWT +4. API 进入 ISearchAdapter.SearchAsync: + - 默认 LikeSearchAdapter(ILIKE %kw%) + - 配置 Search:Adapter=Inverted 时走倒排索引(C04 启用后) +5. API 在 PostgreSQL 拼装查询:Status=OnShelf 是硬约束,与任何筛选条件组合 +6. API 按分页执行,返回 items + total + page + pageSize + appliedFilters +7. API 写访问日志(userId/role/keyword/total/costMs)与指标 +8. 前端渲染:卡片网格、已选筛选条、分页器、排序下拉、关键词高亮 +9. 用户点击卡片 → 跳 /products/{slug}-{id} → M02-02(§3) +10. 用户点击"加入购物车":游客弹登录(登录成功后自动回放);买家调 M03;商家/管理员引导登录 +11. 状态变更时(商家改价/上下架):写事务提交后 ≤2s 触发 Redis 精准失效,下次查询读到新值 +``` + +### 2.5 业务规则与权限 + +**核心规则**: +- R-L01:购物端查询的商品集合**只能是 `Status=OnShelf`**;下架、草稿商品绝不出现。 +- R-L02:关键词为空时按时间倒序返回全部已上架商品。 +- R-L03:同一商品在结果中只出现一次(按 ID 去重)。 +- R-L04:关键词命中商品名才进入召回;命中分类名仅参与相关度加权。 +- R-L05:价格区间作用于商品当前售价。 +- R-L06:默认排序为上架时间倒序。 +- R-L07:分页总数必须等于数据库真实命中数。 +- R-L08:列表接口对游客、买家、商家、管理员**均开放**。 + +**权限**:公开接口;不得返回 `CostPrice` / `MerchantId` / 用户手机号等敏感字段。 + +**禁止项**: +- M02-01-NO01:不允许返回草稿/已下架/已软删商品。 +- M02-01-NO02:不允许在 SQL 中拼接关键词。 +- M02-01-NO03:不允许使用无限滚动(验收口径清晰 + URL 可还原)。 +- M02-01-NO04:不允许在前端本地计算价格。 +- M02-01-NO05:不允许提供物理删除分类入口。 +- M02-01-NO06:不允许搜索结果中混入下架商品(缓存失效前 2s 内的强一致由"三重门"保证:写事务 → 缓存精准失效 → 列表接口最终过滤)。 + +### 2.6 异常与边界场景 + +| 场景 | 表现 | 处理 | +|---|---|---| +| 加载中 | 6 个骨架屏 shimmer | 不阻塞交互 | +| 空结果(筛选无命中) | 居中插画 + "清除全部筛选"按钮 | 一键回到无筛选状态 | +| 空结果(搜索无命中) | 居中插画 + "热门分类"推荐卡 4 个 | 引导发现 | +| 网络错误 | 顶部红条 toast + 重试按钮 | 不显示技术堆栈 | +| 服务异常 | 友好提示 + traceId | 不显示内部堆栈 | +| 参数非法(page<1 等) | 静默修正为默认值 | 控制台 warn | +| 当前实例停止(C10) | 短暂 502 | Nginx 切到存活实例;前端 toast"已自动重试" | +| 搜索建议返回空 | 不显示建议下拉 | — | +| 分类被停用 | 购物端分类树不显示;已有商品直链仍可访问 | — | +| 长尾词(>50 字) | 自动截断 + toast"搜索词过长,已截断" | — | + +### 2.7 验收标准 + +| 编号 | 验收项 | 验证 | +|---|---|---| +| F04-1 | 分类树渲染正确,二级展开 | Playwright:点击一级分类,二级展开 | +| F04-2 | 分类筛选生效,只显示该分类商品 | 选"手机",结果全部 `categoryId=手机` | +| F04-3 | 分页总数正确 | DB 实际命中与 `total` 一致 | +| F04-4 | 分页器翻页无丢失 | 翻到第 5 页刷新,仍在第 5 页同样的筛选与排序 | +| F04-5 | 下架商品不出现在购物端列表 | DB 插入 Status=已下架,列表 API 不返回 | +| F05-1 | 关键词命中商品名 | 搜"手机",结果含"小米14 Pro 智能手机" | +| F05-2 | 关键词高亮显示 | 搜索结果商品名中"手机"两字带黄色背景 | +| F05-3 | 搜索 + 分类 + 价格组合 | 组合 3 个条件,结果符合交集 | +| F05-4 | 排序切换生效 | 切到价格升序,结果按 price asc | +| F05-5 | 空结果友好 | 搜"xxxxxxxxxx",进入空状态 | +| F04-7 | 游客点击加入购物车引导登录 | 未登录点击 → 登录页,登录后自动回放 | +| F04-8 | 商家/管理员加入购物车被禁用 | 按钮文案改为"去管理"/"去监督" | +| 非功能 | 列表首屏 ≤ 2s | Lighthouse / k6 压测 | + +**4 类身份验收**(M09 风格叙事化): +- **游客**打开首页看到商品列表与分类胶囊;点击任一商品进入详情,价格/库存/分类/描述均显示正确;点击"加入购物车"被引导登录,登录后**自动**加入刚才那件商品。 +- **买家**登录后可在搜索框输入"手"或"生日"等词,看到下拉建议;回车后命中商品在结果中以"高亮"形式出现;切换排序、筛选价格区间、点击分页第 3 页、刷新浏览器后仍停留在第 3 页同样的筛选与排序条件下。 +- **商家**在后台新建商品并立即上架,1–2 秒后**任意身份**在购物端都能看到;将价格从 ¥4999 改为 ¥4599,1–2 秒后所有身份在详情页与列表都看到新价;将商品下架,列表与搜索立即移除。 +- **管理员**进入 `/admin/products`,可见商品列表但**所有写按钮均 disable**;使用 curl 调任一后台写接口,得到 `403 forbidden_role_merchant`。 + +--- + +## 三、M02-02 商品详情(F06) + +### 3.1 功能目标与范围 + +商品详情页是用户**做购买决策**的最后一站。展示商品名称、主图/图片、描述、价格、库存、分类、评分、评价、服务承诺与发货信息。本期不实现多规格 SKU、预售/团购、AR 展示、3D 看货、视频详情等扩展功能。 + +**体验目标**:价格与库存以服务端为准(防"幽灵价格");下架商品直链进入"不可售页 + 同分类推荐"(保护收藏与分享);移动端 360px 不出现横向滚动;色弱友好(库存状态不只用颜色区分)。 + +### 3.2 身份处理与触发来源 + +#### 3.2.1 身份处理矩阵 + +| 身份 | 是否涉及 | 接收内容 | 入口与操作 | +|---|:-:|---|---| +| **游客** | ✓ | 商品全部展示 | "加入购物车"按钮 → 引导登录;"立即购买" → 引导登录 | +| **买家** | ✓ | 同上 + 加购 + 立即购买 + 收藏(X02)+ 评价入口 | 全部写操作可用 | +| **商家** | ✓ | 同上 + "去管理"入口 | "加入购物车"按钮改为"去管理",不可自助下单 | +| **管理员** | ✓ | 同上 + "去监督"入口 | "加入购物车"按钮改为"去监督" | + +#### 3.2.2 触发来源与职责 + +| 来源 | 职责 | +|---|---| +| M01 | 提供身份(决定按钮文案) | +| M06-01 写接口 | 触发详情缓存失效(§1.4 SLA) | +| M03 购物车 | 接收"加入购物车"调用(仅买家) | +| M04 订单 | 接收"立即购买"调用(仅买家) | +| M07 评价 | 提供评价列表与评分汇总 | +| M08 收藏/历史 | 接收"收藏"调用(X02 上线后,仅买家) | + +### 3.3 功能需求(FR 表) + +| 编号 | 功能 | 详细要求 | 适用身份 | 优先级 | 关联验收 | +|---|---|---|---|:-:|---| +| M02-02-FR01 | 详情页基础展示 | 商品名、主图、缩略图、价格、库存、分类、描述、服务承诺、发货信息 | 全部 4 类 | P0 | F06-1 | +| M02-02-FR02 | 卖点展示 | 商家填写 ≤30 字,红色小字 | 全部 4 类 | P1 | F06-1 | +| M02-02-FR03 | 划线价 | `OriginalPrice > Price` 时显示 | 全部 4 类 | P1 | F06-1 | +| M02-02-FR04 | 库存颜色与文案 | 0=已售罄/1-4=仅剩 N 件/≥5=有货(U9 色弱友好) | 全部 4 类 | P0 | F06-6 | +| M02-02-FR05 | 评分汇总 | 来自 M07;N=0 时"暂无评价" | 全部 4 类 | P0 | X01 | +| M02-02-FR06 | 评价 Tab 列表 | 全部/有图/追评/好评/中评/差评筛选 | 全部 4 类 | P0 | X01 | +| M02-02-FR07 | 同分类推荐 | `GET /api/products/{id}/related?limit=4` | 全部 4 类 | P1 | F06-8 | +| M02-02-FR08 | 加入购物车按钮 | 库存>0 时可点;按身份切换行为/文案 | 全部 4 类 | P0 | F06-7 | +| M02-02-FR09 | 立即购买按钮 | 游客→登录;买家→M04 结算页 | 全部 4 类 | P0 | F08 | +| M02-02-FR10 | 收藏按钮(X02 上线后) | 仅买家可点击;其余身份入口隐藏 | 仅买家 | P1 | X02 | +| M02-02-FR11 | 直链已下架处理 | 返回"不可售页" + 同分类推荐,不 404 | 全部 4 类 | P0 | F06-4 | +| M02-02-FR12 | 商品不存在 | 404 + 友好页 + 返回列表按钮 | 全部 4 类 | P0 | F06-5 | + +### 3.4 主流程 + +``` +1. 用户在列表页或外部链接点击商品 +2. 前端进入 /products/{slug}-{id},调 GET /api/products/{id} 与 /rating-summary 并行 +3. API 查 Product:若 Status≠OnShelf: + - 已下架:返回 200 + status:off_shelf 标记 → 前端进入"不可售页 + 同分类推荐" + - 已软删/草稿/不存在:返回 404 product_not_found +4. API 拼装响应 DTO(含分类路径、评分汇总、规格参数、服务承诺) +5. 前端渲染详情页:图片区(左)/ 购买区(右)/ Tab 区(下) +6. 用户切换 Tab:商品介绍(富文本)/ 规格参数(key-value)/ 商品评价(调 M07)/ 购买须知(静态) +7. 用户点击"加入购物车": + - 游客 → 弹登录模态框,登录后自动回放 + - 买家 → 调 M03 POST /api/cart/items + - 商家/管理员 → 按钮改为"去管理"/"去监督",进入相应后台 +8. 用户点击"立即购买"(仅买家)→ 跳 M04 结算页,URL 带 items= +9. 用户点击"收藏"(X02 上线后,仅买家)→ 调 POST /api/my/favorites +10. 后台写操作触发缓存失效 ≤2s:下次进详情页或列表看到新值 +11. 当前实例停止时(C10 演示):Nginx 切到存活实例,前端 toast +``` + +### 3.5 业务规则与权限 + +**核心规则**: +- R-D01:详情页只允许展示 `Status=OnShelf` 的商品(直链场景见 §3.6)。 +- R-D02:价格、库存**以服务端响应为准**,前端不做任何计算。 +- R-D03:划线价(OriginalPrice)必须 > 当前价(Price),否则不显示。 +- R-D04:主图必须存在;草稿保存时强制要求上传主图。 +- R-D05:商品描述支持富文本(HTML),后端存储时消毒防 XSS。 +- R-D06:详情页必须显示同分类推荐(即使已下架仍显示)。 +- R-D07:库存 ≤ 0 时禁止加入购物车,前后端双重校验。 + +**库存颜色与文案**(U9 色弱友好): + +| 库存 | 文本 | 颜色 | +|---|---|---| +| 0 | 已售罄 | 灰色 + 删除线(按钮 disable) | +| 1–4 | 仅剩 N 件 | 红色 + 文字"紧张" | +| 5–99 | 有货 | 绿色 + 文字"充足" | +| ≥100 | 有货 | 绿色 + 文字"充足" | + +**权限**:公开接口;不返回 `UserId` / `MerchantId` / `CostPrice` 等敏感字段。 + +**禁止项**: +- M02-02-NO01:不允许在已下架/软删/草稿商品的详情页展示购买区按钮。 +- M02-02-NO02:不允许让前端持有价格/库存的可信副本。 +- M02-02-NO03:不允许直接 404 已下架商品的直链(保护收藏与分享)。 +- M02-02-NO04:不允许富文本描述执行 ` + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..9ced7a4 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1267 @@ +{ + "name": "eshop-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "eshop-frontend", + "version": "0.0.0", + "dependencies": { + "vue": "^3.5.39" + }, + "devDependencies": { + "@types/node": "^24.13.2", + "@vitejs/plugin-vue": "^6.0.7", + "@vue/tsconfig": "^0.9.1", + "typescript": "~6.0.2", + "vite": "^8.1.1", + "vue-tsc": "^3.3.5" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.8", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", + "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.40.tgz", + "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.40", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz", + "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz", + "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.40", + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-ssr": "3.5.40", + "@vue/shared": "3.5.40", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz", + "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/language-core": { + "version": "3.3.8", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.3.8.tgz", + "integrity": "sha512-ieGT8jJdhhy0mGzStZhsg/qPw5bQZJg5yF+3+XU6saf4sM7yo9ZXy3h+nCwrm2+b4qS/SypkNdR2jAF3uei9tA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.2.1", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.40.tgz", + "integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.40.tgz", + "integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz", + "integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/runtime-core": "3.5.40", + "@vue/shared": "3.5.40", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.40.tgz", + "integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.40.tgz", + "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.9.1", + "resolved": "https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.9.1.tgz", + "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 5.8", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/alien-signals": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.22", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmmirror.com/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.40", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.40.tgz", + "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-sfc": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/server-renderer": "3.5.40", + "@vue/shared": "3.5.40" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-tsc": { + "version": "3.3.8", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.3.8.tgz", + "integrity": "sha512-xXmYlVQpcwJDWyGlqbHrGVOl1h3UOsASymRibrHc+iy9j/UNnOrOn4u+fntHz4D6Cs74RtapeqVV6CzJeg+UlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.3.8" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..5f81d88 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "eshop-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.5.39" + }, + "devDependencies": { + "@types/node": "^24.13.2", + "@vitejs/plugin-vue": "^6.0.7", + "@vue/tsconfig": "^0.9.1", + "typescript": "~6.0.2", + "vite": "^8.1.1", + "vue-tsc": "^3.3.5" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..a7eec63 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,9 @@ + diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..2425c0f --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..631b647 --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,53 @@ +:root { + font-family: Inter, "Segoe UI", sans-serif; + color: #1f2937; + background: #f5f7fb; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; +} + +#app { + min-height: 100vh; + display: grid; + place-items: center; +} + +.shell { + width: min(680px, calc(100% - 48px)); + box-sizing: border-box; + padding: 48px; + border: 1px solid #e5e7eb; + border-radius: 20px; + background: #ffffff; + box-shadow: 0 20px 50px rgb(15 23 42 / 8%); +} + +.eyebrow { + margin: 0 0 16px; + color: #2563eb; + font-size: 14px; + font-weight: 700; + letter-spacing: 0.16em; +} + +h1 { + margin: 0; + color: #111827; + font-size: clamp(32px, 6vw, 52px); + line-height: 1.1; +} + +.summary { + margin: 24px 0 0; + color: #4b5563; + font-size: 18px; + line-height: 1.75; +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..d72aa75 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..bbcf80c --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue()], +}) -- Gitee From 74415ccfd517ab51076ee76cee8010491865943b Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 09:28:04 +0800 Subject: [PATCH 042/118] =?UTF-8?q?docs(api):=20=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E4=B8=8E=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interface-lhc.md" | 697 ++++++++++++++++++ 1 file changed, 697 insertions(+) create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-lhc.md" diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-lhc.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-lhc.md" new file mode 100644 index 0000000..1fd4033 --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-lhc.md" @@ -0,0 +1,697 @@ +# 罗皓晨接口设计 + +> 负责人:罗皓晨 +> 负责范围:M00 公共基建与集成、M09 站内消息通知(X03)、C06 实时消息推送、C07 缓存与性能优化、C10 容器化部署与负载均衡 +> 接口编号范围:`A501`~`A600` +> 当前状态:待交叉评审 +> 编写日期:2026-07-24 + +## 一、范围与设计结论 + +本文件只登记罗皓晨负责的 Messaging HTTP 接口和 M00 公共健康检查接口。全部接口遵循[《接口设计》](接口设计.md)第一章通用约定;本文未重复定义的认证、响应包装、ProblemDetails、分页和安全规则均以该文档为准。 + +本轮范围结论: + +- M09 使用 5 个 HTTP 接口完成消息列表、消息详情、未读数、单条已读和全部已读。 +- C06 使用 SignalR Hub 和服务端推送事件,不占用 Axxx 编号;实时推送失败不影响 M09 持久化消息。 +- C07 不新增缓存管理 HTTP 接口,继续复用 Catalog 的首页和商品详情接口;缓存命中与降级不得改变公开契约。 +- C10 登记存活和就绪两个公共健康检查接口。它们不使用 `/api` 前缀,也不返回通用业务包装。 +- 对象存储、Redis、RabbitMQ、Outbox/Inbox 和 Worker 属于内部基础设施或异步契约,不为了占用编号而创建无业务依据的公共 HTTP 接口。 +- 当前 `database-lhc.md` 尚未建立,Messaging 接口的关联数据表暂标记为“待数据库设计确认”;接口实现前必须补齐 DBxxx、字段、约束和索引追踪。 + +## 二、接口清单 + +| 编号 | 模块 | 需求编号 | 名称 | 方法 | 路径 | operationId | 请求 Schema | 响应 Schema | 鉴权 | 关联 DBxxx | 状态 | +|---|---|---|---|---|---|---|---|---|---|---|---| +| A501 | Messaging | X03-FR03 | 查询本人消息列表 | GET | `/api/messages` | `Messaging_ListMessages` | Query 参数 | `MessageListResponse` | 买家或商家 JWT | 待 `database-lhc.md` 确认 | 待评审 | +| A502 | Messaging | X03-FR04 | 查询本人消息详情 | GET | `/api/messages/{messageId}` | `Messaging_GetMessage` | Route 参数 | `MessageDetailResponse` | 买家或商家 JWT | 待 `database-lhc.md` 确认 | 待评审 | +| A503 | Messaging | X03-FR05 | 查询本人未读消息数 | GET | `/api/messages/unread-count` | `Messaging_GetUnreadCount` | 无 | `UnreadMessageCountResponse` | 买家或商家 JWT | 待 `database-lhc.md` 确认 | 待评审 | +| A504 | Messaging | X03-FR06 | 标记本人单条消息已读 | POST | `/api/messages/{messageId}/read` | `Messaging_MarkMessageRead` | Route 参数 | `MarkMessageReadResponse` | 买家或商家 JWT | 待 `database-lhc.md` 确认 | 待评审 | +| A505 | Messaging | X03-FR07 | 标记本人当前消息全部已读 | POST | `/api/messages/read-all` | `Messaging_MarkAllMessagesRead` | 无 | `MarkAllMessagesReadResponse` | 买家或商家 JWT | 待 `database-lhc.md` 确认 | 待评审 | +| A506 | M00 | C10-FR06 | API 存活检查 | GET | `/health/live` | `Infrastructure_GetLiveness` | 无 | `HealthStatusResponse` | 无 | 无 | 待评审 | +| A507 | M00 | C10-FR06 | API 就绪检查 | GET | `/health/ready` | `Infrastructure_GetReadiness` | 无 | `ReadinessStatusResponse` | 无 | 无 | 待评审 | + +## 三、公共 Schema 与枚举 + +### 3.1 `MessageType` + +受控字符串枚举: + +| 值 | 含义 | +|---|---| +| `OrderCreated` | 买家订单创建成功 | +| `OrderCancelled` | 订单取消 | +| `PaymentSucceeded` | 支付成功 | +| `OrderShipped` | 商家已发货 | +| `OrderCompleted` | 订单完成 | +| `AfterSalesSubmitted` | 售后申请已提交,提醒指定商家处理 | +| `AfterSalesReviewed` | 售后审核完成,通知买家结果 | + +后续新增消息类型属于兼容性扩展。客户端必须对未知值使用“业务通知”兜底展示,不能因此白屏。 + +### 3.2 `RelatedResourceType` + +受控字符串枚举:`Order`、`Payment`、`AfterSales`。 + +### 3.3 `MessageAction` + +安全跳转描述,不包含前端内部路由字符串: + +| 字段 | 类型 | 必需 | 说明 | +|---|---|---:|---| +| `target` | string | 是 | `OrderDetail` 或 `AfterSalesDetail` | +| `resourceId` | UUID | 是 | 目标业务资源 ID;进入目标页面时仍须重新鉴权 | + +当关联资源不存在、已归档或当前用户已无权访问时,`action` 返回 `null`。 + +### 3.4 `MessageSummaryResponse` + +| 字段 | 类型 | 可空 | 说明 | +|---|---|---:|---| +| `messageId` | UUID | 否 | 消息 ID | +| `type` | `MessageType` | 否 | 消息类型 | +| `title` | string | 否 | 标题,最长 100 个字符 | +| `summary` | string | 否 | 摘要,最长 200 个字符 | +| `relatedResourceType` | `RelatedResourceType` | 是 | 关联业务类型 | +| `relatedResourceId` | UUID | 是 | 关联业务 ID | +| `action` | `MessageAction` | 是 | 安全跳转描述 | +| `isRead` | boolean | 否 | 是否已读 | +| `readAt` | UTC 时间 | 是 | 首次标记已读时间 | +| `createdAt` | UTC 时间 | 否 | 消息创建时间 | + +### 3.5 `MessageDetailResponse` + +包含 `MessageSummaryResponse` 的全部字段,并增加: + +| 字段 | 类型 | 可空 | 说明 | +|---|---|---:|---| +| `body` | string | 否 | 消息正文,最长 2000 个字符 | + +正文和摘要是消息创建时保存的历史快照,不随商品名称、订单展示文本或用户昵称变化。 + +## 四、HTTP 接口详细定义 + +### A501 查询本人消息列表 + +- 模块 / Tag:Messaging +- 需求编号:X03-FR03、X03-FR10、X03-FR11 +- 负责人:罗皓晨 +- 关联数据表:待 `database-lhc.md` 确认 +- 当前状态:待评审 +- 用途:按创建时间倒序分页查询当前用户自己的消息。 +- 方法与路径:`GET /api/messages` +- operationId:`Messaging_ListMessages` +- 请求 Schema:Query 参数 +- 响应 Schema:`MessageListResponse` +- 身份与 Policy:有效 JWT;仅买家或商家 +- 资源归属:接收用户必须等于当前认证用户;服务端不接收 `userId` +- 幂等要求:只读接口,天然幂等 + +#### 请求 + +- Route 参数:无。 +- Query 参数: + +| 参数 | 类型 | 必需 | 默认值 | 规则 | +|---|---|---:|---|---| +| `page` | integer | 否 | 1 | 大于等于 1 | +| `pageSize` | integer | 否 | 10 | 1~100 | +| `readStatus` | string | 否 | `all` | 仅允许 `all`、`unread` | +| `type` | `MessageType` | 否 | 无 | 只允许已登记消息类型 | + +- Header:`Authorization: Bearer `。 +- Body:无。 +- 校验规则:空字符串筛选值按参数错误处理,不静默当作未提供;排序固定为 `createdAt desc, messageId desc`,不开放任意 `sortBy`。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`MessageListResponse` +- `data` 字段: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `items` | `MessageSummaryResponse[]` | 当前页消息 | +| `page` | integer | 当前页 | +| `pageSize` | integer | 每页数量 | +| `total` | integer | 满足筛选条件的消息总数 | +| `totalPages` | integer | 总页数 | + +- 示例: + +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "messageId": "6aa23ac8-8cf7-4cb5-8387-69f463df4b77", + "type": "OrderShipped", + "title": "订单已发货", + "summary": "你的订单已由商家发出,可进入订单详情查看。", + "relatedResourceType": "Order", + "relatedResourceId": "93c8d8e1-bdac-4a13-8ba2-0c21c327b81e", + "action": { + "target": "OrderDetail", + "resourceId": "93c8d8e1-bdac-4a13-8ba2-0c21c327b81e" + }, + "isRead": false, + "readAt": null, + "createdAt": "2026-07-24T02:30:00Z" + } + ], + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 页码、页大小、已读筛选或消息类型非法 | +| 401 | `AUTH.UNAUTHENTICATED` | JWT 缺失或无效 | +| 403 | `AUTH.FORBIDDEN` | 当前身份不是买家或商家 | + +#### 业务规则与并发 + +- 查询条件必须始终包含当前认证用户 ID,不能先按消息 ID 或筛选条件读取后再做客户端过滤。 +- 翻页期间新消息到达可能使后续页发生位移;本期按页码分页验收,不提前引入游标分页。 + +#### 缓存、事件或外部依赖 + +- 未读状态和消息内容以 PostgreSQL 为准。 +- 私人消息响应使用 `Cache-Control: no-store`,不得进入共享 HTTP 缓存。 + +#### 验证场景 + +- 分别验证默认列表、未读筛选、各消息类型筛选、空页和超出末页。 +- 使用另一个买家和商家账号确认不会返回他人消息。 + +### A502 查询本人消息详情 + +- 模块 / Tag:Messaging +- 需求编号:X03-FR04、X03-FR11 +- 负责人:罗皓晨 +- 关联数据表:待 `database-lhc.md` 确认 +- 当前状态:待评审 +- 用途:查询当前用户拥有的一条完整站内消息。 +- 方法与路径:`GET /api/messages/{messageId}` +- operationId:`Messaging_GetMessage` +- 请求 Schema:Route 参数 +- 响应 Schema:`MessageDetailResponse` +- 身份与 Policy:有效 JWT;仅买家或商家 +- 资源归属:消息接收用户必须等于当前认证用户 +- 幂等要求:只读接口,天然幂等 + +#### 请求 + +- Route 参数:`messageId`,必需,UUID。 +- Query 参数:无。 +- Header:`Authorization: Bearer `。 +- Body:无。 +- 校验规则:`messageId` 必须是标准 UUID。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`MessageDetailResponse` +- 示例: + +```json +{ + "code": "success", + "message": "ok", + "data": { + "messageId": "6aa23ac8-8cf7-4cb5-8387-69f463df4b77", + "type": "OrderShipped", + "title": "订单已发货", + "summary": "你的订单已由商家发出,可进入订单详情查看。", + "body": "订单已发货。请关注后续配送状态,收货后可在订单详情确认收货。", + "relatedResourceType": "Order", + "relatedResourceId": "93c8d8e1-bdac-4a13-8ba2-0c21c327b81e", + "action": { + "target": "OrderDetail", + "resourceId": "93c8d8e1-bdac-4a13-8ba2-0c21c327b81e" + }, + "isRead": false, + "readAt": null, + "createdAt": "2026-07-24T02:30:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `messageId` 格式非法 | +| 401 | `AUTH.UNAUTHENTICATED` | JWT 缺失或无效 | +| 403 | `AUTH.FORBIDDEN` | 当前身份不是买家或商家 | +| 404 | `MESSAGE.NOT_FOUND` | 消息不存在或不属于当前用户 | + +#### 业务规则与并发 + +- 他人消息与不存在消息统一返回 `404` 和 `MESSAGE.NOT_FOUND`,不泄露消息是否存在。 +- 查询详情不会自动标记已读;客户端在用户实际打开消息后调用 A504。 +- 每次返回 `action` 前重新校验当前用户对关联资源的访问资格;无资格时返回 `null`,消息正文仍可查看。 + +#### 缓存、事件或外部依赖 + +- 响应使用 `Cache-Control: no-store`。 +- 关联资源暂时不可用时不应导致历史消息查询失败。 + +#### 验证场景 + +- 验证本人未读和已读消息详情。 +- 使用另一用户访问相同 `messageId`,确认返回与不存在消息一致的 `404`。 +- 关联订单已不可访问时确认 `action` 为 `null`。 + +### A503 查询本人未读消息数 + +- 模块 / Tag:Messaging +- 需求编号:X03-FR05、C06-FR05 +- 负责人:罗皓晨 +- 关联数据表:待 `database-lhc.md` 确认 +- 当前状态:待评审 +- 用途:为消息入口角标、首次连接和断线重连补偿提供当前未读总数。 +- 方法与路径:`GET /api/messages/unread-count` +- operationId:`Messaging_GetUnreadCount` +- 请求 Schema:无 +- 响应 Schema:`UnreadMessageCountResponse` +- 身份与 Policy:有效 JWT;仅买家或商家 +- 资源归属:只统计当前认证用户 +- 幂等要求:只读接口,天然幂等 + +#### 请求 + +- Route 参数:无。 +- Query 参数:无。 +- Header:`Authorization: Bearer `。 +- Body:无。 +- 校验规则:无额外输入。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`UnreadMessageCountResponse` +- 示例: + +```json +{ + "code": "success", + "message": "ok", + "data": { + "unreadCount": 3 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | JWT 缺失或无效 | +| 403 | `AUTH.FORBIDDEN` | 当前身份不是买家或商家 | + +#### 业务规则与并发 + +- `unreadCount` 为大于等于 0 的整数,以查询时 PostgreSQL 中当前用户未读记录为准。 +- 实时角标只用于即时展示,重连和页面恢复时必须以本接口结果校正。 + +#### 缓存、事件或外部依赖 + +- 本期不使用 Redis 保存唯一未读数。 +- 响应使用 `Cache-Control: no-store`。 + +#### 验证场景 + +- 新增消息后数量增加;首次标记已读后减少;重复标记不再次减少。 +- 多标签页分别刷新本接口时结果一致。 + +### A504 标记本人单条消息已读 + +- 模块 / Tag:Messaging +- 需求编号:X03-FR06 +- 负责人:罗皓晨 +- 关联数据表:待 `database-lhc.md` 确认 +- 当前状态:待评审 +- 用途:幂等地记录当前用户一条消息的首次已读时间。 +- 方法与路径:`POST /api/messages/{messageId}/read` +- operationId:`Messaging_MarkMessageRead` +- 请求 Schema:Route 参数 +- 响应 Schema:`MarkMessageReadResponse` +- 身份与 Policy:有效 JWT;仅买家或商家 +- 资源归属:消息接收用户必须等于当前认证用户 +- 幂等要求:同一用户对同一消息重复调用返回相同首次 `readAt` + +#### 请求 + +- Route 参数:`messageId`,必需,UUID。 +- Query 参数:无。 +- Header:`Authorization: Bearer `。 +- Body:无。 +- 校验规则:`messageId` 必须是标准 UUID。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`MarkMessageReadResponse` +- 示例: + +```json +{ + "code": "success", + "message": "ok", + "data": { + "messageId": "6aa23ac8-8cf7-4cb5-8387-69f463df4b77", + "isRead": true, + "readAt": "2026-07-24T02:35:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `messageId` 格式非法 | +| 401 | `AUTH.UNAUTHENTICATED` | JWT 缺失或无效 | +| 403 | `AUTH.FORBIDDEN` | 当前身份不是买家或商家 | +| 404 | `MESSAGE.NOT_FOUND` | 消息不存在或不属于当前用户 | + +#### 业务规则与并发 + +- 更新条件同时包含消息 ID、当前认证用户 ID 和未读状态。 +- 首次更新由服务端生成 UTC `readAt`;重复或并发调用读取并返回首次值,不覆盖时间。 +- 他人消息与不存在消息统一返回 `404`。 + +#### 缓存、事件或外部依赖 + +- 已读事实必须写入 PostgreSQL。 +- 操作成功后前端可以乐观更新本标签页角标,但仍应通过 A503 校正。 + +#### 验证场景 + +- 验证首次已读、重复已读、两个并发请求和越权访问。 +- 确认重复操作不重复减少未读数。 + +### A505 标记本人当前消息全部已读 + +- 模块 / Tag:Messaging +- 需求编号:X03-FR07 +- 负责人:罗皓晨 +- 关联数据表:待 `database-lhc.md` 确认 +- 当前状态:待评审 +- 用途:将操作开始时当前用户已经存在的未读消息批量标记为已读。 +- 方法与路径:`POST /api/messages/read-all` +- operationId:`Messaging_MarkAllMessagesRead` +- 请求 Schema:无 +- 响应 Schema:`MarkAllMessagesReadResponse` +- 身份与 Policy:有效 JWT;仅买家或商家 +- 资源归属:只更新当前认证用户 +- 幂等要求:没有新的未读消息时重复调用返回 `markedCount = 0` + +#### 请求 + +- Route 参数:无。 +- Query 参数:无。 +- Header:`Authorization: Bearer `。 +- Body:无。 +- 校验规则:服务端在操作开始时生成 UTC 截止时间,不接受客户端传入用户 ID 或截止时间。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`MarkAllMessagesReadResponse` +- 示例: + +```json +{ + "code": "success", + "message": "ok", + "data": { + "markedCount": 5, + "readAt": "2026-07-24T02:40:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | JWT 缺失或无效 | +| 403 | `AUTH.FORBIDDEN` | 当前身份不是买家或商家 | + +#### 业务规则与并发 + +- 更新条件必须包含当前认证用户、`isRead = false` 和 `createdAt <= readAt`。 +- 操作期间在截止时间之后到达的新消息保持未读。 +- `markedCount` 是本次首次变为已读的记录数,不是用户历史消息总数。 + +#### 缓存、事件或外部依赖 + +- 批量更新和未读状态以 PostgreSQL 为准。 +- 响应使用 `Cache-Control: no-store`。 + +#### 验证场景 + +- 验证存在多条未读、没有未读、重复调用和操作期间并发到达新消息。 +- 使用两个用户确认只更新当前用户数据。 + +### A506 API 存活检查 + +- 模块 / Tag:Infrastructure +- 需求编号:C10-FR06 +- 负责人:罗皓晨 +- 关联数据表:无 +- 当前状态:待评审 +- 用途:供 Compose、Nginx 和运维检查 API 进程能否响应。 +- 方法与路径:`GET /health/live` +- operationId:`Infrastructure_GetLiveness` +- 请求 Schema:无 +- 响应 Schema:`HealthStatusResponse` +- 身份与 Policy:无需认证 +- 资源归属:不适用 +- 幂等要求:只读接口,天然幂等 + +#### 请求 + +- Route 参数:无。 +- Query 参数:无。 +- Header:无必需 Header。 +- Body:无。 +- 校验规则:不接受外部传入检查目标。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`HealthStatusResponse` +- 本接口是健康检查例外,不使用通用 `code/message/data` 包装。 +- 示例: + +```json +{ + "status": "healthy", + "service": "mall-api", + "instanceId": "api-1", + "checkedAt": "2026-07-24T02:45:00Z" +} +``` + +#### 失败响应 + +进程无法响应时通常表现为连接失败或网关错误,不由当前进程构造 ProblemDetails。 + +#### 业务规则与并发 + +- 存活检查只验证进程响应能力,不访问 PostgreSQL、Redis、RabbitMQ 或对象存储。 +- `instanceId` 由部署环境注入,只用于 C10 请求分布证明,不包含主机名、IP 或敏感配置。 + +#### 缓存、事件或外部依赖 + +- 响应使用 `Cache-Control: no-store`。 + +#### 验证场景 + +- 两个 API 实例分别返回自身实例标识。 +- 进程运行时返回 `200`;进程停止时探针失败。 + +### A507 API 就绪检查 + +- 模块 / Tag:Infrastructure +- 需求编号:C10-FR06、C10-FR12 +- 负责人:罗皓晨 +- 关联数据表:无 +- 当前状态:待评审 +- 用途:判断实例是否具备接收业务流量的必要依赖。 +- 方法与路径:`GET /health/ready` +- operationId:`Infrastructure_GetReadiness` +- 请求 Schema:无 +- 响应 Schema:`ReadinessStatusResponse` +- 身份与 Policy:无需认证 +- 资源归属:不适用 +- 幂等要求:只读接口,天然幂等 + +#### 请求 + +- Route 参数:无。 +- Query 参数:无。 +- Header:无必需 Header。 +- Body:无。 +- 校验规则:不接受外部传入检查目标。 + +#### 成功响应 + +- HTTP 状态:全部必需依赖可用时为 `200 OK` +- 响应 Schema:`ReadinessStatusResponse` +- 本接口不使用通用业务包装。 +- 示例: + +```json +{ + "status": "healthy", + "service": "mall-api", + "instanceId": "api-1", + "checkedAt": "2026-07-24T02:45:00Z", + "checks": [ + { + "name": "postgres", + "status": "healthy" + }, + { + "name": "redis", + "status": "healthy" + } + ] +} +``` + +#### 失败响应 + +| HTTP 状态 | 响应 | 触发条件 | +|---|---|---| +| 503 | `ReadinessStatusResponse` | PostgreSQL 或当前阶段已启用且被配置为必需的依赖不可用 | + +`503` 示例: + +```json +{ + "status": "unhealthy", + "service": "mall-api", + "instanceId": "api-1", + "checkedAt": "2026-07-24T02:46:00Z", + "checks": [ + { + "name": "postgres", + "status": "unhealthy" + } + ] +} +``` + +#### 业务规则与并发 + +- PostgreSQL 始终属于就绪必需依赖。 +- Redis、RabbitMQ 和对象存储仅在当前阶段启用且配置为该实例必要依赖时参与就绪判断;未启用依赖不能错误阻塞就绪。 +- 响应不得包含连接字符串、主机、端口、异常消息、堆栈或凭据。 + +#### 缓存、事件或外部依赖 + +- 检查设置短超时,避免探针堆积拖垮实例。 +- 响应使用 `Cache-Control: no-store`。 + +#### 验证场景 + +- PostgreSQL 正常时返回 `200`。 +- PostgreSQL 不可用时返回 `503`。 +- 未启用 RabbitMQ 或对象存储时不把它们报告为失败。 +- 两个实例使用同一契约并返回不同 `instanceId`。 + +## 五、SignalR 实时契约(不占 Axxx 编号) + +### 5.1 Hub 连接 + +| 项目 | 契约 | +|---|---| +| Hub 路径 | `/hubs/messaging` | +| 鉴权 | 有效买家或商家 JWT | +| 身份来源 | 服务端认证上下文中的用户 ID 和角色 | +| 客户端订阅参数 | 无,不接受客户端传入任意 `userId`、角色或组名 | +| 多实例 | 使用 Redis Backplane | +| 事实来源 | PostgreSQL 中的 M09 消息 | + +浏览器在 WebSocket 握手限制下可通过 SignalR `accessTokenFactory` 传递令牌。服务端只允许在 `/hubs/messaging` 握手路径读取受控的 `access_token` Query,并必须在 Nginx、ASP.NET Core、Serilog 和 Trace 中完整脱敏;集成、演示和发布环境只使用 HTTPS/WSS。 + +客户端主动退出后关闭连接。非主动断线使用有限退避自动重连;初次连接和每次重连成功后调用 A503,并按需调用 A501 补查断线期间消息。 + +### 5.2 服务端事件 `MessageCreated` + +服务端向目标认证用户的全部在线连接推送 `MessageCreated`。载荷 Schema 为 `MessageCreatedPayload`: + +| 字段 | 类型 | 可空 | 说明 | +|---|---|---:|---| +| `messageId` | UUID | 否 | 已持久化消息 ID,也是客户端去重键 | +| `type` | `MessageType` | 否 | 消息类型 | +| `title` | string | 否 | 标题 | +| `summary` | string | 否 | 摘要 | +| `relatedResourceType` | `RelatedResourceType` | 是 | 关联业务类型 | +| `relatedResourceId` | UUID | 是 | 关联业务 ID | +| `action` | `MessageAction` | 是 | 安全跳转描述 | +| `createdAt` | UTC 时间 | 否 | 服务端消息创建时间 | + +示例: + +```json +{ + "messageId": "6aa23ac8-8cf7-4cb5-8387-69f463df4b77", + "type": "OrderShipped", + "title": "订单已发货", + "summary": "你的订单已由商家发出,可进入订单详情查看。", + "relatedResourceType": "Order", + "relatedResourceId": "93c8d8e1-bdac-4a13-8ba2-0c21c327b81e", + "action": { + "target": "OrderDetail", + "resourceId": "93c8d8e1-bdac-4a13-8ba2-0c21c327b81e" + }, + "createdAt": "2026-07-24T02:30:00Z" +} +``` + +规则: + +- 只有消息数据库事务成功提交后才能推送。 +- 推送失败不回滚业务事务或消息记录,也不把消息重新标记为未生成。 +- 客户端按 `messageId` 去重轻提示;不得仅凭推送载荷修改订单、支付或售后最终状态。 +- 本期不提供客户端调用的聊天、广播、已送达回执、任意加组或按用户订阅 Hub 方法。 + +## 六、错误码登记 + +| 错误码 | HTTP 状态 | 含义 | +|---|---:|---| +| `MESSAGE.NOT_FOUND` | 404 | 消息不存在或不属于当前用户 | + +认证、验证、限流、依赖不可用和未知错误复用[《接口设计》](接口设计.md)第一章登记的通用错误码,不创建同义错误码。 + +## 七、跨模块影响与待确认项 + +### 7.1 需要其他负责人评审的协作点 + +- Ordering、Payment 和 AfterSales 负责人需确认会触发通知的业务事实、事件 ID、业务 ID、接收用户和发生时间。 +- Identity 负责人需确认买家与商家认证身份、账号禁用和令牌失效规则可以同时约束 HTTP 与 SignalR。 +- 商家通知必须由来源模块明确指定接收账号或受控接收范围,不允许 Messaging 自行向全部商家广播私人订单或售后信息。 +- Catalog 负责人继续拥有 C07 商品缓存失效业务规则;罗皓晨只提供 Redis 与多实例缓存基础设施,不新增公开缓存控制接口。 + +### 7.2 实现前必须补齐 + +- 创建并评审 `database-lhc.md`,登记消息表及必要的唯一约束、用户未读查询索引和关联 A501~A505。 +- 单独评审 Ordering、Payment、AfterSales 到 Messaging 的集成事件 Schema、Routing Key、Outbox/Inbox 幂等键和失败处理;这些不是 HTTP Axxx 接口。 +- 在后端脚手架建立后形成真实 OpenAPI,并保证 `operationId`、Schema、状态码和错误码与本文件一致。 +- 在测试计划中登记 X03、C06 和 C10 的分页、越权、重复已读、并发全部已读、断线重连、多标签页、多实例和健康检查场景。 + +当前文档只能证明接口契约已形成待评审草案,不能证明接口已经实现、联调或通过验收。 -- Gitee From b6e264bbaaa010c636b2790fdfcc977f30651a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=83=A0=E6=83=A0?= <2205590672@qq.com> Date: Fri, 24 Jul 2026 09:33:53 +0800 Subject: [PATCH 043/118] =?UTF-8?q?docs(interface):=20=E6=96=B0=E5=A2=9Ein?= =?UTF-8?q?terface-zhh=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interface-zhh.md" | 1225 +++++++++++++++++ 1 file changed, 1225 insertions(+) create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhh.md" diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhh.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhh.md" new file mode 100644 index 0000000..a7bca4c --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhh.md" @@ -0,0 +1,1225 @@ +# 个人接口文件 — 朱惠惠(Cart、Seckill) + +> 组别:24级1班第7组 负责人:朱惠惠(zhh) 接口编号区间:`A201`~`A300` +> 负责模块:Cart(购物车 CRUD、选中、结算预览、清空)、Seckill(商家活动维护、买家抢购下单、秒杀订单查询) +> 关联教师验收编号:F07、C01 +> 当前状态:部分定义;清单已给出,详细定义按接口设计 1.20 模板补齐 +> 本文件为协作阶段材料,评审通过后由罗皓晨汇总到 `接口设计.md` + +## 修订记录 + +| 版本 | 日期 | 修改人 | 修改说明 | +|---|---|---|---| +| v0.1 | 2026-07-24 | 朱惠惠 | 建立 `A201`~`A230` 接口清单并补齐全部详细定义 | + +## 一、接口清单 + +| 编号 | 模块 | 需求编号 | 名称 | 方法 | 路径 | operationId | 请求 Schema | 响应 Schema | 鉴权 Policy | 关联 DBxxx | 当前状态 | +|---|---|---|---|---|---|---|---|---|---|---|---| +| A201 | Cart | F07 | 加入购物车 | POST | `/api/cart/items` | `Cart_AddItem` | `AddCartItemRequest` | `CartItemResponse` | BuyerOnly | DB041 | 已定义 | +| A202 | Cart | F07 | 查看购物车 | GET | `/api/cart/items` | `Cart_ListItems` | 无(Query 分页/筛选) | `CartListResponse` | BuyerOnly | DB041 | 已定义 | +| A203 | Cart | F07 | 修改购物车条目数量 | PATCH | `/api/cart/items/{cartItemId}` | `Cart_UpdateItemQuantity` | `UpdateCartItemQuantityRequest` | `CartItemResponse` | BuyerOnly | DB041 | 已定义 | +| A204 | Cart | F07 | 删除购物车条目 | DELETE | `/api/cart/items/{cartItemId}` | `Cart_RemoveItem` | 无 | 无(204) | BuyerOnly | DB041 | 已定义 | +| A205 | Cart | F07 | 批量删除购物车条目 | POST | `/api/cart/items/batch-delete` | `Cart_BatchRemoveItems` | `BatchRemoveCartItemsRequest` | `BatchRemoveCartItemsResponse` | BuyerOnly | DB041 | 已定义 | +| A206 | Cart | F07 | 修改选中状态(全选/反选/单选) | PATCH | `/api/cart/items/selection` | `Cart_UpdateSelection` | `UpdateCartItemSelectionRequest` | `CartListResponse` | BuyerOnly | DB041 | 已定义 | +| A207 | Cart | F07 | 清空购物车 | DELETE | `/api/cart` | `Cart_Clear` | 无 | 无(204) | BuyerOnly | DB041 | 已定义 | +| A208 | Cart | F07 | 获取结算预览 | GET | `/api/cart/checkout-preview` | `Cart_GetCheckoutPreview` | 无(Query 可选 `cartItemIds`) | `CheckoutPreviewResponse` | BuyerOnly | DB041 | 已定义 | +| A220 | Seckill | C01 | 商家创建秒杀活动 | POST | `/api/merchant/seckill-activities` | `Seckill_CreateActivity` | `CreateSeckillActivityRequest` | `SeckillActivityResponse` | MerchantOnly | DB042、DB043 | 已定义 | +| A221 | Seckill | C01 | 商家更新秒杀活动 | PATCH | `/api/seckill-activities/{activityId}` | `Seckill_UpdateActivity` | `UpdateSeckillActivityRequest` | `SeckillActivityResponse` | MerchantOnly | DB042、DB043 | 已定义 | +| A222 | Seckill | C01 | 商家发布秒杀活动 | POST | `/api/seckill-activities/{activityId}/publish` | `Seckill_PublishActivity` | 无 | `SeckillActivityResponse` | MerchantOnly | DB042、DB043 | 已定义 | +| A223 | Seckill | C01 | 商家取消秒杀活动 | POST | `/api/seckill-activities/{activityId}/cancel` | `Seckill_CancelActivity` | `CancelSeckillActivityRequest` | `SeckillActivityResponse` | MerchantOnly | DB042、DB043 | 已定义 | +| A224 | Seckill | C01 | 商家秒杀活动列表 | GET | `/api/merchant/seckill-activities` | `Seckill_ListMerchantActivities` | 无(Query 分页/筛选) | `SeckillActivityListResponse` | MerchantOnly | DB042、DB043 | 已定义 | +| A225 | Seckill | C01 | 商家秒杀活动详情 | GET | `/api/seckill-activities/{activityId}` | `Seckill_GetMerchantActivityDetail` | 无 | `SeckillActivityDetailResponse` | MerchantOnly | DB042、DB043、DB044 | 已定义 | +| A226 | Seckill | C01 | 买家秒杀活动列表 | GET | `/api/seckill-activities` | `Seckill_ListActiveActivities` | 无(Query 分页) | `SeckillActivityListResponse` | 允许游客 | DB042、DB043 | 已定义 | +| A227 | Seckill | C01 | 买家秒杀活动详情 | GET | `/api/seckill-activities/{activityId}/public` | `Seckill_GetActiveActivityDetail` | 无 | `SeckillActivityDetailResponse` | 允许游客 | DB042、DB043 | 已定义 | +| A228 | Seckill | C01 | 秒杀下单 | POST | `/api/seckill-orders` | `Seckill_PlaceOrder` | `PlaceSeckillOrderRequest` | `PlaceSeckillOrderResponse` | BuyerOnly | DB043、DB044、DB045 | 已定义 | +| A229 | Seckill | C01 | 买家秒杀订单列表 | GET | `/api/seckill-orders` | `Seckill_ListMyOrders` | 无(Query 分页/筛选) | `SeckillOrderListResponse` | BuyerOnly | DB044、DB045 | 已定义 | +| A230 | Seckill | C01 | 买家秒杀订单详情 | GET | `/api/seckill-orders/{orderId}` | `Seckill_GetMyOrder` | 无 | `SeckillOrderDetailResponse` | BuyerOnly | DB044、DB045 | 已定义 | + +接口路径补充说明: + +- Cart 业务接口位于 `/api/cart` 前缀之下,遵守《接口设计》1.2 节小写复数 + 动宾资源原则。 +- Seckill 业务接口位于 `/api/seckill-activities`(活动)和 `/api/seckill-orders`(订单)两个根路径;商家维护入口额外加 `/api/merchant` 前缀,与公开购物端入口物理隔离。 +- `/api/cart/items` 表达购物车条目集合;`/api/cart/items/selection` 为选中状态专用子资源,避免在 GET 之上覆盖副作用。 +- `/api/seckill-orders` 与 M04 普通订单共用 `orders` / `order_items` 表与状态机,仅在订单上记录 `seckill_activity_id` 快照;不再建立平行订单接口。 +- A225 商家详情与 A227 买家详情返回字段范围不同:A225 含商家内部字段(取消原因、回补策略),A227 仅返回公开可见字段。 + +## 二、接口详细定义 + +> 每个接口按《接口设计》1.20 节模板补齐。Schema 名称遵守 OpenAPI 7.2 节:PascalCase + 用途后缀;`operationId` 使用 `_`;路径参数使用单数对象 + `Id`。 + +### A201 加入购物车 + +- 模块 / Tag:Cart +- 需求编号:F07、M03-01 +- 负责人:朱惠惠 +- 关联数据表:DB041 +- 当前状态:已定义 +- 用途:已登录买家将商品加入购物车;同一买家同一商品只保留一条记录,重复加入按累加处理;服务端实时校验上下架、库存与数量上限。 +- 方法与路径:`POST /api/cart/items` +- operationId:`Cart_AddItem` + +#### 请求 + +- Route 参数:无 +- Query 参数:无 +- Header:`Authorization: Bearer `(必填);`Idempotency-Key: `(推荐,防止重复点击) +- Body: + +```text +AddCartItemRequest { + productId: uuid // 必填 + quantity: integer // 必填,1 ≤ quantity ≤ 商品当前实时可售库存 +} +``` + +- 校验规则: + - `quantity` 必须为正整数,1~当前实时可售库存。 + - 服务端忽略请求中任何尝试指定 `userId`、`cartItemId`、`createdAt` 的字段;条目归属固定为当前买家。 + - 商品必须处于已上架状态;库存不足、商品下架或被禁用时拒绝。 + +#### 成功响应 + +- HTTP 状态:`201 Created`(新增条目)或 `200 OK`(重复加入累加) +- Response Header:`Location: /api/cart/items/{cartItemId}` +- 响应 Schema:`CartItemResponse` + +```text +CartItemResponse { + cartItemId: uuid + productId: uuid + productSummary: ProductSummaryResponse + unitPrice: number // 服务端实时单价(decimal) + quantity: integer + subtotal: number // unitPrice × quantity,由服务端计算 + isSelected: boolean + isAvailable: boolean + unavailableReason: string? // 例如 "ProductUnpublished"、"OutOfStock" + maxAllowedQuantity: integer // 商品当前实时可售库存,供前端截断 + createdAt: string + updatedAt: string +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `quantity` 缺失、非整数、≤0 或超过实时库存 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | +| 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品已下架或被禁用 | +| 409 | `CART.QUANTITY_EXCEEDS_STOCK` | 累加后数量超过商品实时可售库存,返回当前最大允许值 | +| 429 | `COMMON.RATE_LIMITED` | 触发限流 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Redis 幂等存储或商品服务暂时不可用 | +| 500 | `COMMON.INTERNAL_ERROR` | 未处理的服务端错误 | + +#### 业务规则与并发 + +- 主键为 `(buyer_id, product_id)`;同一组合只能保留一条记录,重复加入时新数量累加到已有条目。 +- 累加过程在同一数据库事务内完成:读取已有条目、加锁或条件更新、`quantity = quantity + :newQty`;影响行数为 0 即失败。 +- 条目归属固定为当前买家;客户端传入的 `userId`、`cartItemId` 被忽略;越权访问他人条目返回 404。 +- 商品不可加时返回明确错误码与 `maxAllowedQuantity`;前端按此截断。 +- 接受 `Idempotency-Key` Header;同一 `(userId, key)` 在约定窗口(默认 5 分钟)内重复提交只生效一次,返回首次已确认结果且不重复累加数量。 + +#### 缓存、事件或外部依赖 + +- 不缓存购物车条目;商品价格、库存与上下架状态由 Catalog 模块实时提供。 +- 幂等键记录写入 Redis:`cart:idempotency:{userId}:{key}`,TTL = 5 分钟。 +- 不发布集成事件。 + +#### 验证场景 + +- 已上架商品、合法 `quantity` → 201,返回最新条目。 +- 同一商品二次加入 → 200,条目数量累加,库存上限生效。 +- 数量 ≤ 0 或超过库存 → 400 / `COMMON.VALIDATION_FAILED`,附 `maxAllowedQuantity`。 +- 商品已下架 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`,不创建条目。 +- 商品被禁用 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 +- 已存在购物车条目累加后超库存 → 409 / `CART.QUANTITY_EXCEEDS_STOCK`,原条目数量不超上限。 +- 同一 `Idempotency-Key` 重复提交 → 仅首次创建/累加,后续返回首次结果且 `quantity` 不再累加。 + +### A202 查看购物车 + +- 模块 / Tag:Cart +- 需求编号:F07、M03-01 +- 负责人:朱惠惠 +- 关联数据表:DB041 +- 当前状态:已定义 +- 用途:买家查看本人购物车全部条目;返回实时单价、选中状态、可用性与失效原因。 +- 方法与路径:`GET /api/cart/items` +- operationId:`Cart_ListItems` + +#### 请求 + +- Route 参数:无 +- Query 参数: + - `page`(默认 1) + - `pageSize`(默认 50,上限 100) + - `selectedOnly`(可选,默认 `false`,仅返回选中条目) + - `availableOnly`(可选,默认 `false`,仅返回可结算条目) +- Header:`Authorization: Bearer `(必填) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`CartListResponse` + +```text +CartListResponse { + items: CartItemResponse[] + page: integer + pageSize: integer + total: integer + totalPages: integer + selectedCount: integer // 当前选中条目数量 + selectedTotalAmount: number // 选中条目按实时单价计算的总额 + availableSelectedCount: integer // 选中且可结算的条目数量 +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 分页或筛选参数非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 严格按 `buyer_id = current_user_id` 过滤;不允许跨用户查看。 +- 排序默认按 `updatedAt desc`;相同 `updatedAt` 时按 `productId` 稳定排序。 +- 商品下架、库存归零或被禁用时,条目仍可见但标记 `isAvailable=false` 并附 `unavailableReason`;不参与 `selectedTotalAmount` 与 `availableSelectedCount` 计算。 +- 实时单价与库存来自 Catalog 模块;不接受客户端传入的价格或库存覆盖。 + +#### 缓存、事件或外部依赖 + +- 不缓存购物车内容;价格、库存与上下架状态由 Catalog 模块实时返回。 +- 不发布集成事件。 + +#### 验证场景 + +- 买家购物车 0 条 → `items=[]`,`selectedCount=0`,`selectedTotalAmount=0`。 +- 包含已下架商品 → 仍可见,`isAvailable=false`,`selectedTotalAmount` 不计入。 +- 包含失效商品但被选中 → `availableSelectedCount` 仅统计可用条目。 +- 跨用户访问 → 403 / `AUTH.FORBIDDEN`,不泄露他人条目。 +- 翻页查询 → 总数与分页元数据稳定,按 `updatedAt desc` 一致排序。 + +### A203 修改购物车条目数量 + +- 模块 / Tag:Cart +- 需求编号:F07、M03-01 +- 负责人:朱惠惠 +- 关联数据表:DB041 +- 当前状态:已定义 +- 用途:买家调整购物车条目数量;调大时校验实时库存上限,调小或调为 1 不受库存约束;不允许改为 0 或负数。 +- 方法与路径:`PATCH /api/cart/items/{cartItemId}` +- operationId:`Cart_UpdateItemQuantity` + +#### 请求 + +- Route 参数:`cartItemId: uuid` +- Header:`Authorization: Bearer `(必填) +- Body: + +```text +UpdateCartItemQuantityRequest { + quantity: integer // 必填,1 ≤ quantity ≤ 商品当前实时可售库存 +} +``` + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`CartItemResponse`(同 A201,含最新 `quantity`、`subtotal`、`maxAllowedQuantity`) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `quantity` 缺失、非整数、≤0 或超过实时库存 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 404 | `RESOURCE.NOT_FOUND` | 条目不存在或不属于当前用户 | +| 409 | `CART.ITEM_UNAVAILABLE` | 商品已下架或被禁用,不允许调大 | +| 409 | `CART.QUANTITY_EXCEEDS_STOCK` | 调大后超过实时可售库存 | + +#### 业务规则与并发 + +- 严格按 `cart_item_id = :cartItemId AND buyer_id = current_user_id` 条件更新;不存在的条目返回 404。 +- 调小或调到 1 不受实时库存上限约束;商品下架或被禁用时允许调小或删除,但禁止调大或累加。 +- 库存上限校验以 Catalog 模块实时库存为准;不允许客户端传入目标库存。 +- 服务端不接受修改 `productId`、`isSelected`、`userId` 等字段;选中状态变更走 A206。 + +#### 缓存、事件或外部依赖 + +- 不缓存、不发布集成事件。 + +#### 验证场景 + +- 条目数量从 2 改到 5,库存充足 → 200,条目更新。 +- 条目数量从 5 改到 10,超过库存 → 409 / `CART.QUANTITY_EXCEEDS_STOCK`,附 `maxAllowedQuantity`。 +- 条目数量改为 0 或 -1 → 400 / `COMMON.VALIDATION_FAILED`。 +- 商品已下架,条目从 2 调到 1 → 200;条目从 1 调到 2 → 409 / `CART.ITEM_UNAVAILABLE`。 +- 修改他人条目 → 404,不泄露归属。 + +### A204 删除购物车条目 + +- 模块 / Tag:Cart +- 需求编号:F07、M03-01 +- 负责人:朱惠惠 +- 关联数据表:DB041 +- 当前状态:已定义 +- 用途:买家单条删除购物车条目;幂等执行,已删除条目再次删除返回 204。 +- 方法与路径:`DELETE /api/cart/items/{cartItemId}` +- operationId:`Cart_RemoveItem` + +#### 请求 + +- Route 参数:`cartItemId: uuid` +- Header:`Authorization: Bearer `(必填) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`204 No Content` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 删除按 `cart_item_id = :cartItemId AND buyer_id = current_user_id` 过滤;影响行数为 0 时返回 204,保持幂等。 +- 不返回 404,避免暴露条目归属;删除请求仅在鉴权失败时返回 401/403。 +- 默认地址或失效条目也可删除;删除后不自动选择其他默认地址或恢复库存。 + +#### 缓存、事件或外部依赖 + +- 不缓存、不发布集成事件。 + +#### 验证场景 + +- 删除本人条目 → 204,列表更新。 +- 重复删除同一 `cartItemId` → 204,幂等。 +- 删除他人条目 → 204,不报错也不泄露归属。 +- 未登录调用 → 401 / `AUTH.UNAUTHENTICATED`。 + +### A205 批量删除购物车条目 + +- 模块 / Tag:Cart +- 需求编号:F07、M03-01 +- 负责人:朱惠惠 +- 关联数据表:DB041 +- 当前状态:已定义 +- 用途:买家一次性删除多个购物车条目;不在本人购物车中的条目被忽略,整体请求返回成功。 +- 方法与路径:`POST /api/cart/items/batch-delete` +- operationId:`Cart_BatchRemoveItems` + +#### 请求 + +- Header:`Authorization: Bearer `(必填) +- Body: + +```text +BatchRemoveCartItemsRequest { + cartItemIds: uuid[] // 必填,1~100 个;超过上限返回 400 +} +``` + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`BatchRemoveCartItemsResponse` + +```text +BatchRemoveCartItemsResponse { + removedCount: integer + skippedCount: integer // 不存在或不属于当前买家的条目数量 +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `cartItemIds` 缺失、为空、超过 100 个或包含非法 UUID | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 同一数据库事务内按 `cart_item_id IN (:ids) AND buyer_id = current_user_id` 删除;返回实际删除数量。 +- 不在本人购物车中的条目被忽略并计入 `skippedCount`;整体请求不报错。 +- 删除成功后 `selectedCount` 与 `selectedTotalAmount` 自动按剩余条目重算。 + +#### 缓存、事件或外部依赖 + +- 不缓存、不发布集成事件。 + +#### 验证场景 + +- 选中 3 条有效条目批量删除 → 200,`removedCount=3`,`skippedCount=0`。 +- 提交 1 条他人条目 + 2 条本人条目 → 200,`removedCount=2`,`skippedCount=1`。 +- 提交 0 条或 101 条 `cartItemIds` → 400 / `COMMON.VALIDATION_FAILED`。 + +### A206 修改选中状态(全选/反选/单选) + +- 模块 / Tag:Cart +- 需求编号:F07、M03-01 +- 负责人:朱惠惠 +- 关联数据表:DB041 +- 当前状态:已定义 +- 用途:买家设置购物车条目选中状态;支持全选、反选、单条切换;失效条目不允许被选中。 +- 方法与路径:`PATCH /api/cart/items/selection` +- operationId:`Cart_UpdateSelection` + +#### 请求 + +- Header:`Authorization: Bearer `(必填) +- Body: + +```text +UpdateCartItemSelectionRequest { + mode: "SelectAll" | "DeselectAll" | "SetExplicit" + cartItemIds: uuid[]? // 仅当 mode = "SetExplicit" 时必填;最多 100 个 + isSelected: boolean? // 仅当 mode = "SetExplicit" 时必填 +} +``` + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`CartListResponse`(同 A206,按当前选中状态返回完整购物车) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `mode` 非法、`cartItemIds` 缺失/超限或 `isSelected` 缺失 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 409 | `CART.ITEM_UNAVAILABLE` | 尝试选中已下架或失效的条目 | + +#### 业务规则与并发 + +- 全选/反选按 `buyer_id = current_user_id` 过滤;失效条目保持未选中,不被强制选中。 +- `SetExplicit` 仅修改 `cartItemIds` 中属于当前买家的条目;他人条目被忽略并计入 `skippedCount`(由响应 `selectedCount`/`availableSelectedCount` 体现)。 +- 单条切换并发安全:服务端使用条件更新 `WHERE cart_item_id = :id AND buyer_id = current_user_id`。 +- 选中状态保存在服务端;前端刷新或重新登录后状态保留。 + +#### 缓存、事件或外部依赖 + +- 不缓存、不发布集成事件。 + +#### 验证场景 + +- 全选 → 200,所有可用条目 `isSelected=true`,失效条目仍 `isSelected=false`。 +- 反选 → 200,所有可用条目 `isSelected=false`。 +- 单选切换某条目 → 200,仅该条目 `isSelected` 变更。 +- 尝试选中失效条目 → 409 / `CART.ITEM_UNAVAILABLE`,不修改状态。 +- 跨用户 ID 提交 → 仅本人条目被修改,他人条目被忽略。 + +### A207 清空购物车 + +- 模块 / Tag:Cart +- 需求编号:F07、M03-01 +- 负责人:朱惠惠 +- 关联数据表:DB041 +- 当前状态:已定义 +- 用途:买家一键清空本人购物车的全部条目;幂等执行,重复清空返回 204。 +- 方法与路径:`DELETE /api/cart` +- operationId:`Cart_Clear` + +#### 请求 + +- Header:`Authorization: Bearer `(必填) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`204 No Content` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 按 `buyer_id = current_user_id` 物理删除全部条目;只影响当前用户。 +- 重复清空 → 204,幂等。 +- 不影响浏览记录、收藏、消息或默认地址等其他模块数据。 + +#### 缓存、事件或外部依赖 + +- 不缓存、不发布集成事件。 + +#### 验证场景 + +- 购物车含 5 条条目 → 204,后续列表为空。 +- 重复清空 → 204,幂等。 +- 未登录调用 → 401。 + +### A208 获取结算预览 + +- 模块 / Tag:Cart +- 需求编号:F07、M03-01 +- 负责人:朱惠惠 +- 关联数据表:DB041 +- 当前状态:已定义 +- 用途:买家进入结算页前查看选中条目总价、可用性与失效原因;服务端再次校验实时价格、库存与归属。 +- 方法与路径:`GET /api/cart/checkout-preview` +- operationId:`Cart_GetCheckoutPreview` + +#### 请求 + +- Query 参数:`cartItemIds`(可选,多个 UUID;不传则按当前 `isSelected=true` 过滤) +- Header:`Authorization: Bearer `(必填) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`CheckoutPreviewResponse` + +```text +CheckoutPreviewResponse { + items: CartItemResponse[] // 当前可用于结算的条目 + unavailableItems: CartItemResponse[] // 失效条目(不下单但提示买家) + totalAmount: number // 服务端按实时单价计算的总额 + availableForCheckout: boolean // 是否有至少一条可结算条目 +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `cartItemIds` 超过 100 个或包含非法 UUID | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 不传 `cartItemIds` 时按 `isSelected=true AND buyer_id = current_user_id` 过滤。 +- 传入 `cartItemIds` 时取交集;不在本人购物车或失效条目归入 `unavailableItems`。 +- `totalAmount` 由服务端实时计算并返回;前端不得自行覆盖金额。 +- 返回 `availableForCheckout=false` 时前端禁用提交订单按钮。 + +#### 缓存、事件或外部依赖 + +- 不缓存;价格与库存由 Catalog 模块实时返回。 +- 不发布集成事件;提交订单由 M04 处理。 + +#### 验证场景 + +- 选中 2 条可用 + 1 条失效 → `items=2`、`unavailableItems=1`、`availableForCheckout=true`。 +- 全部失效 → `items=[]`、`availableForCheckout=false`,前端禁用提交。 +- 传入他人 `cartItemId` → 归入 `unavailableItems`,不报错也不泄露归属。 + +### A220 商家创建秒杀活动 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB042、DB043 +- 当前状态:已定义 +- 用途:商家维护秒杀活动;活动绑定一个已上架商品,保存秒杀价、独立库存总量与单用户限购;保存后状态为 `Draft`。 +- 方法与路径:`POST /api/merchant/seckill-activities` +- operationId:`Seckill_CreateActivity` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Merchant) +- Body: + +```text +CreateSeckillActivityRequest { + productId: uuid // 必填,必须是当前商家已上架商品 + activityName: string // 必填,1~50 字 + seckillPrice: number // 必填,>0 且 < 商品当前上架价 + totalStock: integer // 必填,1 ≤ totalStock ≤ 商品当前可售库存 + perBuyerLimit: integer // 必填,1 ≤ perBuyerLimit ≤ totalStock + startAt: string // 必填,UTC ISO 8601,≥ now() + 5min + endAt: string // 必填,UTC ISO 8601,> startAt 且 ≤ startAt + 30d +} +``` + +#### 成功响应 + +- HTTP 状态:`201 Created` +- Response Header:`Location: /api/seckill-activities/{activityId}` +- 响应 Schema:`SeckillActivityResponse` + +```text +SeckillActivityResponse { + activityId: uuid + productId: uuid + activityName: string + seckillPrice: number + originalPrice: number // 商品当前上架价 + totalStock: integer + remainingStock: integer // 创建后等于 totalStock + soldCount: integer // 创建后等于 0 + perBuyerLimit: integer + startAt: string + endAt: string + status: "Draft" + createdAt: string + updatedAt: string +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段缺失、格式错误或金额/数量/时间窗口非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | +| 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品未上架或不属于当前商家 | +| 409 | `SECKILL.STOCK_EXCEEDS_AVAILABLE` | `totalStock` 超过商品当前可售库存 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 商品服务或分布式锁不可用 | + +#### 业务规则与并发 + +- 同一商品同一时间段(`startAt`、`endAt` 与已存在活动存在重叠)不允许重复创建;重叠返回 `409 / SECKILL.TIME_WINDOW_CONFLICT`。 +- `seckillPrice < originalPrice` 由服务端校验;不接受等于或高于原价的秒杀活动。 +- `startAt ≥ now() + 5min` 避免立刻开始的发布影响压测一致性。 +- 创建活动时同步在 `seckill_inventory`(DB043)写入 `remainingStock=totalStock`、`soldCount=0`、`frozenCount=0`;两者在同一事务。 +- 商品归属:仅当 `product.owner_merchant_id = current_user_id` 才允许创建;越权访问返回 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 + +#### 缓存、事件或外部依赖 + +- 活动创建后向 Redis 写入分布式锁 Key:`lock:seckill:activity:create:{productId}`,事务结束释放。 +- 不缓存、不发布集成事件。 + +#### 验证场景 + +- 合法参数创建 → 201,状态 `Draft`,库存=总量。 +- `totalStock` 超过商品库存 → 409 / `SECKILL.STOCK_EXCEEDS_AVAILABLE`。 +- `seckillPrice ≥ originalPrice` → 400 / `COMMON.VALIDATION_FAILED`。 +- `startAt < now() + 5min` → 400 / `COMMON.VALIDATION_FAILED`。 +- 时间窗口与已存在活动重叠 → 409 / `SECKILL.TIME_WINDOW_CONFLICT`。 +- 尝试绑定他人商品 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 + +### A221 商家更新秒杀活动 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB042、DB043 +- 当前状态:已定义 +- 用途:商家在 `Draft` 或 `Scheduled` 状态下更新秒杀活动参数;`Ongoing`/`Finished`/`Cancelled` 状态不允许修改。 +- 方法与路径:`PATCH /api/seckill-activities/{activityId}` +- operationId:`Seckill_UpdateActivity` + +#### 请求 + +- Route 参数:`activityId: uuid` +- Header:`Authorization: Bearer `(必填,角色 Merchant) +- Body: + +```text +UpdateSeckillActivityRequest { + activityName?: string // 可选 + seckillPrice?: number // 可选 + totalStock?: integer // 可选;只能调大或保持;不得小于已售数量 + perBuyerLimit?: integer // 可选 + startAt?: string // 可选;不得早于 now() + 5min + endAt?: string // 可选 +} +``` + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillActivityResponse` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段格式或时间窗口非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | +| 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | +| 409 | `SECKILL.INVALID_STATUS` | 活动已 `Ongoing`/`Finished`/`Cancelled` | +| 409 | `SECKILL.STOCK_BELOW_SOLD` | `totalStock` 小于已售数量 | +| 409 | `SECKILL.TIME_WINDOW_CONFLICT` | 与其他活动时间窗口重叠 | + +#### 业务规则与并发 + +- 仅允许在 `Draft` 或 `Scheduled` 状态更新;状态字段由 `status='Draft' OR status='Scheduled'` 条件更新保证。 +- `totalStock` 只允许调大或保持;调整后必须满足 `remainingStock + soldCount + frozenCount = totalStock`。 +- 修改后 `startAt` 与 `endAt` 必须保持 `startAt ≥ now() + 5min` 与 `endAt > startAt`。 + +#### 缓存、事件或外部依赖 + +- 同步更新 Redis 缓存:`cache:seckill:activity:{activityId}`(仅元数据,不含库存)。 + +#### 验证场景 + +- 草稿活动更新名称与价格 → 200。 +- 草稿活动 `totalStock` 调小到 `soldCount` 以下 → 409 / `SECKILL.STOCK_BELOW_SOLD`。 +- 进行中活动尝试改价 → 409 / `SECKILL.INVALID_STATUS`。 +- 时间窗口与他人活动重叠 → 409 / `SECKILL.TIME_WINDOW_CONFLICT`。 + +### A222 商家发布秒杀活动 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB042、DB043 +- 当前状态:已定义 +- 用途:商家将 `Draft` 状态活动提交审核后立即变为 `Scheduled`;系统按 `startAt` 自动推进到 `Ongoing`。 +- 方法与路径:`POST /api/seckill-activities/{activityId}/publish` +- operationId:`Seckill_PublishActivity` + +#### 请求 + +- Route 参数:`activityId: uuid` +- Header:`Authorization: Bearer `(必填,角色 Merchant) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillActivityResponse`(`status="Scheduled"`) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | +| 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | +| 409 | `SECKILL.INVALID_STATUS` | 活动已发布或已结束 | +| 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品已下架,禁止发布 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 缓存写入失败 | + +#### 业务规则与并发 + +- 条件更新:`UPDATE ... SET status='Scheduled' WHERE activity_id=:id AND status='Draft' AND owner_merchant_id=:mid`;影响行数为 0 时按 409 处理。 +- 商品已下架时拒绝发布;商家需先恢复上架。 +- 发布成功后刷新 Redis 缓存并预热活动详情 Key;Worker 按 `startAt` 自动推进到 `Ongoing`。 + +#### 缓存、事件或外部依赖 + +- Redis:`cache:seckill:activity:{activityId}`、`cache:seckill:list:active`。 + +#### 验证场景 + +- 草稿活动发布 → 200,状态 `Scheduled`。 +- 重复发布 → 409 / `SECKILL.INVALID_STATUS`。 +- 商品已下架 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 + +### A223 商家取消秒杀活动 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB042、DB043 +- 当前状态:已定义 +- 用途:商家取消 `Draft` / `Scheduled` / `Ongoing` 状态活动;取消后入口立即失效,已存在秒杀订单按既有流程走完;本期不回收已分配库存。 +- 方法与路径:`POST /api/seckill-activities/{activityId}/cancel` +- operationId:`Seckill_CancelActivity` + +#### 请求 + +- Route 参数:`activityId: uuid` +- Header:`Authorization: Bearer `(必填,角色 Merchant) +- Body: + +```text +CancelSeckillActivityRequest { + reason?: string // 可选,0~200 字 +} +``` + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillActivityResponse`(`status="Cancelled"`,含 `cancelReason`) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段格式错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | +| 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | +| 409 | `SECKILL.INVALID_STATUS` | 活动已 `Finished` 或已 `Cancelled` | + +#### 业务规则与并发 + +- 条件更新:`status IN ('Draft','Scheduled','Ongoing') → 'Cancelled'`;影响行数为 0 时按 409 处理。 +- 取消时 `remainingStock` 保留为冻结状态,不自动回收到普通商品库存。 +- 已存在秒杀订单沿用 M04 状态机;C03 超时取消时回补到原秒杀库存通道。 + +#### 缓存、事件或外部依赖 + +- 删除 Redis 缓存:`cache:seckill:activity:{activityId}`、`cache:seckill:list:active`。 + +#### 验证场景 + +- `Ongoing` 活动取消 → 200,状态 `Cancelled`,抢购入口立即失效。 +- 重复取消 → 409 / `SECKILL.INVALID_STATUS`。 +- 已取消活动 → 409。 + +### A224 商家秒杀活动列表 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB042、DB043 +- 当前状态:已定义 +- 用途:商家分页查询本人维护的秒杀活动,支持按状态、时间窗口和关键词筛选。 +- 方法与路径:`GET /api/merchant/seckill-activities` +- operationId:`Seckill_ListMerchantActivities` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Merchant) +- Query 参数: + - `page`(默认 1) + - `pageSize`(默认 10,上限 50) + - `status`(可选,可多值:`Draft` / `Scheduled` / `Ongoing` / `Finished` / `Cancelled`) + - `keyword`(可选,对活动名称做模糊匹配) + - `startFrom`、`startTo`(可选,时间范围) + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillActivityListResponse` + +```text +SeckillActivityListResponse { + items: SeckillActivityResponse[] + page: integer + pageSize: integer + total: integer + totalPages: integer +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 分页或筛选参数非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家 | + +#### 业务规则与并发 + +- 严格按 `owner_merchant_id = current_user_id` 过滤;不允许查询他人活动。 +- 排序默认按 `startAt desc`;相同 `startAt` 时按 `activityId` 稳定排序。 + +#### 缓存、事件或外部依赖 + +- 不缓存商家管理端列表。 + +#### 验证场景 + +- 商家查询本人活动 → 200,按 `startAt desc` 排序。 +- 状态筛选 `Ongoing` → 仅返回进行中活动。 +- 商家访问他人活动 → 403,不泄露他人数据。 + +### A225 商家秒杀活动详情 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB042、DB043、DB044 +- 当前状态:已定义 +- 用途:商家查看本人秒杀活动详情;包含库存、已售、单用户限购、订单统计与取消原因等内部字段。 +- 方法与路径:`GET /api/seckill-activities/{activityId}` +- operationId:`Seckill_GetMerchantActivityDetail` + +#### 请求 + +- Route 参数:`activityId: uuid` +- Header:`Authorization: Bearer `(必填,角色 Merchant) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillActivityDetailResponse` + +```text +SeckillActivityDetailResponse { + activity: SeckillActivityResponse + orderStats: SeckillOrderStatsResponse + cancelReason: string? + cancelledAt: string? +} + +SeckillOrderStatsResponse { + totalOrders: integer + paidOrders: integer + cancelledOrders: integer + totalSoldAmount: number // 秒杀价 × 数量(不含退款) +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | +| 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | + +#### 业务规则与并发 + +- 严格按 `owner_merchant_id = current_user_id` 过滤;跨商家访问返回 404,避免泄露活动存在性。 +- 订单统计来自 DB044(`seckill_orders`,与 M04 `orders` 共享事实库,通过 `seckill_activity_id` 关联)。 +- `cancelReason` 与 `cancelledAt` 仅在 `status=Cancelled` 时返回。 + +#### 缓存、事件或外部依赖 + +- 不缓存;订单统计每次实时计算。 + +#### 验证场景 + +- 商家查询本人进行中活动 → 200,包含订单统计与内部字段。 +- 商家查询他人活动 → 404,不泄露归属。 +- 已取消活动 → 200,含 `cancelReason` 与 `cancelledAt`。 + +### A226 买家秒杀活动列表 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB042、DB043 +- 当前状态:已定义 +- 用途:游客和买家查看正在进行或即将开始的秒杀活动;仅返回公开字段。 +- 方法与路径:`GET /api/seckill-activities` +- operationId:`Seckill_ListActiveActivities` + +#### 请求 + +- Header:无强制要求 +- Query 参数: + - `page`(默认 1) + - `pageSize`(默认 10,上限 50) + - `window`(可选,`Ongoing` / `Upcoming` / `All`;默认 `All`,过滤已结束/已取消) + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillActivityListResponse`(仅公开字段,`status` 仅返回 `Scheduled` / `Ongoing`) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 分页或 `window` 参数非法 | + +#### 业务规则与并发 + +- 仅返回 `status IN ('Scheduled','Ongoing')` 的活动;已结束、已取消或草稿活动不公开。 +- 排序默认按 `startAt asc`(即将开始优先),相同 `startAt` 时按 `activityId` 稳定排序。 +- 公开响应中 `remainingStock` 不返回具体数字,仅返回 `isSoldOut` 布尔;具体剩余库存通过 A227 查询。 + +#### 缓存、事件或外部依赖 + +- Redis:`cache:seckill:list:active`,TTL 30 秒;活动状态变更或售罄时主动失效。 + +#### 验证场景 + +- 游客访问 → 200,仅返回进行中和即将开始的活动。 +- `window=Ongoing` → 仅返回进行中活动。 +- 已结束或已取消活动不出现。 + +### A227 买家秒杀活动详情 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB042、DB043 +- 当前状态:已定义 +- 用途:游客和买家查看秒杀活动详情;返回公开字段、商品基础信息与抢购入口。 +- 方法与路径:`GET /api/seckill-activities/{activityId}/public` +- operationId:`Seckill_GetActiveActivityDetail` + +#### 请求 + +- Route 参数:`activityId: uuid` +- Header:无强制要求 +- Body:无 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillActivityDetailResponse`(仅公开字段,`cancelReason` 等内部字段不返回) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 404 | `RESOURCE.NOT_FOUND` | 活动不存在或未公开 | +| 410 | `SECKILL.ACTIVITY_GONE` | 活动已结束或已取消 | + +#### 业务规则与并发 + +- 仅返回 `status IN ('Scheduled','Ongoing')` 的活动;其他状态返回 410。 +- 已登录买家响应额外包含 `currentBuyerOrderCount`、`currentBuyerRemaining`(用于限购提示),按 `(activity_id, buyer_id)` 实时统计。 + +#### 缓存、事件或外部依赖 + +- Redis:`cache:seckill:activity:{activityId}`,TTL 30 秒;活动状态变更或库存售罄时主动失效。 + +#### 验证场景 + +- 游客访问进行中活动 → 200,含商品基础信息、秒杀价、开始/结束时间。 +- 已结束活动 → 410 / `SECKILL.ACTIVITY_GONE`。 +- 已登录买家访问 → 额外返回当前用户已下单数量与剩余可购数量。 + +### A228 秒杀下单 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB043、DB044、DB045 +- 当前状态:已定义 +- 用途:买家抢购秒杀商品;服务端以数据库条件更新扣减秒杀库存、创建订单与秒杀订单项快照;事务保证不超卖、不少卖、不产生孤立记录。 +- 方法与路径:`POST /api/seckill-orders` +- operationId:`Seckill_PlaceOrder` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Buyer);`Idempotency-Key: `(必填,防止重复点击与网络重试) +- Body: + +```text +PlaceSeckillOrderRequest { + activityId: uuid // 必填 + quantity: integer // 必填,1 ≤ quantity ≤ perBuyerLimit + addressId: uuid // 必填,必须属于当前买家 +} +``` + +#### 成功响应 + +- HTTP 状态:`201 Created` +- Response Header:`Location: /api/seckill-orders/{orderId}` +- 响应 Schema:`PlaceSeckillOrderResponse` + +```text +PlaceSeckillOrderResponse { + orderId: uuid + activityId: uuid + quantity: integer + seckillPrice: number + totalAmount: number // seckillPrice × quantity,服务端计算 + status: "PendingPayment" + expiresAt: string // 订单支付截止时间,UTC ISO 8601 + remainingStock: integer // 扣减后剩余库存(供前端展示) +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或格式错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | +| 409 | `SECKILL.NOT_STARTED` | 活动尚未开始 | +| 409 | `SECKILL.ALREADY_ENDED` | 活动已结束 | +| 409 | `SECKILL.SOLD_OUT` | 秒杀库存售罄 | +| 409 | `SECKILL.PER_BUYER_LIMIT_EXCEEDED` | 超过单用户限购 | +| 409 | `SECKILL.QUANTITY_EXCEEDS_LIMIT` | 单次购买数量超过限购或库存 | +| 409 | `RESOURCE.CONFLICT` | 地址不存在或不归属当前买家 | +| 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品已下架 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键被用于不同请求内容 | +| 429 | `COMMON.RATE_LIMITED` | 触发限流(秒杀入口限流阈值) | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Redis 限流、库存通道或下游服务不可用 | +| 500 | `COMMON.INTERNAL_ERROR` | 未处理的服务端错误 | + +#### 业务规则与并发 + +- 秒杀下单为高风险操作,必须使用 `Idempotency-Key`;缺失时返回 400 / `COMMON.VALIDATION_FAILED`。 +- 同一数据库事务内顺序: + 1. 按 `UPDATE seckill_inventory SET remaining = remaining - :qty, sold = sold + :qty, updated_at = now() WHERE activity_id = :aid AND status='Ongoing' AND start_at <= now() AND end_at > now() AND remaining >= :qty` 条件扣减秒杀库存;影响行数为 0 时整体事务回滚。 + 2. 校验 `(activity_id, buyer_id)` 维度已下单数量(含 `PendingPayment`、`Paid`、`Cancelled`)+ 本次 `quantity` 不超过 `perBuyerLimit`;超出时事务回滚。 + 3. 写入 `seckill_orders`(DB044,`orderId = order.id`)与 `seckill_order_items`(DB045,含 `seckillPrice` 快照与 `originalPrice`)。 + 4. 写入 Outbox `SeckillOrderCreated` 事件。 +- 不写入普通商品库存;`products.stock` 不受秒杀下单影响。 +- 失败优先级:限流 429 < 未开始 / 已结束 409 < 售罄 409 < 超过单用户限购 409 < 幂等键复用 409 < 业务异常 5xx。 + +#### 缓存、事件或外部依赖 + +- Redis:`lock:seckill:order:{activityId}`(细粒度互斥,避免活动行成为热点)、`cache:seckill:activity:{activityId}`(事务成功后失效)、`cart:idempotency:{userId}:{key}`(幂等记录,TTL 24 小时)。 +- Outbox:`SeckillOrderCreated`,由 M09 站内消息与 C03 超时取消消费。 + +#### 验证场景 + +- 100 并发抢 10 件库存、单用户限购 1 → 恰好 10 笔成功订单,其余 90 笔以 `SOLD_OUT` 或 `PER_BUYER_LIMIT_EXCEEDED` 失败;库存 `remaining=0`、`sold=10`。 +- 同一买家两次提交限购 1 的活动 → 第二次返回 `PER_BUYER_LIMIT_EXCEEDED`,不重复扣减。 +- 同一幂等键重复提交 → 第二次返回首次成功订单号,不重复扣减。 +- 活动未开始 → 409 / `SECKILL.NOT_STARTED`。 +- 活动已结束 → 409 / `SECKILL.ALREADY_ENDED`。 +- 商品已下架 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 +- 地址不属于当前买家 → 409 / `RESOURCE.CONFLICT`,不泄露地址存在性。 + +### A229 买家秒杀订单列表 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB044、DB045 +- 当前状态:已定义 +- 用途:买家分页查询本人秒杀订单,支持按状态、活动和时间筛选。 +- 方法与路径:`GET /api/seckill-orders` +- operationId:`Seckill_ListMyOrders` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Buyer) +- Query 参数: + - `page`(默认 1) + - `pageSize`(默认 10,上限 50) + - `status`(可选,`PendingPayment` / `Paid` / `Shipped` / `Completed` / `Cancelled`) + - `activityId`(可选,按活动过滤) + - `createdFrom`、`createdTo`(可选,时间范围) + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillOrderListResponse` + +```text +SeckillOrderListResponse { + items: SeckillOrderSummaryResponse[] + page: integer + pageSize: integer + total: integer + totalPages: integer +} + +SeckillOrderSummaryResponse { + orderId: uuid + activityId: uuid + activityName: string + productId: uuid + productName: string + productImageUrl: string + quantity: integer + seckillPrice: number + totalAmount: number + status: "PendingPayment" | "Paid" | "Shipped" | "Completed" | "Cancelled" + createdAt: string + expiresAt: string // PendingPayment 时返回 +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 分页或筛选参数非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 严格按 `buyer_id = current_user_id AND seckill_activity_id IS NOT NULL` 过滤。 +- 排序默认按 `createdAt desc`;相同 `createdAt` 时按 `orderId` 稳定排序。 +- 不返回完整地址或支付敏感信息;详细快照在 A230。 + +#### 缓存、事件或外部依赖 + +- 不缓存;订单状态实时读取 DB044。 + +#### 验证场景 + +- 买家查询本人秒杀订单 → 200,仅返回与当前买家关联的记录。 +- 按活动过滤 → 200,仅返回该活动的订单。 +- 跨用户查询 → 403,不泄露他人订单。 + +### A230 买家秒杀订单详情 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB044、DB045 +- 当前状态:已定义 +- 用途:买家查看本人秒杀订单完整详情;包含活动快照、订单项快照、地址快照与状态时间线。 +- 方法与路径:`GET /api/seckill-orders/{orderId}` +- operationId:`Seckill_GetMyOrder` + +#### 请求 + +- Route 参数:`orderId: uuid` +- Header:`Authorization: Bearer `(必填,角色 Buyer) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillOrderDetailResponse` + +```text +SeckillOrderDetailResponse { + orderId: uuid + activityId: uuid + activityName: string + productId: uuid + productName: string + productImageUrl: string + quantity: integer + seckillPrice: number + originalPrice: number // 商品原价快照 + totalAmount: number + status: "PendingPayment" | "Paid" | "Shipped" | "Completed" | "Cancelled" + addressSnapshot: AddressSnapshotResponse + timeline: OrderTimelineEntryResponse[] + paymentInfo: PaymentInfoResponse? + createdAt: string + expiresAt: string + paidAt: string? + cancelledAt: string? +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 404 | `RESOURCE.NOT_FOUND` | 订单不存在、不属于当前用户或非秒杀订单 | + +#### 业务规则与并发 + +- 严格按 `order_id = :id AND buyer_id = current_user_id AND seckill_activity_id IS NOT NULL` 过滤;不满足任一条件返回 404,避免泄露订单存在性。 +- 地址快照来自下单时刻保存的 `orders.address_snapshot`,与 M04 共享字段。 +- 时间线包含创建、支付、发货、完成、取消等关键节点;时间均以 UTC 存储,前端按本地时区展示。 +- 支付信息(`paymentInfo`)仅在订单已支付后返回;支付卡号、Token 等敏感字段不出现。 + +#### 缓存、事件或外部依赖 + +- 不缓存;订单详情实时读取 DB044、DB045 与 M04 `orders`、`order_items`、`payments`。 + +#### 验证场景 + +- 买家查询本人秒杀订单 → 200,含活动快照、订单项快照、地址快照、时间线。 +- 跨用户访问 → 404,不泄露归属。 +- 已支付订单 → `paymentInfo` 返回;未支付订单不返回。 +- 已取消订单 → `cancelledAt` 与取消节点返回。 +- 普通订单(非秒杀)通过此接口访问 → 404,避免与 M04 详情接口混淆。 \ No newline at end of file -- Gitee From 72365623cc0afbc99e15e6cc65e79b021f5ba71b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9F=A6=E4=B9=BE=E5=BC=BA?= <3195306445@qq.com> Date: Fri, 24 Jul 2026 09:42:30 +0800 Subject: [PATCH 044/118] =?UTF-8?q?docs(interface):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E6=A8=A1=E5=9D=97=E6=8E=A5=E5=8F=A3=E8=AF=A6?= =?UTF-8?q?=E7=BB=86=E5=AE=9A=E4=B9=89=20A301-A307?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interface-wqq.md" | 621 ++++++++++++++++++ 1 file changed, 621 insertions(+) create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-wqq.md" diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-wqq.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-wqq.md" new file mode 100644 index 0000000..5b3f6c4 --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-wqq.md" @@ -0,0 +1,621 @@ +# 韦乾强 - 订单模块接口详细定义 + +> 负责人:韦乾强 +> 模块:Ordering(订单模块)、Merchant后台订单管理 +> 接口编号范围:A301~A307 +> 编写日期:2026-07-24 + +## 接口清单 + +| 编号 | 模块 | 需求编号 | 名称 | 方法 | 路径 | operationId | 请求Schema | 响应Schema | 鉴权 | 关联DB | 状态 | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| A301 | Ordering | F08 | 提交订单 | POST | /api/orders | Ordering_CreateOrder | CreateOrderRequest | CreateOrderResponse | BuyerOnly | DB001,DB003 | 部分定义 | +| A302 | Ordering | F09 | 查询订单列表 | GET | /api/orders | Ordering_GetOrders | - | OrderListResponse | BuyerOnly | DB001 | 部分定义 | +| A303 | Ordering | F09 | 查询订单详情 | GET | /api/orders/{orderId} | Ordering_GetOrderById | - | OrderDetailResponse | BuyerOnly | DB001,DB003 | 部分定义 | +| A304 | Ordering | F09 | 取消订单 | POST | /api/orders/{orderId}/cancel | Ordering_CancelOrder | - | CancelOrderResponse | BuyerOnly | DB001,DB003 | 部分定义 | +| A305 | Merchant | F12 | 商家查询订单列表 | GET | /api/merchant/orders | Merchant_GetOrders | - | MerchantOrderListResponse | MerchantOnly | DB001,DB003 | 部分定义 | +| A306 | Merchant | F12 | 商家查询订单详情 | GET | /api/merchant/orders/{orderId} | Merchant_GetOrderById | - | MerchantOrderDetailResponse | MerchantOnly | DB001,DB003 | 部分定义 | +| A307 | Merchant | F12 | 商家发货 | POST | /api/merchant/orders/{orderId}/ship | Merchant_ShipOrder | ShipOrderRequest | ShipOrderResponse | MerchantOnly | DB001 | 部分定义 | + +--- + +## A301 提交订单 + +- **模块 / Tag**:Ordering +- **需求编号**:F08 +- **负责人**:韦乾强 +- **关联数据表**:DB001(orders)、DB003(order_items) +- **当前状态**:部分定义 +- **用途**:买家选择购物车商品和收货地址提交订单,系统原子扣减库存并返回订单号 +- **方法与路径**:`POST /api/orders` +- **operationId**:`Ordering_CreateOrder` +- **请求Schema**:`CreateOrderRequest` +- **响应Schema**:`CreateOrderResponse` +- **身份与Policy**:BuyerOnly +- **资源归属**:订单归属于当前登录买家 +- **幂等要求**:客户端生成幂等键 `Idempotency-Key`,服务端以 `(buyerId, idempotencyKey)` 保证幂等 + +### 请求 + +- **Route参数**:无 +- **Query参数**:无 +- **Header**: + - `Authorization: Bearer `(必需) + - `Idempotency-Key: `(必需) + - `Content-Type: application/json` +- **Body**: +```json +{ + "addressId": "uuid", + "cartItemIds": ["uuid"], + "idempotencyKey": "uuid" +} +``` +- **校验规则**: + - `addressId`:必填,UUID格式,必须属于当前买家 + - `cartItemIds`:必填,非空数组,每个元素为UUID格式 + - `idempotencyKey`:必填,UUID格式 + +### 成功响应 + +- **HTTP状态**:`201 Created` +- **响应Schema**:`CreateOrderResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "orderId": "3f0ed9a9-3c61-4ab6-a8dd-a54ea8dd78af", + "orderNo": "ORD20260724001", + "totalAmount": 299.00, + "status": "PendingPayment", + "createdAt": "2026-07-24T10:00:00Z" + } +} +``` + +### 失败响应 + +| HTTP状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | ORDER.INVALID_PARAM | 参数格式错误 | +| 400 | ORDER.EMPTY_CART_ITEMS | 购物车商品列表为空 | +| 400 | ORDER.INVALID_ADDRESS | 收货地址无效或不归属当前用户 | +| 409 | ORDER.STOCK_INSUFFICIENT | 商品库存不足 | +| 409 | ORDER.ITEM_NOT_AVAILABLE | 商品已下架或不可售 | +| 409 | ORDER.IDEMPOTENT_CONFLICT | 幂等键重复,返回原订单 | + +### 业务规则与并发 + +1. 同一幂等键只创建一张订单,重复请求返回首次成功结果 +2. 库存扣减使用条件更新 `WHERE stock >= quantity`,避免超卖 +3. 订单金额由服务端计算,不接受客户端传入 +4. 订单项保存商品名称、图片、单价快照 + +### 缓存、事件或外部依赖 + +- 发布 `OrderCreatedEvent` 到 Outbox +- 依赖 DB001(orders)、DB003(order_items)、DB004(products) + +### 验证场景 + +1. 正常提交订单:返回201,订单号 +2. 库存不足:返回409,订单未创建 +3. 地址无效:返回400 +4. 幂等键重复:返回原订单号,不重复扣库存 + +--- + +## A302 查询订单列表 + +- **模块 / Tag**:Ordering +- **需求编号**:F09 +- **负责人**:韦乾强 +- **关联数据表**:DB001(orders) +- **当前状态**:部分定义 +- **用途**:买家分页查询自己的订单列表,支持按状态筛选 +- **方法与路径**:`GET /api/orders` +- **operationId**:`Ordering_GetOrders` +- **请求Schema**:无 +- **响应Schema**:`OrderListResponse` +- **身份与Policy**:BuyerOnly +- **资源归属**:只返回当前买家订单 +- **幂等要求**:GET请求天然幂等 + +### 请求 + +- **Route参数**:无 +- **Query参数**: + - `page`(可选,默认1):页码 + - `pageSize`(可选,默认10,上限50):每页条数 + - `status`(可选):筛选订单状态,`PendingPayment`/`Paid`/`Shipped`/`Completed`/`Cancelled` +- **Header**:`Authorization: Bearer `(必需) +- **Body**:无 + +### 成功响应 + +- **HTTP状态**:`200 OK` +- **响应Schema**:`OrderListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "orderId": "uuid", + "orderNo": "ORD20260724001", + "status": "PendingPayment", + "totalAmount": 299.00, + "createdAt": "2026-07-24T10:00:00Z", + "itemSummary": "商品A x1,商品B x2" + } + ], + "page": 1, + "pageSize": 10, + "totalCount": 25, + "totalPages": 3 + } +} +``` + +### 失败响应 + +| HTTP状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | ORDER.INVALID_PAGE_PARAM | 分页参数非法 | + +### 业务规则与并发 + +1. 订单按创建时间倒序排列 +2. `itemSummary`最多展示3个商品名称,多的显示"+X件" + +### 缓存、事件或外部依赖 + +无 + +### 验证场景 + +1. 正常查询:返回订单列表 +2. 分页参数非法:返回400 +3. 无订单:返回空列表 + +--- + +## A303 查询订单详情 + +- **模块 / Tag**:Ordering +- **需求编号**:F09 +- **负责人**:韦乾强 +- **关联数据表**:DB001(orders)、DB003(order_items) +- **当前状态**:部分定义 +- **用途**:买家查看单个订单的完整详情 +- **方法与路径**:`GET /api/orders/{orderId}` +- **operationId**:`Ordering_GetOrderById` +- **请求Schema**:无 +- **响应Schema**:`OrderDetailResponse` +- **身份与Policy**:BuyerOnly +- **资源归属**:订单必须属于当前买家 +- **幂等要求**:GET请求天然幂等 + +### 请求 + +- **Route参数**:`orderId`(必需,UUID) +- **Query参数**:无 +- **Header**:`Authorization: Bearer `(必需) +- **Body**:无 + +### 成功响应 + +- **HTTP状态**:`200 OK` +- **响应Schema**:`OrderDetailResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "orderId": "uuid", + "orderNo": "ORD20260724001", + "status": "PendingPayment", + "totalAmount": 299.00, + "createdAt": "2026-07-24T10:00:00Z", + "addressSnapshot": { + "receiverName": "张三", + "phone": "138****8888", + "province": "广东省", + "city": "深圳市", + "district": "南山区", + "detailAddress": "科技园路1号" + }, + "items": [ + { + "productId": "uuid", + "productName": "商品A", + "imageUrl": "https://...", + "unitPrice": 199.00, + "quantity": 1, + "subtotal": 199.00 + } + ], + "statusHistory": [ + {"status": "PendingPayment", "time": "2026-07-24T10:00:00Z"}, + {"status": "Paid", "time": "2026-07-24T10:05:00Z"} + ], + "availableActions": ["cancel"] + } +} +``` + +### 失败响应 + +| HTTP状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 404 | ORDER.NOT_FOUND | 订单不存在 | +| 403 | ORDER.ACCESS_DENIED | 订单不属于当前用户 | + +### 业务规则与并发 + +1. 订单项为快照,包含下单时的商品名称、图片、单价 +2. 地址为快照,包含下单时的收货信息 +3. `availableActions`根据当前状态展示可执行操作 + +### 缓存、事件或外部依赖 + +无 + +### 验证场景 + +1. 正常查询:返回完整订单详情 +2. 订单不存在:返回404 +3. 跨用户访问:返回403 + +--- + +## A304 取消订单 + +- **模块 / Tag**:Ordering +- **需求编号**:F09 +- **负责人**:韦乾强 +- **关联数据表**:DB001(orders)、DB003(order_items)、DB004(products) +- **当前状态**:部分定义 +- **用途**:买家取消自己待支付的订单,触发库存回补 +- **方法与路径**:`POST /api/orders/{orderId}/cancel` +- **operationId**:`Ordering_CancelOrder` +- **请求Schema**:无 +- **响应Schema**:`CancelOrderResponse` +- **身份与Policy**:BuyerOnly +- **资源归属**:订单必须属于当前买家 +- **幂等要求**:以订单号为幂等键,重复取消返回成功 + +### 请求 + +- **Route参数**:`orderId`(必需,UUID) +- **Query参数**:无 +- **Header**:`Authorization: Bearer `(必需) +- **Body**:无 + +### 成功响应 + +- **HTTP状态**:`200 OK` +- **响应Schema**:`CancelOrderResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "orderId": "uuid", + "status": "Cancelled", + "cancelledAt": "2026-07-24T11:00:00Z", + "cancelReason": "BUYER_CANCELLED" + } +} +``` + +### 失败响应 + +| HTTP状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 404 | ORDER.NOT_FOUND | 订单不存在 | +| 403 | ORDER.ACCESS_DENIED | 订单不属于当前用户 | +| 409 | ORDER.INVALID_STATUS | 订单状态不允许取消(已支付/已发货/已完成/已取消) | + +### 业务规则与并发 + +1. 只有 `PendingPayment` 状态可取消 +2. 取消与库存回补在同一事务内完成 +3. 使用条件更新 `WHERE status = 'PendingPayment'` 保证幂等 +4. `cancelReason` 记录为 `BUYER_CANCELLED` + +### 缓存、事件或外部依赖 + +- 发布 `OrderCancelledEvent` 到 Outbox +- 库存回补操作 DB004(products) + +### 验证场景 + +1. 正常取消:返回成功,库存回补 +2. 重复取消:返回幂等成功 +3. 订单已支付:返回409 +4. 跨用户取消:返回403 + +--- + +## A305 商家查询订单列表 + +- **模块 / Tag**:Merchant +- **需求编号**:F12 +- **负责人**:韦乾强 +- **关联数据表**:DB001(orders)、DB003(order_items) +- **当前状态**:部分定义 +- **用途**:商家分页查询本店订单,支持按状态筛选 +- **方法与路径**:`GET /api/merchant/orders` +- **operationId**:`Merchant_GetOrders` +- **请求Schema**:无 +- **响应Schema**:`MerchantOrderListResponse` +- **身份与Policy**:MerchantOnly +- **资源归属**:只返回当前商家的订单 +- **幂等要求**:GET请求天然幂等 + +### 请求 + +- **Route参数**:无 +- **Query参数**: + - `page`(可选,默认1):页码 + - `pageSize`(可选,默认10,上限50):每页条数 + - `status`(可选):筛选订单状态 +- **Header**:`Authorization: Bearer `(必需) +- **Body**:无 + +### 成功响应 + +- **HTTP状态**:`200 OK` +- **响应Schema**:`MerchantOrderListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "orderId": "uuid", + "orderNo": "ORD20260724001", + "buyerUsername": "user123", + "status": "Paid", + "totalAmount": 299.00, + "createdAt": "2026-07-24T10:00:00Z", + "itemCount": 2 + } + ], + "page": 1, + "pageSize": 10, + "totalCount": 15, + "totalPages": 2 + } +} +``` + +### 失败响应 + +| HTTP状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | ORDER.INVALID_PAGE_PARAM | 分页参数非法 | + +### 业务规则与并发 + +1. 只返回与当前商家商品相关的订单 +2. 订单按创建时间倒序排列 + +### 缓存、事件或外部依赖 + +无 + +### 验证场景 + +1. 正常查询:返回订单列表 +2. 无订单:返回空列表 + +--- + +## A306 商家查询订单详情 + +- **模块 / Tag**:Merchant +- **需求编号**:F12 +- **负责人**:韦乾强 +- **关联数据表**:DB001(orders)、DB003(order_items) +- **当前状态**:部分定义 +- **用途**:商家查看本店订单的完整详情 +- **方法与路径**:`GET /api/merchant/orders/{orderId}` +- **operationId**:`Merchant_GetOrderById` +- **请求Schema**:无 +- **响应Schema**:`MerchantOrderDetailResponse` +- **身份与Policy**:MerchantOnly +- **资源归属**:订单必须属于当前商家的商品 +- **幂等要求**:GET请求天然幂等 + +### 请求 + +- **Route参数**:`orderId`(必需,UUID) +- **Query参数**:无 +- **Header**:`Authorization: Bearer `(必需) +- **Body**:无 + +### 成功响应 + +- **HTTP状态**:`200 OK` +- **响应Schema**:`MerchantOrderDetailResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "orderId": "uuid", + "orderNo": "ORD20260724001", + "buyerUsername": "user123", + "status": "Paid", + "totalAmount": 299.00, + "createdAt": "2026-07-24T10:00:00Z", + "paidAt": "2026-07-24T10:05:00Z", + "addressSnapshot": { + "receiverName": "张三", + "phone": "138****8888", + "province": "广东省", + "city": "深圳市", + "district": "南山区", + "detailAddress": "科技园路1号" + }, + "items": [ + { + "productId": "uuid", + "productName": "商品A", + "imageUrl": "https://...", + "unitPrice": 199.00, + "quantity": 1, + "subtotal": 199.00 + } + ], + "availableActions": ["ship"] + } +} +``` + +### 失败响应 + +| HTTP状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 404 | ORDER.NOT_FOUND | 订单不存在 | +| 403 | ORDER.ACCESS_DENIED | 订单不归属当前商家 | + +### 业务规则与并发 + +1. 只返回与当前商家商品相关的订单项 +2. `availableActions`根据当前状态展示可执行操作 + +### 缓存、事件或外部依赖 + +无 + +### 验证场景 + +1. 正常查询:返回完整订单详情 +2. 订单不存在:返回404 +3. 跨商家访问:返回403 + +--- + +## A307 商家发货 + +- **模块 / Tag**:Merchant +- **需求编号**:F12 +- **负责人**:韦乾强 +- **关联数据表**:DB001(orders) +- **当前状态**:部分定义 +- **用途**:商家对已支付订单执行发货操作 +- **方法与路径**:`POST /api/merchant/orders/{orderId}/ship` +- **operationId**:`Merchant_ShipOrder` +- **请求Schema**:`ShipOrderRequest` +- **响应Schema**:`ShipOrderResponse` +- **身份与Policy**:MerchantOnly +- **资源归属**:订单必须属于当前商家的商品 +- **幂等要求**:以订单号为幂等键,重复发货返回成功 + +### 请求 + +- **Route参数**:`orderId`(必需,UUID) +- **Query参数**:无 +- **Header**:`Authorization: Bearer `(必需) +- **Body**: +```json +{ + "expressCompany": "顺丰速运", + "trackingNo": "SF1234567890" +} +``` +- **校验规则**: + - `expressCompany`:必填,1-50字符 + - `trackingNo`:必填,1-50字符 + +### 成功响应 + +- **HTTP状态**:`200 OK` +- **响应Schema**:`ShipOrderResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "orderId": "uuid", + "status": "Shipped", + "shippedAt": "2026-07-24T12:00:00Z", + "expressCompany": "顺丰速运", + "trackingNo": "SF1234567890" + } +} +``` + +### 失败响应 + +| HTTP状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 404 | ORDER.NOT_FOUND | 订单不存在 | +| 403 | ORDER.ACCESS_DENIED | 订单不归属当前商家 | +| 409 | ORDER.INVALID_STATUS | 订单状态不允许发货(只有已支付可发货) | + +### 业务规则与并发 + +1. 只有 `Paid` 状态可发货 +2. 使用条件更新 `WHERE status = 'Paid'` 保证幂等 +3. 记录发货时间、物流公司和物流单号 + +### 缓存、事件或外部依赖 + +- 发布 `OrderShippedEvent` 到 Outbox + +### 验证场景 + +1. 正常发货:返回成功,状态变为Shipped +2. 重复发货:返回幂等成功 +3. 订单未支付:返回409 +4. 跨商家发货:返回403 + +--- + +## C03 订单超时自动取消(Worker接口) + +> **说明**:C03订单超时自动取消由Worker后台任务执行,不对外提供HTTP API。接口设计记录其与外部系统的交互关系。 + +### 业务规则 + +1. **超时时间配置**:订单超时时间通过配置项 `OrderTimeoutMinutes` 管理,默认30分钟 +2. **扫描策略**:Worker定时扫描 `PendingPayment` 状态且 `created_at + timeout < now()` 的订单 +3. **取消事务**:在同一事务内完成状态变更 `PendingPayment → Cancelled`、库存回补、写入 `cancelled_at` 和 `cancel_reason = 'TIMEOUT'` +4. **幂等保证**:使用条件更新 `WHERE status = 'PendingPayment'`,同一订单多次扫描只有一次成功 +5. **支付竞争**:与M05支付并发时,条件更新确保只有一个成功 + +### 事件消费 + +- 消费 `OrderCreatedEvent`(由M04-01发布)触发后续超时跟踪 + +### 事件发布 + +- 发布 `OrderCancelledEvent`(`cancel_reason = 'TIMEOUT'`)到Outbox,供给M09站内消息 + +### 关键实现点 + +1. 扫描间隔建议 ≤ 超时时间/2 +2. 每批次处理上限100条,避免长时间锁表 +3. 失败重试3次后告警,订单保留待处理状态 +4. 多实例Worker使用 `SELECT FOR UPDATE SKIP LOCKED` 避免重复处理 + +### 验证场景 + +1. 超时订单被自动取消,库存回补 +2. 买家在超时前支付成功,取消被跳过 +3. 并发取消与支付只有一个成功 +4. Worker重启后继续扫描,不漏扫 -- Gitee From 8a5d6155f113bb6bded49ac186217db736e4c031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=B5=B7=E6=B4=8B?= <2040454617@qq.com> Date: Fri, 24 Jul 2026 09:57:44 +0800 Subject: [PATCH 045/118] docs(interface): add A401-A425 Payment + AfterSales API contract draft MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 25 个接口详细定义(M05-01 模拟支付 A401-A408 + M10 售后 A411-A419 + M10 退款 A431-A433 + C08 回调与对账 A421-A425) - 按接口设计.md 1.20 模板逐项填写(13 项必填字段 + 请求/响应/失败/验证场景) - 9 个接口强制 Idempotency-Key(A402/A405/A412/A415/A416/A417/A419/A421/A425/A431) - 27 个稳定业务错误码(PAYMENT./AFTER_SALES./RECONCILIATION./AUTH./RESOURCE./COMMON./IDEMPOTENCY.) - 关联数据表标注 DB081-DB100(待 database-zhy.md 评审) Refs: M05-01 / M10 / C08 Scope: payment + after-sales --- .../interface-zhy.md" | 2181 +++++++++++++++++ 1 file changed, 2181 insertions(+) create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhy.md" diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhy.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhy.md" new file mode 100644 index 0000000..563c83a --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhy.md" @@ -0,0 +1,2181 @@ +# 张海洋个人接口文件(A401-A500) + +> **模块**:Payment / AfterSales / Reconciliation +> **负责人**:张海洋(zhy) +> **范围**:M05-01 模拟支付 + M10 售后流程 + C08 支付回调幂等与对账 +> **创建日期**:2026-07-24 +> **当前状态**:个人协作评审中(未入主文档,由 zhy 维护;评审通过后由罗皓晨汇总到 `接口设计.md`) +> **关联规范**:`docs/02-设计文档/接口设计.md` v0.1 + `docs/02-设计文档/命名规范.md` + `docs/02-设计文档/Git团队协作流程.md` +> **关联根命名空间**:`Mall.Modules.Payment`、`Mall.Modules.AfterSales` +> **关联数据库**:DB081~DB100(**待评审**:`database-zhy.md` 尚未创建,本文档字段暂时按命名规范推断) + +--- + +## 0. 阅读须知 + +1. 每个接口按《接口设计.md》1.20 节模板逐项填写。 +2. 所有路径遵循 `/api//...`,小写 + 复数 + kebab-case,多个单词用连字符。 +3. JSON 字段统一 `camelCase`;Schema 名称统一 `PascalCase` + 用途后缀。 +4. `operationId` 统一 `_`,全小写 PascalCase 拼接。 +5. 业务错误码格式:`PAYMENT.` / `AFTER_SALES.` / `RECONCILIATION.`,全大写下划线。 +6. 涉及资金、状态、回调的接口强制 `Idempotency-Key`(按 1.12.1)。 +7. 字段同时承担数据库来源的,在"关联数据表"标注推断;正式评审以 `database-zhy.md` 为准。 + +--- + +## 1. 接口清单 + +### 1.1 M05-01 模拟支付(A401-A408) + +| 编号 | operationId | 方法 | 路径 | 需求 | Tag | 鉴权 | 幂等 | +|---|---|---|---|---|---|---|---| +| A401 | `Payment_GetWalletBalance` | GET | `/api/payment/wallet` | M05-01-FR01 | Payment | BuyerOnly | 否 | +| A402 | `Payment_CreateTopup` | POST | `/api/payment/wallet/topups` | M05-01-FR02/FR03 | Payment | BuyerOnly | 是 | +| A403 | `Payment_ListTopups` | GET | `/api/payment/wallet/topups` | M05-01-FR04 | Payment | BuyerOnly | 否 | +| A404 | `Payment_GetCheckout` | GET | `/api/payment/checkout/{orderId}` | M05-01-FR05 | Payment | BuyerOnly | 否 | +| A405 | `Payment_PayOrder` | POST | `/api/payment/orders/{orderId}/pay` | M05-01-FR05~FR09 | Payment | BuyerOnly | 是 | +| A406 | `Payment_GetPaymentByOrder` | GET | `/api/payment/orders/{orderId}` | M05-01-FR09 | Payment | BuyerOnly | 否 | +| A407 | `Payment_ListPayments` | GET | `/api/payments` | M05-01-FR07 | Payment | BuyerOnly | 否 | +| A408 | `Payment_GetPayment` | GET | `/api/payments/{paymentId}` | M05-01-FR07 | Payment | BuyerOnly | 否 | + +### 1.2 M10 售后流程(A411-A419) + +| 编号 | operationId | 方法 | 路径 | 需求 | Tag | 鉴权 | 幂等 | +|---|---|---|---|---|---|---|---| +| A411 | `AfterSales_CheckEligibility` | GET | `/api/after-sales/eligibility` | M10-FR01 | AfterSales | BuyerOnly | 否 | +| A412 | `AfterSales_CreateRequest` | POST | `/api/after-sales/requests` | M10-FR02 | AfterSales | BuyerOnly | 是 | +| A413 | `AfterSales_ListRequests` | GET | `/api/after-sales/requests` | M10-FR03 | AfterSales | BuyerOnly/MerchantOnly | 否 | +| A414 | `AfterSales_GetRequest` | GET | `/api/after-sales/requests/{requestId}` | M10-FR04 | AfterSales | BuyerOnly/MerchantOnly | 否 | +| A415 | `AfterSales_CancelRequest` | POST | `/api/after-sales/requests/{requestId}/cancel` | M10-FR10 | AfterSales | BuyerOnly | 是 | +| A416 | `AfterSales_AuditRequest` | POST | `/api/after-sales/requests/{requestId}/audit` | M10-FR05 | AfterSales | MerchantOnly | 是 | +| A417 | `AfterSales_ConfirmReturn` | POST | `/api/after-sales/requests/{requestId}/confirm-return` | M10-FR11 | AfterSales | MerchantOnly | 是 | +| A418 | `AfterSales_ListAuditLogs` | GET | `/api/after-sales/requests/{requestId}/audit-logs` | M10-FR04 | AfterSales | BuyerOnly/MerchantOnly | 否 | +| A419 | `AfterSales_RetryRefund` | POST | `/api/after-sales/requests/{requestId}/retry-refund` | M10-FR07 | AfterSales | MerchantOnly | 是 | + +### 1.3 C08 支付回调与对账(A421-A425) + +| 编号 | operationId | 方法 | 路径 | 需求 | Tag | 鉴权 | 幂等 | +|---|---|---|---|---|---|---|---| +| A421 | `Payment_ReceiveCallback` | POST | `/api/payment/callbacks` | C08-FR01~FR05 | Payment | Service(模拟渠道) | 是 | +| A422 | `Reconciliation_ListBatches` | GET | `/api/admin/reconciliation/batches` | C08-FR06 | Reconciliation | AdminOnly | 否 | +| A423 | `Reconciliation_GetBatch` | GET | `/api/admin/reconciliation/batches/{batchId}` | C08-FR06 | Reconciliation | AdminOnly | 否 | +| A424 | `Reconciliation_ListDifferences` | GET | `/api/admin/reconciliation/batches/{batchId}/differences` | C08-FR07/FR08 | Reconciliation | AdminOnly | 否 | +| A425 | `Reconciliation_ProcessDifference` | POST | `/api/admin/reconciliation/differences/{differenceId}/process` | C08-FR08 | Reconciliation | AdminOnly | 是 | + +### 1.4 M10 退款入账(A431-A433) + +| 编号 | operationId | 方法 | 路径 | 需求 | Tag | 鉴权 | 幂等 | +|---|---|---|---|---|---|---|---| +| A431 | `Refund_Create` | POST | `/api/after-sales/requests/{requestId}/refund` | M10-FR07 | Payment | MerchantOnly(系统内部) | 是 | +| A432 | `Refund_Get` | GET | `/api/refunds/{refundId}` | M10-FR04 | Payment | BuyerOnly/MerchantOnly | 否 | +| A433 | `Refund_List` | GET | `/api/refunds` | M10-FR03 | Payment | BuyerOnly/MerchantOnly | 否 | + +> **A431 触发说明**:A431 实际由 AfterSales 审核通过后系统内部调用(来源 A416),不属于买家/商家直接调用的接口;保留在 Payment 区间因关联交易事项本质是钱包入账。 + +--- + +## 2. 详细定义 + +### A401 查询钱包余额 + +- **模块 / Tag**:Payment +- **需求编号**:M05-01-FR01 +- **负责人**:张海洋 +- **关联数据表**:DB081(待评审)— `wallets` +- **当前状态**:已设计 +- **用途**:查询当前买家钱包余额 +- **方法与路径**:`GET /api/payment/wallet` +- **operationId**:`Payment_GetWalletBalance` +- **请求 Schema**:(无) +- **响应 Schema**:`WalletBalanceResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 钱包 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**:`currency`(可选,默认 `CNY`) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - JWT 有效、账号状态正常、令牌版本未过期 + - 钱包不存在时按需初始化(业务策略可由实现层决定,本接口约定返回余额 0) + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`WalletBalanceResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "walletId": "f5c2a8b9-3c61-4ab6-a8dd-a54ea8dd78af", + "balance": 100.50, + "currency": "CNY", + "updatedAt": "2026-07-23T08:30:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少或格式错误的 JWT | +| 401 | `AUTH.TOKEN_EXPIRED` | JWT 已过期 | +| 401 | `AUTH.TOKEN_REVOKED` | JWT 已撤销或账号令牌版本失效 | +| 403 | `AUTH.FORBIDDEN` | 当前角色非 Buyer | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 数据库不可用 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 仅返回当前 buyerId 钱包;商家/管理员无访问权限(按 1.6.2 资源归属规则) +- 余额以 PostgreSQL 实时值为准,不使用 Redis 缓存 +- 不返回钱包创建时间、内部审计字段 + +#### 缓存、事件或外部依赖 + +- 缓存:默认不缓存(私人数据按 1.13) +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:JWT 有效 → 返回当前余额 +- 异常:JWT 过期 → 401 + `AUTH.TOKEN_EXPIRED` +- 异常:商家账号调用 → 403 + `AUTH.FORBIDDEN` + +--- + +### A402 模拟充值 + +- **模块 / Tag**:Payment +- **需求编号**:M05-01-FR02 / FR03 +- **负责人**:张海洋 +- **关联数据表**:DB082(待评审)— `wallet_topups`、DB083(待评审)— `wallet_ledgers` +- **当前状态**:已设计 +- **用途**:买家向本人钱包充值(使用模拟支付通道) +- **方法与路径**:`POST /api/payment/wallet/topups` +- **operationId**:`Payment_CreateTopup` +- **请求 Schema**:`CreateTopupRequest` +- **响应 Schema**:`TopupDetailResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 钱包 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 模拟充值) + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "amount": 100.50, + "channelNote": "MOCK_TOPUP" +} +``` +- **校验规则**: + - `amount` 必填,decimal,最多 2 位小数,`> 0` 且 `≤ 10000.00`(`PAYMENT.TOPUP_EXCEEDS_LIMIT`) + - `channelNote` 选填,默认 `MOCK_TOPUP`,仅作观测标识 + - `Idempotency-Key` 必填,UUID 格式;相同 buyerId + 相同 Key + 相同 amount → 返回首次结果 + - 同一 Key 不同 amount → 409 + `IDEMPOTENCY.KEY_REUSED` + +#### 成功响应 + +- **HTTP 状态**:`201 Created` +- **响应 Schema**:`TopupDetailResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "topupId": "c7a1d4e6-...", + "walletId": "f5c2a8b9-...", + "amount": 100.50, + "currency": "CNY", + "status": "Succeeded", + "createdAt": "2026-07-23T08:30:00Z", + "succeededAt": "2026-07-23T08:30:01Z", + "newBalance": 200.50 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段格式错误或 0/负数 | +| 400 | `PAYMENT.TOPUP_EXCEEDS_LIMIT` | 单笔金额 > 10000.00 或小数 > 2 位 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同金额 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 数据库不可用 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 钱包余额增加 + 钱包流水写入同一事务(按 PAY-R06) +- 幂等键级别唯一约束存于 DB082;命中直接返回首次成功结果 +- 单笔上限 10000.00 元(业务规则 PAY-R16,zhy 7-23 提交 db840e4 强调) +- 充值成功后才更新余额;不为重试创建多条 `wallet_ledgers` + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:发布 `WalletTopupSucceededIntegrationEvent`(待罗皓晨 M00 集成事件规范确认) +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:金额 100.50 + Idempotency-Key → 充值成功 + 余额 +100.50 +- 重复:相同 Key + 相同金额 → 返回首次结果,不重复加余额 +- 异常:金额 10000.01 → 400 + `PAYMENT.TOPUP_EXCEEDS_LIMIT` +- 异常:金额 100.555 → 400 + `PAYMENT.TOPUP_EXCEEDS_LIMIT`(小数 > 2 位) +- 异常:相同 Key + 不同金额 → 409 + `IDEMPOTENCY.KEY_REUSED` + +--- + +### A403 查询充值记录 + +- **模块 / Tag**:Payment +- **需求编号**:M05-01-FR04 +- **负责人**:张海洋 +- **关联数据表**:DB082(待评审)— `wallet_topups` +- **当前状态**:已设计 +- **用途**:分页查询当前买家充值记录 +- **方法与路径**:`GET /api/payment/wallet/topups` +- **operationId**:`Payment_ListTopups` +- **请求 Schema**:`ListTopupsQuery` +- **响应 Schema**:`TopupListResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 充值记录 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**: + - `status`(可选):`Succeeded` / `Failed` / `Pending` + - `createdFrom`(可选):ISO 8601 UTC,包含 + - `createdTo`(可选):ISO 8601 UTC,不包含 + - `page`(默认 `1`) + - `pageSize`(默认 `10`,1-100) + - `sortBy`(白名单:`createdAt`,默认 `createdAt desc`) + - `sortOrder`(`asc` / `desc`) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `createdFrom ≤ createdTo`(否则 400) + - `status` 枚举必须白名单 + - `pageSize` ∈ [1, 100] + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`TopupListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "topupId": "c7a1d4e6-...", + "amount": 100.50, + "currency": "CNY", + "status": "Succeeded", + "createdAt": "2026-07-23T08:30:00Z", + "succeededAt": "2026-07-23T08:30:01Z" + } + ], + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | Query 参数错误(status 不在白名单、时间范围非法) | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 仅返回当前 buyerId 记录 +- 列表按 `createdAt desc, topupId desc` 稳定排序,避免翻页重复 + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:返回当前用户充值记录 +- 异常:page=0 → 400 + `COMMON.VALIDATION_FAILED` +- 异常:createdFrom > createdTo → 400 + `COMMON.VALIDATION_FAILED` + +--- + +### A404 收银台查询 + +- **模块 / Tag**:Payment +- **需求编号**:M05-01-FR05 +- **负责人**:张海洋 +- **关联数据表**:DB084(待评审)— `orders`(只读,用于查询订单金额/状态) +- **当前状态**:已设计 +- **用途**:进入支付前的订单金额、应付、钱包余额、可用渠道聚合查询 +- **方法与路径**:`GET /api/payment/checkout/{orderId}` +- **operationId**:`Payment_GetCheckout` +- **请求 Schema**:(无) +- **响应 Schema**:`CheckoutResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 订单 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:`orderId`(UUID,必填) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `orderId` UUID 格式 + - 订单归属当前 buyerId + - 订单状态为 `PendingPayment`(否则 409 + `PAYMENT.ORDER_NOT_PAYABLE`) + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`CheckoutResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "orderId": "3f0ed9a9-...", + "orderAmount": 199.00, + "paidAmount": 0.00, + "currency": "CNY", + "walletBalance": 100.50, + "insufficient": true, + "availableChannels": ["MOCK_WALLET"], + "expiresAt": "2026-07-23T09:00:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 404 | `RESOURCE.NOT_FOUND` | 订单不存在或非本人 | +| 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 订单状态非 `PendingPayment` | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 不修改订单或钱包状态,纯查询 +- 余额、订单金额、应付以服务端实时值(按 PAY-R01) +- 订单已支付 → 返回 `PAID` 状态但 `CheckoutResponse` 仍可读 + +#### 缓存、事件或外部依赖 + +- 缓存:可短暂缓存(短 TTL 5s),不允许跨用户复用 +- 事件:无 +- 外部依赖:PostgreSQL + 钱包表 + +#### 验证场景 + +- 正常:订单本人 + `PendingPayment` + 余额不足 → 返回 `insufficient=true` +- 正常:订单本人 + `PendingPayment` + 余额充足 → 返回 `insufficient=false` +- 异常:他人订单 → 404 + `RESOURCE.NOT_FOUND` +- 异常:订单已支付 → 409 + `PAYMENT.ORDER_NOT_PAYABLE` + +--- + +### A405 模拟支付 + +- **模块 / Tag**:Payment +- **需求编号**:M05-01-FR05~FR09 +- **负责人**:张海洋 +- **关联数据表**:DB081(待评审)— `wallets`、DB083(待评审)— `wallet_ledgers`、DB085(待评审)— `payments`、DB084(待评审)— `orders` +- **当前状态**:已设计 +- **用途**:从买家钱包扣款并完成订单支付 +- **方法与路径**:`POST /api/payment/orders/{orderId}/pay` +- **operationId**:`Payment_PayOrder` +- **请求 Schema**:`PayOrderRequest` +- **响应 Schema**:`PaymentResultResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 订单 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 模拟支付) + +#### 请求 + +- **Route 参数**:`orderId`(UUID,必填) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "expectedAmount": 199.00, + "currency": "CNY" +} +``` +- **校验规则**: + - `Idempotency-Key` 必填 + - `expectedAmount` 必填,订单金额由服务端校验(PAY-R01),与订单金额不一致 → 409 + `PAYMENT.AMOUNT_MISMATCH` + - 订单状态必须为 `PendingPayment`,否则 409 + `PAYMENT.ORDER_NOT_PAYABLE` + - 订单归属当前 buyerId + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`PaymentResultResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "paymentId": "8d2e9d11-...", + "orderId": "3f0ed9a9-...", + "amount": 199.00, + "currency": "CNY", + "status": "Succeeded", + "walletBalanceAfter": 1.50, + "paidAt": "2026-07-23T08:35:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段格式错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 404 | `RESOURCE.NOT_FOUND` | 订单不存在或非本人 | +| 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 订单状态非 `PendingPayment` 或已取消 | +| 409 | `PAYMENT.ALREADY_PAID` | 订单已支付成功(幂等命中首次结果) | +| 409 | `PAYMENT.AMOUNT_MISMATCH` | `expectedAmount` 与订单金额不一致 | +| 409 | `PAYMENT.INSUFFICIENT_BALANCE` | 钱包余额不足 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 数据库不可用 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 钱包条件扣减 + 钱包流水 + 支付记录 + 订单状态 + Outbox **同一事务**(按 PAY-R06) +- 与 C03 订单超时取消通过 `WHERE order.status = 'PendingPayment'` 条件竞争,唯一胜出(按 PAY-R05) +- 余额不得为负(条件更新 + CHECK 约束)(按 PAY-R02) +- 成功提交后写入 `Idempotency-Key` 记录,相同 Key + 相同 amount + 相同 orderId → 返回首次结果 +- 重复支付请求返回原成功结果,不重复写入或重复发布事件(按 PAY-R11) + +#### 缓存、事件或外部依赖 + +- 缓存:写入幂等结果到 `Idempotency-Key` 存储(DB 或 Redis) +- 事件:发布 `OrderPaidIntegrationEvent`(架构 §7.4 已确定第一条集成事件) +- 外部依赖:PostgreSQL + Ordering 模块 `orders` 表 + +#### 验证场景 + +- 正常:订单 `PendingPayment` + 余额充足 + 金额一致 → 200 + `PaymentResultResponse` +- 重复:相同 Idempotency-Key → 返回首次结果,不重复扣款 +- 异常:余额不足 → 409 + `PAYMENT.INSUFFICIENT_BALANCE` +- 异常:订单已支付 → 409 + `PAYMENT.ALREADY_PAID` +- 异常:订单已取消 → 409 + `PAYMENT.ORDER_NOT_PAYABLE` +- 异常:金额不一致 → 409 + `PAYMENT.AMOUNT_MISMATCH` +- 并发:与 C03 同时操作 → 唯一胜出,败方 409 + `PAYMENT.ORDER_NOT_PAYABLE` + +--- + +### A406 查询订单支付结果 + +- **模块 / Tag**:Payment +- **需求编号**:M05-01-FR09 +- **负责人**:张海洋 +- **关联数据表**:DB085(待评审)— `payments` +- **当前状态**:已设计 +- **用途**:查询指定订单的支付结果与支付记录 +- **方法与路径**:`GET /api/payment/orders/{orderId}` +- **operationId**:`Payment_GetPaymentByOrder` +- **请求 Schema**:(无) +- **响应 Schema**:`PaymentResultResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 订单 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:`orderId`(UUID,必填) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `orderId` UUID 格式 + - 订单归属当前 buyerId + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`PaymentResultResponse`(同 A405) +- **示例**:(同 A405 成功响应) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 404 | `RESOURCE.NOT_FOUND` | 订单不存在或非本人 | +| 404 | `PAYMENT.NOT_FOUND` | 订单未发起过支付(订单未处于 `PendingPayment` / `Paid`) | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 同一订单只返回最新一笔成功支付;如有多笔识别为异常(P420 回调场景) +- 订单已取消但有迟到成功支付 → 返回 `PaymentResult`,订单状态仍为 `Cancelled`,并标注对账状态(架构 §7.12) + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:订单已支付 → 返回支付结果 +- 异常:订单未支付 → 404 + `PAYMENT.NOT_FOUND` +- 异常:他人订单 → 404 + `RESOURCE.NOT_FOUND` + +--- + +### A407 支付记录列表 + +- **模块 / Tag**:Payment +- **需求编号**:M05-01-FR07 +- **负责人**:张海洋 +- **关联数据表**:DB085(待评审)— `payments` +- **当前状态**:已设计 +- **用途**:分页查询当前买家支付记录 +- **方法与路径**:`GET /api/payments` +- **operationId**:`Payment_ListPayments` +- **请求 Schema**:`ListPaymentsQuery` +- **响应 Schema**:`PaymentListResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 支付记录 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**: + - `status`(可选):`Succeeded` / `Failed` / `Pending` + - `orderId`(可选):按订单过滤 + - `createdFrom` / `createdTo`(可选):时间范围 + - `page` / `pageSize` / `sortBy` / `sortOrder`(标准分页) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - 标准分页 + 时间范围 + 枚举白名单 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`PaymentListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "paymentId": "8d2e9d11-...", + "orderId": "3f0ed9a9-...", + "amount": 199.00, + "currency": "CNY", + "status": "Succeeded", + "createdAt": "2026-07-23T08:35:00Z", + "succeededAt": "2026-07-23T08:35:01Z" + } + ], + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 参数错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 仅返回当前 buyerId 记录 +- 默认排序 `createdAt desc, paymentId desc` + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:返回本人支付记录 +- 异常:他人 orderId → 即使订单存在也过滤掉(不暴露归属) + +--- + +### A408 支付详情 + +- **模块 / Tag**:Payment +- **需求编号**:M05-01-FR07 +- **负责人**:张海洋 +- **关联数据表**:DB085(待评审)— `payments` +- **当前状态**:已设计 +- **用途**:查询单笔支付详情 +- **方法与路径**:`GET /api/payments/{paymentId}` +- **operationId**:`Payment_GetPayment` +- **请求 Schema**:(无) +- **响应 Schema**:`PaymentDetailResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 支付记录 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:`paymentId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `paymentId` UUID 格式 + - 支付归属当前 buyerId + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`PaymentDetailResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "paymentId": "8d2e9d11-...", + "orderId": "3f0ed9a9-...", + "amount": 199.00, + "currency": "CNY", + "status": "Succeeded", + "createdAt": "2026-07-23T08:35:00Z", + "succeededAt": "2026-07-23T08:35:01Z", + "idempotencyKey": "uuid-..." + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 404 | `RESOURCE.NOT_FOUND` | 支付不存在或非本人 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 不返回内部审计字段;幂等键可对外展示以便客户端排错 + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:本人支付 → 返回详情 +- 异常:他人支付 → 404 + `RESOURCE.NOT_FOUND` + +--- + +### A411 售后资格预检 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR01 +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB084(待评审)— `orders` +- **当前状态**:已设计 +- **用途**:预检指定订单项是否可申请售后 +- **方法与路径**:`GET /api/after-sales/eligibility` +- **operationId**:`AfterSales_CheckEligibility` +- **请求 Schema**:`EligibilityQuery` +- **响应 Schema**:`EligibilityResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 订单 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**: + - `orderId`(必填,UUID) + - `orderItemId`(必填,UUID) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `orderId` / `orderItemId` UUID 格式 + - 订单归属当前 buyerId + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`EligibilityResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "orderId": "3f0ed9a9-...", + "orderItemId": "5a7c...", + "eligible": true, + "reason": null, + "maxRefundableAmount": 100.00, + "maxRefundableQuantity": 1, + "availableTypes": ["RefundOnly", "ReturnAndRefund"], + "deadlineAt": "2026-07-30T08:30:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 404 | `RESOURCE.NOT_FOUND` | 订单或订单项不存在 | +| 409 | `AFTER_SALES.NOT_ELIGIBLE` | 订单未支付、已发货超期、不可售后状态 | + +#### 业务规则与并发 + +- 仅返回当前 buyerId 订单的可申请性 +- 退款金额上限 = 实付单价 × 剩余可售后数量(M10 业务规则) +- 可申请类型根据订单状态决定:已支付/已发货 → RefundOnly;已发货 + 确认收货后 → ReturnAndRefund + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + Orders 模块 + +#### 验证场景 + +- 正常:已支付订单 → 返回可申请 +- 异常:订单未支付 → 409 + `AFTER_SALES.NOT_ELIGIBLE` +- 异常:完成 > 7 天 → 409 + `AFTER_SALES.NOT_ELIGIBLE` + +--- + +### A412 提交售后申请 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR02 +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests` +- **当前状态**:已设计 +- **用途**:买家提交退款/退货申请 +- **方法与路径**:`POST /api/after-sales/requests` +- **operationId**:`AfterSales_CreateRequest` +- **请求 Schema**:`CreateAfterSalesRequest` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 订单项 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "orderId": "3f0ed9a9-...", + "orderItemId": "5a7c...", + "type": "RefundOnly", + "quantity": 1, + "reason": "DAMAGED", + "reasonNote": "外包装破损", + "evidence": ["https://...", "https://..."] +} +``` +- **校验规则**: + - `orderId` / `orderItemId` 必填,UUID 格式 + - `type` 枚举:`RefundOnly` / `ReturnAndRefund` + - `quantity` 整数 ≥ 1 且 ≤ 剩余可售后数量 + - `reason` 枚举白名单(待 6.2 M10 业务规则定义) + - `reasonNote` 选填,≤ 500 字 + - `evidence` 选填,最多 9 张图 URL + - 退款金额由 `quantity × 实付单价` 后端计算(按 M10 业务规则"不接受任意金额") + - `Idempotency-Key` 必填 + +#### 成功响应 + +- **HTTP 状态**:`201 Created` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "requestId": "b9c1...", + "orderId": "3f0ed9a9-...", + "orderItemId": "5a7c...", + "type": "RefundOnly", + "quantity": 1, + "calculatedAmount": 100.00, + "currency": "CNY", + "reason": "DAMAGED", + "reasonNote": "外包装破损", + "evidence": ["https://..."], + "status": "PendingReview", + "createdAt": "2026-07-23T08:30:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 404 | `RESOURCE.NOT_FOUND` | 订单或订单项不存在 | +| 409 | `AFTER_SALES.NOT_ELIGIBLE` | 订单不满足售后条件 | +| 409 | `AFTER_SALES.AMOUNT_EXCEEDS_PAID` | 申请数量超过剩余可售后数量 | +| 409 | `AFTER_SALES.DUPLICATE_APPLICATION` | 同一订单项已有"待审核"申请 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 退款金额由服务端计算(M10 规则:"不接受任意金额") +- 申请数量不得超过剩余可售后数量(防重复申请) +- 状态写入 `PendingReview`(M10 状态机) +- 同一 buyerId 同一订单项已有 `PendingReview` → 拒绝重复申请 + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:发布 `AfterSalesApplicationSubmittedIntegrationEvent`(待 M00 集成事件确认) +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:订单项可申请 → 201 + 详情 +- 重复:相同 Idempotency-Key → 返回首次结果 +- 异常:申请数量 > 剩余可售后 → 409 + `AFTER_SALES.AMOUNT_EXCEEDS_PAID` +- 异常:订单项已有 PendingReview → 409 + `AFTER_SALES.DUPLICATE_APPLICATION` + +--- + +### A413 申请列表 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR03 +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests` +- **当前状态**:已设计 +- **用途**:买家本人或商家按范围分页查询售后申请 +- **方法与路径**:`GET /api/after-sales/requests` +- **operationId**:`AfterSales_ListRequests` +- **请求 Schema**:`ListAfterSalesRequestsQuery` +- **响应 Schema**:`AfterSalesRequestListResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` +- **资源归属**:当前 buyerId 申请 / 当前 merchant 授权范围申请 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**: + - `status`(可选):`PendingReview` / `PendingReturn` / `PendingReceipt` / `Refunding` / `Refunded` / `RefundFailed` / `Rejected` / `Cancelled` + - `type`(可选):`RefundOnly` / `ReturnAndRefund` + - `createdFrom` / `createdTo`(可选):时间范围 + - `page` / `pageSize` / `sortBy` / `sortOrder`(标准分页) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - 买家仅看本人申请;商家仅看授权范围内申请 + - 标准分页 + 时间范围 + 枚举白名单 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`AfterSalesRequestListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "requestId": "b9c1...", + "orderId": "3f0ed9a9-...", + "orderItemId": "5a7c...", + "type": "RefundOnly", + "quantity": 1, + "calculatedAmount": 100.00, + "currency": "CNY", + "status": "PendingReview", + "createdAt": "2026-07-23T08:30:00Z" + } + ], + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 参数错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer / Merchant | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 商家视图按 `merchantId` 过滤订单范围 +- 默认排序 `createdAt desc, requestId desc` + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:买家 → 返回本人申请 +- 正常:商家 → 返回授权范围申请 +- 异常:跨商家查询 → 自动过滤,不返回他人数据 + +--- + +### A414 申请详情 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR04 +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_audit_logs` +- **当前状态**:已设计 +- **用途**:查询单条售后申请的详细信息与状态时间线 +- **方法与路径**:`GET /api/after-sales/requests/{requestId}` +- **operationId**:`AfterSales_GetRequest` +- **请求 Schema**:(无) +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` +- **资源归属**:当前 buyerId 应用 / 当前 merchant 授权范围内 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:`requestId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `requestId` UUID 格式 + - 资源归属买家或授权商家 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`AfterSalesRequestDetailResponse`(含 `timeline` 字段) +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "requestId": "b9c1...", + "orderId": "3f0ed9a9-...", + "orderItemId": "5a7c...", + "type": "RefundOnly", + "quantity": 1, + "calculatedAmount": 100.00, + "currency": "CNY", + "reason": "DAMAGED", + "reasonNote": "外包装破损", + "evidence": ["https://..."], + "status": "PendingReview", + "createdAt": "2026-07-23T08:30:00Z", + "timeline": [ + { "status": "PendingReview", "at": "2026-07-23T08:30:00Z", "actor": "buyer" } + ] + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer / Merchant | +| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 时间线读 `after_sales_audit_logs` 表(按 DB087 推断) +- 不返回内部审计字段(如 merchant 内部 ID) + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:本人申请 → 返回详情 + timeline +- 异常:他人申请 → 404 + `RESOURCE.NOT_FOUND` + +--- + +### A415 撤销申请 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR10 +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests` +- **当前状态**:已设计 +- **用途**:买家撤销本人仍处 `PendingReview` 状态的申请 +- **方法与路径**:`POST /api/after-sales/requests/{requestId}/cancel` +- **operationId**:`AfterSales_CancelRequest` +- **请求 Schema**:`CancelAfterSalesRequest` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 申请 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) + +#### 请求 + +- **Route 参数**:`requestId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "reason": "买家自愿撤销" +} +``` +- **校验规则**: + - 申请归属当前 buyerId + - 申请状态必须为 `PendingReview`,否则 409 + `AFTER_SALES.INVALID_STATUS` + - `Idempotency-Key` 必填 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **示例**:(同 A412 详情) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或非本人 | +| 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `PendingReview` | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 审核通过后不允许撤销(M10 业务规则) +- 状态条件更新:`WHERE status = 'PendingReview' AND buyer_id = currentBuyerId` +- 撤销后保留 `audit_log` 记录 + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:发布 `AfterSalesApplicationCancelledIntegrationEvent` +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:本人 `PendingReview` 申请 → 撤销成功 +- 异常:他人申请 → 404 + `RESOURCE.NOT_FOUND` +- 异常:已审核申请 → 409 + `AFTER_SALES.INVALID_STATUS` + +--- + +### A416 商家审核 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR05 +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_audit_logs` +- **当前状态**:已设计 +- **用途**:商家同意或拒绝售后申请 +- **方法与路径**:`POST /api/after-sales/requests/{requestId}/audit` +- **operationId**:`AfterSales_AuditRequest` +- **请求 Schema**:`AuditAfterSalesRequest` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **身份与 Policy**:JWT Bearer + `MerchantOnly` +- **资源归属**:当前 merchant 授权范围内申请 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) + +#### 请求 + +- **Route 参数**:`requestId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "decision": "Approve", + "auditNote": "同意申请", + "expectRefund": true +} +``` +- **校验规则**: + - 申请归属当前 merchant + - 申请状态必须为 `PendingReview` + - `decision` 枚举:`Approve` / `Reject` + - `expectRefund=true` 表示审核通过后系统将自动触发退款(A431) + - `Idempotency-Key` 必填 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **示例**:(同 A412 详情,status 变为 `PendingReturn` 或 `Refunding`) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Merchant | +| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | +| 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `PendingReview` | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 商家不能修改买家原始申请内容(业务规则) +- 状态条件更新:`WHERE status = 'PendingReview' AND merchant_id = currentMerchantId` +- 审核通过后若 `expectRefund=true` → 异步触发 A431 退款 +- `audit_log` 记录审核人与审核意见 + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:发布 `AfterSalesApplicationAuditedIntegrationEvent` +- 外部依赖:PostgreSQL + Payment 模块(通过应用能力调用 A431) + +#### 验证场景 + +- 正常:商家 Approve → 状态进入 `PendingReturn` 或 `Refunding` +- 正常:商家 Reject → 状态进入 `Rejected` +- 异常:买家角色调用 → 403 + `AUTH.FORBIDDEN` +- 异常:他人商家申请 → 404 + `RESOURCE.NOT_FOUND` +- 异常:状态已非 `PendingReview` → 409 + `AFTER_SALES.INVALID_STATUS` + +--- + +### A417 商家确认退货 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR11 +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_audit_logs` +- **当前状态**:已设计 +- **用途**:商家确认收到退货,触发退款流程 +- **方法与路径**:`POST /api/after-sales/requests/{requestId}/confirm-return` +- **operationId**:`AfterSales_ConfirmReturn` +- **请求 Schema**:`ConfirmReturnRequest` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **身份与 Policy**:JWT Bearer + `MerchantOnly` +- **资源归属**:当前 merchant 授权范围内申请 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) + +#### 请求 + +- **Route 参数**:`requestId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "receivedQuantity": 1, + "note": "已收到退货" +} +``` +- **校验规则**: + - 申请归属当前 merchant + - 申请类型必须为 `ReturnAndRefund` + - 申请状态必须为 `PendingReceipt` + - `receivedQuantity` ∈ [1, 申请数量] + - `Idempotency-Key` 必填 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **示例**:(同 A412 详情,status 变为 `Refunding`) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Merchant | +| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | +| 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `PendingReceipt` | +| 409 | `AFTER_SALES.WRONG_TYPE` | 申请类型非 `ReturnAndRefund` | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 状态条件更新:`WHERE status = 'PendingReceipt' AND merchant_id = currentMerchantId` +- 确认收到后异步触发 A431 退款 +- 库存按退货数量回补(按 M10 业务规则"已发货或已完成订单仅在退货且商家确认收货后按退货数量回补") + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:发布 `AfterSalesReturnConfirmedIntegrationEvent` + 库存回补事件 +- 外部依赖:PostgreSQL + Payment(A431)+ Inventory + +#### 验证场景 + +- 正常:商家确认退货 → 状态进入 `Refunding`,触发退款 +- 异常:申请类型为 `RefundOnly` → 409 + `AFTER_SALES.WRONG_TYPE` +- 异常:状态非 `PendingReceipt` → 409 + `AFTER_SALES.INVALID_STATUS` + +--- + +### A418 审核日志 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR04 +- **负责人**:张海洋 +- **关联数据表**:DB087(待评审)— `after_sales_audit_logs` +- **当前状态**:已设计 +- **用途**:查询申请审核日志 +- **方法与路径**:`GET /api/after-sales/requests/{requestId}/audit-logs` +- **operationId**:`AfterSales_ListAuditLogs` +- **请求 Schema**:`ListAuditLogsQuery` +- **响应 Schema**:`AuditLogListResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` +- **资源归属**:当前 buyerId 申请 / 当前 merchant 授权范围内 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:`requestId`(UUID) +- **Query 参数**:`page` / `pageSize` / `sortBy` / `sortOrder` +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - 申请归属当前 buyerId 或当前 merchant + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`AuditLogListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "logId": "...", + "action": "Submitted", + "actor": "buyer", + "fromStatus": null, + "toStatus": "PendingReview", + "note": null, + "at": "2026-07-23T08:30:00Z" + } + ], + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer / Merchant | +| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 默认排序 `at asc, logId asc`(按时间顺序) +- 不返回内部审计字段(如 `merchant_internal_id`) + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:本人申请 → 返回审核日志 +- 异常:他人申请 → 404 + `RESOURCE.NOT_FOUND` + +--- + +### A419 退款失败重试 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR07 +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB088(待评审)— `refunds` +- **当前状态**:已设计 +- **用途**:商家或系统对状态为 `RefundFailed` 的申请触发重试 +- **方法与路径**:`POST /api/after-sales/requests/{requestId}/retry-refund` +- **operationId**:`AfterSales_RetryRefund` +- **请求 Schema**:`RetryRefundRequest` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **身份与 Policy**:JWT Bearer + `MerchantOnly` +- **资源归属**:当前 merchant 授权范围内申请 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) + +#### 请求 + +- **Route 参数**:`requestId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "note": "重试退款" +} +``` +- **校验规则**: + - 申请归属当前 merchant + - 申请状态必须为 `RefundFailed` + - `Idempotency-Key` 必填 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **示例**:(同 A412 详情,status 变为 `Refunding`) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Merchant | +| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | +| 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `RefundFailed` | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 状态条件更新:`WHERE status = 'RefundFailed' AND merchant_id = currentMerchantId` +- 重试时异步触发 A431 退款 + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:发布 `AfterSalesRefundRetriedIntegrationEvent` +- 外部依赖:PostgreSQL + Payment(A431) + +#### 验证场景 + +- 正常:商家对 `RefundFailed` 重试 → 状态进入 `Refunding` +- 异常:状态非 `RefundFailed` → 409 + `AFTER_SALES.INVALID_STATUS` + +--- + +### A421 接收支付回调 + +- **模块 / Tag**:Payment +- **需求编号**:C08-FR01 / FR02 / FR03 / FR04 / FR05 +- **负责人**:张海洋 +- **关联数据表**:DB089(待评审)— `payment_callbacks`、DB085(待评审)— `payments`、DB084(待评审)— `orders` +- **当前状态**:已设计 +- **用途**:接收模拟支付渠道的回调,更新支付与订单状态 +- **方法与路径**:`POST /api/payment/callbacks` +- **operationId**:`Payment_ReceiveCallback` +- **请求 Schema**:`PaymentCallbackRequest` +- **响应 Schema**:`PaymentCallbackResponse` +- **身份与 Policy**:内部服务级鉴权(Mock Channel Service,签名验证) +- **资源归属**:N/A(系统级) +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 支付回调) + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`X-Callback-Signature: `、`Content-Type: application/json` +- **Body**: +```json +{ + "callbackId": "5a8e...", + "paymentSerialNumber": "psn-...", + "orderId": "3f0ed9a9-...", + "result": "Success", + "occurredAt": "2026-07-23T08:35:00Z", + "amount": 199.00, + "currency": "CNY" +} +``` +- **校验规则**: + - `callbackId` 必填,全局唯一 + - `paymentSerialNumber` 必填 + - `result` 枚举:`Success` / `Failed` + - `amount` 必填,decimal + - 签名验证:`X-Callback-Signature` 通过 HMAC 校验(按 C08-FR02) + - `Idempotency-Key` 必填(与 `callbackId` 同值) + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`PaymentCallbackResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "callbackId": "5a8e...", + "status": "Processed", + "processedAt": "2026-07-23T08:35:01Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 401 | `PAYMENT.CALLBACK_INVALID_SIGNATURE` | 签名验证失败 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少服务 JWT | +| 409 | `PAYMENT.CALLBACK_DUPLICATE` | 同一 `callbackId` 重复到达 | +| 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 已取消订单收到迟到成功回调 → 进入对账差异 | +| 422 | `PAYMENT.CALLBACK_AMOUNT_MISMATCH` | 回调金额与订单金额不一致 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 回调 ID 与支付流水号建**唯一约束**(按 C08 业务规则) +- 同事务:支付记录 + 订单状态 + Inbox/处理记录 + Outbox(按 C08-FR05) +- 重复回调返回首次结果,不重复记账 +- 乱序:按订单当前状态 + 事件时间决定接受/忽略/登记差异 +- 已取消订单收到迟到成功回调 → **进入对账差异**,不得直接改已支付(按 C08 业务规则) + +#### 缓存、事件或外部依赖 + +- 缓存:幂等记录存在 DB(不依赖 Redis) +- 事件:发布 `PaymentCallbackProcessedIntegrationEvent` / `OrderPaidIntegrationEvent`(按结果) +- 外部依赖:PostgreSQL + Ordering 模块 + +#### 验证场景 + +- 正常:未处理过的回调 → 处理成功 +- 重复:相同 `callbackId` → 返回首次结果,不重复处理 +- 异常:签名错误 → 401 + `PAYMENT.CALLBACK_INVALID_SIGNATURE` +- 异常:金额不一致 → 422 + `PAYMENT.CALLBACK_AMOUNT_MISMATCH` +- 异常:已取消订单收到 Success 回调 → 进入对账差异状态,订单不直接改 `Paid` + +--- + +### A422 对账批次列表 + +- **模块 / Tag**:Reconciliation +- **需求编号**:C08-FR06 +- **负责人**:张海洋 +- **关联数据表**:DB090(待评审)— `reconciliation_batches` +- **当前状态**:已设计 +- **用途**:分页查询每日对账批次 +- **方法与路径**:`GET /api/admin/reconciliation/batches` +- **operationId**:`Reconciliation_ListBatches` +- **请求 Schema**:`ListBatchesQuery` +- **响应 Schema**:`ReconciliationBatchListResponse` +- **身份与 Policy**:JWT Bearer + `AdminOnly` +- **资源归属**:N/A(管理员) +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**: + - `dateFrom` / `dateTo`(可选):按对账日期过滤 + - `status`(可选):`Pending` / `Matched` / `HasDifferences` / `Resolved` + - 标准分页 + 排序 +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `dateFrom ≤ dateTo` + - 枚举白名单 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`ReconciliationBatchListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "batchId": "...", + "reconciliationDate": "2026-07-23", + "rangeFrom": "2026-07-22T00:00:00Z", + "rangeTo": "2026-07-23T00:00:00Z", + "totalCount": 100, + "matchedCount": 98, + "differenceCount": 2, + "status": "HasDifferences", + "createdAt": "2026-07-23T01:00:00Z" + } + ], + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 参数错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Admin | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 仅管理员访问(按 C08 业务规则:"对账数据仅向管理员开放") +- 默认排序 `reconciliationDate desc, batchId desc` + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:管理员查询 → 返回批次列表 +- 异常:买家调用 → 403 + `AUTH.FORBIDDEN` + +--- + +### A423 对账批次详情 + +- **模块 / Tag**:Reconciliation +- **需求编号**:C08-FR06 +- **负责人**:张海洋 +- **关联数据表**:DB090(待评审)— `reconciliation_batches`、DB091(待评审)— `reconciliation_differences` +- **当前状态**:已设计 +- **用途**:查询单批对账详情 +- **方法与路径**:`GET /api/admin/reconciliation/batches/{batchId}` +- **operationId**:`Reconciliation_GetBatch` +- **请求 Schema**:(无) +- **响应 Schema**:`ReconciliationBatchDetailResponse` +- **身份与 Policy**:JWT Bearer + `AdminOnly` +- **资源归属**:N/A(管理员) +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:`batchId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `batchId` UUID 格式 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`ReconciliationBatchDetailResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "batchId": "...", + "reconciliationDate": "2026-07-23", + "rangeFrom": "2026-07-22T00:00:00Z", + "rangeTo": "2026-07-23T00:00:00Z", + "totalCount": 100, + "matchedCount": 98, + "differenceCount": 2, + "status": "HasDifferences", + "summary": { + "byType": { "MissingPayment": 1, "AmountMismatch": 1 } + }, + "createdAt": "2026-07-23T01:00:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Admin | +| 404 | `RECONCILIATION.BATCH_NOT_FOUND` | 批次不存在 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 详情含按差异类型汇总(按 C08-FR07 至少识别"支付成功但订单未更新"等) + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:管理员查询 → 返回详情 +- 异常:批次不存在 → 404 + `RECONCILIATION.BATCH_NOT_FOUND` + +--- + +### A424 差异列表 + +- **模块 / Tag**:Reconciliation +- **需求编号**:C08-FR07 / FR08 +- **负责人**:张海洋 +- **关联数据表**:DB091(待评审)— `reconciliation_differences` +- **当前状态**:已设计 +- **用途**:分页查询某批次的所有差异 +- **方法与路径**:`GET /api/admin/reconciliation/batches/{batchId}/differences` +- **operationId**:`Reconciliation_ListDifferences` +- **请求 Schema**:`ListDifferencesQuery` +- **响应 Schema**:`ReconciliationDifferenceListResponse` +- **身份与 Policy**:JWT Bearer + `AdminOnly` +- **资源归属**:N/A(管理员) +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:`batchId`(UUID) +- **Query 参数**: + - `type`(可选):`MissingPayment` / `AmountMismatch` / `DuplicateRefund` / `LateCallback` 等 + - `status`(可选):`Pending` / `InProgress` / `Resolved` + - 标准分页 + 排序 +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - 批次存在 + - 枚举白名单 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`ReconciliationDifferenceListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "differenceId": "...", + "batchId": "...", + "type": "LateCallback", + "orderId": "3f0ed9a9-...", + "paymentId": "8d2e9d11-...", + "callbackId": "5a8e...", + "description": "已取消订单收到迟到成功回调", + "status": "Pending", + "createdAt": "2026-07-23T01:00:00Z" + } + ], + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Admin | +| 404 | `RECONCILIATION.BATCH_NOT_FOUND` | 批次不存在 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 差异类型至少识别(按 C08-FR07): + - `LateCallback`:已取消订单收到迟到成功回调 + - `MissingPayment`:订单已支付但缺支付流水 + - `AmountMismatch`:支付/退款金额不一致 + - `DuplicateRefund`:退款重复 +- 状态管理(按 C08-FR08):`Pending` / `InProgress` / `Resolved` + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:管理员查询 → 返回差异列表 +- 异常:批次不存在 → 404 + `RECONCILIATION.BATCH_NOT_FOUND` + +--- + +### A425 差异处理 + +- **模块 / Tag**:Reconciliation +- **需求编号**:C08-FR08 +- **负责人**:张海洋 +- **关联数据表**:DB091(待评审)— `reconciliation_differences` +- **当前状态**:已设计 +- **用途**:管理员处理对账差异并标记状态 +- **方法与路径**:`POST /api/admin/reconciliation/differences/{differenceId}/process` +- **operationId**:`Reconciliation_ProcessDifference` +- **请求 Schema**:`ProcessDifferenceRequest` +- **响应 Schema**:`ReconciliationDifferenceDetailResponse` +- **身份与 Policy**:JWT Bearer + `AdminOnly` +- **资源归属**:N/A(管理员) +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) + +#### 请求 + +- **Route 参数**:`differenceId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "action": "MarkResolved", + "resolutionNote": "确认为模拟渠道测试回调,已通知商家" +} +``` +- **校验规则**: + - `differenceId` 必填 + - `action` 枚举:`MarkInProgress` / `MarkResolved` / `MarkIgnored` + - 当前状态必须为 `Pending`(`MarkInProgress`)或 `InProgress`(`MarkResolved`) + - `resolutionNote` 必填,≤ 1000 字 + - `Idempotency-Key` 必填 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`ReconciliationDifferenceDetailResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "differenceId": "...", + "batchId": "...", + "type": "LateCallback", + "status": "Resolved", + "resolutionNote": "确认为模拟渠道测试回调,已通知商家", + "resolvedAt": "2026-07-23T03:00:00Z", + "resolvedBy": "admin-uuid" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Admin | +| 404 | `RECONCILIATION.DIFFERENCE_NOT_FOUND` | 差异不存在 | +| 409 | `RECONCILIATION.DIFFERENCE_ALREADY_PROCESSED` | 状态非法 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 状态条件更新:`WHERE status = 'Pending'` 或 `WHERE status = 'InProgress'` +- 差异修复必须可追踪(按 C08 业务规则),不能通过直接改库隐藏原因 +- 修复后保留 `resolutionNote` 和处理人 + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:发布 `ReconciliationDifferenceProcessedIntegrationEvent` +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:管理员 MarkResolved → 状态进入 `Resolved` +- 异常:状态已为 `Resolved` → 409 + `RECONCILIATION.DIFFERENCE_ALREADY_PROCESSED` +- 异常:买家调用 → 403 + `AUTH.FORBIDDEN` + +--- + +### A431 模拟退款 + +- **模块 / Tag**:Payment +- **需求编号**:M10-FR07 +- **负责人**:张海洋 +- **关联数据表**:DB088(待评审)— `refunds`、DB081(待评审)— `wallets`、DB083(待评审)— `wallet_ledgers` +- **当前状态**:已设计 +- **用途**:将售后金额幂等退回买家小金库 +- **方法与路径**:`POST /api/after-sales/requests/{requestId}/refund` +- **operationId**:`Refund_Create` +- **请求 Schema**:`CreateRefundRequest` +- **响应 Schema**:`RefundDetailResponse` +- **身份与 Policy**:JWT Bearer + `MerchantOnly`(系统内部调用) +- **资源归属**:当前 merchant 授权范围内申请 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 退款入账) + +#### 请求 + +- **Route 参数**:`requestId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "expectedAmount": 100.00, + "currency": "CNY" +} +``` +- **校验规则**: + - 申请归属当前 merchant(或系统内部) + - 申请状态必须为 `Refunding`(已通过 A416 / A417 触发) + - `expectedAmount` 必须等于申请计算金额 + - `Idempotency-Key` 必填 + - 同一 Key + 相同 amount → 返回首次结果 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`RefundDetailResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "refundId": "...", + "requestId": "b9c1...", + "buyerId": "...", + "amount": 100.00, + "currency": "CNY", + "status": "Succeeded", + "walletBalanceAfter": 200.50, + "createdAt": "2026-07-23T09:00:00Z", + "succeededAt": "2026-07-23T09:00:01Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Merchant | +| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | +| 409 | `AFTER_SALES.INVALID_STATUS` | 申请状态非 `Refunding` | +| 409 | `PAYMENT.AMOUNT_MISMATCH` | `expectedAmount` 与申请计算金额不一致 | +| 409 | `PAYMENT.REFUND_FAILED` | 退款执行失败(写流水失败等) | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 钱包入账 + 退款记录 + 钱包流水 + 申请状态更新 **同事务**(按架构 §7.2) +- 退款成功后订单**保持原核心状态**(按 M10 业务规则 + 架构 §7.6) +- 退款流水必须纳入 C08 每日对账(按 M10 业务规则) +- AfterSales 不直接改钱包数据;通过 Payment 公开应用能力**幂等**退回(按架构 §7.2) + +#### 缓存、事件或外部依赖 + +- 缓存:幂等记录存在 DB +- 事件:发布 `RefundCompletedIntegrationEvent` +- 外部依赖:PostgreSQL + AfterSales 模块 + +#### 验证场景 + +- 正常:审核通过触发 → 退款成功,余额增加 +- 重复:相同 Idempotency-Key → 返回首次结果,不重复入账 +- 异常:金额不一致 → 409 + `PAYMENT.AMOUNT_MISMATCH` +- 异常:写流水失败 → 409 + `PAYMENT.REFUND_FAILED`,申请状态回滚 + +--- + +### A432 退款详情 + +- **模块 / Tag**:Payment +- **需求编号**:M10-FR04 +- **负责人**:张海洋 +- **关联数据表**:DB088(待评审)— `refunds` +- **当前状态**:已设计 +- **用途**:查询单笔退款详情 +- **方法与路径**:`GET /api/refunds/{refundId}` +- **operationId**:`Refund_Get` +- **请求 Schema**:(无) +- **响应 Schema**:`RefundDetailResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` +- **资源归属**:当前 buyerId 退款 / 当前 merchant 授权范围 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:`refundId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `refundId` UUID 格式 + - 资源归属当前 buyerId 或当前 merchant + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`RefundDetailResponse`(同 A431) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer / Merchant | +| 404 | `RESOURCE.NOT_FOUND` | 退款不存在或不在授权范围 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 不返回内部审计字段 + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:本人退款 → 返回详情 +- 异常:他人退款 → 404 + `RESOURCE.NOT_FOUND` + +--- + +### A433 退款列表 + +- **模块 / Tag**:Payment +- **需求编号**:M10-FR03 +- **负责人**:张海洋 +- **关联数据表**:DB088(待评审)— `refunds` +- **当前状态**:已设计 +- **用途**:分页查询退款记录 +- **方法与路径**:`GET /api/refunds` +- **operationId**:`Refund_List` +- **请求 Schema**:`ListRefundsQuery` +- **响应 Schema**:`RefundListResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` +- **资源归属**:当前 buyerId 退款 / 当前 merchant 授权范围 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**: + - `status`(可选):`Pending` / `Succeeded` / `Failed` + - `createdFrom` / `createdTo`(可选):时间范围 + - 标准分页 + 排序 +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - 标准分页 + 时间范围 + 枚举白名单 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`RefundListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "refundId": "...", + "requestId": "b9c1...", + "amount": 100.00, + "currency": "CNY", + "status": "Succeeded", + "createdAt": "2026-07-23T09:00:00Z", + "succeededAt": "2026-07-23T09:00:01Z" + } + ], + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 参数错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer / Merchant | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 买家视图按 `buyerId` 过滤;商家视图按授权范围过滤 +- 默认排序 `createdAt desc, refundId desc` + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:买家 → 返回本人退款 +- 正常:商家 → 返回授权范围退款 + +--- + +## 3. 跨接口的一致性约束 + +### 3.1 错误码统一 + +- `AUTH.*` / `RESOURCE.*` / `IDEMPOTENCY.*` / `COMMON.*` 按接口设计 1.10 节基础 +- 模块错误码:`PAYMENT.*` / `AFTER_SALES.*` / `RECONCILIATION.*` +- 同一错误场景使用同一错误码 + HTTP 状态 + +### 3.2 幂等键一致性 + +涉及资金 / 状态 / 回调的接口统一: + +| 幂等范围 | 字段格式 | +|---|---| +| 客户端生成 | UUID v4 | +| 服务端 Key | `Idempotency-Key` Header | +| 储存 | DB082 / DB085 / DB088 / DB089 对应表唯一约束 | +| 保留期 | 与对应业务表相当(≥ 90 天) | + +### 3.3 响应包装 + +所有成功响应统一为 `data` 包装(204 除外),按接口设计 1.7 节。 + +### 3.4 失败响应 + +全部使用 `application/problem+json`,按接口设计 1.8 节。 + +### 3.5 操作审计 + +- 涉及状态变更的接口(A402 / A405 / A412 / A415 / A416 / A417 / A419 / A421 / A425 / A431)在 `audit_logs` 或对应表登记 actor、at、from_status、to_status +- 涉及资金的接口在 `wallet_ledgers` 写入流水 + +--- + +## 4. 协作与不写项 + +### 4.1 不在本文件范围 + +- `database-zhy.md`(DB081~DB100)—— 单独分支 `chore/database-zhy` 推进 +- C08 FR11~FR15 扩展接口(dev 当前只有 FR01~FR10)—— 单独 PR 处理 +- 集成事件命名最终确认(与罗皓晨对齐 M00 集成事件规范) +- AdminOnly Policy 命名(与罗皓晨 M00 公共 HTTP 接口对齐) + +### 4.2 评审清单 + +- [ ] 罗皓晨:对照接口设计 1.20 模板核对字段完整性 +- [ ] 韦乾强:核对 A401-A408 与 Ordering 的协作边界(订单状态、回调联动) +- [ ] 顾欣月:核对 A402 / A405 与 Catalog 库存联动(如有) +- [ ] 张海洋自审:核对 17 项 PAY 业务规则 + 12 项 M10 规则 + 11 项 C08 规则全部覆盖 + +### 4.3 汇总时机 + +- 全部接口评审通过后由罗皓晨按编号汇总到 `docs/02-设计文档/接口设计.md` 第三章 +- 个人文件 `interface-zhy.md` 在汇总同 PR 中删除 +- 历史贡献通过 Git 记录保留 -- Gitee From 4897eecc1a2b6f3791080d73fff1aeacb8f6eaac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9F=A6=E4=B9=BE=E5=BC=BA?= <3195306445@qq.com> Date: Fri, 24 Jul 2026 10:08:03 +0800 Subject: [PATCH 046/118] =?UTF-8?q?docs(daily):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=9F=A6=E4=B9=BE=E5=BC=BA=E6=97=A5=E6=8A=A5=2020260722-202607?= =?UTF-8?q?23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...2-\351\237\246\344\271\276\345\274\272.md" | 20 +++++++++++++++++ ...3-\351\237\246\344\271\276\345\274\272.md" | 22 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 "reports/daily/20260722-\351\237\246\344\271\276\345\274\272.md" create mode 100644 "reports/daily/20260723-\351\237\246\344\271\276\345\274\272.md" diff --git "a/reports/daily/20260722-\351\237\246\344\271\276\345\274\272.md" "b/reports/daily/20260722-\351\237\246\344\271\276\345\274\272.md" new file mode 100644 index 0000000..277cdea --- /dev/null +++ "b/reports/daily/20260722-\351\237\246\344\271\276\345\274\272.md" @@ -0,0 +1,20 @@ +# 日报 - 韦乾强 - 2026-07-22 + +## 今日完成 + +1. **通读项目要求文档**:阅读 `docs/00-项目要求/` 下全部 3 份文档(项目要求、验收标准、评分标准),明确该项目需求和要求。 + 2 **参与完成《需求规格说明书》初稿**:基于 `docs/01-需求文档/需求规格说明书.md` 模板,完成初稿 +2. **参加小组分工会议**:按"模块负责制"认领模块包,结合难度权重细分为6 人分工表,敲定技术栈。 + +## 遇到的问题 + +暂无 + +## 明日计划 + +1. 完善《需求规格说明书》,本周完全确定。 +1. 按照自己负责的模块做计划,应该从大框架做起。 + +## 今日工时 + +约 6小时(上午 8:00–11:20 通读文档 + 梳理节点;下午 14:30–17:30 编写需求文档 + 起草分工方案) diff --git "a/reports/daily/20260723-\351\237\246\344\271\276\345\274\272.md" "b/reports/daily/20260723-\351\237\246\344\271\276\345\274\272.md" new file mode 100644 index 0000000..8be86fa --- /dev/null +++ "b/reports/daily/20260723-\351\237\246\344\271\276\345\274\272.md" @@ -0,0 +1,22 @@ +# 日报 - 韦乾强 - 2026-07-23 + +## 今日完成 + +1. 梳理并补充本人负责模块的需求与身份处理说明,明确各功能模块的负责人和命名空间对应关系。 +2. **负责模块功能详述撰写** + M04 订单 + C03 订单超时自动取消 + M06 商家运营与后台管理 +3. 对齐需求规格、系统架构和接口命名规范,补充 PC Web 与后端 Web API 的当前交付边界;同步完善团队项目级规则和分支命名规范。 + +## 遇到的问题 + +无 + +## 明日计划 + +1. 跟进系统架构中的待确认事项,确认后完成对应文档调整。 + +## 今日工时 + +约 6 小时 -- Gitee From 9c25f26e30e20b28d95be59fea8722c0ea490d5a Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 10:37:07 +0800 Subject: [PATCH 047/118] docs(api): consolidate team interface contracts --- ...74\350\257\264\346\230\216\344\271\246.md" | 56 +- .../interface/interface-gxy.md" | 11 +- .../interface/interface-lhc.md" | 4 +- .../interface/interface-tyh.md" | 4 +- .../interface/interface-wqq.md" | 1 + .../interface/interface-zhh.md" | 4 +- .../interface/interface-zhy.md" | 4 +- ...75\345\220\215\350\247\204\350\214\203.md" | 5 +- ...45\345\217\243\350\256\276\350\256\241.md" | 7232 ++++++++++++++++- eshop-project-rules-upload/AGENTS.md | 2 + .../document-routing.reference.md | 25 +- .../eshop-align-docs.SKILL.md | 7 + .../eshop-project-workflow.SKILL.md | 1 + 13 files changed, 7278 insertions(+), 78 deletions(-) rename "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-gxy.md" => "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" (98%) rename "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-lhc.md" => "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" (98%) rename "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-tyh.md" => "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" (99%) rename "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-wqq.md" => "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" (99%) rename "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhh.md" => "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" (99%) rename "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhy.md" => "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" (99%) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 324181b..7709cc9 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -2412,36 +2412,36 @@ Electron 后续原则上复用 PC Web 原型,只补充窗口、桌面导航和 ### 9.1 需求追踪矩阵 -> 本表用于把教师验收编号落实到负责人、页面、OpenAPI 和测试用例。当前处于需求阶段,尚未产生的接口与测试编号统一标记为“待登记”,不得据此宣称已经实现或验证。 +> 本表用于把教师验收编号落实到负责人、页面、接口契约和测试用例。接口列引用《接口设计》中的 Axxx;当前 102 个已登记接口均仍处于汇总或交叉评审阶段,缺少项直接标明,不得据此宣称已经实现、冻结或验证。 -| 教师编号 | 模块与负责人 | 页面或操作入口 | OpenAPI | 测试用例 | 当前状态 | +| 教师编号 | 模块与负责人 | 页面或操作入口 | 接口契约(Axxx) | 测试用例 | 当前状态 | |---|---|---|---|---|---| -| F01 | M01-01 用户注册—唐宇昊 | 注册页 | 待接口设计登记 | 待测试计划登记 | 需求与校验规则已确认 | -| F02 | M01-02 登录与退出—唐宇昊 | 统一登录页、全端退出入口 | 待接口设计登记 | 待测试计划登记 | 需求与令牌撤销规则已确认 | -| F03 | M01-03 个人信息与地址—唐宇昊 | 买家个人中心、地址管理 | 待接口设计登记 | 待测试计划登记 | 身份字段规则已确认,地址接口待设计 | -| F04 | M02-01 商品列表与分类—顾欣月 | 购物端商品列表、分类入口 | 待接口设计登记 | 待测试计划登记 | 需求已定义 | -| F05 | M02-01 商品搜索—顾欣月 | 购物端搜索框、商品列表 | 待接口设计登记 | 待测试计划登记 | 需求已定义 | -| F06 | M02-02 商品详情—顾欣月 | 商品详情页 | 待接口设计登记 | 待测试计划登记 | 需求已定义 | -| F07 | M03-01 购物车—朱惠惠 | 商品加购入口、购物车页 | 待接口设计登记 | 待测试计划登记 | 需求已定义 | -| F08 | M04-01 提交订单—韦乾强 | 购物车结算、提交订单页 | 待接口设计登记 | 待测试计划登记 | 需求与下单事务边界已确认 | -| F09 | M04-02/M04-03/M04-04 订单查询、取消与完成—韦乾强 | 买家订单列表、订单详情、确认收货 | 待接口设计登记 | 待测试计划登记 | 需求与订单完成规则已确认 | -| F10 | M05-01 模拟支付—张海洋 | 小金库、统一收银台、支付记录 | 待接口设计登记 | 待测试计划登记 | 小金库方案已确认,接口待设计 | -| F11 | M06-01 后台商品管理—顾欣月 | 商家分类管理、商品管理 | 待接口设计登记 | 待测试计划登记 | 需求已定义 | -| F12 | M06-02 后台订单管理—韦乾强 | 商家订单列表、详情和发货入口 | 待接口设计登记 | 待测试计划登记 | 需求已定义 | -| F13 | M06-03 后台用户管理—唐宇昊 | 管理端用户管理 | 待接口设计登记 | 待测试计划登记 | 需求与禁用后旧令牌立即失效规则已确认 | -| X01 | M07 商品评价与晒图—顾欣月 | 订单评价入口、商品评价区 | 待接口设计登记 | 待测试计划登记 | 已选,图片规则已确认 | -| X02 | M08 收藏与浏览历史—唐宇昊 | 商品收藏、收藏页、浏览历史页 | 待接口设计登记 | 待测试计划登记 | 已选,需求已定义 | -| X03 | M09 站内消息—罗皓晨 | 买家/商家消息中心 | 待接口设计登记 | 待测试计划登记 | 已选,需求已定义 | -| X04 | M10 售后流程—张海洋 | 买家售后申请、商家售后审核 | 待接口设计登记 | 待测试计划登记 | 已选,售后业务规则已确认 | -| C01 | 秒杀与防超卖—朱惠惠 | 秒杀活动页、商家活动管理、压测脚本 | 待接口设计登记 | 待挑战测试登记 | 挑战已选,需求已定义 | -| C03 | 订单超时取消—韦乾强 | 订单倒计时、Worker 验收脚本 | 待接口设计登记 | 待挑战测试登记 | 挑战已选,需求已定义 | -| C04 | 商品搜索进阶—顾欣月 | 商品搜索页、性能对比报告 | 待接口设计登记 | 待挑战测试登记 | 挑战已选,搜索方案与验收口径已确认 | -| C06 | 实时消息推送—罗皓晨 | 实时通知、连接状态、消息中心 | 待实时契约登记 | 待挑战测试登记 | 挑战已选,需求已定义 | -| C07 | 缓存与性能优化—罗皓晨,顾欣月协作 | 首页、商品详情、压测报告 | 复用商品接口,待缓存约定登记 | 待挑战测试登记 | 挑战已选,需求已定义 | -| C08 | 支付回调幂等与对账—张海洋 | 回调重放脚本、管理端对账 | 待接口设计登记 | 待挑战测试登记 | 挑战已选,需求已定义 | -| C10 | 容器化与负载均衡—罗皓晨 | 统一访问入口、健康与实例验证 | 待健康检查契约登记 | 待部署验收登记 | 挑战已选,需求已定义 | - -接口设计完成后,应把“待接口设计登记”替换为真实 OpenAPI `operationId` 或接口编号;测试计划完成后,应替换为真实测试用例编号,并随需求变更同步维护状态。 +| F01 | M01-01 用户注册—唐宇昊 | 注册页 | A001 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | +| F02 | M01-02 登录与退出—唐宇昊 | 统一登录页、全端退出入口 | A002~A005 | 待测试计划登记 | 接口草案已汇总,令牌规则待统一 | +| F03 | M01-03 个人信息与地址—唐宇昊 | 买家个人中心、地址管理 | A006~A014 | 待测试计划登记 | 接口草案已汇总,身份范围待修正 | +| F04 | M02-01 商品列表与分类—顾欣月 | 购物端商品列表、分类入口 | A101、A102 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | +| F05 | M02-01 商品搜索—顾欣月 | 购物端搜索框、商品列表 | A102 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | +| F06 | M02-02 商品详情—顾欣月 | 商品详情页 | A103 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | +| F07 | M03-01 购物车—朱惠惠 | 商品加购入口、购物车页 | A201~A208 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | +| F08 | M04-01 提交订单—韦乾强 | 购物车结算、提交订单页 | A301 | 待测试计划登记 | 部分定义,幂等与事务字段待确认 | +| F09 | M04-02/M04-03/M04-04 订单查询、取消与完成—韦乾强 | 买家订单列表、订单详情、确认收货 | A302~A304;缺 A308 | 待测试计划登记 | 缺确认收货接口,尚未闭环 | +| F10 | M05-01 模拟支付—张海洋 | 小金库、统一收银台、支付记录 | A401~A408 | 待测试计划登记 | 接口草案已汇总,支付语义待统一 | +| F11 | M06-01 后台商品管理—顾欣月 | 商家分类管理、商品管理 | A110~A114、A120~A128 | 待测试计划登记 | 接口草案已汇总,创建与图片顺序待统一 | +| F12 | M06-02 后台订单管理—韦乾强 | 商家订单列表、详情和发货入口 | A305~A307 | 待测试计划登记 | 部分定义,模块命名与状态字段待确认 | +| F13 | M06-03 后台用户管理—唐宇昊 | 管理端用户管理 | A015~A017 | 待测试计划登记 | 接口草案已汇总,重复操作语义待修正 | +| X01 | M07 商品评价与晒图—顾欣月 | 订单评价入口、商品评价区 | A140~A143;A144 待决策 | 待测试计划登记 | 单条评价读取需新增接口或删除不可达引用 | +| X02 | M08 收藏与浏览历史—唐宇昊 | 商品收藏、收藏页、浏览历史页 | A018~A023;缺 A024、A025 | 待测试计划登记 | 缺浏览记录写入与设置查询,尚未闭环 | +| X03 | M09 站内消息—罗皓晨 | 买家/商家消息中心 | A501~A505 | 待测试计划登记 | HTTP 草案已汇总,事件契约待确认 | +| X04 | M10 售后流程—张海洋 | 买家售后申请、商家售后审核 | A411~A419、A431~A433;缺 A434 | 待测试计划登记 | 缺买家退货提交接口,内部退款边界待修正 | +| C01 | 秒杀与防超卖—朱惠惠 | 秒杀活动页、商家活动管理、压测脚本 | A220~A228;订单查询复用 A302/A303 | 待挑战测试登记 | A229/A230 边界冲突,暂不实施 | +| C03 | 订单超时取消—韦乾强 | 订单倒计时、Worker 验收脚本 | 复用 A304;超时扫描为 Worker 内部契约 | 待挑战测试登记 | 无新增外部 HTTP,内部任务契约待确认 | +| C04 | 商品搜索进阶—顾欣月 | 商品搜索页、性能对比报告 | A102 | 待挑战测试登记 | 接口草案已汇总,待交叉评审 | +| C06 | 实时消息推送—罗皓晨 | 实时通知、连接状态、消息中心 | SignalR 契约(无 Axxx),补查复用 A501~A505 | 待挑战测试登记 | 实时与持久化边界待确认 | +| C07 | 缓存与性能优化—罗皓晨,顾欣月协作 | 首页、商品详情、压测报告 | 复用 A102、A103 | 待挑战测试登记 | 不新增业务 HTTP,缓存约定待确认 | +| C08 | 支付回调幂等与对账—张海洋 | 回调重放脚本、管理端对账 | A421~A425、A431~A433 | 待挑战测试登记 | 接口草案已汇总,回调与退款边界待修正 | +| C10 | 容器化与负载均衡—罗皓晨 | 统一访问入口、健康与实例验证 | A506、A507 | 待部署验收登记 | 健康检查草案已汇总,待交叉评审 | + +负责人补齐缺少接口并完成交叉评审后,应把对应状态更新为“已确认”;生成真实 OpenAPI 后再补充 `operationId` 校验结果。测试计划完成后,应替换为真实测试用例编号,并随需求变更同步维护状态。 ### 9.2 已确认范围 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-gxy.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" similarity index 98% rename from "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-gxy.md" rename to "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" index e010738..22f7fba 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-gxy.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" @@ -1,20 +1,19 @@ # 接口设计(顾欣月)— Catalog、Review -> 组别:24级1班第7组 编写人:顾欣月 编写日期:2026-07-24 版本:v0.2 +> 组别:24级1班第7组 编写人:顾欣月 编写日期:2026-07-24 版本:v0.1 > 编号区间:`A101`~`A200` 负责模块:Catalog(商品目录)、Review(评价) ## 修订记录 | 版本 | 日期 | 修改人 | 修改说明 | |------|------|--------|----------| -| v0.1 | 2026-07-24 | 顾欣月 | 建立 Catalog、Review 接口清单与 A101~A143 详细定义 | -| v0.2 | 2026-07-24 | 顾欣月 | 状态枚举统一为对外 PascalCase 并新增枚举附录;清理未触发错误码;补充上传体积码与显示名依赖的待确认项 | +| v0.1 | 2026-07-24 | 顾欣月 | 建立并完善 Catalog、Review 接口清单、A101~A143 详细定义与枚举附录 | ## 一、说明与约定引用 -- 本文件是《[接口设计.md](接口设计.md)》第二章要求的个人协作文件,只登记本人 `A101`~`A200` 区间、本人负责模块的 HTTP 接口,通过交叉评审后由罗皓晨汇总进总设计文档,随后删除本文件。 -- 通用约定(前缀、鉴权、成功/失败响应包装、ProblemDetails、分页、幂等、状态码等)一律以《接口设计.md》第一章为准,本文件不重复,只在接口内标注差异。 -- 命名(模块词根、路径、`operationId`、Schema、错误码、字段大小写)以《[命名规范.md](命名规范.md)》为准:Catalog 词根 `categories`/`products`/`product_images`,Review 词根 `reviews`/`review_images`。 +- 本文件是《[接口设计.md](../接口设计.md)》第二章要求的个人协作文件,只登记本人 `A101`~`A200` 区间和本人负责模块的 HTTP 接口。汇总后继续保留用于贡献与评审追踪;实现、OpenAPI 和联调一律以总《接口设计》为准。 +- 通用约定(前缀、鉴权、成功/失败响应包装、ProblemDetails、分页、幂等、状态码等)一律以总《接口设计》第一章为准,本文件不重复,只在接口内标注差异。 +- 命名(模块词根、路径、`operationId`、Schema、错误码、字段大小写)以《[命名规范.md](../命名规范.md)》为准:Catalog 词根 `categories`/`products`/`product_images`,Review 词根 `reviews`/`review_images`。 - 状态枚举对外统一使用英文 `PascalCase`(规范 2.2),不暴露整数序号;数据库落库使用 `lower_snake_case`,二者映射见第六章枚举附录。本文件所有 `status`、`stockStatus`、`reason` 字段值均为对外 PascalCase。 - 关联的 `DBxxx` 为本人 `DB021`~`DB040` 区间的临时登记,需与 `database-gxy.md` 交叉确认后固定;当前标记“待数据库确认”。 - 覆盖需求:M02-01(F04、F05)、M02-02(F06)、M06-01(F11)、M07(X01)、C04;其中 C04 复用 M02-01 的同一列表接口,仅替换底层搜索实现,返回口径不变。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-lhc.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" similarity index 98% rename from "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-lhc.md" rename to "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" index 1fd4033..fb2b8a2 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-lhc.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" @@ -8,7 +8,7 @@ ## 一、范围与设计结论 -本文件只登记罗皓晨负责的 Messaging HTTP 接口和 M00 公共健康检查接口。全部接口遵循[《接口设计》](接口设计.md)第一章通用约定;本文未重复定义的认证、响应包装、ProblemDetails、分页和安全规则均以该文档为准。 +本文件只登记罗皓晨负责的 Messaging HTTP 接口和 M00 公共健康检查接口。全部接口遵循[《接口设计》](../接口设计.md)第一章通用约定;本文未重复定义的认证、响应包装、ProblemDetails、分页和安全规则均以该文档为准。汇总后本文件继续保留用于贡献与评审追踪,但不得覆盖总文档中的最终契约。 本轮范围结论: @@ -676,7 +676,7 @@ |---|---:|---| | `MESSAGE.NOT_FOUND` | 404 | 消息不存在或不属于当前用户 | -认证、验证、限流、依赖不可用和未知错误复用[《接口设计》](接口设计.md)第一章登记的通用错误码,不创建同义错误码。 +认证、验证、限流、依赖不可用和未知错误复用[《接口设计》](../接口设计.md)第一章登记的通用错误码,不创建同义错误码。 ## 七、跨模块影响与待确认项 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-tyh.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" similarity index 99% rename from "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-tyh.md" rename to "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" index 8d3432b..6efedcf 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-tyh.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" @@ -4,7 +4,7 @@ > 负责模块:Identity(注册、登录退出、JWT、用户资料、收货地址、后台账号治理)、Engagement(收藏、浏览历史) > 关联教师验收编号:F01、F02、F03、F13、X02 > 当前状态:部分定义;清单已给出,详细定义按接口设计 1.20 模板补齐 -> 本文件为协作阶段材料,评审通过后由罗皓晨汇总到 `接口设计.md` +> 本文件为协作与评审追踪材料;汇总后继续保留,实现、OpenAPI 和联调以 `../接口设计.md` 为准 ## 修订记录 @@ -1304,4 +1304,4 @@ BrowsingHistorySettingResponse { #### 验证场景 - 清空本人浏览历史 → 204,后续列表为空。 -- 重复清空 → 204,幂等。 \ No newline at end of file +- 重复清空 → 204,幂等。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-wqq.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" similarity index 99% rename from "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-wqq.md" rename to "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" index 5b3f6c4..be6e144 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-wqq.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" @@ -4,6 +4,7 @@ > 模块:Ordering(订单模块)、Merchant后台订单管理 > 接口编号范围:A301~A307 > 编写日期:2026-07-24 +> 本文件为协作与评审追踪材料;汇总后继续保留,实现、OpenAPI 和联调以 `../接口设计.md` 为准 ## 接口清单 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhh.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" similarity index 99% rename from "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhh.md" rename to "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" index a7bca4c..5745c0e 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhh.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" @@ -4,7 +4,7 @@ > 负责模块:Cart(购物车 CRUD、选中、结算预览、清空)、Seckill(商家活动维护、买家抢购下单、秒杀订单查询) > 关联教师验收编号:F07、C01 > 当前状态:部分定义;清单已给出,详细定义按接口设计 1.20 模板补齐 -> 本文件为协作阶段材料,评审通过后由罗皓晨汇总到 `接口设计.md` +> 本文件为协作与评审追踪材料;汇总后继续保留,实现、OpenAPI 和联调以 `../接口设计.md` 为准 ## 修订记录 @@ -1222,4 +1222,4 @@ SeckillOrderDetailResponse { - 跨用户访问 → 404,不泄露归属。 - 已支付订单 → `paymentInfo` 返回;未支付订单不返回。 - 已取消订单 → `cancelledAt` 与取消节点返回。 -- 普通订单(非秒杀)通过此接口访问 → 404,避免与 M04 详情接口混淆。 \ No newline at end of file +- 普通订单(非秒杀)通过此接口访问 → 404,避免与 M04 详情接口混淆。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhy.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" similarity index 99% rename from "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhy.md" rename to "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" index 563c83a..7e65569 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhy.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" @@ -4,7 +4,7 @@ > **负责人**:张海洋(zhy) > **范围**:M05-01 模拟支付 + M10 售后流程 + C08 支付回调幂等与对账 > **创建日期**:2026-07-24 -> **当前状态**:个人协作评审中(未入主文档,由 zhy 维护;评审通过后由罗皓晨汇总到 `接口设计.md`) +> **当前状态**:已汇总到主文档,个人文件继续保留用于贡献与评审追踪;实现、OpenAPI 和联调以 `../接口设计.md` 为准 > **关联规范**:`docs/02-设计文档/接口设计.md` v0.1 + `docs/02-设计文档/命名规范.md` + `docs/02-设计文档/Git团队协作流程.md` > **关联根命名空间**:`Mall.Modules.Payment`、`Mall.Modules.AfterSales` > **关联数据库**:DB081~DB100(**待评审**:`database-zhy.md` 尚未创建,本文档字段暂时按命名规范推断) @@ -2177,5 +2177,5 @@ ### 4.3 汇总时机 - 全部接口评审通过后由罗皓晨按编号汇总到 `docs/02-设计文档/接口设计.md` 第三章 -- 个人文件 `interface-zhy.md` 在汇总同 PR 中删除 +- 个人文件 `interface-zhy.md` 汇总后继续保留,不单独作为实现事实源 - 历史贡献通过 Git 记录保留 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" index c3cfe99..8a8ac04 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" @@ -100,8 +100,9 @@ - Markdown业务文档可使用清晰中文名称;同一目录存在编号体系时延续现有编号。 - 图片和附件使用小写 `kebab-case`,例如 `order-checkout-flow.png`,不使用 `截图1.png`、`最终版2.png`。 - 文件名不得包含姓名、日期或版本,除非日报、周报、Migration、发布材料等规则明确要求。 -- 接口与数据库并行设计阶段允许在 `docs/02-设计文档/` 创建个人协作文件,固定使用 `interface-<姓名拼音首字母>.md` 和 `database-<姓名拼音首字母>.md`,例如 `interface-tyh.md`、`database-lhc.md`;首字母必须全小写,不创建个人文件夹。 -- 个人接口与数据库文件只用于协作和交叉评审。内容汇总到 `接口设计.md`、`数据库设计.md` 并确认无遗漏后删除个人文件,最终事实源仍只有两份总设计文档。 +- 接口并行设计阶段统一在 `docs/02-设计文档/interface/` 保存个人原稿,固定使用 `interface-<姓名拼音首字母>.md`,例如 `interface-tyh.md`;首字母必须全小写。 +- 数据库并行设计阶段仍在 `docs/02-设计文档/` 保存 `database-<姓名拼音首字母>.md`,例如 `database-lhc.md`,不另建个人文件夹。 +- 个人接口原稿用于贡献与交叉评审追踪,汇总后继续保留;`接口设计.md` 是实现、OpenAPI、联调和测试的唯一接口事实源,个人原稿不得覆盖总文档。个人数据库文件按《数据库设计》的汇总规则处理,最终数据库事实源仍为 `数据库设计.md`。 ## 四、Vue 3、TypeScript、Vite、Pinia、Axios与UI diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" index b70c3fc..1dd3161 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" @@ -7,7 +7,7 @@ | 版本 | 日期 | 修改人 | 修改说明 | |------|------|--------|----------| -| v0.1 | 2026-07-24 | 罗皓晨 | 完善接口通用约定,增加 Axxx 六人编号区间、登记规则和详细定义模板 | +| v0.1 | 2026-07-24 | 罗皓晨、各模块负责人 | 建立通用约定,汇总六份个人接口原稿、102 个 Axxx 清单与详细定义,并登记冻结阻塞项 | ## 一、通用约定 @@ -550,30 +550,7218 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 编号规则: 1. 一个 HTTP 方法与路径组合占用一个接口编号;同一路径使用不同方法时分别编号。 -2. 编号合入 `dev` 后保持稳定。接口重命名但业务含义不变时保留编号;业务含义根本变化时使用新编号。 -3. 废弃接口保留原编号并标记“已废弃”,不得把该编号重新分配给其他接口。 -4. SignalR 事件、领域事件、集成事件、Redis Key、RabbitMQ 资源和对象存储路径不占用 Axxx 编号,分别遵循《命名规范》的对应章节。 -5. 不得为了占满区间提前设计无需求依据的接口;超出本人区间时由全组评审后重新分配。 -6. 每个成员必须在本人 `interface-<姓名拼音首字母>.md` 中同时维护接口清单和同编号详细定义;只有清单而没有详细定义时,接口仍属于“部分定义”,不得直接作为编码依据。 +2. 编号合入 `dev` 后保持稳定。接口重命名但业务含义不变时保留编号;废弃接口保留原编号并标记“已废弃”,不得复用。 +3. SignalR 事件、领域事件、集成事件、Redis Key、RabbitMQ 资源和对象存储路径不占用 Axxx。 +4. 不得为了占满区间提前设计无需求依据的接口。 +5. 只有本文件中同时具备清单、同编号详细定义且状态为“已确认”的接口,才可作为实现与 OpenAPI 事实源。 -### 2.2 个人接口文件与汇总要求 +### 2.2 个人接口文件与保留规则 -六名成员分别在 `docs/02-设计文档/` 下创建以下文件,不创建个人文件夹: +六份个人原稿统一保存在 `docs/02-设计文档/interface/`: -| 负责人 | 个人接口文件 | 接口编号范围 | +| 负责人 | 个人接口文件 | 已登记数量 | 接口编号范围 | +|---|---|---:|---| +| 唐宇昊 | [`interface-tyh.md`](interface/interface-tyh.md) | 23 | `A001`~`A100` | +| 顾欣月 | [`interface-gxy.md`](interface/interface-gxy.md) | 21 | `A101`~`A200` | +| 朱惠惠 | [`interface-zhh.md`](interface/interface-zhh.md) | 19 | `A201`~`A300` | +| 韦乾强 | [`interface-wqq.md`](interface/interface-wqq.md) | 7 | `A301`~`A400` | +| 张海洋 | [`interface-zhy.md`](interface/interface-zhy.md) | 25 | `A401`~`A500` | +| 罗皓晨 | [`interface-lhc.md`](interface/interface-lhc.md) | 7 | `A501`~`A600` | + +保留与同步规则: + +1. 个人原稿保留用于成员贡献、原始设计和交叉评审追踪,不再删除。 +2. 本文件是实现、OpenAPI、联调和测试的唯一接口事实源;个人原稿与本文件冲突时,不得直接按个人原稿编码。 +3. 负责人修正个人原稿时,必须在同一任务中同步本文件的统一清单和同编号详细定义;只修改个人原稿不构成契约变更完成。 +4. 总文档不得掩盖个人原稿中的缺口。尚未确认的字段、状态、跨模块边界或 DBxxx 必须标记为“部分定义”或“待交叉评审”。 +5. 当前共汇总 102 个不重复编号;编号、`operationId` 和“方法 + 路径”未发现全局重复,但这不代表全部接口已经冻结。 + +### 2.3 统一接口登记 + +#### 唐宇昊(A001~A100) + +| 编号 | 模块/Tag | 需求 | 名称 | 方法 | 路径 | operationId | 鉴权 | 汇总状态 | +|---|---|---|---|---|---|---|---|---| +| A001 | Identity | F01 | 买家注册 | POST | `/api/auth/register` | `Identity_RegisterUser` | 允许游客 | 待交叉评审 | +| A002 | Identity | F02 | 登录 | POST | `/api/auth/login` | `Identity_Login` | 允许游客 | 待交叉评审 | +| A003 | Identity | F02 | 退出当前令牌 | POST | `/api/auth/logout` | `Identity_Logout` | BuyerOnly / MerchantOnly / AdminOnly | 待交叉评审 | +| A004 | Identity | F02 | 获取当前用户 | GET | `/api/auth/me` | `Identity_GetCurrentUser` | BuyerOnly / MerchantOnly / AdminOnly | 待交叉评审 | +| A005 | Identity | F02 | 刷新访问令牌 | POST | `/api/auth/refresh-token` | `Identity_RefreshToken` | 已认证用户 | 待交叉评审 | +| A006 | Identity | F03 | 修改手机号 | POST | `/api/auth/change-phone` | `Identity_ChangePhone` | BuyerOnly / MerchantOnly | 待交叉评审 | +| A007 | Identity | F03 | 重置用户名 | POST | `/api/auth/reset-username` | `Identity_ResetUsername` | BuyerOnly / MerchantOnly | 待交叉评审 | +| A008 | Identity | F03 | 获取本人资料 | GET | `/api/users/me` | `Identity_GetMyProfile` | BuyerOnly / MerchantOnly | 待交叉评审 | +| A009 | Identity | F03 | 修改本人资料 | PATCH | `/api/users/me` | `Identity_UpdateMyProfile` | BuyerOnly / MerchantOnly | 待交叉评审 | +| A010 | Identity | F03 | 我的地址列表 | GET | `/api/users/me/addresses` | `Identity_ListMyAddresses` | BuyerOnly / MerchantOnly | 待交叉评审 | +| A011 | Identity | F03 | 新增地址 | POST | `/api/users/me/addresses` | `Identity_CreateMyAddress` | BuyerOnly / MerchantOnly | 待交叉评审 | +| A012 | Identity | F03 | 编辑地址 | PATCH | `/api/users/me/addresses/{addressId}` | `Identity_UpdateMyAddress` | BuyerOnly / MerchantOnly | 待交叉评审 | +| A013 | Identity | F03 | 删除地址 | DELETE | `/api/users/me/addresses/{addressId}` | `Identity_DeleteMyAddress` | BuyerOnly / MerchantOnly | 待交叉评审 | +| A014 | Identity | F03 | 设置默认地址 | POST | `/api/users/me/addresses/{addressId}/default` | `Identity_SetDefaultAddress` | BuyerOnly / MerchantOnly | 待交叉评审 | +| A015 | Identity | F13 | 后台账号列表 | GET | `/api/admin/users` | `Identity_AdminListUsers` | AdminOnly | 待交叉评审 | +| A016 | Identity | F13 | 禁用账号 | POST | `/api/admin/users/{userId}/disable` | `Identity_AdminDisableUser` | AdminOnly | 待交叉评审 | +| A017 | Identity | F13 | 启用账号 | POST | `/api/admin/users/{userId}/enable` | `Identity_AdminEnableUser` | AdminOnly | 待交叉评审 | +| A018 | Engagement | X02 | 收藏列表 | GET | `/api/favorites` | `Engagement_ListFavorites` | BuyerOnly | 待交叉评审 | +| A019 | Engagement | X02 | 收藏商品 | POST | `/api/favorites` | `Engagement_AddFavorite` | BuyerOnly | 待交叉评审 | +| A020 | Engagement | X02 | 取消收藏 | DELETE | `/api/favorites/{productId}` | `Engagement_RemoveFavorite` | BuyerOnly | 待交叉评审 | +| A021 | Engagement | X02 | 浏览历史列表 | GET | `/api/browsing-history` | `Engagement_ListBrowsingHistory` | BuyerOnly | 待交叉评审 | +| A022 | Engagement | X02 | 修改浏览记录开关 | PATCH | `/api/browsing-history/settings` | `Engagement_UpdateBrowsingHistorySetting` | BuyerOnly | 待交叉评审 | +| A023 | Engagement | X02 | 清空浏览历史 | DELETE | `/api/browsing-history` | `Engagement_ClearBrowsingHistory` | BuyerOnly | 待交叉评审 | + +#### 顾欣月(A101~A200) + +| 编号 | 模块/Tag | 需求 | 名称 | 方法 | 路径 | operationId | 鉴权 | 汇总状态 | +|---|---|---|---|---|---|---|---|---| +| A101 | Catalog | M02-01-FR02 | 购物端有效分类列表 | GET | `/api/categories` | `Catalog_ListCategories` | 游客可访问 | 待交叉评审 | +| A102 | Catalog | M02-01、F05、C04 | 商品分页列表/搜索/筛选/排序 | GET | `/api/products` | `Catalog_ListProducts` | 游客可访问 | 待交叉评审 | +| A103 | Catalog | M02-02、F06 | 购物端商品详情 | GET | `/api/products/{productId}` | `Catalog_GetProduct` | 游客可访问 | 待交叉评审 | +| A110 | Catalog | M06-01-FR01 | 后台分类列表(全状态) | GET | `/api/merchant/categories` | `Catalog_ListMerchantCategories` | MerchantOnly | 待交叉评审 | +| A111 | Catalog | M06-01-FR02 | 新建分类 | POST | `/api/merchant/categories` | `Catalog_CreateCategory` | MerchantOnly | 待交叉评审 | +| A112 | Catalog | M06-01-FR02 | 编辑分类 | PUT | `/api/merchant/categories/{categoryId}` | `Catalog_UpdateCategory` | MerchantOnly | 待交叉评审 | +| A113 | Catalog | M06-01-FR02 | 启用分类 | POST | `/api/merchant/categories/{categoryId}/enable` | `Catalog_EnableCategory` | MerchantOnly | 待交叉评审 | +| A114 | Catalog | M06-01-FR03 | 停用分类 | POST | `/api/merchant/categories/{categoryId}/disable` | `Catalog_DisableCategory` | MerchantOnly | 待交叉评审 | +| A120 | Catalog | M06-01-FR04 | 后台商品分页(全状态) | GET | `/api/merchant/products` | `Catalog_ListMerchantProducts` | MerchantOnly | 待交叉评审 | +| A121 | Catalog | M06-01-FR04 | 后台商品详情 | GET | `/api/merchant/products/{productId}` | `Catalog_GetMerchantProduct` | MerchantOnly | 待交叉评审 | +| A122 | Catalog | M06-01-FR05 | 新建商品 | POST | `/api/merchant/products` | `Catalog_CreateProduct` | MerchantOnly | 待交叉评审 | +| A123 | Catalog | M06-01-FR06、FR10 | 编辑商品(乐观并发) | PUT | `/api/merchant/products/{productId}` | `Catalog_UpdateProduct` | MerchantOnly | 待交叉评审 | +| A124 | Catalog | M06-01-FR08 | 删除商品(受约束) | DELETE | `/api/merchant/products/{productId}` | `Catalog_DeleteProduct` | MerchantOnly | 待交叉评审 | +| A125 | Catalog | M06-01-FR07 | 商品上架 | POST | `/api/merchant/products/{productId}/publish` | `Catalog_PublishProduct` | MerchantOnly | 待交叉评审 | +| A126 | Catalog | M06-01-FR07 | 商品下架 | POST | `/api/merchant/products/{productId}/unpublish` | `Catalog_UnpublishProduct` | MerchantOnly | 待交叉评审 | +| A127 | Catalog | M06-01-FR09 | 上传商品图片 | POST | `/api/merchant/products/{productId}/images` | `Catalog_UploadProductImage` | MerchantOnly | 待交叉评审 | +| A128 | Catalog | M06-01-FR09 | 删除商品图片 | DELETE | `/api/merchant/products/{productId}/images/{imageId}` | `Catalog_DeleteProductImage` | MerchantOnly | 待交叉评审 | +| A140 | Review | M07-FR06、FR07 | 商品公开评价分页 + 评分汇总 | GET | `/api/products/{productId}/reviews` | `Review_ListProductReviews` | 游客可访问 | 待交叉评审 | +| A141 | Review | M07-FR03 | 上传评价图片(提交前暂存) | POST | `/api/reviews/images` | `Review_UploadReviewImage` | BuyerOnly | 待交叉评审 | +| A142 | Review | M07-FR04、FR05 | 提交商品评价(幂等) | POST | `/api/reviews` | `Review_CreateReview` | BuyerOnly | 待交叉评审 | +| A143 | Review | M07-FR01 | 查询订单项评价资格/结果 | GET | `/api/reviews/eligibility` | `Review_GetReviewEligibility` | BuyerOnly | 待交叉评审 | + +#### 朱惠惠(A201~A300) + +| 编号 | 模块/Tag | 需求 | 名称 | 方法 | 路径 | operationId | 鉴权 | 汇总状态 | +|---|---|---|---|---|---|---|---|---| +| A201 | Cart | F07 | 加入购物车 | POST | `/api/cart/items` | `Cart_AddItem` | BuyerOnly | 待交叉评审 | +| A202 | Cart | F07 | 查看购物车 | GET | `/api/cart/items` | `Cart_ListItems` | BuyerOnly | 待交叉评审 | +| A203 | Cart | F07 | 修改购物车条目数量 | PATCH | `/api/cart/items/{cartItemId}` | `Cart_UpdateItemQuantity` | BuyerOnly | 待交叉评审 | +| A204 | Cart | F07 | 删除购物车条目 | DELETE | `/api/cart/items/{cartItemId}` | `Cart_RemoveItem` | BuyerOnly | 待交叉评审 | +| A205 | Cart | F07 | 批量删除购物车条目 | POST | `/api/cart/items/batch-delete` | `Cart_BatchRemoveItems` | BuyerOnly | 待交叉评审 | +| A206 | Cart | F07 | 修改选中状态(全选/反选/单选) | PATCH | `/api/cart/items/selection` | `Cart_UpdateSelection` | BuyerOnly | 待交叉评审 | +| A207 | Cart | F07 | 清空购物车 | DELETE | `/api/cart` | `Cart_Clear` | BuyerOnly | 待交叉评审 | +| A208 | Cart | F07 | 获取结算预览 | GET | `/api/cart/checkout-preview` | `Cart_GetCheckoutPreview` | BuyerOnly | 待交叉评审 | +| A220 | Seckill | C01 | 商家创建秒杀活动 | POST | `/api/merchant/seckill-activities` | `Seckill_CreateActivity` | MerchantOnly | 待交叉评审 | +| A221 | Seckill | C01 | 商家更新秒杀活动 | PATCH | `/api/seckill-activities/{activityId}` | `Seckill_UpdateActivity` | MerchantOnly | 待交叉评审 | +| A222 | Seckill | C01 | 商家发布秒杀活动 | POST | `/api/seckill-activities/{activityId}/publish` | `Seckill_PublishActivity` | MerchantOnly | 待交叉评审 | +| A223 | Seckill | C01 | 商家取消秒杀活动 | POST | `/api/seckill-activities/{activityId}/cancel` | `Seckill_CancelActivity` | MerchantOnly | 待交叉评审 | +| A224 | Seckill | C01 | 商家秒杀活动列表 | GET | `/api/merchant/seckill-activities` | `Seckill_ListMerchantActivities` | MerchantOnly | 待交叉评审 | +| A225 | Seckill | C01 | 商家秒杀活动详情 | GET | `/api/seckill-activities/{activityId}` | `Seckill_GetMerchantActivityDetail` | MerchantOnly | 待交叉评审 | +| A226 | Seckill | C01 | 买家秒杀活动列表 | GET | `/api/seckill-activities` | `Seckill_ListActiveActivities` | 允许游客 | 待交叉评审 | +| A227 | Seckill | C01 | 买家秒杀活动详情 | GET | `/api/seckill-activities/{activityId}/public` | `Seckill_GetActiveActivityDetail` | 允许游客 | 待交叉评审 | +| A228 | Seckill | C01 | 秒杀下单 | POST | `/api/seckill-orders` | `Seckill_PlaceOrder` | BuyerOnly | 待交叉评审 | +| A229 | Seckill | C01 | 买家秒杀订单列表 | GET | `/api/seckill-orders` | `Seckill_ListMyOrders` | BuyerOnly | 边界冲突,暂不实施 | +| A230 | Seckill | C01 | 买家秒杀订单详情 | GET | `/api/seckill-orders/{orderId}` | `Seckill_GetMyOrder` | BuyerOnly | 边界冲突,暂不实施 | + +#### 韦乾强(A301~A400) + +| 编号 | 模块/Tag | 需求 | 名称 | 方法 | 路径 | operationId | 鉴权 | 汇总状态 | +|---|---|---|---|---|---|---|---|---| +| A301 | Ordering | F08 | 提交订单 | POST | `/api/orders` | `Ordering_CreateOrder` | BuyerOnly | 部分定义 | +| A302 | Ordering | F09 | 查询订单列表 | GET | `/api/orders` | `Ordering_GetOrders` | BuyerOnly | 部分定义 | +| A303 | Ordering | F09 | 查询订单详情 | GET | `/api/orders/{orderId}` | `Ordering_GetOrderById` | BuyerOnly | 部分定义 | +| A304 | Ordering | F09 | 取消订单 | POST | `/api/orders/{orderId}/cancel` | `Ordering_CancelOrder` | BuyerOnly | 部分定义 | +| A305 | Merchant | F12 | 商家查询订单列表 | GET | `/api/merchant/orders` | `Merchant_GetOrders` | MerchantOnly | 部分定义 | +| A306 | Merchant | F12 | 商家查询订单详情 | GET | `/api/merchant/orders/{orderId}` | `Merchant_GetOrderById` | MerchantOnly | 部分定义 | +| A307 | Merchant | F12 | 商家发货 | POST | `/api/merchant/orders/{orderId}/ship` | `Merchant_ShipOrder` | MerchantOnly | 部分定义 | + +#### 张海洋(A401~A500) + +| 编号 | 模块/Tag | 需求 | 名称 | 方法 | 路径 | operationId | 鉴权 | 汇总状态 | +|---|---|---|---|---|---|---|---|---| +| A401 | Payment | M05-01-FR01 | 查询钱包余额 | GET | `/api/payment/wallet` | `Payment_GetWalletBalance` | BuyerOnly | 待交叉评审 | +| A402 | Payment | M05-01-FR02/FR03 | 模拟充值 | POST | `/api/payment/wallet/topups` | `Payment_CreateTopup` | BuyerOnly | 待交叉评审 | +| A403 | Payment | M05-01-FR04 | 查询充值记录 | GET | `/api/payment/wallet/topups` | `Payment_ListTopups` | BuyerOnly | 待交叉评审 | +| A404 | Payment | M05-01-FR05 | 收银台查询 | GET | `/api/payment/checkout/{orderId}` | `Payment_GetCheckout` | BuyerOnly | 待交叉评审 | +| A405 | Payment | M05-01-FR05~FR09 | 模拟支付 | POST | `/api/payment/orders/{orderId}/pay` | `Payment_PayOrder` | BuyerOnly | 待交叉评审 | +| A406 | Payment | M05-01-FR09 | 查询订单支付结果 | GET | `/api/payment/orders/{orderId}` | `Payment_GetPaymentByOrder` | BuyerOnly | 待交叉评审 | +| A407 | Payment | M05-01-FR07 | 支付记录列表 | GET | `/api/payments` | `Payment_ListPayments` | BuyerOnly | 待交叉评审 | +| A408 | Payment | M05-01-FR07 | 支付详情 | GET | `/api/payments/{paymentId}` | `Payment_GetPayment` | BuyerOnly | 待交叉评审 | +| A411 | AfterSales | M10-FR01 | 售后资格预检 | GET | `/api/after-sales/eligibility` | `AfterSales_CheckEligibility` | BuyerOnly | 待交叉评审 | +| A412 | AfterSales | M10-FR02 | 提交售后申请 | POST | `/api/after-sales/requests` | `AfterSales_CreateRequest` | BuyerOnly | 待交叉评审 | +| A413 | AfterSales | M10-FR03 | 申请列表 | GET | `/api/after-sales/requests` | `AfterSales_ListRequests` | BuyerOnly/MerchantOnly | 待交叉评审 | +| A414 | AfterSales | M10-FR04 | 申请详情 | GET | `/api/after-sales/requests/{requestId}` | `AfterSales_GetRequest` | BuyerOnly/MerchantOnly | 待交叉评审 | +| A415 | AfterSales | M10-FR10 | 撤销申请 | POST | `/api/after-sales/requests/{requestId}/cancel` | `AfterSales_CancelRequest` | BuyerOnly | 待交叉评审 | +| A416 | AfterSales | M10-FR05 | 商家审核 | POST | `/api/after-sales/requests/{requestId}/audit` | `AfterSales_AuditRequest` | MerchantOnly | 待交叉评审 | +| A417 | AfterSales | M10-FR11 | 商家确认退货 | POST | `/api/after-sales/requests/{requestId}/confirm-return` | `AfterSales_ConfirmReturn` | MerchantOnly | 待交叉评审 | +| A418 | AfterSales | M10-FR04 | 审核日志 | GET | `/api/after-sales/requests/{requestId}/audit-logs` | `AfterSales_ListAuditLogs` | BuyerOnly/MerchantOnly | 待交叉评审 | +| A419 | AfterSales | M10-FR07 | 退款失败重试 | POST | `/api/after-sales/requests/{requestId}/retry-refund` | `AfterSales_RetryRefund` | MerchantOnly | 待交叉评审 | +| A421 | Payment | C08-FR01~FR05 | 接收支付回调 | POST | `/api/payment/callbacks` | `Payment_ReceiveCallback` | Service(模拟渠道) | 待交叉评审 | +| A422 | Reconciliation | C08-FR06 | 对账批次列表 | GET | `/api/admin/reconciliation/batches` | `Reconciliation_ListBatches` | AdminOnly | 待交叉评审 | +| A423 | Reconciliation | C08-FR06 | 对账批次详情 | GET | `/api/admin/reconciliation/batches/{batchId}` | `Reconciliation_GetBatch` | AdminOnly | 待交叉评审 | +| A424 | Reconciliation | C08-FR07/FR08 | 差异列表 | GET | `/api/admin/reconciliation/batches/{batchId}/differences` | `Reconciliation_ListDifferences` | AdminOnly | 待交叉评审 | +| A425 | Reconciliation | C08-FR08 | 差异处理 | POST | `/api/admin/reconciliation/differences/{differenceId}/process` | `Reconciliation_ProcessDifference` | AdminOnly | 待交叉评审 | +| A431 | Payment | M10-FR07 | 模拟退款 | POST | `/api/after-sales/requests/{requestId}/refund` | `Refund_Create` | MerchantOnly(系统内部) | 待交叉评审 | +| A432 | Payment | M10-FR04 | 退款详情 | GET | `/api/refunds/{refundId}` | `Refund_Get` | BuyerOnly/MerchantOnly | 待交叉评审 | +| A433 | Payment | M10-FR03 | 退款列表 | GET | `/api/refunds` | `Refund_List` | BuyerOnly/MerchantOnly | 待交叉评审 | + +#### 罗皓晨(A501~A600) + +| 编号 | 模块/Tag | 需求 | 名称 | 方法 | 路径 | operationId | 鉴权 | 汇总状态 | +|---|---|---|---|---|---|---|---|---| +| A501 | Messaging | X03-FR03 | 查询本人消息列表 | GET | `/api/messages` | `Messaging_ListMessages` | 买家或商家 JWT | 待交叉评审 | +| A502 | Messaging | X03-FR04 | 查询本人消息详情 | GET | `/api/messages/{messageId}` | `Messaging_GetMessage` | 买家或商家 JWT | 待交叉评审 | +| A503 | Messaging | X03-FR05 | 查询本人未读消息数 | GET | `/api/messages/unread-count` | `Messaging_GetUnreadCount` | 买家或商家 JWT | 待交叉评审 | +| A504 | Messaging | X03-FR06 | 标记本人单条消息已读 | POST | `/api/messages/{messageId}/read` | `Messaging_MarkMessageRead` | 买家或商家 JWT | 待交叉评审 | +| A505 | Messaging | X03-FR07 | 标记本人当前消息全部已读 | POST | `/api/messages/read-all` | `Messaging_MarkAllMessagesRead` | 买家或商家 JWT | 待交叉评审 | +| A506 | M00 | C10-FR06 | API 存活检查 | GET | `/health/live` | `Infrastructure_GetLiveness` | 无 | 待交叉评审 | +| A507 | M00 | C10-FR06 | API 就绪检查 | GET | `/health/ready` | `Infrastructure_GetReadiness` | 无 | 待交叉评审 | + +## 三、统一接口详细定义 + +以下内容按 Axxx 编号区间汇总。每段开头保留个人原稿来源;汇总状态以第二章和第五章为准,原稿内的“已定义/已设计”不能替代交叉评审。 + +> 来源:[interface-tyh.md](interface/interface-tyh.md)。已完成结构汇总,但仍须处理身份范围、浏览历史写入和错误语义后才能冻结。 + +> 每个接口按《接口设计》1.20 节模板补齐。Schema 名称遵守 OpenAPI 7.2 节:PascalCase + 用途后缀;`operationId` 使用 `_`;路径参数使用单数对象 + `Id`。 + +### A001 买家注册 + +- 模块 / Tag:Identity +- 需求编号:F01、M01-01 +- 负责人:唐宇昊 +- 关联数据表:DB001 +- 当前状态:待交叉评审 +- 用途:游客使用手机号和密码创建买家账号;系统生成全局唯一用户名与默认头像,并始终产出 Buyer 角色。 +- 方法与路径:`POST /api/auth/register` +- operationId:`Identity_RegisterUser` + +#### 请求 + +- Route 参数:无 +- Query 参数:无 +- Header:无强制要求 +- Body: + +```text +RegisterUserRequest { + phone: string // 必填,^1[3-9]\d{9}$,中国大陆 11 位手机号 + password: string // 必填,8~16 位且同时包含字母和数字 + confirmPassword: string // 必填,必须等于 password +} +``` + +- 校验规则: + - `phone` 必须匹配 `^1[3-9]\d{9}$`,不接受 `+86`、`0086`、固话、空格。 + - `password` 长度 8~16,必须同时包含字母和数字;不得等于 `phone`、不得等于 `phone` 倒序字符串。 + - `confirmPassword` 必须等于 `password`。 + - 服务端忽略请求中任何尝试指定 `role`、`username`、`status` 的字段;公开注册结果固定为 Buyer。 + +#### 成功响应 + +- HTTP 状态:`201 Created` +- Response Header:`Location: /api/users/me` +- 响应 Schema:`RegisteredUserResponse` + +```text +RegisteredUserResponse { + userId: uuid + username: string // 自动生成的 u_xxxxxxxx + phoneMasked: string // 形如 138****8888 + avatarUrl: string + role: "Buyer" + createdAt: string // UTC ISO 8601 +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 手机号格式错误、密码强度不足或两次密码不一致 | +| 400 | `COMMON.MALFORMED_JSON` | 请求体无法解析 | +| 409 | `AUTH.PHONE_ALREADY_REGISTERED` | 手机号已存在有效账号 | +| 409 | `AUTH.USERNAME_GENERATION_RETRY_EXHAUSTED` | 用户名生成冲突且超过重试上限 | +| 500 | `COMMON.INTERNAL_ERROR` | 未处理的服务端错误 | + +#### 业务规则与并发 + +- 用户名生成规则:`u_` + 8 位不易混淆字符(去除 0/O/1/I/L),最多重试 3 次;最终不重复。 +- 密码使用可靠哈希算法(如 Argon2id)保存;明文密码、确认密码和哈希结果均不得出现在响应、日志或 ProblemDetails 中。 +- 手机号唯一性由 PostgreSQL 唯一约束 `ux_users_phone` 保证;并发注册同一手机号时仅一笔成功,其余返回 `409 / AUTH.PHONE_ALREADY_REGISTERED`。 +- 公开注册固定产出 Buyer;客户端传入的角色字段被忽略,且不被任何后续接口读取。 + +#### 缓存、事件或外部依赖 + +- 不缓存、不发布集成事件。 +- 成功后建议客户端调用 `A004 GetCurrentUser` 校验登录态恢复。 + +#### 验证场景 + +- 合法手机号 + 合规密码 → 201,并返回自动生成用户名。 +- 7 位密码、纯字母、纯数字、与手机号相同、与手机号倒序相同 → 400 / `COMMON.VALIDATION_FAILED`。 +- 两次密码不一致 → 400 / `COMMON.VALIDATION_FAILED`。 +- 已注册手机号 → 409 / `AUTH.PHONE_ALREADY_REGISTERED`,不暴露其他用户资料。 +- 请求体注入 `role=Admin` → 忽略字段,最终账号仍为 Buyer。 +- 并发注册同一手机号 → 仅一笔 201,另一笔 409。 + +### A002 登录 + +- 模块 / Tag:Identity +- 需求编号:F02、M01-02 +- 负责人:唐宇昊 +- 关联数据表:DB001、DB004 +- 当前状态:待交叉评审 +- 用途:用户使用手机号和密码登录,签发由任一 API 实例可验证的访问令牌与刷新令牌;登录结果在多实例间一致。 +- 方法与路径:`POST /api/auth/login` +- operationId:`Identity_Login` + +#### 请求 + +- Route 参数:无 +- Query 参数:无 +- Header:无强制要求 +- Body: + +```text +LoginRequest { + phone: string // 必填 + password: string // 必填 +} +``` + +- 校验规则:手机号做基础格式校验;密码仅做非空校验,具体错误不区分。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`LoginResponse` + +```text +LoginResponse { + accessToken: string + accessTokenExpiresAt: string // UTC ISO 8601 + refreshToken: string + refreshTokenExpiresAt: string // UTC ISO 8601 + tokenType: "Bearer" + user: CurrentUserResponse +} +``` + +其中 `CurrentUserResponse`: + +```text +CurrentUserResponse { + userId: uuid + username: string + phoneMasked: string + avatarUrl: string + role: "Buyer" | "Merchant" | "Admin" +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或格式错误 | +| 401 | `AUTH.INVALID_CREDENTIALS` | 手机号或密码错误;账号不存在统一返回 | +| 403 | `AUTH.ACCOUNT_DISABLED` | 账号已被禁用 | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态校验或令牌服务暂时不可用 | +| 500 | `COMMON.INTERNAL_ERROR` | 未处理的服务端错误 | + +#### 业务规则与并发 + +- 账号不存在和密码错误统一返回 `401 / AUTH.INVALID_CREDENTIALS`,不泄露账号是否存在。 +- 禁用账号返回 `403 / AUTH.ACCOUNT_DISABLED` 并明确说明联系管理员。 +- 签发的 JWT 至少包含 `sub`、`role`、`jti`、过期时间与 `tokenVersion`;签名、Issuer、Audience、过期、撤销、账号状态、版本号任一校验失败即拒绝。 +- 刷新令牌与访问令牌通过受控 Redis 列表记录 `jti`,实现多实例撤销共享。 +- 当令牌服务或 Redis 撤销校验不可用时,宁可拒绝登录也不放过无法确认的请求(`503 / AUTH.TOKEN_SERVICE_UNAVAILABLE`)。 + +#### 缓存、事件或外部依赖 + +- 登录成功后向 Redis 写入撤销/版本共享:`auth:revoked:{jti}` 与 `auth:user:{userId}:tokenVersion`。 +- 不发布集成事件;用户级会话不持久化到数据库。 + +#### 验证场景 + +- 正确买家账号 → 200,访问令牌 + 刷新令牌返回;切换 API 实例后同一令牌仍可通过 `A004` 校验。 +- 错误密码 → 401 / `AUTH.INVALID_CREDENTIALS`。 +- 不存在手机号 → 401 / `AUTH.INVALID_CREDENTIALS`,与错误密码文案一致。 +- 禁用账号 → 403 / `AUTH.ACCOUNT_DISABLED`。 +- Redis 撤销校验暂时不可用 → 503,不放行任何登录。 + +### A003 退出当前令牌 + +- 模块 / Tag:Identity +- 需求编号:F02、M01-02 +- 负责人:唐宇昊 +- 关联数据表:DB001、DB004 +- 当前状态:待交叉评审 +- 用途:使当前访问令牌与刷新令牌在自然过期前不可继续使用;只影响本令牌,不影响同一账号其他设备。 +- 方法与路径:`POST /api/auth/logout` +- operationId:`Identity_Logout` + +#### 请求 + +- Route 参数:无 +- Query 参数:无 +- Header:`Authorization: Bearer `(必填) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`LogoutResponse` + +```text +LogoutResponse { + revoked: true + revokedAt: string // UTC ISO 8601 +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少或无效访问令牌 | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态共享不可用 | + +#### 业务规则与并发 + +- 当前令牌与刷新令牌均被加入 Redis 撤销集合;过期时间不晚于原令牌过期时间。 +- 同一账号在其他设备的有效令牌不受影响。 +- 退出后前端必须清理本地令牌和登录态;后续 `A004` 使用已退出的令牌必须返回 `401 / AUTH.TOKEN_REVOKED`。 + +#### 缓存、事件或外部依赖 + +- Redis Key:`auth:revoked:{jti}`、`auth:revoked:refresh:{jti}`。 + +#### 验证场景 + +- 已登录用户调用 → 200;同一令牌再次访问 `A004` 返回 401 / `AUTH.TOKEN_REVOKED`。 +- 第二个设备登录后的令牌仍可正常使用。 + +### A004 获取当前用户 + +- 模块 / Tag:Identity +- 需求编号:F02、M01-02 +- 负责人:唐宇昊 +- 关联数据表:DB001 +- 当前状态:待交叉评审 +- 用途:返回当前登录账号的简要信息,用于登录态恢复与前端路由守卫。 +- 方法与路径:`GET /api/auth/me` +- operationId:`Identity_GetCurrentUser` + +#### 请求 + +- Route 参数:无 +- Query 参数:无 +- Header:`Authorization: Bearer `(必填) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`CurrentUserResponse` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少访问令牌 | +| 401 | `AUTH.TOKEN_EXPIRED` | 访问令牌已过期 | +| 401 | `AUTH.TOKEN_REVOKED` | 访问令牌已退出或账号版本失效 | +| 403 | `AUTH.ACCOUNT_DISABLED` | 账号被禁用 | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态校验不可用 | + +#### 业务规则与并发 + +- 返回字段严格按 `CurrentUserResponse`;不返回密码哈希、内部审计字段或会话信息。 +- 多个 API 实例对同一令牌结果一致。 + +#### 缓存、事件或外部依赖 + +- 不缓存;直接读取数据库与 Redis 撤销状态。 + +#### 验证场景 + +- 有效令牌 → 200,返回角色与掩码手机号。 +- 过期令牌 → 401 / `AUTH.TOKEN_EXPIRED`。 +- 已退出令牌 → 401 / `AUTH.TOKEN_REVOKED`。 + +### A005 刷新访问令牌 + +- 模块 / Tag:Identity +- 需求编号:F02、M01-02 +- 负责人:唐宇昊 +- 关联数据表:DB001、DB004 +- 当前状态:待交叉评审 +- 用途:使用有效刷新令牌换取新的访问令牌和刷新令牌。 +- 方法与路径:`POST /api/auth/refresh-token` +- operationId:`Identity_RefreshToken` + +#### 请求 + +- Body: + +```text +RefreshTokenRequest { + refreshToken: string // 必填 +} +``` + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`LoginResponse`(与 A002 一致) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 缺少刷新令牌 | +| 401 | `AUTH.TOKEN_REVOKED` | 刷新令牌已撤销 | +| 401 | `AUTH.TOKEN_EXPIRED` | 刷新令牌已过期 | +| 403 | `AUTH.ACCOUNT_DISABLED` | 账号被禁用 | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态共享不可用 | + +#### 业务规则与并发 + +- 旧刷新令牌随新令牌签发一起撤销,避免长期重放。 +- 访问令牌与刷新令牌均加入撤销集合。 + +#### 缓存、事件或外部依赖 + +- Redis Key:`auth:revoked:refresh:{jti}`。 + +#### 验证场景 + +- 有效刷新令牌 → 200,返回新令牌;旧刷新令牌再次使用返回 401。 +- 过期刷新令牌 → 401 / `AUTH.TOKEN_EXPIRED`。 + +### A006 修改手机号 + +- 模块 / Tag:Identity +- 需求编号:F03、M01-03 +- 负责人:唐宇昊 +- 关联数据表:DB001、DB004 +- 当前状态:待交叉评审 +- 用途:买家或商家修改本人手机号,提交后旧登录态全部失效并要求重新登录。 +- 方法与路径:`POST /api/auth/change-phone` +- operationId:`Identity_ChangePhone` + +#### 请求 + +- Header:`Authorization: Bearer `(必填) +- Body: + +```text +ChangePhoneRequest { + currentPassword: string // 必填,必须匹配当前密码哈希 + newPhone: string // 必填,^1[3-9]\d{9}$ +} +``` + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`CurrentUserResponse`(含更新后的掩码手机号) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或新手机号格式错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 401 | `AUTH.INVALID_CREDENTIALS` | 当前密码错误 | +| 403 | `AUTH.ACCOUNT_DISABLED` | 账号已被禁用 | +| 409 | `AUTH.PHONE_ALREADY_REGISTERED` | 新手机号已被他人使用 | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态共享不可用,无法签发新令牌 | + +#### 业务规则与并发 + +- 修改成功后:账号令牌版本号 +1,Redis 中该用户全部未过期令牌记录按版本失效;当前访问令牌立即失效。 +- 成功后强制要求重新登录;前端需要清理本地登录态。 +- 新手机号唯一性由 PostgreSQL 唯一约束 `ux_users_phone` 保证。 + +#### 缓存、事件或外部依赖 + +- Redis Key:`auth:user:{userId}:tokenVersion`。 +- 不发布集成事件。 + +#### 验证场景 + +- 正确当前密码 + 未占用新手机号 → 200;旧令牌立即返回 401 / `AUTH.TOKEN_REVOKED`。 +- 错误当前密码 → 401 / `AUTH.INVALID_CREDENTIALS`,手机号不变。 +- 新手机号已被使用 → 409 / `AUTH.PHONE_ALREADY_REGISTERED`,手机号不变。 +- Redis 撤销不可用 → 503,提示用户暂不可用,不修改手机号。 + +### A007 重置用户名 + +- 模块 / Tag:Identity +- 需求编号:F03、M01-03 +- 负责人:唐宇昊 +- 关联数据表:DB001 +- 当前状态:待交叉评审 +- 用途:用户自助重置一次用户名,重置次数用完即返回错误。 +- 方法与路径:`POST /api/auth/reset-username` +- operationId:`Identity_ResetUsername` + +#### 请求 + +- Header:`Authorization: Bearer `(必填) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`ResetUsernameResponse` + +```text +ResetUsernameResponse { + username: string // 重新生成的 u_xxxxxxxx + resetCount: integer // 当前已使用次数(重置后最大为 1) +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 409 | `AUTH.USERNAME_RESET_EXHAUSTED` | 当前账号已使用过一次自助重置 | +| 409 | `AUTH.USERNAME_GENERATION_RETRY_EXHAUSTED` | 新用户名生成冲突且超过重试上限 | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 服务暂不可用 | + +#### 业务规则与并发 + +- 重置次数记录在 `users.username_reset_count`,重置后置为 1;再次调用返回 `409 / AUTH.USERNAME_RESET_EXHAUSTED`。 +- 用户名生成规则与 A001 一致;并发重置时通过乐观更新保证只成功一次。 + +#### 缓存、事件或外部依赖 + +- 不缓存、不发布事件。 + +#### 验证场景 + +- 首次重置 → 200,返回新用户名。 +- 第二次重置 → 409 / `AUTH.USERNAME_RESET_EXHAUSTED`。 +- 并发重置 → 仅一次 200,另一笔 409。 + +### A008 获取本人资料 + +- 模块 / Tag:Identity +- 需求编号:F03、M01-03 +- 负责人:唐宇昊 +- 关联数据表:DB001 +- 当前状态:待交叉评审 +- 用途:买家或商家查看本人资料;不返回内部审计或登录态字段。 +- 方法与路径:`GET /api/users/me` +- operationId:`Identity_GetMyProfile` + +#### 请求 + +- Header:`Authorization: Bearer `(必填) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`MyProfileResponse` + +```text +MyProfileResponse { + userId: uuid + username: string + phoneMasked: string + avatarUrl: string + role: "Buyer" | "Merchant" + canResetUsername: boolean // 是否仍可自助重置用户名 + createdAt: string +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.ACCOUNT_DISABLED` | 账号已被禁用 | +| 404 | `RESOURCE.NOT_FOUND` | 当前用户记录不存在 | + +#### 业务规则与并发 + +- 始终按当前登录用户过滤;不接受路径或 Body 中的 `userId`。 +- 移动端登录后未补充资料场景下字段值仍按合同返回,前端不得假设某些字段必填。 + +#### 缓存、事件或外部依赖 + +- 不缓存。 + +#### 验证场景 + +- 已登录买家 → 200。 +- 已禁用账号 → 403 / `AUTH.ACCOUNT_DISABLED`。 +- 商家账号登录后同样可调用,但 `role` 为 `Merchant`。 + +### A009 修改本人资料 + +- 模块 / Tag:Identity +- 需求编号:F03、M01-03 +- 负责人:唐宇昊 +- 关联数据表:DB001 +- 当前状态:待交叉评审 +- 用途:买家或商家维护本人展示资料;手机号与用户名变更走专门接口,本接口不接受这两类字段。 +- 方法与路径:`PATCH /api/users/me` +- operationId:`Identity_UpdateMyProfile` + +#### 请求 + +- Header:`Authorization: Bearer `(必填) +- Body: + +```text +UpdateMyProfileRequest { + displayName?: string // 可选,昵称或展示名 + bio?: string // 可选,简介,0~200 字 + avatarUrl?: string // 可选;本期不支持自定义头像上传,仅允许系统默认 +} +``` + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`MyProfileResponse` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段长度或格式错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.ACCOUNT_DISABLED` | 账号已被禁用 | +| 409 | `COMMON.VALIDATION_FAILED` | 不接受修改 `phone`、`username`、`role`、`status`、`userId` | + +#### 业务规则与并发 + +- 不允许修改字段:`phone`、`username`、`role`、`status`、`userId`;这些字段变更必须通过专门接口。 +- `avatarUrl` 仅允许在系统默认范围内设置;本期不支持自定义上传。 + +#### 缓存、事件或外部依赖 + +- 不缓存、不发布事件。 + +#### 验证场景 + +- 修改 `displayName` → 200,返回最新资料。 +- 提交 `phone` 字段 → 409 / `COMMON.VALIDATION_FAILED`,字段被忽略。 +- 提交 `role=Admin` → 409,不修改角色。 + +### A010 我的地址列表 + +- 模块 / Tag:Identity +- 需求编号:F03、M01-03 +- 负责人:唐宇昊 +- 关联数据表:DB003 +- 当前状态:待交叉评审 +- 用途:买家或商家分页查询本人收货地址,标记默认地址。 +- 方法与路径:`GET /api/users/me/addresses` +- operationId:`Identity_ListMyAddresses` + +#### 请求 + +- Header:`Authorization: Bearer `(必填) +- Query 参数:`page`(默认 1)、`pageSize`(默认 10,上限 50) + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`AddressListResponse` + +```text +AddressListResponse { + items: AddressResponse[] + page: integer + pageSize: integer + total: integer + totalPages: integer +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 400 | `COMMON.VALIDATION_FAILED` | 分页参数非法 | + +#### 业务规则与并发 + +- 严格按 `user_id = current_user_id` 过滤;不允许查询他人地址。 +- 默认地址按 `is_default = true` 标记;同一用户最多一个默认地址。 + +#### 缓存、事件或外部依赖 + +- 不缓存。 + +#### 验证场景 + +- 买家有 0 条记录 → items=[],total=0。 +- 买家有多条地址 → 仅返回本人地址,默认地址置顶。 + +### A011 新增地址 + +- 模块 / Tag:Identity +- 需求编号:F03、M01-03 +- 负责人:唐宇昊 +- 关联数据表:DB003 +- 当前状态:待交叉评审 +- 用途:买家或商家新增收货地址。 +- 方法与路径:`POST /api/users/me/addresses` +- operationId:`Identity_CreateMyAddress` + +#### 请求 + +- Header:`Authorization: Bearer `(必填) +- Body: + +```text +CreateAddressRequest { + recipientName: string // 必填,1~50 字 + phone: string // 必填,^1[3-9]\d{9}$ + province: string // 必填,省份名称 + city: string // 必填,城市名称 + district: string // 必填,区/县名称 + detail: string // 必填,详细地址 5~120 字 + isDefault: boolean? // 可选;true 时将其他默认地址取消 +} +``` + +#### 成功响应 + +- HTTP 状态:`201 Created` +- Response Header:`Location: /api/users/me/addresses/{addressId}` +- 响应 Schema:`AddressResponse` + +```text +AddressResponse { + addressId: uuid + recipientName: string + phoneMasked: string + province: string + city: string + district: string + detail: string + isDefault: boolean + createdAt: string + updatedAt: string +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或格式错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | + +#### 业务规则与并发 + +- `isDefault=true` 时在同一事务内将其他地址的 `is_default` 置 false;同一用户最多一个默认地址。 +- 单用户地址上限暂定 20 条;超出时返回 409 / `IDENTITY.ADDRESS_LIMIT_REACHED`。 + +#### 缓存、事件或外部依赖 + +- 不缓存、不发布事件。 + +#### 验证场景 + +- 合法地址 + `isDefault=false` → 201。 +- 合法地址 + `isDefault=true` 且已有默认地址 → 201,旧默认地址自动取消。 + +### A012 编辑地址 + +- 模块 / Tag:Identity +- 需求编号:F03、M01-03 +- 负责人:唐宇昊 +- 关联数据表:DB003 +- 当前状态:待交叉评审 +- 用途:买家或商家编辑本人地址;非本人地址返回 404。 +- 方法与路径:`PATCH /api/users/me/addresses/{addressId}` +- operationId:`Identity_UpdateMyAddress` + +#### 请求 + +- Route 参数:`addressId: uuid` +- Header:`Authorization: Bearer `(必填) +- Body:与 `CreateAddressRequest` 一致,所有字段可选,但至少传一个。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`AddressResponse` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或格式错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 404 | `RESOURCE.NOT_FOUND` | 地址不存在或不属于当前用户 | + +#### 业务规则与并发 + +- 严格按 `user_id = current_user_id AND address_id = :addressId` 过滤;不存在的地址返回 404。 +- 不允许通过此接口直接修改 `isDefault`;默认地址切换使用 A014。 + +#### 缓存、事件或外部依赖 + +- 不缓存。 + +#### 验证场景 + +- 编辑本人地址 → 200,字段更新。 +- 编辑他人地址 → 404,不泄露归属。 + +### A013 删除地址 + +- 模块 / Tag:Identity +- 需求编号:F03、M01-03 +- 负责人:唐宇昊 +- 关联数据表:DB003 +- 当前状态:待交叉评审 +- 用途:买家或商家删除本人地址;默认地址被删除时不自动指定其他地址。 +- 方法与路径:`DELETE /api/users/me/addresses/{addressId}` +- operationId:`Identity_DeleteMyAddress` + +#### 请求 + +- Route 参数:`addressId: uuid` +- Header:`Authorization: Bearer `(必填) + +#### 成功响应 + +- HTTP 状态:`204 No Content` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 404 | `RESOURCE.NOT_FOUND` | 地址不存在或不属于当前用户 | +| 409 | `IDENTITY.ADDRESS_IN_USE_BY_ORDER` | 该地址被未完成订单引用,需要先迁移或完成订单 | + +#### 业务规则与并发 + +- 删除默认地址后不自动指定其他默认地址;下单时由买家明确确认。 +- 幂等:已删除地址再次删除返回 204,不报错。 + +#### 缓存、事件或外部依赖 + +- 不缓存。 + +#### 验证场景 + +- 删除非默认地址 → 204,列表更新。 +- 删除默认地址 → 204,列表无默认地址标记。 + +### A014 设置默认地址 + +- 模块 / Tag:Identity +- 需求编号:F03、M01-03 +- 负责人:唐宇昊 +- 关联数据表:DB003 +- 当前状态:待交叉评审 +- 用途:买家或商家将本人某条地址设为默认;同一用户最多一个默认地址。 +- 方法与路径:`POST /api/users/me/addresses/{addressId}/default` +- operationId:`Identity_SetDefaultAddress` + +#### 请求 + +- Route 参数:`addressId: uuid` +- Header:`Authorization: Bearer `(必填) + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`AddressResponse` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 404 | `RESOURCE.NOT_FOUND` | 地址不存在或不属于当前用户 | + +#### 业务规则与并发 + +- 在同一事务内将该地址 `is_default=true`,其他地址 `is_default=false`;保证唯一性。 +- 并发设置多个默认地址时由数据库 `WHERE user_id = :uid AND is_default = true` 条件更新保证最终唯一。 + +#### 缓存、事件或外部依赖 + +- 不缓存。 + +#### 验证场景 + +- 已有默认地址 A,调用此接口将 B 设为默认 → 200,A `isDefault=false`,B `isDefault=true`。 + +### A015 后台账号列表 + +- 模块 / Tag:Identity +- 需求编号:F13、M06-03 +- 负责人:唐宇昊 +- 关联数据表:DB001 +- 当前状态:待交叉评审 +- 用途:管理员分页查询买家和商家账号;手机号默认掩码,不返回密码哈希或完整 Token。 +- 方法与路径:`GET /api/admin/users` +- operationId:`Identity_AdminListUsers` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Admin) +- Query 参数: + - `page`(默认 1) + - `pageSize`(默认 10,上限 50) + - `role`(可选,`Buyer` / `Merchant`;不传表示全部非管理员账号) + - `status`(可选,`Active` / `Disabled`) + - `keyword`(可选,对用户名或手机号做模糊匹配) + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`AdminUserListResponse` + +```text +AdminUserListResponse { + items: AdminUserResponse[] + page: integer + pageSize: integer + total: integer + totalPages: integer +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 分页或筛选参数非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是管理员 | + +#### 业务规则与并发 + +- 永远不返回管理员账号;过滤条件 `role IN ('Buyer','Merchant')`。 +- 列表响应只返回管理操作所需字段;不返回密码哈希、内部审计、登录态。 +- 手机号使用掩码 `138****8888` 形式。 + +#### 缓存、事件或外部依赖 + +- 不缓存。 + +#### 验证场景 + +- 管理员查询全部买家 → 200,按注册时间倒序,手机号掩码。 +- 管理员传入 `role=Admin` → 400 / `COMMON.VALIDATION_FAILED`。 +- 买家调用 → 403 / `AUTH.FORBIDDEN`。 + +### A016 禁用账号 + +- 模块 / Tag:Identity +- 需求编号:F13、M06-03 +- 负责人:唐宇昊 +- 关联数据表:DB001、DB004 +- 当前状态:待交叉评审 +- 用途:管理员禁用指定买家或商家账号;账号禁用前签发的全部令牌立即失效。 +- 方法与路径:`POST /api/admin/users/{userId}/disable` +- operationId:`Identity_AdminDisableUser` + +#### 请求 + +- Route 参数:`userId: uuid` +- Header:`Authorization: Bearer `(必填,角色 Admin) +- Body: + +```text +DisableUserRequest { + reason?: string // 可选,0~200 字 +} +``` + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`AdminUserResponse` + +```text +AdminUserResponse { + userId: uuid + username: string + phoneMasked: string + role: "Buyer" | "Merchant" + status: "Active" | "Disabled" + updatedAt: string +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是管理员 | +| 404 | `RESOURCE.NOT_FOUND` | 目标账号不存在或不是买家/商家 | +| 409 | `RESOURCE.CONFLICT` | 当前账号已处于禁用状态 | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态共享不可用 | + +#### 业务规则与并发 + +- 条件更新:`UPDATE users SET status='Disabled', token_version=token_version+1 WHERE user_id=:uid AND role IN ('Buyer','Merchant') AND status='Active'`;影响行数为 0 时按 409 处理。 +- 禁用成功后通过 `auth:user:{userId}:tokenVersion` 提升版本号;Redis 中保留的令牌记录按版本失效。 +- 状态变更可追踪:操作人、目标账号、原状态、新状态、时间、`traceId` 写入结构化日志;不写入通用操作审计。 + +#### 缓存、事件或外部依赖 + +- Redis Key:`auth:user:{userId}:tokenVersion`。 + +#### 验证场景 + +- 禁用正常买家 → 200,旧令牌 401 / `AUTH.TOKEN_REVOKED`。 +- 重复禁用 → 409 / `RESOURCE.CONFLICT`。 +- 禁用管理员账号 → 404。 +- 禁用过程中 Redis 撤销不可用 → 503,不返回虚假成功。 + +### A017 启用账号 + +- 模块 / Tag:Identity +- 需求编号:F13、M06-03 +- 负责人:唐宇昊 +- 关联数据表:DB001、DB004 +- 当前状态:待交叉评审 +- 用途:管理员启用被禁用的买家或商家账号;启用前已签发令牌不恢复,用户必须重新登录。 +- 方法与路径:`POST /api/admin/users/{userId}/enable` +- operationId:`Identity_AdminEnableUser` + +#### 请求 + +- Route 参数:`userId: uuid` +- Header:`Authorization: Bearer `(必填,角色 Admin) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`AdminUserResponse` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是管理员 | +| 404 | `RESOURCE.NOT_FOUND` | 目标账号不存在或不是买家/商家 | +| 409 | `RESOURCE.CONFLICT` | 当前账号已处于正常状态 | + +#### 业务规则与并发 + +- 条件更新:`status='Active'`,影响行数为 0 时按 409 处理。 +- 启用不改变 `tokenVersion`;禁用前签发的旧令牌仍不可用,需重新登录。 + +#### 缓存、事件或外部依赖 + +- 不修改 Redis 撤销集合。 + +#### 验证场景 + +- 启用已禁用账号 → 200,旧令牌仍 401 / `AUTH.TOKEN_REVOKED`;新登录可用。 +- 启用正常账号 → 409 / `RESOURCE.CONFLICT`。 + +### A018 收藏列表 + +- 模块 / Tag:Engagement +- 需求编号:X02、M08 +- 负责人:唐宇昊 +- 关联数据表:DB005 +- 当前状态:待交叉评审 +- 用途:买家分页查询本人收藏,按最近收藏时间倒序。 +- 方法与路径:`GET /api/favorites` +- operationId:`Engagement_ListFavorites` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Buyer) +- Query 参数:`page`、`pageSize`、`sortBy`(仅允许 `createdAt`,默认 `createdAt desc`)、`sortOrder` + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`FavoriteListResponse` + +```text +FavoriteListResponse { + items: FavoriteResponse[] + page: integer + pageSize: integer + total: integer + totalPages: integer +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 分页或排序参数非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 严格按 `user_id = current_user_id` 过滤;不允许跨用户访问。 +- 排序白名单仅 `createdAt`,方向 `asc` / `desc`;非法字段返回 400。 +- 收藏商品摘要来自 Catalog 模块;若商品已下架仍展示记录但标记不可购买。 + +#### 缓存、事件或外部依赖 + +- 不缓存;商品摘要由 Catalog 模块通过共享 OpenAPI 返回,或在接口层做受控 Join。 + +#### 验证场景 + +- 买家收藏 0 件 → items=[],total=0。 +- 多件收藏 → 按 `createdAt desc` 排序,跨页稳定。 + +### A019 收藏商品 + +- 模块 / Tag:Engagement +- 需求编号:X02、M08 +- 负责人:唐宇昊 +- 关联数据表:DB005 +- 当前状态:待交叉评审 +- 用途:买家收藏商品;同一买家同一商品只保留一条记录。 +- 方法与路径:`POST /api/favorites` +- operationId:`Engagement_AddFavorite` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Buyer) +- Body: + +```text +AddFavoriteRequest { + productId: uuid // 必填 +} +``` + +#### 成功响应 + +- HTTP 状态:`201 Created`(新增)或 `200 OK`(幂等命中已存在) +- 响应 Schema:`FavoriteResponse` + +```text +FavoriteResponse { + productId: uuid + createdAt: string + productSummary: ProductSummaryResponse +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 缺少 `productId` 或格式错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | + +#### 业务规则与并发 + +- 使用 `(user_id, product_id)` 唯一约束保证幂等;重复收藏返回已存在记录。 +- 商品不存在时拒绝,不建立记录。 + +#### 缓存、事件或外部依赖 + +- 不缓存、不发布事件。 + +#### 验证场景 + +- 收藏存在商品 → 201/200。 +- 重复收藏 → 200,已存在记录。 +- 收藏不存在商品 → 404。 + +### A020 取消收藏 + +- 模块 / Tag:Engagement +- 需求编号:X02、M08 +- 负责人:唐宇昊 +- 关联数据表:DB005 +- 当前状态:待交叉评审 +- 用途:买家取消本人对指定商品的收藏;重复取消保持幂等。 +- 方法与路径:`DELETE /api/favorites/{productId}` +- operationId:`Engagement_RemoveFavorite` + +#### 请求 + +- Route 参数:`productId: uuid` +- Header:`Authorization: Bearer `(必填,角色 Buyer) + +#### 成功响应 + +- HTTP 状态:`204 No Content` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 删除按 `(user_id, product_id)` 过滤;不存在记录时返回 204,保持幂等。 + +#### 缓存、事件或外部依赖 + +- 不缓存。 + +#### 验证场景 + +- 取消已收藏商品 → 204。 +- 重复取消 → 204。 + +### A021 浏览历史列表 + +- 模块 / Tag:Engagement +- 需求编号:X02、M08 +- 负责人:唐宇昊 +- 关联数据表:DB006 +- 当前状态:待交叉评审 +- 用途:买家分页查询本人最近浏览的商品,按最近浏览时间倒序。 +- 方法与路径:`GET /api/browsing-history` +- operationId:`Engagement_ListBrowsingHistory` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Buyer) +- Query 参数:`page`、`pageSize`、`sortBy`(仅允许 `viewedAt`,默认 `viewedAt desc`) + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`BrowsingHistoryListResponse` + +```text +BrowsingHistoryListResponse { + items: BrowsingHistoryResponse[] + page: integer + pageSize: integer + total: integer + totalPages: integer +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 分页或排序参数非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 按 `user_id = current_user_id` 过滤;上限默认 200 条,超出后由清理任务移除最早记录。 +- 浏览历史开关关闭时返回空列表;调用 A022 可重新开启。 + +#### 缓存、事件或外部依赖 + +- 不缓存。 + +#### 验证场景 + +- 关闭开关 → items=[]。 +- 启用开关并访问商品 → 按时间倒序展示。 + +### A022 修改浏览记录开关 + +- 模块 / Tag:Engagement +- 需求编号:X02、M08 +- 负责人:唐宇昊 +- 关联数据表:DB006 +- 当前状态:待交叉评审 +- 用途:买家开启或关闭后续浏览记录写入;关闭不等于删除已有历史。 +- 方法与路径:`PATCH /api/browsing-history/settings` +- operationId:`Engagement_UpdateBrowsingHistorySetting` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Buyer) +- Body: + +```text +UpdateBrowsingHistorySettingRequest { + enabled: boolean +} +``` + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`BrowsingHistorySettingResponse` + +```text +BrowsingHistorySettingResponse { + enabled: boolean + updatedAt: string +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 缺少 `enabled` 字段 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 关闭开关不影响已有浏览记录;重新开启后恢复写入。 + +#### 缓存、事件或外部依赖 + +- 不缓存。 + +#### 验证场景 + +- `enabled=false` → 200,后续访问商品不再写入历史。 +- `enabled=true` → 200,重新开启写入。 + +### A023 清空浏览历史 + +- 模块 / Tag:Engagement +- 需求编号:X02、M08 +- 负责人:唐宇昊 +- 关联数据表:DB006 +- 当前状态:待交叉评审 +- 用途:买家清空本人浏览历史;不影响浏览记录开关状态。 +- 方法与路径:`DELETE /api/browsing-history` +- operationId:`Engagement_ClearBrowsingHistory` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Buyer) + +#### 成功响应 + +- HTTP 状态:`204 No Content` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 按 `user_id = current_user_id` 物理删除;只影响当前用户。 +- 清空后再次浏览商品仍按当前开关决定是否写入。 + +#### 缓存、事件或外部依赖 + +- 不缓存。 + +#### 验证场景 + +- 清空本人浏览历史 → 204,后续列表为空。 +- 重复清空 → 204,幂等。 + +--- + +> 来源:[interface-gxy.md](interface/interface-gxy.md)。已完成结构汇总,并已将合写接口拆成独立 Axxx 小节;图片暂存和评价详情引用仍须评审。 + +### A101 购物端有效分类列表 + +- 模块 / Tag:Catalog +- 需求编号:M02-01-FR02 +- 负责人:顾欣月 +- 关联数据表:DB021 `categories` +- 当前状态:待评审 +- 用途:为购物端商品筛选提供当前启用的分类,供列表页分类入口使用。 +- 方法与路径:`GET /api/categories` +- operationId:`Catalog_ListCategories` +- 请求 Schema:无(仅可选 Query) +- 响应 Schema:`CategoryTreeResponse` +- 身份与 Policy:允许游客访问;无需 JWT。 +- 资源归属:公开数据,无归属校验。 +- 幂等要求:只读,天然幂等。 + +#### 请求 + +- Route 参数:无。 +- Query 参数:`includeEmpty`(boolean,可选,默认 `false`,是否包含暂无在架商品的启用分类)。 +- Header:`Accept: application/json`。 +- Body:无。 +- 校验规则:只返回 `status = enabled` 的分类;停用分类不作为购物端筛选入口。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`CategoryTreeResponse`,`data.items` 为分类数组,字段含 `categoryId`、`name`、`parentId`(可空)、`sortOrder`、`productCount`。 +- 示例: + +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { "categoryId": "6f1d…", "name": "手机数码", "parentId": null, "sortOrder": 1, "productCount": 42 } + ] + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 500 | `COMMON.INTERNAL_ERROR` | 未处理服务端错误 | + +#### 业务规则与并发 + +- 购物端只暴露启用分类;层级最多一层父子,`parentId` 为 `null` 表示顶级分类。 +- 排序按 `sortOrder asc, categoryId asc` 稳定排序。 + +#### 缓存、事件或外部依赖 + +- 只读,可使用服务端 Cache-Aside;命中与未命中结构一致。商品或分类变更后由 M06-01 事务提交后触发缓存失效(C07 协作)。 + +#### 验证场景 + +- 停用分类不出现在结果中;`includeEmpty=false` 时不返回无在架商品的分类。 + +--- + +### A102 商品分页列表 / 搜索 / 筛选 / 排序 + +- 模块 / Tag:Catalog +- 需求编号:M02-01(F04、F05)、C04 +- 负责人:顾欣月 +- 关联数据表:DB022 `products`、DB023 `product_images` +- 当前状态:待评审 +- 用途:购物端商品发现入口,支持分页、分类筛选、关键词模糊/分词搜索、价格区间、仅看有货与白名单排序,翻页保持条件。 +- 方法与路径:`GET /api/products` +- operationId:`Catalog_ListProducts` +- 请求 Schema:无(Query) +- 响应 Schema:`ProductListResponse`(分页包装,`items` 为 `ProductSummary`) +- 身份与 Policy:允许游客访问;无需 JWT。 +- 资源归属:公开数据;服务端强制附加“已上架”过滤,任何身份不得绕过。 +- 幂等要求:只读,天然幂等。 + +#### 请求 + +- Route 参数:无。 +- Query 参数: + +| 参数 | 类型 | 必填 | 默认 | 约束 | +|---|---|---|---|---| +| `page` | integer | 否 | 1 | ≥ 1 | +| `pageSize` | integer | 否 | 10 | 1~100 | +| `keyword` | string | 否 | — | 去首尾空白后长度 1~50;为空按未提供处理 | +| `categoryId` | uuid | 否 | — | 必须为存在且启用的分类 | +| `minPrice` | number | 否 | — | ≥ 0,最多两位小数 | +| `maxPrice` | number | 否 | — | ≥ 0,且 ≥ `minPrice` | +| `inStockOnly` | boolean | 否 | false | 为 `true` 时仅返回库存 > 0 | +| `sortBy` | string | 否 | `relevance`(有 `keyword`)/ `createdAt`(无 `keyword`) | 白名单:`relevance`、`price`、`createdAt` | +| `sortOrder` | string | 否 | `desc` | `asc`/`desc`,不区分大小写 | + +- Header:`Accept: application/json`。 +- Body:无。 +- 校验规则:`minPrice > maxPrice` 返回 `CATALOG.INVALID_PRICE_RANGE`;`sortBy` 非白名单返回 `CATALOG.INVALID_SORT_FIELD`;`keyword` 参数化处理,禁止拼接 SQL。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`ProductListResponse`,`items` 元素 `ProductSummary` 含 `productId`、`name`、`categoryId`、`price`、`stockStatus`(`InStock`/`SoldOut`)、`thumbnailUrl`、`createdAt`。 +- 示例: + +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { "productId": "3f0e…", "name": "示例手机", "categoryId": "6f1d…", "price": 1999.00, "stockStatus": "InStock", "thumbnailUrl": "https://…/thumb.webp", "createdAt": "2026-07-20T02:00:00Z" } + ], + "page": 1, "pageSize": 10, "total": 42, "totalPages": 5 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 分页、类型或枚举字段非法 | +| 400 | `CATALOG.INVALID_PRICE_RANGE` | `minPrice > maxPrice` | +| 400 | `CATALOG.INVALID_SORT_FIELD` | `sortBy` 不在白名单 | +| 404 | `CATALOG.CATEGORY_NOT_FOUND` | `categoryId` 不存在或已停用(按接口固定行为返回,不越权返回商品) | + +#### 业务规则与并发 + +- 服务端始终附加“已上架(`Published`)”过滤;草稿、下架、已删除商品不得泄露。 +- 空结果为正常结果,返回空数组与真实分页元数据。 +- 稳定排序:业务排序字段相同时追加 `productId` 作为次级排序,避免翻页重复或遗漏。 +- C04:`keyword` 存在时走 `IProductSearch` 分词/倒排实现并支持 `relevance` 排序;进阶不可用时在保证“已上架过滤 + 参数安全”的前提下降级为 `ILIKE` 基础模糊查询并记录降级原因,返回口径不变。 + +#### 缓存、事件或外部依赖 + +- 依赖 Catalog 搜索能力契约 `IProductSearch`(C04)。列表可服务端缓存(C07 协作),商品变更事务提交后失效。 + +#### 验证场景 + +- 分类、关键词、价格区间、库存与白名单排序可独立与组合生效;翻页保持条件。 +- 任意身份都搜索不到草稿/下架/已删除商品;非法排序字段返回 400。 + +--- + +### A103 购物端商品详情 + +- 模块 / Tag:Catalog +- 需求编号:M02-02(F06) +- 负责人:顾欣月 +- 关联数据表:DB022 `products`、DB023 `product_images` +- 当前状态:待评审 +- 用途:展示已上架商品的名称、图片、描述、当前价格、库存与分类,供购买决策。 +- 方法与路径:`GET /api/products/{productId}` +- operationId:`Catalog_GetProduct` +- 请求 Schema:无(Route) +- 响应 Schema:`ProductDetailResponse` +- 身份与 Policy:允许游客访问;无需 JWT。 +- 资源归属:公开数据;仅公开已上架商品。 +- 幂等要求:只读,天然幂等。 + +#### 请求 + +- Route 参数:`productId`(uuid,必填)。 +- Query 参数:无。 +- Header:`Accept: application/json`。 +- Body:无。 +- 校验规则:`productId` 格式校验;仅返回已上架(`Published`)商品。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`ProductDetailResponse`,`data` 含 `productId`、`name`、`categoryId`、`categoryName`、`description`、`price`、`stock`、`stockStatus`、`images`(数组:`imageId`、`url`、`sortOrder`、`isPrimary`、`altText`)、`createdAt`、`updatedAt`。评分汇总与评价列表由 A140 单独获取,本响应不内联。 +- 示例: + +```json +{ + "code": "success", + "message": "ok", + "data": { + "productId": "3f0e…", "name": "示例手机", "categoryId": "6f1d…", "categoryName": "手机数码", + "description": "受控富文本或纯文本描述", "price": 1999.00, "stock": 12, "stockStatus": "InStock", + "images": [ { "imageId": "a1…", "url": "https://…/1.webp", "sortOrder": 1, "isPrimary": true, "altText": "正面图" } ], + "createdAt": "2026-07-20T02:00:00Z", "updatedAt": "2026-07-22T06:00:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `productId` 格式非法 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在、草稿、下架或已删除(对购物端统一按不存在处理) | + +#### 业务规则与并发 + +- 价格、库存、状态以服务端最新数据为准,前端缓存不得作为下单依据。 +- 商品描述按受控内容返回,不含脚本;不返回内部备注或未公开状态字段。 +- 已下架商品旧链接返回 404,不提供购买操作;历史订单快照不受影响(由 Ordering 保存)。 + +#### 缓存、事件或外部依赖 + +- 图片 `url` 由对象存储(S3 兼容 / SeaweedFS)受控访问地址提供;详情可缓存,商品变更后失效。 + +#### 验证场景 + +- 有货、售罄、下架/不存在状态可区分;下架商品详情返回 404;图片失败时前端占位不阻断其余信息。 + +--- + +### A110 后台分类列表(全状态) + +- 模块 / Tag:Catalog +- 需求编号:M06-01-FR01 +- 负责人:顾欣月 +- 关联数据表:DB021 `categories` +- 当前状态:待评审 +- 用途:商家维护商品时查看全部(含停用)分类及层级、排序与启停状态。 +- 方法与路径:`GET /api/merchant/categories` +- operationId:`Catalog_ListMerchantCategories` +- 请求 Schema:无(Query) +- 响应 Schema:`MerchantCategoryListResponse` +- 身份与 Policy:MerchantOnly。 +- 资源归属:本期不做多商家分类隔离;商家可见平台分类。管理员/买家/游客调用返回 403。 +- 幂等要求:只读,天然幂等。 + +#### 请求 + +- Route 参数:无。 +- Query 参数:`status`(可选,多值,`Enabled`/`Disabled`)、`keyword`(可选,名称模糊)。 +- Header:`Authorization: Bearer `、`Accept: application/json`。 +- Body:无。 +- 校验规则:`status` 枚举白名单。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`MerchantCategoryListResponse`,元素含 `categoryId`、`name`、`parentId`、`sortOrder`、`status`(`Enabled`/`Disabled`)、`productCount`。 +- 示例:见 A101 结构,额外含 `status` 字段。 + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效认证 | +| 403 | `AUTH.FORBIDDEN` | 非商家身份 | + +#### 业务规则与并发 + +- 与购物端 A101 分开:本接口返回全状态分类,购物端只返回启用分类。 + +#### 缓存、事件或外部依赖 + +- 无强制缓存。 + +#### 验证场景 + +- 买家/管理员调用返回 403;停用分类可在后台查询到。 + +--- + +### A111 新建分类 + +- 模块 / Tag:Catalog +- 需求编号:M06-01-FR02 +- 负责人:顾欣月 +- 关联数据表:DB021 `categories` +- 当前状态:待评审 +- 用途:商家新增分类。 +- 方法与路径:`POST /api/merchant/categories` +- operationId:`Catalog_CreateCategory` +- 请求 Schema:`CreateCategoryRequest` +- 响应 Schema:`MerchantCategoryResponse` +- 身份与 Policy:MerchantOnly。 +- 资源归属:见 A110。 +- 幂等要求:非幂等;靠名称+父级唯一约束防重复。 + +#### 请求 + +- Route 参数:无。 +- Query 参数:无。 +- Header:`Authorization`、`Content-Type: application/json`。 +- Body:`CreateCategoryRequest`:`name`(string,必填,1~30,去首尾空白)、`parentId`(uuid,可空,最多一层)、`sortOrder`(integer,可选,默认 0,≥ 0)。 +- 校验规则:同一父级下 `name` 唯一;`parentId` 必须存在且为顶级分类(避免超过一层)。 + +#### 成功响应 + +- HTTP 状态:`201 Created` +- 响应 Schema:`MerchantCategoryResponse`;`Location` 指向新分类。 +- 示例:`data` 含新 `categoryId` 与回显字段。 + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段校验失败 | +| 401 | `AUTH.UNAUTHENTICATED` | 未认证 | +| 403 | `AUTH.FORBIDDEN` | 非商家 | +| 404 | `CATALOG.CATEGORY_NOT_FOUND` | `parentId` 不存在 | +| 409 | `CATALOG.CATEGORY_NAME_CONFLICT` | 同父级下名称重复 | + +#### 业务规则与并发 + +- 层级不超过一层;名称唯一约束由数据库保障。 + +#### 缓存、事件或外部依赖 + +- 提交后触发购物端分类缓存失效。 + +#### 验证场景 + +- 重名返回 409;两层以上父级被拒绝。 + +--- + +### A112 编辑分类 + +- 模块 / Tag:Catalog +- 需求编号:M06-01-FR02 +- 负责人:顾欣月 +- 关联数据表:DB021 `categories` +- 当前状态:待评审 +- 用途:商家修改分类名称、父级、排序。 +- 方法与路径:`PUT /api/merchant/categories/{categoryId}` +- operationId:`Catalog_UpdateCategory` +- 请求 Schema:`UpdateCategoryRequest` +- 响应 Schema:`MerchantCategoryResponse` +- 身份与 Policy:MerchantOnly。 +- 幂等要求:PUT 语义幂等。 + +#### 请求 + +- Route 参数:`categoryId`(uuid,必填)。 +- Body:`UpdateCategoryRequest`:`name`、`parentId`(可空)、`sortOrder`;约束同 A111。 +- 校验规则:不允许将分类设为自身或其子级的子级;名称在同父级唯一。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`MerchantCategoryResponse`(更新后数据)。 + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 未认证 | +| 403 | `AUTH.FORBIDDEN` | 非商家 | +| 404 | `CATALOG.CATEGORY_NOT_FOUND` | 分类或父级不存在 | +| 409 | `CATALOG.CATEGORY_NAME_CONFLICT` | 名称重复 | + +#### 业务规则与并发 + +- 禁止形成环或超过一层层级。 + +#### 缓存、事件或外部依赖 + +- 提交后失效购物端分类缓存与相关商品列表缓存。 + +#### 验证场景 + +- 改名冲突返回 409;不存在分类返回 404。 + +--- + +### A113 启用分类 + +- 模块 / Tag:Catalog +- 需求编号:M06-01-FR02 +- 负责人:顾欣月 +- 关联数据表:DB021 `categories` +- 当前状态:待评审 +- 用途:启用分类,使其可以重新作为购物端筛选入口和商品上架分类。 +- 方法与路径:`POST /api/merchant/categories/{categoryId}/enable` +- operationId:`Catalog_EnableCategory` +- 请求 Schema:无(Route) +- 响应 Schema:`MerchantCategoryResponse` +- 身份与 Policy:MerchantOnly。 +- 幂等要求:重复启用保持幂等,返回当前状态。 + +#### 请求 + +- Route 参数:`categoryId`(uuid,必填)。 +- Body:无。 + +#### 成功响应 + +- HTTP 状态:`200 OK`,返回更新后 `status = Enabled`。 + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 未认证 | +| 403 | `AUTH.FORBIDDEN` | 非商家 | +| 404 | `CATALOG.CATEGORY_NOT_FOUND` | 分类不存在 | + +#### 业务规则与并发 + +- 启用不恢复已删除数据,也不自动上架该分类下的商品。 + +#### 缓存、事件或外部依赖 + +- 状态变更后失效购物端分类缓存。 + +#### 验证场景 + +- 启用后购物端分类列表包含该分类;重复启用幂等成功。 + +--- + +### A114 停用分类 + +- 模块 / Tag:Catalog +- 需求编号:M06-01-FR03 +- 负责人:顾欣月 +- 关联数据表:DB021 `categories` +- 当前状态:待评审 +- 用途:停用分类,使其退出购物端筛选,并替代破坏性删除。 +- 方法与路径:`POST /api/merchant/categories/{categoryId}/disable` +- operationId:`Catalog_DisableCategory` +- 请求 Schema:无(Route) +- 响应 Schema:`MerchantCategoryResponse` +- 身份与 Policy:MerchantOnly。 +- 幂等要求:重复停用保持幂等,返回当前状态。 + +#### 请求 + +- Route 参数:`categoryId`(uuid,必填)。 +- Body:无。 + +#### 成功响应 + +- HTTP 状态:`200 OK`,返回更新后 `status = Disabled`。 + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 未认证 | +| 403 | `AUTH.FORBIDDEN` | 非商家 | +| 404 | `CATALOG.CATEGORY_NOT_FOUND` | 分类不存在 | + +#### 业务规则与并发 + +- 停用分类不做物理删除,不影响历史引用;停用后 A101/A102 不再以其作为筛选入口。 +- 停用分类不能用于新建或上架商品(见 A122、A125)。 + +#### 缓存、事件或外部依赖 + +- 状态变更后失效购物端分类缓存。 + +#### 验证场景 + +- 停用后购物端分类列表不含该分类;重复停用幂等成功。 + +--- + +### A120 后台商品分页(全状态) + +- 模块 / Tag:Catalog +- 需求编号:M06-01-FR04 +- 负责人:顾欣月 +- 关联数据表:DB022 `products` +- 当前状态:待评审 +- 用途:商家按关键词、分类、上下架状态分页查询本方商品,展示价格、库存与状态。 +- 方法与路径:`GET /api/merchant/products` +- operationId:`Catalog_ListMerchantProducts` +- 请求 Schema:无(Query) +- 响应 Schema:`MerchantProductListResponse` +- 身份与 Policy:MerchantOnly。 +- 资源归属:本期不做多商家隔离;管理员/买家不得调用。 +- 幂等要求:只读。 + +#### 请求 + +- Query 参数:`page`、`pageSize`(同通用分页)、`keyword`(可选)、`categoryId`(可选)、`status`(可选,多值:`Draft`/`Published`/`Unpublished`)、`sortBy`(白名单:`createdAt`/`price`/`stock`)、`sortOrder`。 +- Header:`Authorization`、`Accept`。 +- 校验规则:`status`、`sortBy` 白名单。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`MerchantProductListResponse`,元素 `MerchantProductSummary` 含 `productId`、`name`、`categoryId`、`price`、`stock`、`status`(`Draft`/`Published`/`Unpublished`)、`primaryImageUrl`、`createdAt`、`updatedAt`。 + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 参数非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 未认证 | +| 403 | `AUTH.FORBIDDEN` | 非商家 | + +#### 业务规则与并发 + +- 与购物端 A102 严格区分:本接口可返回草稿、下架商品,不做“已上架”强制过滤。 + +#### 缓存、事件或外部依赖 + +- 无强制缓存(后台需实时)。 + +#### 验证场景 + +- 商家可按状态筛出草稿/下架商品;买家/管理员返回 403。 + +--- + +### A121 后台商品详情 + +- 模块 / Tag:Catalog +- 需求编号:M06-01-FR04、FR06 +- 负责人:顾欣月 +- 关联数据表:DB022 `products`、DB023 `product_images` +- 当前状态:待评审 +- 用途:商家编辑前获取商品完整信息(含并发版本号 `version`)。 +- 方法与路径:`GET /api/merchant/products/{productId}` +- operationId:`Catalog_GetMerchantProduct` +- 响应 Schema:`MerchantProductDetailResponse` +- 身份与 Policy:MerchantOnly。 +- 幂等要求:只读。 + +#### 请求 + +- Route 参数:`productId`(uuid,必填)。 +- 校验规则:格式校验。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`MerchantProductDetailResponse`,含 A121 全字段:`productId`、`name`、`categoryId`、`description`、`price`、`stock`、`status`(`Draft`/`Published`/`Unpublished`)、`images`、`version`(乐观并发标记)、`createdAt`、`updatedAt`。 + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 未认证 | +| 403 | `AUTH.FORBIDDEN` | 非商家 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | + +#### 业务规则与并发 + +- 返回 `version` 供 A123 编辑提交做乐观并发校验。 + +#### 缓存、事件或外部依赖 + +- 无。 + +#### 验证场景 + +- 草稿/下架商品在后台可查看;不存在返回 404。 + +--- + +### A122 新建商品 + +- 模块 / Tag:Catalog +- 需求编号:M06-01-FR05 +- 负责人:顾欣月 +- 关联数据表:DB022 `products` +- 当前状态:待评审 +- 用途:商家录入商品基础信息,创建为草稿状态。 +- 方法与路径:`POST /api/merchant/products` +- operationId:`Catalog_CreateProduct` +- 请求 Schema:`CreateProductRequest` +- 响应 Schema:`MerchantProductDetailResponse` +- 身份与 Policy:MerchantOnly。 +- 幂等要求:非幂等;由前端防抖 + 服务端校验控制重复。 + +#### 请求 + +- Body:`CreateProductRequest`: + +| 字段 | 类型 | 必填 | 约束 | +|---|---|---|---| +| `name` | string | 是 | 1~100,去首尾空白 | +| `categoryId` | uuid | 是 | 存在且启用 | +| `price` | number | 是 | ≥ 0,最多两位小数 | +| `stock` | integer | 是 | ≥ 0 非负整数 | +| `description` | string | 否 | ≤ 2000,受控内容 | +| `imageIds` | uuid[] | 否 | 引用已通过 A127 上传的图片,≤ 8 | + +- 校验规则:分类须启用;价格非负;库存非负整数;图片数 ≤ 8。 + +#### 成功响应 + +- HTTP 状态:`201 Created`;`Location` 指向 `/api/merchant/products/{productId}`。 +- 响应 Schema:`MerchantProductDetailResponse`(`status = Draft`,含 `version`)。 + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段校验失败 | +| 401 | `AUTH.UNAUTHENTICATED` | 未认证 | +| 403 | `AUTH.FORBIDDEN` | 非商家 | +| 404 | `CATALOG.CATEGORY_NOT_FOUND` | 分类不存在 | +| 409 | `CATALOG.CATEGORY_DISABLED` | 分类已停用,不能用于新建 | + +#### 业务规则与并发 + +- 新建默认草稿,需通过 A125 上架前完整性校验后才对购物端可见。 + +#### 缓存、事件或外部依赖 + +- 创建成功发布领域事件用于后续搜索索引同步(C04-FR05);索引随商品数据在同一 PostgreSQL 事务/同步流程更新。 + +#### 验证场景 + +- 缺必填、负价、停用分类被拒;成功后为草稿态且不出现在购物端。 + +--- + +### A123 编辑商品(乐观并发) + +- 模块 / Tag:Catalog +- 需求编号:M06-01-FR06、M06-01-FR10 +- 负责人:顾欣月 +- 关联数据表:DB022 `products` +- 当前状态:待评审 +- 用途:修改允许变更的商品信息,使用并发标记防止静默覆盖。 +- 方法与路径:`PUT /api/merchant/products/{productId}` +- operationId:`Catalog_UpdateProduct` +- 请求 Schema:`UpdateProductRequest` +- 响应 Schema:`MerchantProductDetailResponse` +- 身份与 Policy:MerchantOnly。 +- 幂等要求:PUT 语义幂等(相同 `version` + 相同内容重复提交结果一致)。 + +#### 请求 + +- Route 参数:`productId`(uuid,必填)。 +- Body:`UpdateProductRequest`:字段同 A122(`name`、`categoryId`、`price`、`stock`、`description`、`imageIds`),另加必填 `version`(integer,来自 A121)。 +- 校验规则:`version` 必填;分类须启用;其余同 A122。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`MerchantProductDetailResponse`(`version` 自增)。 + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段非法或缺 `version` | +| 401 | `AUTH.UNAUTHENTICATED` | 未认证 | +| 403 | `AUTH.FORBIDDEN` | 非商家 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | +| 409 | `CATALOG.CATEGORY_DISABLED` | 目标分类停用 | +| 409 | `CATALOG.PRODUCT_VERSION_CONFLICT` | 提交 `version` 与当前不一致(并发编辑冲突) | + +#### 业务规则与并发 + +- 服务端使用条件更新(`WHERE version = @version`)实现乐观并发;冲突时返回 409,前端保留已填写内容并提示刷新确认。 + +#### 缓存、事件或外部依赖 + +- 提交后发布领域事件:失效商品详情/列表缓存并同步搜索索引(失败可重试或重建,不回滚已提交商品事务)。 + +#### 验证场景 + +- 并发编辑后提交者收到 409;成功后购物端按最新数据展示。 + +--- + +### A124 删除商品(受约束) + +- 模块 / Tag:Catalog +- 需求编号:M06-01-FR08 +- 负责人:顾欣月 +- 关联数据表:DB022 `products` +- 当前状态:待评审 +- 用途:无历史订单关联时删除商品;有关联时禁止破坏性删除并建议下架。 +- 方法与路径:`DELETE /api/merchant/products/{productId}` +- operationId:`Catalog_DeleteProduct` +- 请求 Schema:无(Route) +- 响应 Schema:无(204) +- 身份与 Policy:MerchantOnly。 +- 幂等要求:DELETE 幂等;重复删除已删除资源返回 404 或 204(本接口固定返回 404)。 + +#### 请求 + +- Route 参数:`productId`(uuid,必填)。 +- Body:无。 + +#### 成功响应 + +- HTTP 状态:`204 No Content`(响应体为空)。 + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 未认证 | +| 403 | `AUTH.FORBIDDEN` | 非商家 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在或已删除 | +| 409 | `CATALOG.PRODUCT_HAS_ORDERS` | 存在历史订单关联,禁止破坏性删除,建议改为下架 | + +#### 业务规则与并发 + +- 存在订单项关联时拒绝物理删除;下架(A126)不删除购物车、收藏、浏览记录与历史订单快照。 + +#### 缓存、事件或外部依赖 + +- 是否存在订单关联需查询 Ordering 提供的应用契约/只读视图,不直接跨模块改表。 + +#### 验证场景 + +- 有订单商品删除返回 409;无关联商品删除返回 204。 + +--- + +### A125 商品上架 + +- 模块 / Tag:Catalog +- 需求编号:M06-01-FR07 +- 负责人:顾欣月 +- 关联数据表:DB022 `products` +- 当前状态:待评审 +- 用途:完成商品销售前校验并将商品设为已上架。 +- 方法与路径:`POST /api/merchant/products/{productId}/publish` +- operationId:`Catalog_PublishProduct` +- 请求 Schema:无(Route) +- 响应 Schema:`MerchantProductDetailResponse` +- 身份与 Policy:MerchantOnly。 +- 幂等要求:重复上架保持幂等,返回当前状态。 + +#### 请求 + +- Route 参数:`productId`(uuid,必填)。 +- Body:无。 + +#### 成功响应 + +- HTTP 状态:`200 OK`,返回更新后 `status = Published`。 + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 未认证 | +| 403 | `AUTH.FORBIDDEN` | 非商家 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | +| 409 | `CATALOG.PRODUCT_INCOMPLETE` | 必填项或主图缺失 | +| 409 | `CATALOG.CATEGORY_DISABLED` | 商品分类已停用 | + +#### 业务规则与并发 + +- 上架前必须满足:名称、有效分类、价格、库存以及至少一张主图。 + +#### 缓存、事件或外部依赖 + +- 上架成功后发布领域事件,并失效购物端缓存与搜索索引。 + +#### 验证场景 + +- 缺主图或分类停用时返回 409;重复上架幂等成功。 + +--- + +### A126 商品下架 + +- 模块 / Tag:Catalog +- 需求编号:M06-01-FR07 +- 负责人:顾欣月 +- 关联数据表:DB022 `products` +- 当前状态:待评审 +- 用途:停止商品销售,使购物端列表、详情和搜索不再公开该商品。 +- 方法与路径:`POST /api/merchant/products/{productId}/unpublish` +- operationId:`Catalog_UnpublishProduct` +- 请求 Schema:无(Route) +- 响应 Schema:`MerchantProductDetailResponse` +- 身份与 Policy:MerchantOnly。 +- 幂等要求:重复下架保持幂等,返回当前状态。 + +#### 请求 + +- Route 参数:`productId`(uuid,必填)。 +- Body:无。 + +#### 成功响应 + +- HTTP 状态:`200 OK`,返回更新后 `status = Unpublished`。 + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 未认证 | +| 403 | `AUTH.FORBIDDEN` | 非商家 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | + +#### 业务规则与并发 + +- 下架后 A102/A103 不再返回该商品,旧链接不再允许购买;历史订单快照不受影响。 + +#### 缓存、事件或外部依赖 + +- 下架成功后发布领域事件,并失效购物端缓存与搜索索引;旧索引不得重新公开商品。 + +#### 验证场景 + +- 下架后购物端与搜索均不返回该商品;重复下架幂等成功。 + +--- + +### A127 上传商品图片 + +- 模块 / Tag:Catalog +- 需求编号:M06-01-FR09 +- 负责人:顾欣月 +- 关联数据表:DB023 `product_images` +- 当前状态:待评审 +- 用途:为商品上传图片到 S3 兼容对象存储,返回图片记录。 +- 方法与路径:`POST /api/merchant/products/{productId}/images` +- operationId:`Catalog_UploadProductImage` +- 请求 Schema:`multipart/form-data` +- 响应 Schema:`ProductImageResponse` +- 身份与 Policy:MerchantOnly。 +- 幂等要求:非幂等;每次上传生成新图片记录。 + +#### 请求 + +- Route 参数:`productId`(uuid,必填)。 +- Header:`Authorization`、`Content-Type: multipart/form-data`。 +- Body(form-data):`file`(图片文件,必填)、`isPrimary`(boolean,可选)、`altText`(string,可选)。 +- 校验规则:同时校验扩展名、声明 MIME、实际文件特征、大小与尺寸;仅接受 JPEG、PNG、WebP;单图 ≤ 5 MB;宽高均 400~4096 像素;单商品累计 ≤ 8 张。首图作为主图并生成方形缩略图。 + +#### 成功响应 + +- HTTP 状态:`201 Created` +- 响应 Schema:`ProductImageResponse`,含 `imageId`、`url`、`thumbnailUrl`、`sortOrder`、`isPrimary`。 + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 未认证 | +| 403 | `AUTH.FORBIDDEN` | 非商家 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | +| 409 | `CATALOG.IMAGE_LIMIT_EXCEEDED` | 超过 8 张上限 | +| 413 | `COMMON.PAYLOAD_TOO_LARGE` | 文件超过大小限制 | +| 415 | `CATALOG.INVALID_IMAGE` | 格式或尺寸不符合要求 | + +#### 业务规则与并发 + +- 原始文件名只用于安全展示,不作为对象存储 Key;对象 Key 采用 `products/{productId}/{fileId}.` 格式。 +- 对象存储失败时不写入指向不存在对象的成功记录。 + +#### 缓存、事件或外部依赖 + +- 依赖 M00 提供的 `IObjectStorage` 公共接口与 SeaweedFS 开发环境。 + +#### 验证场景 + +- 超 8 张返回 409;非法格式/尺寸返回 415;超大文件返回 413。 + +--- + +### A128 删除商品图片 + +- 模块 / Tag:Catalog +- 需求编号:M06-01-FR09 +- 负责人:顾欣月 +- 关联数据表:DB023 `product_images` +- 当前状态:待评审 +- 用途:删除某张商品图片。 +- 方法与路径:`DELETE /api/merchant/products/{productId}/images/{imageId}` +- operationId:`Catalog_DeleteProductImage` +- 请求 Schema:无(Route) +- 响应 Schema:无(204) +- 身份与 Policy:MerchantOnly。 +- 幂等要求:DELETE 幂等。 + +#### 请求 + +- Route 参数:`productId`(uuid,必填)、`imageId`(uuid,必填)。 +- 校验规则:`imageId` 必须属于该 `productId`。 + +#### 成功响应 + +- HTTP 状态:`204 No Content`。 + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 未认证 | +| 403 | `AUTH.FORBIDDEN` | 非商家 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品或图片不存在,或图片不属于该商品 | +| 409 | `CATALOG.PRIMARY_IMAGE_REQUIRED` | 已上架商品删除后将无主图 | + +#### 业务规则与并发 + +- 删除主图后需存在其余图片可自动/手动指定新主图;已上架商品不得删至无主图。 + +#### 缓存、事件或外部依赖 + +- 删除数据库记录并清理对象存储对象(清理失败记录可追踪错误,不阻断主流程)。 + +#### 验证场景 + +- 跨商品删图返回 404;已上架商品删至无主图返回 409。 + +--- + +### A140 商品公开评价分页 + 评分汇总 + +- 模块 / Tag:Review +- 需求编号:M07-FR06、M07-FR07 +- 负责人:顾欣月 +- 关联数据表:DB024 `reviews`、DB025 `review_images` +- 当前状态:待评审 +- 用途:商品详情页分页展示公开评价与评分汇总(总数、平均分、星级分布)。 +- 方法与路径:`GET /api/products/{productId}/reviews` +- operationId:`Review_ListProductReviews` +- 请求 Schema:无(Route/Query) +- 响应 Schema:`ProductReviewListResponse` +- 身份与 Policy:允许游客访问。 +- 资源归属:公开评价;不返回买家敏感信息。 +- 幂等要求:只读。 + +#### 请求 + +- Route 参数:`productId`(uuid,必填)。 +- Query 参数:`page`、`pageSize`(通用分页);`sortBy`(白名单:`createdAt`,默认);`sortOrder`(默认 `desc`)。 +- Header:`Accept`。 +- 校验规则:分页与白名单校验。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`ProductReviewListResponse`,`data` 含: + - `summary`:`averageRating`(number,一位小数)、`totalCount`(integer)、`ratingDistribution`(对象:`"5"`…`"1"` 计数)。 + - 分页字段 `items`、`page`、`pageSize`、`total`、`totalPages`;`items` 元素含 `reviewId`、`rating`、`content`、`images`(`url` 数组)、`buyerDisplayName`(脱敏昵称)、`createdAt`。 +- 示例: + +```json +{ + "code": "success", + "message": "ok", + "data": { + "summary": { "averageRating": 4.6, "totalCount": 128, "ratingDistribution": { "5": 90, "4": 25, "3": 8, "2": 3, "1": 2 } }, + "items": [ { "reviewId": "r1…", "rating": 5, "content": "很好用", "images": ["https://…/r1.jpg"], "buyerDisplayName": "用***月", "createdAt": "2026-07-21T03:00:00Z" } ], + "page": 1, "pageSize": 10, "total": 128, "totalPages": 13 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 参数非法 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在或不可见 | + +#### 业务规则与并发 + +- 只返回有效评价;不返回手机号、邮箱、内部用户标识,昵称脱敏展示。 +- 评分汇总由有效评价计算;新增评价后最终更新(可接受短暂最终一致)。 + +#### 缓存、事件或外部依赖 + +- 汇总可缓存,新增评价后失效。商品是否存在依赖 Catalog(同库读取或应用契约)。 + +#### 验证场景 + +- 汇总总数/平均分正确;空评价返回空数组与零汇总;不泄露敏感信息。 + +--- + +### A141 上传评价图片(提交前暂存) + +- 模块 / Tag:Review +- 需求编号:M07-FR03 +- 负责人:顾欣月 +- 关联数据表:DB025 `review_images` +- 当前状态:待评审 +- 用途:买家在提交评价前逐张上传晒图,返回图片标识供 A142 引用。 +- 方法与路径:`POST /api/reviews/images` +- operationId:`Review_UploadReviewImage` +- 请求 Schema:`multipart/form-data` +- 响应 Schema:`ReviewImageResponse` +- 身份与 Policy:BuyerOnly。 +- 幂等要求:非幂等;每次上传生成新的暂存图片。 + +#### 请求 + +- Header:`Authorization`、`Content-Type: multipart/form-data`。 +- Body(form-data):`file`(图片文件,必填)。 +- 校验规则:仅接受 JPEG、PNG、WebP;单图 ≤ 5 MB;宽高均 200~4096 像素;同时校验扩展名、声明 MIME 与实际特征。 + +#### 成功响应 + +- HTTP 状态:`201 Created` +- 响应 Schema:`ReviewImageResponse`,含 `imageId`、`url`。 + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 未登录 | +| 403 | `AUTH.FORBIDDEN` | 非买家 | +| 413 | `COMMON.PAYLOAD_TOO_LARGE` | 文件超限 | +| 415 | `REVIEW.INVALID_IMAGE` | 格式或尺寸不符合要求 | + +#### 业务规则与并发 + +- 暂存图片归属当前买家;对象 Key 采用 `reviews/{reviewId}/{fileId}.`,`reviewId` 在 A142 提交成功后关联。未被引用的暂存图片由清理策略回收。 + +#### 缓存、事件或外部依赖 + +- 依赖 M00 `IObjectStorage`。 + +#### 验证场景 + +- 非法格式/尺寸返回 415;超大返回 413;返回可被 A142 引用的 `imageId`。 + +--- + +### A142 提交商品评价(幂等) + +- 模块 / Tag:Review +- 需求编号:M07-FR04、M07-FR05 +- 负责人:顾欣月 +- 关联数据表:DB024 `reviews`、DB025 `review_images` +- 当前状态:待评审 +- 用途:买家对本人已完成订单项提交一次评分、文字与可选图片评价。 +- 方法与路径:`POST /api/reviews` +- operationId:`Review_CreateReview` +- 请求 Schema:`CreateReviewRequest` +- 响应 Schema:`ReviewDetailResponse` +- 身份与 Policy:BuyerOnly。 +- 资源归属:只能评价本人订单项;服务端从 JWT 取买家身份,不信任请求体身份字段。 +- 幂等要求:必需 `Idempotency-Key`;同一订单项唯一评价由数据库唯一约束兜底。 + +#### 请求 + +- Header:`Authorization`、`Content-Type: application/json`、`Idempotency-Key`(uuid,必需)。 +- Body:`CreateReviewRequest`: + +| 字段 | 类型 | 必填 | 约束 | +|---|---|---|---| +| `orderItemId` | uuid | 是 | 属于当前买家且订单已完成的订单项 | +| `rating` | integer | 是 | 1~5 整数 | +| `content` | string | 是 | 1~500,纯文本/受控内容 | +| `imageIds` | uuid[] | 否 | 引用 A141 暂存图片,≤ 6 | + +- 校验规则:服务端重新校验身份、订单项归属、订单完成状态、评分范围、文字长度、图片数量与是否已评价。 + +#### 成功响应 + +- HTTP 状态:`201 Created`;`Location` 指向该评价(如 `/api/reviews/{reviewId}`)。 +- 响应 Schema:`ReviewDetailResponse`,含 `reviewId`、`productId`、`orderItemId`、`rating`、`content`、`images`、`createdAt`。 + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 评分、文字或图片字段非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 未登录或登录失效 | +| 403 | `AUTH.FORBIDDEN` | 非买家,或商家/管理员尝试提交 | +| 404 | `REVIEW.ORDER_ITEM_NOT_FOUND` | 订单项不存在或不属于当前买家(不泄露归属) | +| 409 | `REVIEW.ORDER_NOT_COMPLETED` | 订单未完成 | +| 409 | `REVIEW.ALREADY_REVIEWED` | 该订单项已评价 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同请求内容 | + +#### 业务规则与并发 + +- 同一订单项只能形成一条评价:唯一约束 `ux_reviews_order_item_id` 作为最终保障;重复点击/重复请求返回首次已确认结果,不新增记录。 +- 订单完成状态、订单项归属由 Ordering 提供的应用契约校验,不直接改订单表。 +- 提交成功后触发商品评分汇总更新(A140 汇总最终一致)。 + +#### 缓存、事件或外部依赖 + +- 依赖 Ordering 校验订单项;依赖 M00 幂等基础设施与对象存储图片关联。 + +#### 验证场景 + +- 未完成订单、他人订单项、游客/商家/管理员提交被拒;重复提交/连点不产生第二条;幂等键复用冲突返回 409。 + +--- + +### A143 查询订单项评价资格 / 结果 + +- 模块 / Tag:Review +- 需求编号:M07-FR01 +- 负责人:顾欣月 +- 关联数据表:DB024 `reviews` +- 当前状态:待评审 +- 用途:买家订单详情判断某订单项是否可评价、是否已评价,用于显示“评价商品”入口。 +- 方法与路径:`GET /api/reviews/eligibility` +- operationId:`Review_GetReviewEligibility` +- 请求 Schema:无(Query) +- 响应 Schema:`ReviewEligibilityResponse` +- 身份与 Policy:BuyerOnly。 +- 资源归属:只查询本人订单项资格。 +- 幂等要求:只读。 + +#### 请求 + +- Query 参数:`orderItemId`(uuid,必填)。 +- Header:`Authorization`。 +- 校验规则:订单项须属于当前买家;否则按不存在处理。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`ReviewEligibilityResponse`,含 `eligible`(boolean)、`reason`(枚举字符串:`Eligible`/`OrderNotCompleted`/`AlreadyReviewed`/`NotOwner`)、`existingReviewId`(uuid,可空)。 +- 示例: + +```json +{ "code": "success", "message": "ok", "data": { "eligible": true, "reason": "Eligible", "existingReviewId": null } } +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `orderItemId` 非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 未登录 | +| 403 | `AUTH.FORBIDDEN` | 非买家 | +| 404 | `REVIEW.ORDER_ITEM_NOT_FOUND` | 订单项不存在或不属于当前买家 | + +#### 业务规则与并发 + +- 资格由订单项归属、订单完成状态与是否已评价共同决定;作为前端入口显示依据,最终提交仍由 A142 完整校验。 + +#### 缓存、事件或外部依赖 + +- 依赖 Ordering 应用契约获取订单项归属与订单状态。 + +#### 验证场景 + +- 已评价返回 `eligible=false, reason=AlreadyReviewed` 且带 `existingReviewId`;未完成返回 `OrderNotCompleted`。 + +--- + +> 来源:[interface-zhh.md](interface/interface-zhh.md)。已完成结构汇总;A229、A230 与 Ordering 查询边界冲突,当前明确标记为暂不实施。 + +> 每个接口按《接口设计》1.20 节模板补齐。Schema 名称遵守 OpenAPI 7.2 节:PascalCase + 用途后缀;`operationId` 使用 `_`;路径参数使用单数对象 + `Id`。 + +### A201 加入购物车 + +- 模块 / Tag:Cart +- 需求编号:F07、M03-01 +- 负责人:朱惠惠 +- 关联数据表:DB041 +- 当前状态:待交叉评审 +- 用途:已登录买家将商品加入购物车;同一买家同一商品只保留一条记录,重复加入按累加处理;服务端实时校验上下架、库存与数量上限。 +- 方法与路径:`POST /api/cart/items` +- operationId:`Cart_AddItem` + +#### 请求 + +- Route 参数:无 +- Query 参数:无 +- Header:`Authorization: Bearer `(必填);`Idempotency-Key: `(推荐,防止重复点击) +- Body: + +```text +AddCartItemRequest { + productId: uuid // 必填 + quantity: integer // 必填,1 ≤ quantity ≤ 商品当前实时可售库存 +} +``` + +- 校验规则: + - `quantity` 必须为正整数,1~当前实时可售库存。 + - 服务端忽略请求中任何尝试指定 `userId`、`cartItemId`、`createdAt` 的字段;条目归属固定为当前买家。 + - 商品必须处于已上架状态;库存不足、商品下架或被禁用时拒绝。 + +#### 成功响应 + +- HTTP 状态:`201 Created`(新增条目)或 `200 OK`(重复加入累加) +- Response Header:`Location: /api/cart/items/{cartItemId}` +- 响应 Schema:`CartItemResponse` + +```text +CartItemResponse { + cartItemId: uuid + productId: uuid + productSummary: ProductSummaryResponse + unitPrice: number // 服务端实时单价(decimal) + quantity: integer + subtotal: number // unitPrice × quantity,由服务端计算 + isSelected: boolean + isAvailable: boolean + unavailableReason: string? // 例如 "ProductUnpublished"、"OutOfStock" + maxAllowedQuantity: integer // 商品当前实时可售库存,供前端截断 + createdAt: string + updatedAt: string +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `quantity` 缺失、非整数、≤0 或超过实时库存 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | +| 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品已下架或被禁用 | +| 409 | `CART.QUANTITY_EXCEEDS_STOCK` | 累加后数量超过商品实时可售库存,返回当前最大允许值 | +| 429 | `COMMON.RATE_LIMITED` | 触发限流 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Redis 幂等存储或商品服务暂时不可用 | +| 500 | `COMMON.INTERNAL_ERROR` | 未处理的服务端错误 | + +#### 业务规则与并发 + +- 主键为 `(buyer_id, product_id)`;同一组合只能保留一条记录,重复加入时新数量累加到已有条目。 +- 累加过程在同一数据库事务内完成:读取已有条目、加锁或条件更新、`quantity = quantity + :newQty`;影响行数为 0 即失败。 +- 条目归属固定为当前买家;客户端传入的 `userId`、`cartItemId` 被忽略;越权访问他人条目返回 404。 +- 商品不可加时返回明确错误码与 `maxAllowedQuantity`;前端按此截断。 +- 接受 `Idempotency-Key` Header;同一 `(userId, key)` 在约定窗口(默认 5 分钟)内重复提交只生效一次,返回首次已确认结果且不重复累加数量。 + +#### 缓存、事件或外部依赖 + +- 不缓存购物车条目;商品价格、库存与上下架状态由 Catalog 模块实时提供。 +- 幂等键记录写入 Redis:`cart:idempotency:{userId}:{key}`,TTL = 5 分钟。 +- 不发布集成事件。 + +#### 验证场景 + +- 已上架商品、合法 `quantity` → 201,返回最新条目。 +- 同一商品二次加入 → 200,条目数量累加,库存上限生效。 +- 数量 ≤ 0 或超过库存 → 400 / `COMMON.VALIDATION_FAILED`,附 `maxAllowedQuantity`。 +- 商品已下架 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`,不创建条目。 +- 商品被禁用 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 +- 已存在购物车条目累加后超库存 → 409 / `CART.QUANTITY_EXCEEDS_STOCK`,原条目数量不超上限。 +- 同一 `Idempotency-Key` 重复提交 → 仅首次创建/累加,后续返回首次结果且 `quantity` 不再累加。 + +### A202 查看购物车 + +- 模块 / Tag:Cart +- 需求编号:F07、M03-01 +- 负责人:朱惠惠 +- 关联数据表:DB041 +- 当前状态:待交叉评审 +- 用途:买家查看本人购物车全部条目;返回实时单价、选中状态、可用性与失效原因。 +- 方法与路径:`GET /api/cart/items` +- operationId:`Cart_ListItems` + +#### 请求 + +- Route 参数:无 +- Query 参数: + - `page`(默认 1) + - `pageSize`(默认 50,上限 100) + - `selectedOnly`(可选,默认 `false`,仅返回选中条目) + - `availableOnly`(可选,默认 `false`,仅返回可结算条目) +- Header:`Authorization: Bearer `(必填) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`CartListResponse` + +```text +CartListResponse { + items: CartItemResponse[] + page: integer + pageSize: integer + total: integer + totalPages: integer + selectedCount: integer // 当前选中条目数量 + selectedTotalAmount: number // 选中条目按实时单价计算的总额 + availableSelectedCount: integer // 选中且可结算的条目数量 +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 分页或筛选参数非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 严格按 `buyer_id = current_user_id` 过滤;不允许跨用户查看。 +- 排序默认按 `updatedAt desc`;相同 `updatedAt` 时按 `productId` 稳定排序。 +- 商品下架、库存归零或被禁用时,条目仍可见但标记 `isAvailable=false` 并附 `unavailableReason`;不参与 `selectedTotalAmount` 与 `availableSelectedCount` 计算。 +- 实时单价与库存来自 Catalog 模块;不接受客户端传入的价格或库存覆盖。 + +#### 缓存、事件或外部依赖 + +- 不缓存购物车内容;价格、库存与上下架状态由 Catalog 模块实时返回。 +- 不发布集成事件。 + +#### 验证场景 + +- 买家购物车 0 条 → `items=[]`,`selectedCount=0`,`selectedTotalAmount=0`。 +- 包含已下架商品 → 仍可见,`isAvailable=false`,`selectedTotalAmount` 不计入。 +- 包含失效商品但被选中 → `availableSelectedCount` 仅统计可用条目。 +- 跨用户访问 → 403 / `AUTH.FORBIDDEN`,不泄露他人条目。 +- 翻页查询 → 总数与分页元数据稳定,按 `updatedAt desc` 一致排序。 + +### A203 修改购物车条目数量 + +- 模块 / Tag:Cart +- 需求编号:F07、M03-01 +- 负责人:朱惠惠 +- 关联数据表:DB041 +- 当前状态:待交叉评审 +- 用途:买家调整购物车条目数量;调大时校验实时库存上限,调小或调为 1 不受库存约束;不允许改为 0 或负数。 +- 方法与路径:`PATCH /api/cart/items/{cartItemId}` +- operationId:`Cart_UpdateItemQuantity` + +#### 请求 + +- Route 参数:`cartItemId: uuid` +- Header:`Authorization: Bearer `(必填) +- Body: + +```text +UpdateCartItemQuantityRequest { + quantity: integer // 必填,1 ≤ quantity ≤ 商品当前实时可售库存 +} +``` + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`CartItemResponse`(同 A201,含最新 `quantity`、`subtotal`、`maxAllowedQuantity`) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `quantity` 缺失、非整数、≤0 或超过实时库存 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 404 | `RESOURCE.NOT_FOUND` | 条目不存在或不属于当前用户 | +| 409 | `CART.ITEM_UNAVAILABLE` | 商品已下架或被禁用,不允许调大 | +| 409 | `CART.QUANTITY_EXCEEDS_STOCK` | 调大后超过实时可售库存 | + +#### 业务规则与并发 + +- 严格按 `cart_item_id = :cartItemId AND buyer_id = current_user_id` 条件更新;不存在的条目返回 404。 +- 调小或调到 1 不受实时库存上限约束;商品下架或被禁用时允许调小或删除,但禁止调大或累加。 +- 库存上限校验以 Catalog 模块实时库存为准;不允许客户端传入目标库存。 +- 服务端不接受修改 `productId`、`isSelected`、`userId` 等字段;选中状态变更走 A206。 + +#### 缓存、事件或外部依赖 + +- 不缓存、不发布集成事件。 + +#### 验证场景 + +- 条目数量从 2 改到 5,库存充足 → 200,条目更新。 +- 条目数量从 5 改到 10,超过库存 → 409 / `CART.QUANTITY_EXCEEDS_STOCK`,附 `maxAllowedQuantity`。 +- 条目数量改为 0 或 -1 → 400 / `COMMON.VALIDATION_FAILED`。 +- 商品已下架,条目从 2 调到 1 → 200;条目从 1 调到 2 → 409 / `CART.ITEM_UNAVAILABLE`。 +- 修改他人条目 → 404,不泄露归属。 + +### A204 删除购物车条目 + +- 模块 / Tag:Cart +- 需求编号:F07、M03-01 +- 负责人:朱惠惠 +- 关联数据表:DB041 +- 当前状态:待交叉评审 +- 用途:买家单条删除购物车条目;幂等执行,已删除条目再次删除返回 204。 +- 方法与路径:`DELETE /api/cart/items/{cartItemId}` +- operationId:`Cart_RemoveItem` + +#### 请求 + +- Route 参数:`cartItemId: uuid` +- Header:`Authorization: Bearer `(必填) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`204 No Content` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 删除按 `cart_item_id = :cartItemId AND buyer_id = current_user_id` 过滤;影响行数为 0 时返回 204,保持幂等。 +- 不返回 404,避免暴露条目归属;删除请求仅在鉴权失败时返回 401/403。 +- 默认地址或失效条目也可删除;删除后不自动选择其他默认地址或恢复库存。 + +#### 缓存、事件或外部依赖 + +- 不缓存、不发布集成事件。 + +#### 验证场景 + +- 删除本人条目 → 204,列表更新。 +- 重复删除同一 `cartItemId` → 204,幂等。 +- 删除他人条目 → 204,不报错也不泄露归属。 +- 未登录调用 → 401 / `AUTH.UNAUTHENTICATED`。 + +### A205 批量删除购物车条目 + +- 模块 / Tag:Cart +- 需求编号:F07、M03-01 +- 负责人:朱惠惠 +- 关联数据表:DB041 +- 当前状态:待交叉评审 +- 用途:买家一次性删除多个购物车条目;不在本人购物车中的条目被忽略,整体请求返回成功。 +- 方法与路径:`POST /api/cart/items/batch-delete` +- operationId:`Cart_BatchRemoveItems` + +#### 请求 + +- Header:`Authorization: Bearer `(必填) +- Body: + +```text +BatchRemoveCartItemsRequest { + cartItemIds: uuid[] // 必填,1~100 个;超过上限返回 400 +} +``` + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`BatchRemoveCartItemsResponse` + +```text +BatchRemoveCartItemsResponse { + removedCount: integer + skippedCount: integer // 不存在或不属于当前买家的条目数量 +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `cartItemIds` 缺失、为空、超过 100 个或包含非法 UUID | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 同一数据库事务内按 `cart_item_id IN (:ids) AND buyer_id = current_user_id` 删除;返回实际删除数量。 +- 不在本人购物车中的条目被忽略并计入 `skippedCount`;整体请求不报错。 +- 删除成功后 `selectedCount` 与 `selectedTotalAmount` 自动按剩余条目重算。 + +#### 缓存、事件或外部依赖 + +- 不缓存、不发布集成事件。 + +#### 验证场景 + +- 选中 3 条有效条目批量删除 → 200,`removedCount=3`,`skippedCount=0`。 +- 提交 1 条他人条目 + 2 条本人条目 → 200,`removedCount=2`,`skippedCount=1`。 +- 提交 0 条或 101 条 `cartItemIds` → 400 / `COMMON.VALIDATION_FAILED`。 + +### A206 修改选中状态(全选/反选/单选) + +- 模块 / Tag:Cart +- 需求编号:F07、M03-01 +- 负责人:朱惠惠 +- 关联数据表:DB041 +- 当前状态:待交叉评审 +- 用途:买家设置购物车条目选中状态;支持全选、反选、单条切换;失效条目不允许被选中。 +- 方法与路径:`PATCH /api/cart/items/selection` +- operationId:`Cart_UpdateSelection` + +#### 请求 + +- Header:`Authorization: Bearer `(必填) +- Body: + +```text +UpdateCartItemSelectionRequest { + mode: "SelectAll" | "DeselectAll" | "SetExplicit" + cartItemIds: uuid[]? // 仅当 mode = "SetExplicit" 时必填;最多 100 个 + isSelected: boolean? // 仅当 mode = "SetExplicit" 时必填 +} +``` + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`CartListResponse`(同 A206,按当前选中状态返回完整购物车) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `mode` 非法、`cartItemIds` 缺失/超限或 `isSelected` 缺失 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 409 | `CART.ITEM_UNAVAILABLE` | 尝试选中已下架或失效的条目 | + +#### 业务规则与并发 + +- 全选/反选按 `buyer_id = current_user_id` 过滤;失效条目保持未选中,不被强制选中。 +- `SetExplicit` 仅修改 `cartItemIds` 中属于当前买家的条目;他人条目被忽略并计入 `skippedCount`(由响应 `selectedCount`/`availableSelectedCount` 体现)。 +- 单条切换并发安全:服务端使用条件更新 `WHERE cart_item_id = :id AND buyer_id = current_user_id`。 +- 选中状态保存在服务端;前端刷新或重新登录后状态保留。 + +#### 缓存、事件或外部依赖 + +- 不缓存、不发布集成事件。 + +#### 验证场景 + +- 全选 → 200,所有可用条目 `isSelected=true`,失效条目仍 `isSelected=false`。 +- 反选 → 200,所有可用条目 `isSelected=false`。 +- 单选切换某条目 → 200,仅该条目 `isSelected` 变更。 +- 尝试选中失效条目 → 409 / `CART.ITEM_UNAVAILABLE`,不修改状态。 +- 跨用户 ID 提交 → 仅本人条目被修改,他人条目被忽略。 + +### A207 清空购物车 + +- 模块 / Tag:Cart +- 需求编号:F07、M03-01 +- 负责人:朱惠惠 +- 关联数据表:DB041 +- 当前状态:待交叉评审 +- 用途:买家一键清空本人购物车的全部条目;幂等执行,重复清空返回 204。 +- 方法与路径:`DELETE /api/cart` +- operationId:`Cart_Clear` + +#### 请求 + +- Header:`Authorization: Bearer `(必填) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`204 No Content` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 按 `buyer_id = current_user_id` 物理删除全部条目;只影响当前用户。 +- 重复清空 → 204,幂等。 +- 不影响浏览记录、收藏、消息或默认地址等其他模块数据。 + +#### 缓存、事件或外部依赖 + +- 不缓存、不发布集成事件。 + +#### 验证场景 + +- 购物车含 5 条条目 → 204,后续列表为空。 +- 重复清空 → 204,幂等。 +- 未登录调用 → 401。 + +### A208 获取结算预览 + +- 模块 / Tag:Cart +- 需求编号:F07、M03-01 +- 负责人:朱惠惠 +- 关联数据表:DB041 +- 当前状态:待交叉评审 +- 用途:买家进入结算页前查看选中条目总价、可用性与失效原因;服务端再次校验实时价格、库存与归属。 +- 方法与路径:`GET /api/cart/checkout-preview` +- operationId:`Cart_GetCheckoutPreview` + +#### 请求 + +- Query 参数:`cartItemIds`(可选,多个 UUID;不传则按当前 `isSelected=true` 过滤) +- Header:`Authorization: Bearer `(必填) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`CheckoutPreviewResponse` + +```text +CheckoutPreviewResponse { + items: CartItemResponse[] // 当前可用于结算的条目 + unavailableItems: CartItemResponse[] // 失效条目(不下单但提示买家) + totalAmount: number // 服务端按实时单价计算的总额 + availableForCheckout: boolean // 是否有至少一条可结算条目 +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `cartItemIds` 超过 100 个或包含非法 UUID | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 不传 `cartItemIds` 时按 `isSelected=true AND buyer_id = current_user_id` 过滤。 +- 传入 `cartItemIds` 时取交集;不在本人购物车或失效条目归入 `unavailableItems`。 +- `totalAmount` 由服务端实时计算并返回;前端不得自行覆盖金额。 +- 返回 `availableForCheckout=false` 时前端禁用提交订单按钮。 + +#### 缓存、事件或外部依赖 + +- 不缓存;价格与库存由 Catalog 模块实时返回。 +- 不发布集成事件;提交订单由 M04 处理。 + +#### 验证场景 + +- 选中 2 条可用 + 1 条失效 → `items=2`、`unavailableItems=1`、`availableForCheckout=true`。 +- 全部失效 → `items=[]`、`availableForCheckout=false`,前端禁用提交。 +- 传入他人 `cartItemId` → 归入 `unavailableItems`,不报错也不泄露归属。 + +### A220 商家创建秒杀活动 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB042、DB043 +- 当前状态:待交叉评审 +- 用途:商家维护秒杀活动;活动绑定一个已上架商品,保存秒杀价、独立库存总量与单用户限购;保存后状态为 `Draft`。 +- 方法与路径:`POST /api/merchant/seckill-activities` +- operationId:`Seckill_CreateActivity` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Merchant) +- Body: + +```text +CreateSeckillActivityRequest { + productId: uuid // 必填,必须是当前商家已上架商品 + activityName: string // 必填,1~50 字 + seckillPrice: number // 必填,>0 且 < 商品当前上架价 + totalStock: integer // 必填,1 ≤ totalStock ≤ 商品当前可售库存 + perBuyerLimit: integer // 必填,1 ≤ perBuyerLimit ≤ totalStock + startAt: string // 必填,UTC ISO 8601,≥ now() + 5min + endAt: string // 必填,UTC ISO 8601,> startAt 且 ≤ startAt + 30d +} +``` + +#### 成功响应 + +- HTTP 状态:`201 Created` +- Response Header:`Location: /api/seckill-activities/{activityId}` +- 响应 Schema:`SeckillActivityResponse` + +```text +SeckillActivityResponse { + activityId: uuid + productId: uuid + activityName: string + seckillPrice: number + originalPrice: number // 商品当前上架价 + totalStock: integer + remainingStock: integer // 创建后等于 totalStock + soldCount: integer // 创建后等于 0 + perBuyerLimit: integer + startAt: string + endAt: string + status: "Draft" + createdAt: string + updatedAt: string +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段缺失、格式错误或金额/数量/时间窗口非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | +| 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品未上架或不属于当前商家 | +| 409 | `SECKILL.STOCK_EXCEEDS_AVAILABLE` | `totalStock` 超过商品当前可售库存 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 商品服务或分布式锁不可用 | + +#### 业务规则与并发 + +- 同一商品同一时间段(`startAt`、`endAt` 与已存在活动存在重叠)不允许重复创建;重叠返回 `409 / SECKILL.TIME_WINDOW_CONFLICT`。 +- `seckillPrice < originalPrice` 由服务端校验;不接受等于或高于原价的秒杀活动。 +- `startAt ≥ now() + 5min` 避免立刻开始的发布影响压测一致性。 +- 创建活动时同步在 `seckill_inventory`(DB043)写入 `remainingStock=totalStock`、`soldCount=0`、`frozenCount=0`;两者在同一事务。 +- 商品归属:仅当 `product.owner_merchant_id = current_user_id` 才允许创建;越权访问返回 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 + +#### 缓存、事件或外部依赖 + +- 活动创建后向 Redis 写入分布式锁 Key:`lock:seckill:activity:create:{productId}`,事务结束释放。 +- 不缓存、不发布集成事件。 + +#### 验证场景 + +- 合法参数创建 → 201,状态 `Draft`,库存=总量。 +- `totalStock` 超过商品库存 → 409 / `SECKILL.STOCK_EXCEEDS_AVAILABLE`。 +- `seckillPrice ≥ originalPrice` → 400 / `COMMON.VALIDATION_FAILED`。 +- `startAt < now() + 5min` → 400 / `COMMON.VALIDATION_FAILED`。 +- 时间窗口与已存在活动重叠 → 409 / `SECKILL.TIME_WINDOW_CONFLICT`。 +- 尝试绑定他人商品 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 + +### A221 商家更新秒杀活动 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB042、DB043 +- 当前状态:待交叉评审 +- 用途:商家在 `Draft` 或 `Scheduled` 状态下更新秒杀活动参数;`Ongoing`/`Finished`/`Cancelled` 状态不允许修改。 +- 方法与路径:`PATCH /api/seckill-activities/{activityId}` +- operationId:`Seckill_UpdateActivity` + +#### 请求 + +- Route 参数:`activityId: uuid` +- Header:`Authorization: Bearer `(必填,角色 Merchant) +- Body: + +```text +UpdateSeckillActivityRequest { + activityName?: string // 可选 + seckillPrice?: number // 可选 + totalStock?: integer // 可选;只能调大或保持;不得小于已售数量 + perBuyerLimit?: integer // 可选 + startAt?: string // 可选;不得早于 now() + 5min + endAt?: string // 可选 +} +``` + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillActivityResponse` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段格式或时间窗口非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | +| 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | +| 409 | `SECKILL.INVALID_STATUS` | 活动已 `Ongoing`/`Finished`/`Cancelled` | +| 409 | `SECKILL.STOCK_BELOW_SOLD` | `totalStock` 小于已售数量 | +| 409 | `SECKILL.TIME_WINDOW_CONFLICT` | 与其他活动时间窗口重叠 | + +#### 业务规则与并发 + +- 仅允许在 `Draft` 或 `Scheduled` 状态更新;状态字段由 `status='Draft' OR status='Scheduled'` 条件更新保证。 +- `totalStock` 只允许调大或保持;调整后必须满足 `remainingStock + soldCount + frozenCount = totalStock`。 +- 修改后 `startAt` 与 `endAt` 必须保持 `startAt ≥ now() + 5min` 与 `endAt > startAt`。 + +#### 缓存、事件或外部依赖 + +- 同步更新 Redis 缓存:`cache:seckill:activity:{activityId}`(仅元数据,不含库存)。 + +#### 验证场景 + +- 草稿活动更新名称与价格 → 200。 +- 草稿活动 `totalStock` 调小到 `soldCount` 以下 → 409 / `SECKILL.STOCK_BELOW_SOLD`。 +- 进行中活动尝试改价 → 409 / `SECKILL.INVALID_STATUS`。 +- 时间窗口与他人活动重叠 → 409 / `SECKILL.TIME_WINDOW_CONFLICT`。 + +### A222 商家发布秒杀活动 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB042、DB043 +- 当前状态:待交叉评审 +- 用途:商家将 `Draft` 状态活动提交审核后立即变为 `Scheduled`;系统按 `startAt` 自动推进到 `Ongoing`。 +- 方法与路径:`POST /api/seckill-activities/{activityId}/publish` +- operationId:`Seckill_PublishActivity` + +#### 请求 + +- Route 参数:`activityId: uuid` +- Header:`Authorization: Bearer `(必填,角色 Merchant) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillActivityResponse`(`status="Scheduled"`) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | +| 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | +| 409 | `SECKILL.INVALID_STATUS` | 活动已发布或已结束 | +| 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品已下架,禁止发布 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 缓存写入失败 | + +#### 业务规则与并发 + +- 条件更新:`UPDATE ... SET status='Scheduled' WHERE activity_id=:id AND status='Draft' AND owner_merchant_id=:mid`;影响行数为 0 时按 409 处理。 +- 商品已下架时拒绝发布;商家需先恢复上架。 +- 发布成功后刷新 Redis 缓存并预热活动详情 Key;Worker 按 `startAt` 自动推进到 `Ongoing`。 + +#### 缓存、事件或外部依赖 + +- Redis:`cache:seckill:activity:{activityId}`、`cache:seckill:list:active`。 + +#### 验证场景 + +- 草稿活动发布 → 200,状态 `Scheduled`。 +- 重复发布 → 409 / `SECKILL.INVALID_STATUS`。 +- 商品已下架 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 + +### A223 商家取消秒杀活动 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB042、DB043 +- 当前状态:待交叉评审 +- 用途:商家取消 `Draft` / `Scheduled` / `Ongoing` 状态活动;取消后入口立即失效,已存在秒杀订单按既有流程走完;本期不回收已分配库存。 +- 方法与路径:`POST /api/seckill-activities/{activityId}/cancel` +- operationId:`Seckill_CancelActivity` + +#### 请求 + +- Route 参数:`activityId: uuid` +- Header:`Authorization: Bearer `(必填,角色 Merchant) +- Body: + +```text +CancelSeckillActivityRequest { + reason?: string // 可选,0~200 字 +} +``` + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillActivityResponse`(`status="Cancelled"`,含 `cancelReason`) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段格式错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | +| 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | +| 409 | `SECKILL.INVALID_STATUS` | 活动已 `Finished` 或已 `Cancelled` | + +#### 业务规则与并发 + +- 条件更新:`status IN ('Draft','Scheduled','Ongoing') → 'Cancelled'`;影响行数为 0 时按 409 处理。 +- 取消时 `remainingStock` 保留为冻结状态,不自动回收到普通商品库存。 +- 已存在秒杀订单沿用 M04 状态机;C03 超时取消时回补到原秒杀库存通道。 + +#### 缓存、事件或外部依赖 + +- 删除 Redis 缓存:`cache:seckill:activity:{activityId}`、`cache:seckill:list:active`。 + +#### 验证场景 + +- `Ongoing` 活动取消 → 200,状态 `Cancelled`,抢购入口立即失效。 +- 重复取消 → 409 / `SECKILL.INVALID_STATUS`。 +- 已取消活动 → 409。 + +### A224 商家秒杀活动列表 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB042、DB043 +- 当前状态:待交叉评审 +- 用途:商家分页查询本人维护的秒杀活动,支持按状态、时间窗口和关键词筛选。 +- 方法与路径:`GET /api/merchant/seckill-activities` +- operationId:`Seckill_ListMerchantActivities` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Merchant) +- Query 参数: + - `page`(默认 1) + - `pageSize`(默认 10,上限 50) + - `status`(可选,可多值:`Draft` / `Scheduled` / `Ongoing` / `Finished` / `Cancelled`) + - `keyword`(可选,对活动名称做模糊匹配) + - `startFrom`、`startTo`(可选,时间范围) + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillActivityListResponse` + +```text +SeckillActivityListResponse { + items: SeckillActivityResponse[] + page: integer + pageSize: integer + total: integer + totalPages: integer +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 分页或筛选参数非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家 | + +#### 业务规则与并发 + +- 严格按 `owner_merchant_id = current_user_id` 过滤;不允许查询他人活动。 +- 排序默认按 `startAt desc`;相同 `startAt` 时按 `activityId` 稳定排序。 + +#### 缓存、事件或外部依赖 + +- 不缓存商家管理端列表。 + +#### 验证场景 + +- 商家查询本人活动 → 200,按 `startAt desc` 排序。 +- 状态筛选 `Ongoing` → 仅返回进行中活动。 +- 商家访问他人活动 → 403,不泄露他人数据。 + +### A225 商家秒杀活动详情 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB042、DB043、DB044 +- 当前状态:待交叉评审 +- 用途:商家查看本人秒杀活动详情;包含库存、已售、单用户限购、订单统计与取消原因等内部字段。 +- 方法与路径:`GET /api/seckill-activities/{activityId}` +- operationId:`Seckill_GetMerchantActivityDetail` + +#### 请求 + +- Route 参数:`activityId: uuid` +- Header:`Authorization: Bearer `(必填,角色 Merchant) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillActivityDetailResponse` + +```text +SeckillActivityDetailResponse { + activity: SeckillActivityResponse + orderStats: SeckillOrderStatsResponse + cancelReason: string? + cancelledAt: string? +} + +SeckillOrderStatsResponse { + totalOrders: integer + paidOrders: integer + cancelledOrders: integer + totalSoldAmount: number // 秒杀价 × 数量(不含退款) +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | +| 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | + +#### 业务规则与并发 + +- 严格按 `owner_merchant_id = current_user_id` 过滤;跨商家访问返回 404,避免泄露活动存在性。 +- 订单统计来自 DB044(`seckill_orders`,与 M04 `orders` 共享事实库,通过 `seckill_activity_id` 关联)。 +- `cancelReason` 与 `cancelledAt` 仅在 `status=Cancelled` 时返回。 + +#### 缓存、事件或外部依赖 + +- 不缓存;订单统计每次实时计算。 + +#### 验证场景 + +- 商家查询本人进行中活动 → 200,包含订单统计与内部字段。 +- 商家查询他人活动 → 404,不泄露归属。 +- 已取消活动 → 200,含 `cancelReason` 与 `cancelledAt`。 + +### A226 买家秒杀活动列表 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB042、DB043 +- 当前状态:待交叉评审 +- 用途:游客和买家查看正在进行或即将开始的秒杀活动;仅返回公开字段。 +- 方法与路径:`GET /api/seckill-activities` +- operationId:`Seckill_ListActiveActivities` + +#### 请求 + +- Header:无强制要求 +- Query 参数: + - `page`(默认 1) + - `pageSize`(默认 10,上限 50) + - `window`(可选,`Ongoing` / `Upcoming` / `All`;默认 `All`,过滤已结束/已取消) + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillActivityListResponse`(仅公开字段,`status` 仅返回 `Scheduled` / `Ongoing`) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 分页或 `window` 参数非法 | + +#### 业务规则与并发 + +- 仅返回 `status IN ('Scheduled','Ongoing')` 的活动;已结束、已取消或草稿活动不公开。 +- 排序默认按 `startAt asc`(即将开始优先),相同 `startAt` 时按 `activityId` 稳定排序。 +- 公开响应中 `remainingStock` 不返回具体数字,仅返回 `isSoldOut` 布尔;具体剩余库存通过 A227 查询。 + +#### 缓存、事件或外部依赖 + +- Redis:`cache:seckill:list:active`,TTL 30 秒;活动状态变更或售罄时主动失效。 + +#### 验证场景 + +- 游客访问 → 200,仅返回进行中和即将开始的活动。 +- `window=Ongoing` → 仅返回进行中活动。 +- 已结束或已取消活动不出现。 + +### A227 买家秒杀活动详情 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB042、DB043 +- 当前状态:待交叉评审 +- 用途:游客和买家查看秒杀活动详情;返回公开字段、商品基础信息与抢购入口。 +- 方法与路径:`GET /api/seckill-activities/{activityId}/public` +- operationId:`Seckill_GetActiveActivityDetail` + +#### 请求 + +- Route 参数:`activityId: uuid` +- Header:无强制要求 +- Body:无 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillActivityDetailResponse`(仅公开字段,`cancelReason` 等内部字段不返回) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 404 | `RESOURCE.NOT_FOUND` | 活动不存在或未公开 | +| 410 | `SECKILL.ACTIVITY_GONE` | 活动已结束或已取消 | + +#### 业务规则与并发 + +- 仅返回 `status IN ('Scheduled','Ongoing')` 的活动;其他状态返回 410。 +- 已登录买家响应额外包含 `currentBuyerOrderCount`、`currentBuyerRemaining`(用于限购提示),按 `(activity_id, buyer_id)` 实时统计。 + +#### 缓存、事件或外部依赖 + +- Redis:`cache:seckill:activity:{activityId}`,TTL 30 秒;活动状态变更或库存售罄时主动失效。 + +#### 验证场景 + +- 游客访问进行中活动 → 200,含商品基础信息、秒杀价、开始/结束时间。 +- 已结束活动 → 410 / `SECKILL.ACTIVITY_GONE`。 +- 已登录买家访问 → 额外返回当前用户已下单数量与剩余可购数量。 + +### A228 秒杀下单 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB043、DB044、DB045 +- 当前状态:待交叉评审 +- 用途:买家抢购秒杀商品;服务端以数据库条件更新扣减秒杀库存、创建订单与秒杀订单项快照;事务保证不超卖、不少卖、不产生孤立记录。 +- 方法与路径:`POST /api/seckill-orders` +- operationId:`Seckill_PlaceOrder` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Buyer);`Idempotency-Key: `(必填,防止重复点击与网络重试) +- Body: + +```text +PlaceSeckillOrderRequest { + activityId: uuid // 必填 + quantity: integer // 必填,1 ≤ quantity ≤ perBuyerLimit + addressId: uuid // 必填,必须属于当前买家 +} +``` + +#### 成功响应 + +- HTTP 状态:`201 Created` +- Response Header:`Location: /api/seckill-orders/{orderId}` +- 响应 Schema:`PlaceSeckillOrderResponse` + +```text +PlaceSeckillOrderResponse { + orderId: uuid + activityId: uuid + quantity: integer + seckillPrice: number + totalAmount: number // seckillPrice × quantity,服务端计算 + status: "PendingPayment" + expiresAt: string // 订单支付截止时间,UTC ISO 8601 + remainingStock: integer // 扣减后剩余库存(供前端展示) +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或格式错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | +| 409 | `SECKILL.NOT_STARTED` | 活动尚未开始 | +| 409 | `SECKILL.ALREADY_ENDED` | 活动已结束 | +| 409 | `SECKILL.SOLD_OUT` | 秒杀库存售罄 | +| 409 | `SECKILL.PER_BUYER_LIMIT_EXCEEDED` | 超过单用户限购 | +| 409 | `SECKILL.QUANTITY_EXCEEDS_LIMIT` | 单次购买数量超过限购或库存 | +| 409 | `RESOURCE.CONFLICT` | 地址不存在或不归属当前买家 | +| 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品已下架 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键被用于不同请求内容 | +| 429 | `COMMON.RATE_LIMITED` | 触发限流(秒杀入口限流阈值) | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Redis 限流、库存通道或下游服务不可用 | +| 500 | `COMMON.INTERNAL_ERROR` | 未处理的服务端错误 | + +#### 业务规则与并发 + +- 秒杀下单为高风险操作,必须使用 `Idempotency-Key`;缺失时返回 400 / `COMMON.VALIDATION_FAILED`。 +- 同一数据库事务内顺序: + 1. 按 `UPDATE seckill_inventory SET remaining = remaining - :qty, sold = sold + :qty, updated_at = now() WHERE activity_id = :aid AND status='Ongoing' AND start_at <= now() AND end_at > now() AND remaining >= :qty` 条件扣减秒杀库存;影响行数为 0 时整体事务回滚。 + 2. 校验 `(activity_id, buyer_id)` 维度已下单数量(含 `PendingPayment`、`Paid`、`Cancelled`)+ 本次 `quantity` 不超过 `perBuyerLimit`;超出时事务回滚。 + 3. 写入 `seckill_orders`(DB044,`orderId = order.id`)与 `seckill_order_items`(DB045,含 `seckillPrice` 快照与 `originalPrice`)。 + 4. 写入 Outbox `SeckillOrderCreated` 事件。 +- 不写入普通商品库存;`products.stock` 不受秒杀下单影响。 +- 失败优先级:限流 429 < 未开始 / 已结束 409 < 售罄 409 < 超过单用户限购 409 < 幂等键复用 409 < 业务异常 5xx。 + +#### 缓存、事件或外部依赖 + +- Redis:`lock:seckill:order:{activityId}`(细粒度互斥,避免活动行成为热点)、`cache:seckill:activity:{activityId}`(事务成功后失效)、`cart:idempotency:{userId}:{key}`(幂等记录,TTL 24 小时)。 +- Outbox:`SeckillOrderCreated`,由 M09 站内消息与 C03 超时取消消费。 + +#### 验证场景 + +- 100 并发抢 10 件库存、单用户限购 1 → 恰好 10 笔成功订单,其余 90 笔以 `SOLD_OUT` 或 `PER_BUYER_LIMIT_EXCEEDED` 失败;库存 `remaining=0`、`sold=10`。 +- 同一买家两次提交限购 1 的活动 → 第二次返回 `PER_BUYER_LIMIT_EXCEEDED`,不重复扣减。 +- 同一幂等键重复提交 → 第二次返回首次成功订单号,不重复扣减。 +- 活动未开始 → 409 / `SECKILL.NOT_STARTED`。 +- 活动已结束 → 409 / `SECKILL.ALREADY_ENDED`。 +- 商品已下架 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 +- 地址不属于当前买家 → 409 / `RESOURCE.CONFLICT`,不泄露地址存在性。 + +### A229 买家秒杀订单列表 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB044、DB045 +- 当前状态:待交叉评审 +- 用途:买家分页查询本人秒杀订单,支持按状态、活动和时间筛选。 +- 方法与路径:`GET /api/seckill-orders` +- operationId:`Seckill_ListMyOrders` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Buyer) +- Query 参数: + - `page`(默认 1) + - `pageSize`(默认 10,上限 50) + - `status`(可选,`PendingPayment` / `Paid` / `Shipped` / `Completed` / `Cancelled`) + - `activityId`(可选,按活动过滤) + - `createdFrom`、`createdTo`(可选,时间范围) + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillOrderListResponse` + +```text +SeckillOrderListResponse { + items: SeckillOrderSummaryResponse[] + page: integer + pageSize: integer + total: integer + totalPages: integer +} + +SeckillOrderSummaryResponse { + orderId: uuid + activityId: uuid + activityName: string + productId: uuid + productName: string + productImageUrl: string + quantity: integer + seckillPrice: number + totalAmount: number + status: "PendingPayment" | "Paid" | "Shipped" | "Completed" | "Cancelled" + createdAt: string + expiresAt: string // PendingPayment 时返回 +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 分页或筛选参数非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 严格按 `buyer_id = current_user_id AND seckill_activity_id IS NOT NULL` 过滤。 +- 排序默认按 `createdAt desc`;相同 `createdAt` 时按 `orderId` 稳定排序。 +- 不返回完整地址或支付敏感信息;详细快照在 A230。 + +#### 缓存、事件或外部依赖 + +- 不缓存;订单状态实时读取 DB044。 + +#### 验证场景 + +- 买家查询本人秒杀订单 → 200,仅返回与当前买家关联的记录。 +- 按活动过滤 → 200,仅返回该活动的订单。 +- 跨用户查询 → 403,不泄露他人订单。 + +### A230 买家秒杀订单详情 + +- 模块 / Tag:Seckill +- 需求编号:C01 +- 负责人:朱惠惠 +- 关联数据表:DB044、DB045 +- 当前状态:待交叉评审 +- 用途:买家查看本人秒杀订单完整详情;包含活动快照、订单项快照、地址快照与状态时间线。 +- 方法与路径:`GET /api/seckill-orders/{orderId}` +- operationId:`Seckill_GetMyOrder` + +#### 请求 + +- Route 参数:`orderId: uuid` +- Header:`Authorization: Bearer `(必填,角色 Buyer) +- Body:无 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`SeckillOrderDetailResponse` + +```text +SeckillOrderDetailResponse { + orderId: uuid + activityId: uuid + activityName: string + productId: uuid + productName: string + productImageUrl: string + quantity: integer + seckillPrice: number + originalPrice: number // 商品原价快照 + totalAmount: number + status: "PendingPayment" | "Paid" | "Shipped" | "Completed" | "Cancelled" + addressSnapshot: AddressSnapshotResponse + timeline: OrderTimelineEntryResponse[] + paymentInfo: PaymentInfoResponse? + createdAt: string + expiresAt: string + paidAt: string? + cancelledAt: string? +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 404 | `RESOURCE.NOT_FOUND` | 订单不存在、不属于当前用户或非秒杀订单 | + +#### 业务规则与并发 + +- 严格按 `order_id = :id AND buyer_id = current_user_id AND seckill_activity_id IS NOT NULL` 过滤;不满足任一条件返回 404,避免泄露订单存在性。 +- 地址快照来自下单时刻保存的 `orders.address_snapshot`,与 M04 共享字段。 +- 时间线包含创建、支付、发货、完成、取消等关键节点;时间均以 UTC 存储,前端按本地时区展示。 +- 支付信息(`paymentInfo`)仅在订单已支付后返回;支付卡号、Token 等敏感字段不出现。 + +#### 缓存、事件或外部依赖 + +- 不缓存;订单详情实时读取 DB044、DB045 与 M04 `orders`、`order_items`、`payments`。 + +#### 验证场景 + +- 买家查询本人秒杀订单 → 200,含活动快照、订单项快照、地址快照、时间线。 +- 跨用户访问 → 404,不泄露归属。 +- 已支付订单 → `paymentInfo` 返回;未支付订单不返回。 +- 已取消订单 → `cancelledAt` 与取消节点返回。 +- 普通订单(非秒杀)通过此接口访问 → 404,避免与 M04 详情接口混淆。 + +--- + +> 来源:[interface-wqq.md](interface/interface-wqq.md)。七个接口均为部分定义;DBxxx、幂等、状态字段和商家订单边界尚未确认。 + +### A301 提交订单 + +- **模块 / Tag**:Ordering +- **需求编号**:F08 +- **负责人**:韦乾强 +- **关联数据表**:DB001(orders)、DB003(order_items) +- **当前状态**:部分定义 +- **用途**:买家选择购物车商品和收货地址提交订单,系统原子扣减库存并返回订单号 +- **方法与路径**:`POST /api/orders` +- **operationId**:`Ordering_CreateOrder` +- **请求Schema**:`CreateOrderRequest` +- **响应Schema**:`CreateOrderResponse` +- **身份与Policy**:BuyerOnly +- **资源归属**:订单归属于当前登录买家 +- **幂等要求**:客户端生成幂等键 `Idempotency-Key`,服务端以 `(buyerId, idempotencyKey)` 保证幂等 + +#### 请求 + +- **Route参数**:无 +- **Query参数**:无 +- **Header**: + - `Authorization: Bearer `(必需) + - `Idempotency-Key: `(必需) + - `Content-Type: application/json` +- **Body**: +```json +{ + "addressId": "uuid", + "cartItemIds": ["uuid"], + "idempotencyKey": "uuid" +} +``` +- **校验规则**: + - `addressId`:必填,UUID格式,必须属于当前买家 + - `cartItemIds`:必填,非空数组,每个元素为UUID格式 + - `idempotencyKey`:必填,UUID格式 + +#### 成功响应 + +- **HTTP状态**:`201 Created` +- **响应Schema**:`CreateOrderResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "orderId": "3f0ed9a9-3c61-4ab6-a8dd-a54ea8dd78af", + "orderNo": "ORD20260724001", + "totalAmount": 299.00, + "status": "PendingPayment", + "createdAt": "2026-07-24T10:00:00Z" + } +} +``` + +#### 失败响应 + +| HTTP状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | ORDER.INVALID_PARAM | 参数格式错误 | +| 400 | ORDER.EMPTY_CART_ITEMS | 购物车商品列表为空 | +| 400 | ORDER.INVALID_ADDRESS | 收货地址无效或不归属当前用户 | +| 409 | ORDER.STOCK_INSUFFICIENT | 商品库存不足 | +| 409 | ORDER.ITEM_NOT_AVAILABLE | 商品已下架或不可售 | +| 409 | ORDER.IDEMPOTENT_CONFLICT | 幂等键重复,返回原订单 | + +#### 业务规则与并发 + +1. 同一幂等键只创建一张订单,重复请求返回首次成功结果 +2. 库存扣减使用条件更新 `WHERE stock >= quantity`,避免超卖 +3. 订单金额由服务端计算,不接受客户端传入 +4. 订单项保存商品名称、图片、单价快照 + +#### 缓存、事件或外部依赖 + +- 发布 `OrderCreatedEvent` 到 Outbox +- 依赖 DB001(orders)、DB003(order_items)、DB004(products) + +#### 验证场景 + +1. 正常提交订单:返回201,订单号 +2. 库存不足:返回409,订单未创建 +3. 地址无效:返回400 +4. 幂等键重复:返回原订单号,不重复扣库存 + +--- + +### A302 查询订单列表 + +- **模块 / Tag**:Ordering +- **需求编号**:F09 +- **负责人**:韦乾强 +- **关联数据表**:DB001(orders) +- **当前状态**:部分定义 +- **用途**:买家分页查询自己的订单列表,支持按状态筛选 +- **方法与路径**:`GET /api/orders` +- **operationId**:`Ordering_GetOrders` +- **请求Schema**:无 +- **响应Schema**:`OrderListResponse` +- **身份与Policy**:BuyerOnly +- **资源归属**:只返回当前买家订单 +- **幂等要求**:GET请求天然幂等 + +#### 请求 + +- **Route参数**:无 +- **Query参数**: + - `page`(可选,默认1):页码 + - `pageSize`(可选,默认10,上限50):每页条数 + - `status`(可选):筛选订单状态,`PendingPayment`/`Paid`/`Shipped`/`Completed`/`Cancelled` +- **Header**:`Authorization: Bearer `(必需) +- **Body**:无 + +#### 成功响应 + +- **HTTP状态**:`200 OK` +- **响应Schema**:`OrderListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "orderId": "uuid", + "orderNo": "ORD20260724001", + "status": "PendingPayment", + "totalAmount": 299.00, + "createdAt": "2026-07-24T10:00:00Z", + "itemSummary": "商品A x1,商品B x2" + } + ], + "page": 1, + "pageSize": 10, + "totalCount": 25, + "totalPages": 3 + } +} +``` + +#### 失败响应 + +| HTTP状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | ORDER.INVALID_PAGE_PARAM | 分页参数非法 | + +#### 业务规则与并发 + +1. 订单按创建时间倒序排列 +2. `itemSummary`最多展示3个商品名称,多的显示"+X件" + +#### 缓存、事件或外部依赖 + +无 + +#### 验证场景 + +1. 正常查询:返回订单列表 +2. 分页参数非法:返回400 +3. 无订单:返回空列表 + +--- + +### A303 查询订单详情 + +- **模块 / Tag**:Ordering +- **需求编号**:F09 +- **负责人**:韦乾强 +- **关联数据表**:DB001(orders)、DB003(order_items) +- **当前状态**:部分定义 +- **用途**:买家查看单个订单的完整详情 +- **方法与路径**:`GET /api/orders/{orderId}` +- **operationId**:`Ordering_GetOrderById` +- **请求Schema**:无 +- **响应Schema**:`OrderDetailResponse` +- **身份与Policy**:BuyerOnly +- **资源归属**:订单必须属于当前买家 +- **幂等要求**:GET请求天然幂等 + +#### 请求 + +- **Route参数**:`orderId`(必需,UUID) +- **Query参数**:无 +- **Header**:`Authorization: Bearer `(必需) +- **Body**:无 + +#### 成功响应 + +- **HTTP状态**:`200 OK` +- **响应Schema**:`OrderDetailResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "orderId": "uuid", + "orderNo": "ORD20260724001", + "status": "PendingPayment", + "totalAmount": 299.00, + "createdAt": "2026-07-24T10:00:00Z", + "addressSnapshot": { + "receiverName": "张三", + "phone": "138****8888", + "province": "广东省", + "city": "深圳市", + "district": "南山区", + "detailAddress": "科技园路1号" + }, + "items": [ + { + "productId": "uuid", + "productName": "商品A", + "imageUrl": "https://...", + "unitPrice": 199.00, + "quantity": 1, + "subtotal": 199.00 + } + ], + "statusHistory": [ + {"status": "PendingPayment", "time": "2026-07-24T10:00:00Z"}, + {"status": "Paid", "time": "2026-07-24T10:05:00Z"} + ], + "availableActions": ["cancel"] + } +} +``` + +#### 失败响应 + +| HTTP状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 404 | ORDER.NOT_FOUND | 订单不存在 | +| 403 | ORDER.ACCESS_DENIED | 订单不属于当前用户 | + +#### 业务规则与并发 + +1. 订单项为快照,包含下单时的商品名称、图片、单价 +2. 地址为快照,包含下单时的收货信息 +3. `availableActions`根据当前状态展示可执行操作 + +#### 缓存、事件或外部依赖 + +无 + +#### 验证场景 + +1. 正常查询:返回完整订单详情 +2. 订单不存在:返回404 +3. 跨用户访问:返回403 + +--- + +### A304 取消订单 + +- **模块 / Tag**:Ordering +- **需求编号**:F09 +- **负责人**:韦乾强 +- **关联数据表**:DB001(orders)、DB003(order_items)、DB004(products) +- **当前状态**:部分定义 +- **用途**:买家取消自己待支付的订单,触发库存回补 +- **方法与路径**:`POST /api/orders/{orderId}/cancel` +- **operationId**:`Ordering_CancelOrder` +- **请求Schema**:无 +- **响应Schema**:`CancelOrderResponse` +- **身份与Policy**:BuyerOnly +- **资源归属**:订单必须属于当前买家 +- **幂等要求**:以订单号为幂等键,重复取消返回成功 + +#### 请求 + +- **Route参数**:`orderId`(必需,UUID) +- **Query参数**:无 +- **Header**:`Authorization: Bearer `(必需) +- **Body**:无 + +#### 成功响应 + +- **HTTP状态**:`200 OK` +- **响应Schema**:`CancelOrderResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "orderId": "uuid", + "status": "Cancelled", + "cancelledAt": "2026-07-24T11:00:00Z", + "cancelReason": "BUYER_CANCELLED" + } +} +``` + +#### 失败响应 + +| HTTP状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 404 | ORDER.NOT_FOUND | 订单不存在 | +| 403 | ORDER.ACCESS_DENIED | 订单不属于当前用户 | +| 409 | ORDER.INVALID_STATUS | 订单状态不允许取消(已支付/已发货/已完成/已取消) | + +#### 业务规则与并发 + +1. 只有 `PendingPayment` 状态可取消 +2. 取消与库存回补在同一事务内完成 +3. 使用条件更新 `WHERE status = 'PendingPayment'` 保证幂等 +4. `cancelReason` 记录为 `BUYER_CANCELLED` + +#### 缓存、事件或外部依赖 + +- 发布 `OrderCancelledEvent` 到 Outbox +- 库存回补操作 DB004(products) + +#### 验证场景 + +1. 正常取消:返回成功,库存回补 +2. 重复取消:返回幂等成功 +3. 订单已支付:返回409 +4. 跨用户取消:返回403 + +--- + +### A305 商家查询订单列表 + +- **模块 / Tag**:Merchant +- **需求编号**:F12 +- **负责人**:韦乾强 +- **关联数据表**:DB001(orders)、DB003(order_items) +- **当前状态**:部分定义 +- **用途**:商家分页查询本店订单,支持按状态筛选 +- **方法与路径**:`GET /api/merchant/orders` +- **operationId**:`Merchant_GetOrders` +- **请求Schema**:无 +- **响应Schema**:`MerchantOrderListResponse` +- **身份与Policy**:MerchantOnly +- **资源归属**:只返回当前商家的订单 +- **幂等要求**:GET请求天然幂等 + +#### 请求 + +- **Route参数**:无 +- **Query参数**: + - `page`(可选,默认1):页码 + - `pageSize`(可选,默认10,上限50):每页条数 + - `status`(可选):筛选订单状态 +- **Header**:`Authorization: Bearer `(必需) +- **Body**:无 + +#### 成功响应 + +- **HTTP状态**:`200 OK` +- **响应Schema**:`MerchantOrderListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "orderId": "uuid", + "orderNo": "ORD20260724001", + "buyerUsername": "user123", + "status": "Paid", + "totalAmount": 299.00, + "createdAt": "2026-07-24T10:00:00Z", + "itemCount": 2 + } + ], + "page": 1, + "pageSize": 10, + "totalCount": 15, + "totalPages": 2 + } +} +``` + +#### 失败响应 + +| HTTP状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | ORDER.INVALID_PAGE_PARAM | 分页参数非法 | + +#### 业务规则与并发 + +1. 只返回与当前商家商品相关的订单 +2. 订单按创建时间倒序排列 + +#### 缓存、事件或外部依赖 + +无 + +#### 验证场景 + +1. 正常查询:返回订单列表 +2. 无订单:返回空列表 + +--- + +### A306 商家查询订单详情 + +- **模块 / Tag**:Merchant +- **需求编号**:F12 +- **负责人**:韦乾强 +- **关联数据表**:DB001(orders)、DB003(order_items) +- **当前状态**:部分定义 +- **用途**:商家查看本店订单的完整详情 +- **方法与路径**:`GET /api/merchant/orders/{orderId}` +- **operationId**:`Merchant_GetOrderById` +- **请求Schema**:无 +- **响应Schema**:`MerchantOrderDetailResponse` +- **身份与Policy**:MerchantOnly +- **资源归属**:订单必须属于当前商家的商品 +- **幂等要求**:GET请求天然幂等 + +#### 请求 + +- **Route参数**:`orderId`(必需,UUID) +- **Query参数**:无 +- **Header**:`Authorization: Bearer `(必需) +- **Body**:无 + +#### 成功响应 + +- **HTTP状态**:`200 OK` +- **响应Schema**:`MerchantOrderDetailResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "orderId": "uuid", + "orderNo": "ORD20260724001", + "buyerUsername": "user123", + "status": "Paid", + "totalAmount": 299.00, + "createdAt": "2026-07-24T10:00:00Z", + "paidAt": "2026-07-24T10:05:00Z", + "addressSnapshot": { + "receiverName": "张三", + "phone": "138****8888", + "province": "广东省", + "city": "深圳市", + "district": "南山区", + "detailAddress": "科技园路1号" + }, + "items": [ + { + "productId": "uuid", + "productName": "商品A", + "imageUrl": "https://...", + "unitPrice": 199.00, + "quantity": 1, + "subtotal": 199.00 + } + ], + "availableActions": ["ship"] + } +} +``` + +#### 失败响应 + +| HTTP状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 404 | ORDER.NOT_FOUND | 订单不存在 | +| 403 | ORDER.ACCESS_DENIED | 订单不归属当前商家 | + +#### 业务规则与并发 + +1. 只返回与当前商家商品相关的订单项 +2. `availableActions`根据当前状态展示可执行操作 + +#### 缓存、事件或外部依赖 + +无 + +#### 验证场景 + +1. 正常查询:返回完整订单详情 +2. 订单不存在:返回404 +3. 跨商家访问:返回403 + +--- + +### A307 商家发货 + +- **模块 / Tag**:Merchant +- **需求编号**:F12 +- **负责人**:韦乾强 +- **关联数据表**:DB001(orders) +- **当前状态**:部分定义 +- **用途**:商家对已支付订单执行发货操作 +- **方法与路径**:`POST /api/merchant/orders/{orderId}/ship` +- **operationId**:`Merchant_ShipOrder` +- **请求Schema**:`ShipOrderRequest` +- **响应Schema**:`ShipOrderResponse` +- **身份与Policy**:MerchantOnly +- **资源归属**:订单必须属于当前商家的商品 +- **幂等要求**:以订单号为幂等键,重复发货返回成功 + +#### 请求 + +- **Route参数**:`orderId`(必需,UUID) +- **Query参数**:无 +- **Header**:`Authorization: Bearer `(必需) +- **Body**: +```json +{ + "expressCompany": "顺丰速运", + "trackingNo": "SF1234567890" +} +``` +- **校验规则**: + - `expressCompany`:必填,1-50字符 + - `trackingNo`:必填,1-50字符 + +#### 成功响应 + +- **HTTP状态**:`200 OK` +- **响应Schema**:`ShipOrderResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "orderId": "uuid", + "status": "Shipped", + "shippedAt": "2026-07-24T12:00:00Z", + "expressCompany": "顺丰速运", + "trackingNo": "SF1234567890" + } +} +``` + +#### 失败响应 + +| HTTP状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 404 | ORDER.NOT_FOUND | 订单不存在 | +| 403 | ORDER.ACCESS_DENIED | 订单不归属当前商家 | +| 409 | ORDER.INVALID_STATUS | 订单状态不允许发货(只有已支付可发货) | + +#### 业务规则与并发 + +1. 只有 `Paid` 状态可发货 +2. 使用条件更新 `WHERE status = 'Paid'` 保证幂等 +3. 记录发货时间、物流公司和物流单号 + +#### 缓存、事件或外部依赖 + +- 发布 `OrderShippedEvent` 到 Outbox + +#### 验证场景 + +1. 正常发货:返回成功,状态变为Shipped +2. 重复发货:返回幂等成功 +3. 订单未支付:返回409 +4. 跨商家发货:返回403 + +--- + +--- + +> 来源:[interface-zhy.md](interface/interface-zhy.md)。已完成结构汇总;Payment、AfterSales 和对账接口存在模块边界及状态机冲突,当前不得直接冻结。 + +### A401 查询钱包余额 + +- **模块 / Tag**:Payment +- **需求编号**:M05-01-FR01 +- **负责人**:张海洋 +- **关联数据表**:DB081(待评审)— `wallets` +- **当前状态**:待交叉评审 +- **用途**:查询当前买家钱包余额 +- **方法与路径**:`GET /api/payment/wallet` +- **operationId**:`Payment_GetWalletBalance` +- **请求 Schema**:(无) +- **响应 Schema**:`WalletBalanceResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 钱包 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**:`currency`(可选,默认 `CNY`) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - JWT 有效、账号状态正常、令牌版本未过期 + - 钱包不存在时按需初始化(业务策略可由实现层决定,本接口约定返回余额 0) + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`WalletBalanceResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "walletId": "f5c2a8b9-3c61-4ab6-a8dd-a54ea8dd78af", + "balance": 100.50, + "currency": "CNY", + "updatedAt": "2026-07-23T08:30:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少或格式错误的 JWT | +| 401 | `AUTH.TOKEN_EXPIRED` | JWT 已过期 | +| 401 | `AUTH.TOKEN_REVOKED` | JWT 已撤销或账号令牌版本失效 | +| 403 | `AUTH.FORBIDDEN` | 当前角色非 Buyer | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 数据库不可用 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 仅返回当前 buyerId 钱包;商家/管理员无访问权限(按 1.6.2 资源归属规则) +- 余额以 PostgreSQL 实时值为准,不使用 Redis 缓存 +- 不返回钱包创建时间、内部审计字段 + +#### 缓存、事件或外部依赖 + +- 缓存:默认不缓存(私人数据按 1.13) +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:JWT 有效 → 返回当前余额 +- 异常:JWT 过期 → 401 + `AUTH.TOKEN_EXPIRED` +- 异常:商家账号调用 → 403 + `AUTH.FORBIDDEN` + +--- + +### A402 模拟充值 + +- **模块 / Tag**:Payment +- **需求编号**:M05-01-FR02 / FR03 +- **负责人**:张海洋 +- **关联数据表**:DB082(待评审)— `wallet_topups`、DB083(待评审)— `wallet_ledgers` +- **当前状态**:待交叉评审 +- **用途**:买家向本人钱包充值(使用模拟支付通道) +- **方法与路径**:`POST /api/payment/wallet/topups` +- **operationId**:`Payment_CreateTopup` +- **请求 Schema**:`CreateTopupRequest` +- **响应 Schema**:`TopupDetailResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 钱包 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 模拟充值) + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "amount": 100.50, + "channelNote": "MOCK_TOPUP" +} +``` +- **校验规则**: + - `amount` 必填,decimal,最多 2 位小数,`> 0` 且 `≤ 10000.00`(`PAYMENT.TOPUP_EXCEEDS_LIMIT`) + - `channelNote` 选填,默认 `MOCK_TOPUP`,仅作观测标识 + - `Idempotency-Key` 必填,UUID 格式;相同 buyerId + 相同 Key + 相同 amount → 返回首次结果 + - 同一 Key 不同 amount → 409 + `IDEMPOTENCY.KEY_REUSED` + +#### 成功响应 + +- **HTTP 状态**:`201 Created` +- **响应 Schema**:`TopupDetailResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "topupId": "c7a1d4e6-...", + "walletId": "f5c2a8b9-...", + "amount": 100.50, + "currency": "CNY", + "status": "Succeeded", + "createdAt": "2026-07-23T08:30:00Z", + "succeededAt": "2026-07-23T08:30:01Z", + "newBalance": 200.50 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段格式错误或 0/负数 | +| 400 | `PAYMENT.TOPUP_EXCEEDS_LIMIT` | 单笔金额 > 10000.00 或小数 > 2 位 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同金额 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 数据库不可用 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 钱包余额增加 + 钱包流水写入同一事务(按 PAY-R06) +- 幂等键级别唯一约束存于 DB082;命中直接返回首次成功结果 +- 单笔上限 10000.00 元(业务规则 PAY-R16,zhy 7-23 提交 db840e4 强调) +- 充值成功后才更新余额;不为重试创建多条 `wallet_ledgers` + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:发布 `WalletTopupSucceededIntegrationEvent`(待罗皓晨 M00 集成事件规范确认) +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:金额 100.50 + Idempotency-Key → 充值成功 + 余额 +100.50 +- 重复:相同 Key + 相同金额 → 返回首次结果,不重复加余额 +- 异常:金额 10000.01 → 400 + `PAYMENT.TOPUP_EXCEEDS_LIMIT` +- 异常:金额 100.555 → 400 + `PAYMENT.TOPUP_EXCEEDS_LIMIT`(小数 > 2 位) +- 异常:相同 Key + 不同金额 → 409 + `IDEMPOTENCY.KEY_REUSED` + +--- + +### A403 查询充值记录 + +- **模块 / Tag**:Payment +- **需求编号**:M05-01-FR04 +- **负责人**:张海洋 +- **关联数据表**:DB082(待评审)— `wallet_topups` +- **当前状态**:待交叉评审 +- **用途**:分页查询当前买家充值记录 +- **方法与路径**:`GET /api/payment/wallet/topups` +- **operationId**:`Payment_ListTopups` +- **请求 Schema**:`ListTopupsQuery` +- **响应 Schema**:`TopupListResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 充值记录 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**: + - `status`(可选):`Succeeded` / `Failed` / `Pending` + - `createdFrom`(可选):ISO 8601 UTC,包含 + - `createdTo`(可选):ISO 8601 UTC,不包含 + - `page`(默认 `1`) + - `pageSize`(默认 `10`,1-100) + - `sortBy`(白名单:`createdAt`,默认 `createdAt desc`) + - `sortOrder`(`asc` / `desc`) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `createdFrom ≤ createdTo`(否则 400) + - `status` 枚举必须白名单 + - `pageSize` ∈ [1, 100] + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`TopupListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "topupId": "c7a1d4e6-...", + "amount": 100.50, + "currency": "CNY", + "status": "Succeeded", + "createdAt": "2026-07-23T08:30:00Z", + "succeededAt": "2026-07-23T08:30:01Z" + } + ], + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | Query 参数错误(status 不在白名单、时间范围非法) | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 仅返回当前 buyerId 记录 +- 列表按 `createdAt desc, topupId desc` 稳定排序,避免翻页重复 + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:返回当前用户充值记录 +- 异常:page=0 → 400 + `COMMON.VALIDATION_FAILED` +- 异常:createdFrom > createdTo → 400 + `COMMON.VALIDATION_FAILED` + +--- + +### A404 收银台查询 + +- **模块 / Tag**:Payment +- **需求编号**:M05-01-FR05 +- **负责人**:张海洋 +- **关联数据表**:DB084(待评审)— `orders`(只读,用于查询订单金额/状态) +- **当前状态**:待交叉评审 +- **用途**:进入支付前的订单金额、应付、钱包余额、可用渠道聚合查询 +- **方法与路径**:`GET /api/payment/checkout/{orderId}` +- **operationId**:`Payment_GetCheckout` +- **请求 Schema**:(无) +- **响应 Schema**:`CheckoutResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 订单 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:`orderId`(UUID,必填) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `orderId` UUID 格式 + - 订单归属当前 buyerId + - 订单状态为 `PendingPayment`(否则 409 + `PAYMENT.ORDER_NOT_PAYABLE`) + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`CheckoutResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "orderId": "3f0ed9a9-...", + "orderAmount": 199.00, + "paidAmount": 0.00, + "currency": "CNY", + "walletBalance": 100.50, + "insufficient": true, + "availableChannels": ["MOCK_WALLET"], + "expiresAt": "2026-07-23T09:00:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 404 | `RESOURCE.NOT_FOUND` | 订单不存在或非本人 | +| 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 订单状态非 `PendingPayment` | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 不修改订单或钱包状态,纯查询 +- 余额、订单金额、应付以服务端实时值(按 PAY-R01) +- 订单已支付 → 返回 `PAID` 状态但 `CheckoutResponse` 仍可读 + +#### 缓存、事件或外部依赖 + +- 缓存:可短暂缓存(短 TTL 5s),不允许跨用户复用 +- 事件:无 +- 外部依赖:PostgreSQL + 钱包表 + +#### 验证场景 + +- 正常:订单本人 + `PendingPayment` + 余额不足 → 返回 `insufficient=true` +- 正常:订单本人 + `PendingPayment` + 余额充足 → 返回 `insufficient=false` +- 异常:他人订单 → 404 + `RESOURCE.NOT_FOUND` +- 异常:订单已支付 → 409 + `PAYMENT.ORDER_NOT_PAYABLE` + +--- + +### A405 模拟支付 + +- **模块 / Tag**:Payment +- **需求编号**:M05-01-FR05~FR09 +- **负责人**:张海洋 +- **关联数据表**:DB081(待评审)— `wallets`、DB083(待评审)— `wallet_ledgers`、DB085(待评审)— `payments`、DB084(待评审)— `orders` +- **当前状态**:待交叉评审 +- **用途**:从买家钱包扣款并完成订单支付 +- **方法与路径**:`POST /api/payment/orders/{orderId}/pay` +- **operationId**:`Payment_PayOrder` +- **请求 Schema**:`PayOrderRequest` +- **响应 Schema**:`PaymentResultResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 订单 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 模拟支付) + +#### 请求 + +- **Route 参数**:`orderId`(UUID,必填) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "expectedAmount": 199.00, + "currency": "CNY" +} +``` +- **校验规则**: + - `Idempotency-Key` 必填 + - `expectedAmount` 必填,订单金额由服务端校验(PAY-R01),与订单金额不一致 → 409 + `PAYMENT.AMOUNT_MISMATCH` + - 订单状态必须为 `PendingPayment`,否则 409 + `PAYMENT.ORDER_NOT_PAYABLE` + - 订单归属当前 buyerId + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`PaymentResultResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "paymentId": "8d2e9d11-...", + "orderId": "3f0ed9a9-...", + "amount": 199.00, + "currency": "CNY", + "status": "Succeeded", + "walletBalanceAfter": 1.50, + "paidAt": "2026-07-23T08:35:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段格式错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 404 | `RESOURCE.NOT_FOUND` | 订单不存在或非本人 | +| 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 订单状态非 `PendingPayment` 或已取消 | +| 409 | `PAYMENT.ALREADY_PAID` | 订单已支付成功(幂等命中首次结果) | +| 409 | `PAYMENT.AMOUNT_MISMATCH` | `expectedAmount` 与订单金额不一致 | +| 409 | `PAYMENT.INSUFFICIENT_BALANCE` | 钱包余额不足 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 数据库不可用 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 钱包条件扣减 + 钱包流水 + 支付记录 + 订单状态 + Outbox **同一事务**(按 PAY-R06) +- 与 C03 订单超时取消通过 `WHERE order.status = 'PendingPayment'` 条件竞争,唯一胜出(按 PAY-R05) +- 余额不得为负(条件更新 + CHECK 约束)(按 PAY-R02) +- 成功提交后写入 `Idempotency-Key` 记录,相同 Key + 相同 amount + 相同 orderId → 返回首次结果 +- 重复支付请求返回原成功结果,不重复写入或重复发布事件(按 PAY-R11) + +#### 缓存、事件或外部依赖 + +- 缓存:写入幂等结果到 `Idempotency-Key` 存储(DB 或 Redis) +- 事件:发布 `OrderPaidIntegrationEvent`(架构 §7.4 已确定第一条集成事件) +- 外部依赖:PostgreSQL + Ordering 模块 `orders` 表 + +#### 验证场景 + +- 正常:订单 `PendingPayment` + 余额充足 + 金额一致 → 200 + `PaymentResultResponse` +- 重复:相同 Idempotency-Key → 返回首次结果,不重复扣款 +- 异常:余额不足 → 409 + `PAYMENT.INSUFFICIENT_BALANCE` +- 异常:订单已支付 → 409 + `PAYMENT.ALREADY_PAID` +- 异常:订单已取消 → 409 + `PAYMENT.ORDER_NOT_PAYABLE` +- 异常:金额不一致 → 409 + `PAYMENT.AMOUNT_MISMATCH` +- 并发:与 C03 同时操作 → 唯一胜出,败方 409 + `PAYMENT.ORDER_NOT_PAYABLE` + +--- + +### A406 查询订单支付结果 + +- **模块 / Tag**:Payment +- **需求编号**:M05-01-FR09 +- **负责人**:张海洋 +- **关联数据表**:DB085(待评审)— `payments` +- **当前状态**:待交叉评审 +- **用途**:查询指定订单的支付结果与支付记录 +- **方法与路径**:`GET /api/payment/orders/{orderId}` +- **operationId**:`Payment_GetPaymentByOrder` +- **请求 Schema**:(无) +- **响应 Schema**:`PaymentResultResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 订单 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:`orderId`(UUID,必填) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `orderId` UUID 格式 + - 订单归属当前 buyerId + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`PaymentResultResponse`(同 A405) +- **示例**:(同 A405 成功响应) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 404 | `RESOURCE.NOT_FOUND` | 订单不存在或非本人 | +| 404 | `PAYMENT.NOT_FOUND` | 订单未发起过支付(订单未处于 `PendingPayment` / `Paid`) | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 同一订单只返回最新一笔成功支付;如有多笔识别为异常(P420 回调场景) +- 订单已取消但有迟到成功支付 → 返回 `PaymentResult`,订单状态仍为 `Cancelled`,并标注对账状态(架构 §7.12) + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:订单已支付 → 返回支付结果 +- 异常:订单未支付 → 404 + `PAYMENT.NOT_FOUND` +- 异常:他人订单 → 404 + `RESOURCE.NOT_FOUND` + +--- + +### A407 支付记录列表 + +- **模块 / Tag**:Payment +- **需求编号**:M05-01-FR07 +- **负责人**:张海洋 +- **关联数据表**:DB085(待评审)— `payments` +- **当前状态**:待交叉评审 +- **用途**:分页查询当前买家支付记录 +- **方法与路径**:`GET /api/payments` +- **operationId**:`Payment_ListPayments` +- **请求 Schema**:`ListPaymentsQuery` +- **响应 Schema**:`PaymentListResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 支付记录 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**: + - `status`(可选):`Succeeded` / `Failed` / `Pending` + - `orderId`(可选):按订单过滤 + - `createdFrom` / `createdTo`(可选):时间范围 + - `page` / `pageSize` / `sortBy` / `sortOrder`(标准分页) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - 标准分页 + 时间范围 + 枚举白名单 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`PaymentListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "paymentId": "8d2e9d11-...", + "orderId": "3f0ed9a9-...", + "amount": 199.00, + "currency": "CNY", + "status": "Succeeded", + "createdAt": "2026-07-23T08:35:00Z", + "succeededAt": "2026-07-23T08:35:01Z" + } + ], + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 参数错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 仅返回当前 buyerId 记录 +- 默认排序 `createdAt desc, paymentId desc` + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:返回本人支付记录 +- 异常:他人 orderId → 即使订单存在也过滤掉(不暴露归属) + +--- + +### A408 支付详情 + +- **模块 / Tag**:Payment +- **需求编号**:M05-01-FR07 +- **负责人**:张海洋 +- **关联数据表**:DB085(待评审)— `payments` +- **当前状态**:待交叉评审 +- **用途**:查询单笔支付详情 +- **方法与路径**:`GET /api/payments/{paymentId}` +- **operationId**:`Payment_GetPayment` +- **请求 Schema**:(无) +- **响应 Schema**:`PaymentDetailResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 支付记录 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:`paymentId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `paymentId` UUID 格式 + - 支付归属当前 buyerId + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`PaymentDetailResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "paymentId": "8d2e9d11-...", + "orderId": "3f0ed9a9-...", + "amount": 199.00, + "currency": "CNY", + "status": "Succeeded", + "createdAt": "2026-07-23T08:35:00Z", + "succeededAt": "2026-07-23T08:35:01Z", + "idempotencyKey": "uuid-..." + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 404 | `RESOURCE.NOT_FOUND` | 支付不存在或非本人 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 不返回内部审计字段;幂等键可对外展示以便客户端排错 + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:本人支付 → 返回详情 +- 异常:他人支付 → 404 + `RESOURCE.NOT_FOUND` + +--- + +### A411 售后资格预检 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR01 +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB084(待评审)— `orders` +- **当前状态**:待交叉评审 +- **用途**:预检指定订单项是否可申请售后 +- **方法与路径**:`GET /api/after-sales/eligibility` +- **operationId**:`AfterSales_CheckEligibility` +- **请求 Schema**:`EligibilityQuery` +- **响应 Schema**:`EligibilityResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 订单 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**: + - `orderId`(必填,UUID) + - `orderItemId`(必填,UUID) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `orderId` / `orderItemId` UUID 格式 + - 订单归属当前 buyerId + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`EligibilityResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "orderId": "3f0ed9a9-...", + "orderItemId": "5a7c...", + "eligible": true, + "reason": null, + "maxRefundableAmount": 100.00, + "maxRefundableQuantity": 1, + "availableTypes": ["RefundOnly", "ReturnAndRefund"], + "deadlineAt": "2026-07-30T08:30:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 404 | `RESOURCE.NOT_FOUND` | 订单或订单项不存在 | +| 409 | `AFTER_SALES.NOT_ELIGIBLE` | 订单未支付、已发货超期、不可售后状态 | + +#### 业务规则与并发 + +- 仅返回当前 buyerId 订单的可申请性 +- 退款金额上限 = 实付单价 × 剩余可售后数量(M10 业务规则) +- 可申请类型根据订单状态决定:已支付/已发货 → RefundOnly;已发货 + 确认收货后 → ReturnAndRefund + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + Orders 模块 + +#### 验证场景 + +- 正常:已支付订单 → 返回可申请 +- 异常:订单未支付 → 409 + `AFTER_SALES.NOT_ELIGIBLE` +- 异常:完成 > 7 天 → 409 + `AFTER_SALES.NOT_ELIGIBLE` + +--- + +### A412 提交售后申请 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR02 +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests` +- **当前状态**:待交叉评审 +- **用途**:买家提交退款/退货申请 +- **方法与路径**:`POST /api/after-sales/requests` +- **operationId**:`AfterSales_CreateRequest` +- **请求 Schema**:`CreateAfterSalesRequest` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 订单项 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "orderId": "3f0ed9a9-...", + "orderItemId": "5a7c...", + "type": "RefundOnly", + "quantity": 1, + "reason": "DAMAGED", + "reasonNote": "外包装破损", + "evidence": ["https://...", "https://..."] +} +``` +- **校验规则**: + - `orderId` / `orderItemId` 必填,UUID 格式 + - `type` 枚举:`RefundOnly` / `ReturnAndRefund` + - `quantity` 整数 ≥ 1 且 ≤ 剩余可售后数量 + - `reason` 枚举白名单(待 6.2 M10 业务规则定义) + - `reasonNote` 选填,≤ 500 字 + - `evidence` 选填,最多 9 张图 URL + - 退款金额由 `quantity × 实付单价` 后端计算(按 M10 业务规则"不接受任意金额") + - `Idempotency-Key` 必填 + +#### 成功响应 + +- **HTTP 状态**:`201 Created` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "requestId": "b9c1...", + "orderId": "3f0ed9a9-...", + "orderItemId": "5a7c...", + "type": "RefundOnly", + "quantity": 1, + "calculatedAmount": 100.00, + "currency": "CNY", + "reason": "DAMAGED", + "reasonNote": "外包装破损", + "evidence": ["https://..."], + "status": "PendingReview", + "createdAt": "2026-07-23T08:30:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 404 | `RESOURCE.NOT_FOUND` | 订单或订单项不存在 | +| 409 | `AFTER_SALES.NOT_ELIGIBLE` | 订单不满足售后条件 | +| 409 | `AFTER_SALES.AMOUNT_EXCEEDS_PAID` | 申请数量超过剩余可售后数量 | +| 409 | `AFTER_SALES.DUPLICATE_APPLICATION` | 同一订单项已有"待审核"申请 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 退款金额由服务端计算(M10 规则:"不接受任意金额") +- 申请数量不得超过剩余可售后数量(防重复申请) +- 状态写入 `PendingReview`(M10 状态机) +- 同一 buyerId 同一订单项已有 `PendingReview` → 拒绝重复申请 + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:发布 `AfterSalesApplicationSubmittedIntegrationEvent`(待 M00 集成事件确认) +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:订单项可申请 → 201 + 详情 +- 重复:相同 Idempotency-Key → 返回首次结果 +- 异常:申请数量 > 剩余可售后 → 409 + `AFTER_SALES.AMOUNT_EXCEEDS_PAID` +- 异常:订单项已有 PendingReview → 409 + `AFTER_SALES.DUPLICATE_APPLICATION` + +--- + +### A413 申请列表 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR03 +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests` +- **当前状态**:待交叉评审 +- **用途**:买家本人或商家按范围分页查询售后申请 +- **方法与路径**:`GET /api/after-sales/requests` +- **operationId**:`AfterSales_ListRequests` +- **请求 Schema**:`ListAfterSalesRequestsQuery` +- **响应 Schema**:`AfterSalesRequestListResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` +- **资源归属**:当前 buyerId 申请 / 当前 merchant 授权范围申请 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**: + - `status`(可选):`PendingReview` / `PendingReturn` / `PendingReceipt` / `Refunding` / `Refunded` / `RefundFailed` / `Rejected` / `Cancelled` + - `type`(可选):`RefundOnly` / `ReturnAndRefund` + - `createdFrom` / `createdTo`(可选):时间范围 + - `page` / `pageSize` / `sortBy` / `sortOrder`(标准分页) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - 买家仅看本人申请;商家仅看授权范围内申请 + - 标准分页 + 时间范围 + 枚举白名单 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`AfterSalesRequestListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "requestId": "b9c1...", + "orderId": "3f0ed9a9-...", + "orderItemId": "5a7c...", + "type": "RefundOnly", + "quantity": 1, + "calculatedAmount": 100.00, + "currency": "CNY", + "status": "PendingReview", + "createdAt": "2026-07-23T08:30:00Z" + } + ], + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 参数错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer / Merchant | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 商家视图按 `merchantId` 过滤订单范围 +- 默认排序 `createdAt desc, requestId desc` + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:买家 → 返回本人申请 +- 正常:商家 → 返回授权范围申请 +- 异常:跨商家查询 → 自动过滤,不返回他人数据 + +--- + +### A414 申请详情 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR04 +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_audit_logs` +- **当前状态**:待交叉评审 +- **用途**:查询单条售后申请的详细信息与状态时间线 +- **方法与路径**:`GET /api/after-sales/requests/{requestId}` +- **operationId**:`AfterSales_GetRequest` +- **请求 Schema**:(无) +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` +- **资源归属**:当前 buyerId 应用 / 当前 merchant 授权范围内 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:`requestId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `requestId` UUID 格式 + - 资源归属买家或授权商家 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`AfterSalesRequestDetailResponse`(含 `timeline` 字段) +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "requestId": "b9c1...", + "orderId": "3f0ed9a9-...", + "orderItemId": "5a7c...", + "type": "RefundOnly", + "quantity": 1, + "calculatedAmount": 100.00, + "currency": "CNY", + "reason": "DAMAGED", + "reasonNote": "外包装破损", + "evidence": ["https://..."], + "status": "PendingReview", + "createdAt": "2026-07-23T08:30:00Z", + "timeline": [ + { "status": "PendingReview", "at": "2026-07-23T08:30:00Z", "actor": "buyer" } + ] + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer / Merchant | +| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 时间线读 `after_sales_audit_logs` 表(按 DB087 推断) +- 不返回内部审计字段(如 merchant 内部 ID) + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:本人申请 → 返回详情 + timeline +- 异常:他人申请 → 404 + `RESOURCE.NOT_FOUND` + +--- + +### A415 撤销申请 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR10 +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests` +- **当前状态**:待交叉评审 +- **用途**:买家撤销本人仍处 `PendingReview` 状态的申请 +- **方法与路径**:`POST /api/after-sales/requests/{requestId}/cancel` +- **operationId**:`AfterSales_CancelRequest` +- **请求 Schema**:`CancelAfterSalesRequest` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 申请 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) + +#### 请求 + +- **Route 参数**:`requestId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "reason": "买家自愿撤销" +} +``` +- **校验规则**: + - 申请归属当前 buyerId + - 申请状态必须为 `PendingReview`,否则 409 + `AFTER_SALES.INVALID_STATUS` + - `Idempotency-Key` 必填 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **示例**:(同 A412 详情) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或非本人 | +| 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `PendingReview` | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 审核通过后不允许撤销(M10 业务规则) +- 状态条件更新:`WHERE status = 'PendingReview' AND buyer_id = currentBuyerId` +- 撤销后保留 `audit_log` 记录 + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:发布 `AfterSalesApplicationCancelledIntegrationEvent` +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:本人 `PendingReview` 申请 → 撤销成功 +- 异常:他人申请 → 404 + `RESOURCE.NOT_FOUND` +- 异常:已审核申请 → 409 + `AFTER_SALES.INVALID_STATUS` + +--- + +### A416 商家审核 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR05 +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_audit_logs` +- **当前状态**:待交叉评审 +- **用途**:商家同意或拒绝售后申请 +- **方法与路径**:`POST /api/after-sales/requests/{requestId}/audit` +- **operationId**:`AfterSales_AuditRequest` +- **请求 Schema**:`AuditAfterSalesRequest` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **身份与 Policy**:JWT Bearer + `MerchantOnly` +- **资源归属**:当前 merchant 授权范围内申请 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) + +#### 请求 + +- **Route 参数**:`requestId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "decision": "Approve", + "auditNote": "同意申请", + "expectRefund": true +} +``` +- **校验规则**: + - 申请归属当前 merchant + - 申请状态必须为 `PendingReview` + - `decision` 枚举:`Approve` / `Reject` + - `expectRefund=true` 表示审核通过后系统将自动触发退款(A431) + - `Idempotency-Key` 必填 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **示例**:(同 A412 详情,status 变为 `PendingReturn` 或 `Refunding`) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Merchant | +| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | +| 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `PendingReview` | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 商家不能修改买家原始申请内容(业务规则) +- 状态条件更新:`WHERE status = 'PendingReview' AND merchant_id = currentMerchantId` +- 审核通过后若 `expectRefund=true` → 异步触发 A431 退款 +- `audit_log` 记录审核人与审核意见 + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:发布 `AfterSalesApplicationAuditedIntegrationEvent` +- 外部依赖:PostgreSQL + Payment 模块(通过应用能力调用 A431) + +#### 验证场景 + +- 正常:商家 Approve → 状态进入 `PendingReturn` 或 `Refunding` +- 正常:商家 Reject → 状态进入 `Rejected` +- 异常:买家角色调用 → 403 + `AUTH.FORBIDDEN` +- 异常:他人商家申请 → 404 + `RESOURCE.NOT_FOUND` +- 异常:状态已非 `PendingReview` → 409 + `AFTER_SALES.INVALID_STATUS` + +--- + +### A417 商家确认退货 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR11 +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_audit_logs` +- **当前状态**:待交叉评审 +- **用途**:商家确认收到退货,触发退款流程 +- **方法与路径**:`POST /api/after-sales/requests/{requestId}/confirm-return` +- **operationId**:`AfterSales_ConfirmReturn` +- **请求 Schema**:`ConfirmReturnRequest` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **身份与 Policy**:JWT Bearer + `MerchantOnly` +- **资源归属**:当前 merchant 授权范围内申请 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) + +#### 请求 + +- **Route 参数**:`requestId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "receivedQuantity": 1, + "note": "已收到退货" +} +``` +- **校验规则**: + - 申请归属当前 merchant + - 申请类型必须为 `ReturnAndRefund` + - 申请状态必须为 `PendingReceipt` + - `receivedQuantity` ∈ [1, 申请数量] + - `Idempotency-Key` 必填 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **示例**:(同 A412 详情,status 变为 `Refunding`) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Merchant | +| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | +| 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `PendingReceipt` | +| 409 | `AFTER_SALES.WRONG_TYPE` | 申请类型非 `ReturnAndRefund` | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 状态条件更新:`WHERE status = 'PendingReceipt' AND merchant_id = currentMerchantId` +- 确认收到后异步触发 A431 退款 +- 库存按退货数量回补(按 M10 业务规则"已发货或已完成订单仅在退货且商家确认收货后按退货数量回补") + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:发布 `AfterSalesReturnConfirmedIntegrationEvent` + 库存回补事件 +- 外部依赖:PostgreSQL + Payment(A431)+ Inventory + +#### 验证场景 + +- 正常:商家确认退货 → 状态进入 `Refunding`,触发退款 +- 异常:申请类型为 `RefundOnly` → 409 + `AFTER_SALES.WRONG_TYPE` +- 异常:状态非 `PendingReceipt` → 409 + `AFTER_SALES.INVALID_STATUS` + +--- + +### A418 审核日志 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR04 +- **负责人**:张海洋 +- **关联数据表**:DB087(待评审)— `after_sales_audit_logs` +- **当前状态**:待交叉评审 +- **用途**:查询申请审核日志 +- **方法与路径**:`GET /api/after-sales/requests/{requestId}/audit-logs` +- **operationId**:`AfterSales_ListAuditLogs` +- **请求 Schema**:`ListAuditLogsQuery` +- **响应 Schema**:`AuditLogListResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` +- **资源归属**:当前 buyerId 申请 / 当前 merchant 授权范围内 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:`requestId`(UUID) +- **Query 参数**:`page` / `pageSize` / `sortBy` / `sortOrder` +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - 申请归属当前 buyerId 或当前 merchant + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`AuditLogListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "logId": "...", + "action": "Submitted", + "actor": "buyer", + "fromStatus": null, + "toStatus": "PendingReview", + "note": null, + "at": "2026-07-23T08:30:00Z" + } + ], + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer / Merchant | +| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 默认排序 `at asc, logId asc`(按时间顺序) +- 不返回内部审计字段(如 `merchant_internal_id`) + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:本人申请 → 返回审核日志 +- 异常:他人申请 → 404 + `RESOURCE.NOT_FOUND` + +--- + +### A419 退款失败重试 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR07 +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB088(待评审)— `refunds` +- **当前状态**:待交叉评审 +- **用途**:商家或系统对状态为 `RefundFailed` 的申请触发重试 +- **方法与路径**:`POST /api/after-sales/requests/{requestId}/retry-refund` +- **operationId**:`AfterSales_RetryRefund` +- **请求 Schema**:`RetryRefundRequest` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **身份与 Policy**:JWT Bearer + `MerchantOnly` +- **资源归属**:当前 merchant 授权范围内申请 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) + +#### 请求 + +- **Route 参数**:`requestId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "note": "重试退款" +} +``` +- **校验规则**: + - 申请归属当前 merchant + - 申请状态必须为 `RefundFailed` + - `Idempotency-Key` 必填 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **示例**:(同 A412 详情,status 变为 `Refunding`) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Merchant | +| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | +| 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `RefundFailed` | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 状态条件更新:`WHERE status = 'RefundFailed' AND merchant_id = currentMerchantId` +- 重试时异步触发 A431 退款 + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:发布 `AfterSalesRefundRetriedIntegrationEvent` +- 外部依赖:PostgreSQL + Payment(A431) + +#### 验证场景 + +- 正常:商家对 `RefundFailed` 重试 → 状态进入 `Refunding` +- 异常:状态非 `RefundFailed` → 409 + `AFTER_SALES.INVALID_STATUS` + +--- + +### A421 接收支付回调 + +- **模块 / Tag**:Payment +- **需求编号**:C08-FR01 / FR02 / FR03 / FR04 / FR05 +- **负责人**:张海洋 +- **关联数据表**:DB089(待评审)— `payment_callbacks`、DB085(待评审)— `payments`、DB084(待评审)— `orders` +- **当前状态**:待交叉评审 +- **用途**:接收模拟支付渠道的回调,更新支付与订单状态 +- **方法与路径**:`POST /api/payment/callbacks` +- **operationId**:`Payment_ReceiveCallback` +- **请求 Schema**:`PaymentCallbackRequest` +- **响应 Schema**:`PaymentCallbackResponse` +- **身份与 Policy**:内部服务级鉴权(Mock Channel Service,签名验证) +- **资源归属**:N/A(系统级) +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 支付回调) + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`X-Callback-Signature: `、`Content-Type: application/json` +- **Body**: +```json +{ + "callbackId": "5a8e...", + "paymentSerialNumber": "psn-...", + "orderId": "3f0ed9a9-...", + "result": "Success", + "occurredAt": "2026-07-23T08:35:00Z", + "amount": 199.00, + "currency": "CNY" +} +``` +- **校验规则**: + - `callbackId` 必填,全局唯一 + - `paymentSerialNumber` 必填 + - `result` 枚举:`Success` / `Failed` + - `amount` 必填,decimal + - 签名验证:`X-Callback-Signature` 通过 HMAC 校验(按 C08-FR02) + - `Idempotency-Key` 必填(与 `callbackId` 同值) + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`PaymentCallbackResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "callbackId": "5a8e...", + "status": "Processed", + "processedAt": "2026-07-23T08:35:01Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 401 | `PAYMENT.CALLBACK_INVALID_SIGNATURE` | 签名验证失败 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少服务 JWT | +| 409 | `PAYMENT.CALLBACK_DUPLICATE` | 同一 `callbackId` 重复到达 | +| 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 已取消订单收到迟到成功回调 → 进入对账差异 | +| 422 | `PAYMENT.CALLBACK_AMOUNT_MISMATCH` | 回调金额与订单金额不一致 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 回调 ID 与支付流水号建**唯一约束**(按 C08 业务规则) +- 同事务:支付记录 + 订单状态 + Inbox/处理记录 + Outbox(按 C08-FR05) +- 重复回调返回首次结果,不重复记账 +- 乱序:按订单当前状态 + 事件时间决定接受/忽略/登记差异 +- 已取消订单收到迟到成功回调 → **进入对账差异**,不得直接改已支付(按 C08 业务规则) + +#### 缓存、事件或外部依赖 + +- 缓存:幂等记录存在 DB(不依赖 Redis) +- 事件:发布 `PaymentCallbackProcessedIntegrationEvent` / `OrderPaidIntegrationEvent`(按结果) +- 外部依赖:PostgreSQL + Ordering 模块 + +#### 验证场景 + +- 正常:未处理过的回调 → 处理成功 +- 重复:相同 `callbackId` → 返回首次结果,不重复处理 +- 异常:签名错误 → 401 + `PAYMENT.CALLBACK_INVALID_SIGNATURE` +- 异常:金额不一致 → 422 + `PAYMENT.CALLBACK_AMOUNT_MISMATCH` +- 异常:已取消订单收到 Success 回调 → 进入对账差异状态,订单不直接改 `Paid` + +--- + +### A422 对账批次列表 + +- **模块 / Tag**:Reconciliation +- **需求编号**:C08-FR06 +- **负责人**:张海洋 +- **关联数据表**:DB090(待评审)— `reconciliation_batches` +- **当前状态**:待交叉评审 +- **用途**:分页查询每日对账批次 +- **方法与路径**:`GET /api/admin/reconciliation/batches` +- **operationId**:`Reconciliation_ListBatches` +- **请求 Schema**:`ListBatchesQuery` +- **响应 Schema**:`ReconciliationBatchListResponse` +- **身份与 Policy**:JWT Bearer + `AdminOnly` +- **资源归属**:N/A(管理员) +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**: + - `dateFrom` / `dateTo`(可选):按对账日期过滤 + - `status`(可选):`Pending` / `Matched` / `HasDifferences` / `Resolved` + - 标准分页 + 排序 +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `dateFrom ≤ dateTo` + - 枚举白名单 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`ReconciliationBatchListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "batchId": "...", + "reconciliationDate": "2026-07-23", + "rangeFrom": "2026-07-22T00:00:00Z", + "rangeTo": "2026-07-23T00:00:00Z", + "totalCount": 100, + "matchedCount": 98, + "differenceCount": 2, + "status": "HasDifferences", + "createdAt": "2026-07-23T01:00:00Z" + } + ], + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 参数错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Admin | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 仅管理员访问(按 C08 业务规则:"对账数据仅向管理员开放") +- 默认排序 `reconciliationDate desc, batchId desc` + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:管理员查询 → 返回批次列表 +- 异常:买家调用 → 403 + `AUTH.FORBIDDEN` + +--- + +### A423 对账批次详情 + +- **模块 / Tag**:Reconciliation +- **需求编号**:C08-FR06 +- **负责人**:张海洋 +- **关联数据表**:DB090(待评审)— `reconciliation_batches`、DB091(待评审)— `reconciliation_differences` +- **当前状态**:待交叉评审 +- **用途**:查询单批对账详情 +- **方法与路径**:`GET /api/admin/reconciliation/batches/{batchId}` +- **operationId**:`Reconciliation_GetBatch` +- **请求 Schema**:(无) +- **响应 Schema**:`ReconciliationBatchDetailResponse` +- **身份与 Policy**:JWT Bearer + `AdminOnly` +- **资源归属**:N/A(管理员) +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:`batchId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `batchId` UUID 格式 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`ReconciliationBatchDetailResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "batchId": "...", + "reconciliationDate": "2026-07-23", + "rangeFrom": "2026-07-22T00:00:00Z", + "rangeTo": "2026-07-23T00:00:00Z", + "totalCount": 100, + "matchedCount": 98, + "differenceCount": 2, + "status": "HasDifferences", + "summary": { + "byType": { "MissingPayment": 1, "AmountMismatch": 1 } + }, + "createdAt": "2026-07-23T01:00:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Admin | +| 404 | `RECONCILIATION.BATCH_NOT_FOUND` | 批次不存在 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 详情含按差异类型汇总(按 C08-FR07 至少识别"支付成功但订单未更新"等) + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:管理员查询 → 返回详情 +- 异常:批次不存在 → 404 + `RECONCILIATION.BATCH_NOT_FOUND` + +--- + +### A424 差异列表 + +- **模块 / Tag**:Reconciliation +- **需求编号**:C08-FR07 / FR08 +- **负责人**:张海洋 +- **关联数据表**:DB091(待评审)— `reconciliation_differences` +- **当前状态**:待交叉评审 +- **用途**:分页查询某批次的所有差异 +- **方法与路径**:`GET /api/admin/reconciliation/batches/{batchId}/differences` +- **operationId**:`Reconciliation_ListDifferences` +- **请求 Schema**:`ListDifferencesQuery` +- **响应 Schema**:`ReconciliationDifferenceListResponse` +- **身份与 Policy**:JWT Bearer + `AdminOnly` +- **资源归属**:N/A(管理员) +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:`batchId`(UUID) +- **Query 参数**: + - `type`(可选):`MissingPayment` / `AmountMismatch` / `DuplicateRefund` / `LateCallback` 等 + - `status`(可选):`Pending` / `InProgress` / `Resolved` + - 标准分页 + 排序 +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - 批次存在 + - 枚举白名单 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`ReconciliationDifferenceListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "differenceId": "...", + "batchId": "...", + "type": "LateCallback", + "orderId": "3f0ed9a9-...", + "paymentId": "8d2e9d11-...", + "callbackId": "5a8e...", + "description": "已取消订单收到迟到成功回调", + "status": "Pending", + "createdAt": "2026-07-23T01:00:00Z" + } + ], + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Admin | +| 404 | `RECONCILIATION.BATCH_NOT_FOUND` | 批次不存在 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 差异类型至少识别(按 C08-FR07): + - `LateCallback`:已取消订单收到迟到成功回调 + - `MissingPayment`:订单已支付但缺支付流水 + - `AmountMismatch`:支付/退款金额不一致 + - `DuplicateRefund`:退款重复 +- 状态管理(按 C08-FR08):`Pending` / `InProgress` / `Resolved` + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:管理员查询 → 返回差异列表 +- 异常:批次不存在 → 404 + `RECONCILIATION.BATCH_NOT_FOUND` + +--- + +### A425 差异处理 + +- **模块 / Tag**:Reconciliation +- **需求编号**:C08-FR08 +- **负责人**:张海洋 +- **关联数据表**:DB091(待评审)— `reconciliation_differences` +- **当前状态**:待交叉评审 +- **用途**:管理员处理对账差异并标记状态 +- **方法与路径**:`POST /api/admin/reconciliation/differences/{differenceId}/process` +- **operationId**:`Reconciliation_ProcessDifference` +- **请求 Schema**:`ProcessDifferenceRequest` +- **响应 Schema**:`ReconciliationDifferenceDetailResponse` +- **身份与 Policy**:JWT Bearer + `AdminOnly` +- **资源归属**:N/A(管理员) +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) + +#### 请求 + +- **Route 参数**:`differenceId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "action": "MarkResolved", + "resolutionNote": "确认为模拟渠道测试回调,已通知商家" +} +``` +- **校验规则**: + - `differenceId` 必填 + - `action` 枚举:`MarkInProgress` / `MarkResolved` / `MarkIgnored` + - 当前状态必须为 `Pending`(`MarkInProgress`)或 `InProgress`(`MarkResolved`) + - `resolutionNote` 必填,≤ 1000 字 + - `Idempotency-Key` 必填 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`ReconciliationDifferenceDetailResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "differenceId": "...", + "batchId": "...", + "type": "LateCallback", + "status": "Resolved", + "resolutionNote": "确认为模拟渠道测试回调,已通知商家", + "resolvedAt": "2026-07-23T03:00:00Z", + "resolvedBy": "admin-uuid" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Admin | +| 404 | `RECONCILIATION.DIFFERENCE_NOT_FOUND` | 差异不存在 | +| 409 | `RECONCILIATION.DIFFERENCE_ALREADY_PROCESSED` | 状态非法 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 状态条件更新:`WHERE status = 'Pending'` 或 `WHERE status = 'InProgress'` +- 差异修复必须可追踪(按 C08 业务规则),不能通过直接改库隐藏原因 +- 修复后保留 `resolutionNote` 和处理人 + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:发布 `ReconciliationDifferenceProcessedIntegrationEvent` +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:管理员 MarkResolved → 状态进入 `Resolved` +- 异常:状态已为 `Resolved` → 409 + `RECONCILIATION.DIFFERENCE_ALREADY_PROCESSED` +- 异常:买家调用 → 403 + `AUTH.FORBIDDEN` + +--- + +### A431 模拟退款 + +- **模块 / Tag**:Payment +- **需求编号**:M10-FR07 +- **负责人**:张海洋 +- **关联数据表**:DB088(待评审)— `refunds`、DB081(待评审)— `wallets`、DB083(待评审)— `wallet_ledgers` +- **当前状态**:待交叉评审 +- **用途**:将售后金额幂等退回买家小金库 +- **方法与路径**:`POST /api/after-sales/requests/{requestId}/refund` +- **operationId**:`Refund_Create` +- **请求 Schema**:`CreateRefundRequest` +- **响应 Schema**:`RefundDetailResponse` +- **身份与 Policy**:JWT Bearer + `MerchantOnly`(系统内部调用) +- **资源归属**:当前 merchant 授权范围内申请 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 退款入账) + +#### 请求 + +- **Route 参数**:`requestId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "expectedAmount": 100.00, + "currency": "CNY" +} +``` +- **校验规则**: + - 申请归属当前 merchant(或系统内部) + - 申请状态必须为 `Refunding`(已通过 A416 / A417 触发) + - `expectedAmount` 必须等于申请计算金额 + - `Idempotency-Key` 必填 + - 同一 Key + 相同 amount → 返回首次结果 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`RefundDetailResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "refundId": "...", + "requestId": "b9c1...", + "buyerId": "...", + "amount": 100.00, + "currency": "CNY", + "status": "Succeeded", + "walletBalanceAfter": 200.50, + "createdAt": "2026-07-23T09:00:00Z", + "succeededAt": "2026-07-23T09:00:01Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Merchant | +| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | +| 409 | `AFTER_SALES.INVALID_STATUS` | 申请状态非 `Refunding` | +| 409 | `PAYMENT.AMOUNT_MISMATCH` | `expectedAmount` 与申请计算金额不一致 | +| 409 | `PAYMENT.REFUND_FAILED` | 退款执行失败(写流水失败等) | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 钱包入账 + 退款记录 + 钱包流水 + 申请状态更新 **同事务**(按架构 §7.2) +- 退款成功后订单**保持原核心状态**(按 M10 业务规则 + 架构 §7.6) +- 退款流水必须纳入 C08 每日对账(按 M10 业务规则) +- AfterSales 不直接改钱包数据;通过 Payment 公开应用能力**幂等**退回(按架构 §7.2) + +#### 缓存、事件或外部依赖 + +- 缓存:幂等记录存在 DB +- 事件:发布 `RefundCompletedIntegrationEvent` +- 外部依赖:PostgreSQL + AfterSales 模块 + +#### 验证场景 + +- 正常:审核通过触发 → 退款成功,余额增加 +- 重复:相同 Idempotency-Key → 返回首次结果,不重复入账 +- 异常:金额不一致 → 409 + `PAYMENT.AMOUNT_MISMATCH` +- 异常:写流水失败 → 409 + `PAYMENT.REFUND_FAILED`,申请状态回滚 + +--- + +### A432 退款详情 + +- **模块 / Tag**:Payment +- **需求编号**:M10-FR04 +- **负责人**:张海洋 +- **关联数据表**:DB088(待评审)— `refunds` +- **当前状态**:待交叉评审 +- **用途**:查询单笔退款详情 +- **方法与路径**:`GET /api/refunds/{refundId}` +- **operationId**:`Refund_Get` +- **请求 Schema**:(无) +- **响应 Schema**:`RefundDetailResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` +- **资源归属**:当前 buyerId 退款 / 当前 merchant 授权范围 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:`refundId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - `refundId` UUID 格式 + - 资源归属当前 buyerId 或当前 merchant + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`RefundDetailResponse`(同 A431) + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer / Merchant | +| 404 | `RESOURCE.NOT_FOUND` | 退款不存在或不在授权范围 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 不返回内部审计字段 + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:本人退款 → 返回详情 +- 异常:他人退款 → 404 + `RESOURCE.NOT_FOUND` + +--- + +### A433 退款列表 + +- **模块 / Tag**:Payment +- **需求编号**:M10-FR03 +- **负责人**:张海洋 +- **关联数据表**:DB088(待评审)— `refunds` +- **当前状态**:待交叉评审 +- **用途**:分页查询退款记录 +- **方法与路径**:`GET /api/refunds` +- **operationId**:`Refund_List` +- **请求 Schema**:`ListRefundsQuery` +- **响应 Schema**:`RefundListResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` +- **资源归属**:当前 buyerId 退款 / 当前 merchant 授权范围 +- **幂等要求**:GET 天然幂等 + +#### 请求 + +- **Route 参数**:(无) +- **Query 参数**: + - `status`(可选):`Pending` / `Succeeded` / `Failed` + - `createdFrom` / `createdTo`(可选):时间范围 + - 标准分页 + 排序 +- **Header**:`Authorization: Bearer ` +- **Body**:(无) +- **校验规则**: + - 标准分页 + 时间范围 + 枚举白名单 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`RefundListResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "refundId": "...", + "requestId": "b9c1...", + "amount": 100.00, + "currency": "CNY", + "status": "Succeeded", + "createdAt": "2026-07-23T09:00:00Z", + "succeededAt": "2026-07-23T09:00:01Z" + } + ], + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 参数错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer / Merchant | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 买家视图按 `buyerId` 过滤;商家视图按授权范围过滤 +- 默认排序 `createdAt desc, refundId desc` + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:无 +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:买家 → 返回本人退款 +- 正常:商家 → 返回授权范围退款 + +--- + +--- + +> 来源:[interface-lhc.md](interface/interface-lhc.md)。HTTP 主体能力已覆盖,仍须补正式 OpenAPI、DBxxx 和跨模块集成事件契约。 + +### A501 查询本人消息列表 + +- 模块 / Tag:Messaging +- 需求编号:X03-FR03、X03-FR10、X03-FR11 +- 负责人:罗皓晨 +- 关联数据表:待 `database-lhc.md` 确认 +- 当前状态:待评审 +- 用途:按创建时间倒序分页查询当前用户自己的消息。 +- 方法与路径:`GET /api/messages` +- operationId:`Messaging_ListMessages` +- 请求 Schema:Query 参数 +- 响应 Schema:`MessageListResponse` +- 身份与 Policy:有效 JWT;仅买家或商家 +- 资源归属:接收用户必须等于当前认证用户;服务端不接收 `userId` +- 幂等要求:只读接口,天然幂等 + +#### 请求 + +- Route 参数:无。 +- Query 参数: + +| 参数 | 类型 | 必需 | 默认值 | 规则 | +|---|---|---:|---|---| +| `page` | integer | 否 | 1 | 大于等于 1 | +| `pageSize` | integer | 否 | 10 | 1~100 | +| `readStatus` | string | 否 | `all` | 仅允许 `all`、`unread` | +| `type` | `MessageType` | 否 | 无 | 只允许已登记消息类型 | + +- Header:`Authorization: Bearer `。 +- Body:无。 +- 校验规则:空字符串筛选值按参数错误处理,不静默当作未提供;排序固定为 `createdAt desc, messageId desc`,不开放任意 `sortBy`。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`MessageListResponse` +- `data` 字段: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `items` | `MessageSummaryResponse[]` | 当前页消息 | +| `page` | integer | 当前页 | +| `pageSize` | integer | 每页数量 | +| `total` | integer | 满足筛选条件的消息总数 | +| `totalPages` | integer | 总页数 | + +- 示例: + +```json +{ + "code": "success", + "message": "ok", + "data": { + "items": [ + { + "messageId": "6aa23ac8-8cf7-4cb5-8387-69f463df4b77", + "type": "OrderShipped", + "title": "订单已发货", + "summary": "你的订单已由商家发出,可进入订单详情查看。", + "relatedResourceType": "Order", + "relatedResourceId": "93c8d8e1-bdac-4a13-8ba2-0c21c327b81e", + "action": { + "target": "OrderDetail", + "resourceId": "93c8d8e1-bdac-4a13-8ba2-0c21c327b81e" + }, + "isRead": false, + "readAt": null, + "createdAt": "2026-07-24T02:30:00Z" + } + ], + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 页码、页大小、已读筛选或消息类型非法 | +| 401 | `AUTH.UNAUTHENTICATED` | JWT 缺失或无效 | +| 403 | `AUTH.FORBIDDEN` | 当前身份不是买家或商家 | + +#### 业务规则与并发 + +- 查询条件必须始终包含当前认证用户 ID,不能先按消息 ID 或筛选条件读取后再做客户端过滤。 +- 翻页期间新消息到达可能使后续页发生位移;本期按页码分页验收,不提前引入游标分页。 + +#### 缓存、事件或外部依赖 + +- 未读状态和消息内容以 PostgreSQL 为准。 +- 私人消息响应使用 `Cache-Control: no-store`,不得进入共享 HTTP 缓存。 + +#### 验证场景 + +- 分别验证默认列表、未读筛选、各消息类型筛选、空页和超出末页。 +- 使用另一个买家和商家账号确认不会返回他人消息。 + +### A502 查询本人消息详情 + +- 模块 / Tag:Messaging +- 需求编号:X03-FR04、X03-FR11 +- 负责人:罗皓晨 +- 关联数据表:待 `database-lhc.md` 确认 +- 当前状态:待评审 +- 用途:查询当前用户拥有的一条完整站内消息。 +- 方法与路径:`GET /api/messages/{messageId}` +- operationId:`Messaging_GetMessage` +- 请求 Schema:Route 参数 +- 响应 Schema:`MessageDetailResponse` +- 身份与 Policy:有效 JWT;仅买家或商家 +- 资源归属:消息接收用户必须等于当前认证用户 +- 幂等要求:只读接口,天然幂等 + +#### 请求 + +- Route 参数:`messageId`,必需,UUID。 +- Query 参数:无。 +- Header:`Authorization: Bearer `。 +- Body:无。 +- 校验规则:`messageId` 必须是标准 UUID。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`MessageDetailResponse` +- 示例: + +```json +{ + "code": "success", + "message": "ok", + "data": { + "messageId": "6aa23ac8-8cf7-4cb5-8387-69f463df4b77", + "type": "OrderShipped", + "title": "订单已发货", + "summary": "你的订单已由商家发出,可进入订单详情查看。", + "body": "订单已发货。请关注后续配送状态,收货后可在订单详情确认收货。", + "relatedResourceType": "Order", + "relatedResourceId": "93c8d8e1-bdac-4a13-8ba2-0c21c327b81e", + "action": { + "target": "OrderDetail", + "resourceId": "93c8d8e1-bdac-4a13-8ba2-0c21c327b81e" + }, + "isRead": false, + "readAt": null, + "createdAt": "2026-07-24T02:30:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `messageId` 格式非法 | +| 401 | `AUTH.UNAUTHENTICATED` | JWT 缺失或无效 | +| 403 | `AUTH.FORBIDDEN` | 当前身份不是买家或商家 | +| 404 | `MESSAGE.NOT_FOUND` | 消息不存在或不属于当前用户 | + +#### 业务规则与并发 + +- 他人消息与不存在消息统一返回 `404` 和 `MESSAGE.NOT_FOUND`,不泄露消息是否存在。 +- 查询详情不会自动标记已读;客户端在用户实际打开消息后调用 A504。 +- 每次返回 `action` 前重新校验当前用户对关联资源的访问资格;无资格时返回 `null`,消息正文仍可查看。 + +#### 缓存、事件或外部依赖 + +- 响应使用 `Cache-Control: no-store`。 +- 关联资源暂时不可用时不应导致历史消息查询失败。 + +#### 验证场景 + +- 验证本人未读和已读消息详情。 +- 使用另一用户访问相同 `messageId`,确认返回与不存在消息一致的 `404`。 +- 关联订单已不可访问时确认 `action` 为 `null`。 + +### A503 查询本人未读消息数 + +- 模块 / Tag:Messaging +- 需求编号:X03-FR05、C06-FR05 +- 负责人:罗皓晨 +- 关联数据表:待 `database-lhc.md` 确认 +- 当前状态:待评审 +- 用途:为消息入口角标、首次连接和断线重连补偿提供当前未读总数。 +- 方法与路径:`GET /api/messages/unread-count` +- operationId:`Messaging_GetUnreadCount` +- 请求 Schema:无 +- 响应 Schema:`UnreadMessageCountResponse` +- 身份与 Policy:有效 JWT;仅买家或商家 +- 资源归属:只统计当前认证用户 +- 幂等要求:只读接口,天然幂等 + +#### 请求 + +- Route 参数:无。 +- Query 参数:无。 +- Header:`Authorization: Bearer `。 +- Body:无。 +- 校验规则:无额外输入。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`UnreadMessageCountResponse` +- 示例: + +```json +{ + "code": "success", + "message": "ok", + "data": { + "unreadCount": 3 + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | JWT 缺失或无效 | +| 403 | `AUTH.FORBIDDEN` | 当前身份不是买家或商家 | + +#### 业务规则与并发 + +- `unreadCount` 为大于等于 0 的整数,以查询时 PostgreSQL 中当前用户未读记录为准。 +- 实时角标只用于即时展示,重连和页面恢复时必须以本接口结果校正。 + +#### 缓存、事件或外部依赖 + +- 本期不使用 Redis 保存唯一未读数。 +- 响应使用 `Cache-Control: no-store`。 + +#### 验证场景 + +- 新增消息后数量增加;首次标记已读后减少;重复标记不再次减少。 +- 多标签页分别刷新本接口时结果一致。 + +### A504 标记本人单条消息已读 + +- 模块 / Tag:Messaging +- 需求编号:X03-FR06 +- 负责人:罗皓晨 +- 关联数据表:待 `database-lhc.md` 确认 +- 当前状态:待评审 +- 用途:幂等地记录当前用户一条消息的首次已读时间。 +- 方法与路径:`POST /api/messages/{messageId}/read` +- operationId:`Messaging_MarkMessageRead` +- 请求 Schema:Route 参数 +- 响应 Schema:`MarkMessageReadResponse` +- 身份与 Policy:有效 JWT;仅买家或商家 +- 资源归属:消息接收用户必须等于当前认证用户 +- 幂等要求:同一用户对同一消息重复调用返回相同首次 `readAt` + +#### 请求 + +- Route 参数:`messageId`,必需,UUID。 +- Query 参数:无。 +- Header:`Authorization: Bearer `。 +- Body:无。 +- 校验规则:`messageId` 必须是标准 UUID。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`MarkMessageReadResponse` +- 示例: + +```json +{ + "code": "success", + "message": "ok", + "data": { + "messageId": "6aa23ac8-8cf7-4cb5-8387-69f463df4b77", + "isRead": true, + "readAt": "2026-07-24T02:35:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `messageId` 格式非法 | +| 401 | `AUTH.UNAUTHENTICATED` | JWT 缺失或无效 | +| 403 | `AUTH.FORBIDDEN` | 当前身份不是买家或商家 | +| 404 | `MESSAGE.NOT_FOUND` | 消息不存在或不属于当前用户 | + +#### 业务规则与并发 + +- 更新条件同时包含消息 ID、当前认证用户 ID 和未读状态。 +- 首次更新由服务端生成 UTC `readAt`;重复或并发调用读取并返回首次值,不覆盖时间。 +- 他人消息与不存在消息统一返回 `404`。 + +#### 缓存、事件或外部依赖 + +- 已读事实必须写入 PostgreSQL。 +- 操作成功后前端可以乐观更新本标签页角标,但仍应通过 A503 校正。 + +#### 验证场景 + +- 验证首次已读、重复已读、两个并发请求和越权访问。 +- 确认重复操作不重复减少未读数。 + +### A505 标记本人当前消息全部已读 + +- 模块 / Tag:Messaging +- 需求编号:X03-FR07 +- 负责人:罗皓晨 +- 关联数据表:待 `database-lhc.md` 确认 +- 当前状态:待评审 +- 用途:将操作开始时当前用户已经存在的未读消息批量标记为已读。 +- 方法与路径:`POST /api/messages/read-all` +- operationId:`Messaging_MarkAllMessagesRead` +- 请求 Schema:无 +- 响应 Schema:`MarkAllMessagesReadResponse` +- 身份与 Policy:有效 JWT;仅买家或商家 +- 资源归属:只更新当前认证用户 +- 幂等要求:没有新的未读消息时重复调用返回 `markedCount = 0` + +#### 请求 + +- Route 参数:无。 +- Query 参数:无。 +- Header:`Authorization: Bearer `。 +- Body:无。 +- 校验规则:服务端在操作开始时生成 UTC 截止时间,不接受客户端传入用户 ID 或截止时间。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`MarkAllMessagesReadResponse` +- 示例: + +```json +{ + "code": "success", + "message": "ok", + "data": { + "markedCount": 5, + "readAt": "2026-07-24T02:40:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | JWT 缺失或无效 | +| 403 | `AUTH.FORBIDDEN` | 当前身份不是买家或商家 | + +#### 业务规则与并发 + +- 更新条件必须包含当前认证用户、`isRead = false` 和 `createdAt <= readAt`。 +- 操作期间在截止时间之后到达的新消息保持未读。 +- `markedCount` 是本次首次变为已读的记录数,不是用户历史消息总数。 + +#### 缓存、事件或外部依赖 + +- 批量更新和未读状态以 PostgreSQL 为准。 +- 响应使用 `Cache-Control: no-store`。 + +#### 验证场景 + +- 验证存在多条未读、没有未读、重复调用和操作期间并发到达新消息。 +- 使用两个用户确认只更新当前用户数据。 + +### A506 API 存活检查 + +- 模块 / Tag:Infrastructure +- 需求编号:C10-FR06 +- 负责人:罗皓晨 +- 关联数据表:无 +- 当前状态:待评审 +- 用途:供 Compose、Nginx 和运维检查 API 进程能否响应。 +- 方法与路径:`GET /health/live` +- operationId:`Infrastructure_GetLiveness` +- 请求 Schema:无 +- 响应 Schema:`HealthStatusResponse` +- 身份与 Policy:无需认证 +- 资源归属:不适用 +- 幂等要求:只读接口,天然幂等 + +#### 请求 + +- Route 参数:无。 +- Query 参数:无。 +- Header:无必需 Header。 +- Body:无。 +- 校验规则:不接受外部传入检查目标。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`HealthStatusResponse` +- 本接口是健康检查例外,不使用通用 `code/message/data` 包装。 +- 示例: + +```json +{ + "status": "healthy", + "service": "mall-api", + "instanceId": "api-1", + "checkedAt": "2026-07-24T02:45:00Z" +} +``` + +#### 失败响应 + +进程无法响应时通常表现为连接失败或网关错误,不由当前进程构造 ProblemDetails。 + +#### 业务规则与并发 + +- 存活检查只验证进程响应能力,不访问 PostgreSQL、Redis、RabbitMQ 或对象存储。 +- `instanceId` 由部署环境注入,只用于 C10 请求分布证明,不包含主机名、IP 或敏感配置。 + +#### 缓存、事件或外部依赖 + +- 响应使用 `Cache-Control: no-store`。 + +#### 验证场景 + +- 两个 API 实例分别返回自身实例标识。 +- 进程运行时返回 `200`;进程停止时探针失败。 + +### A507 API 就绪检查 + +- 模块 / Tag:Infrastructure +- 需求编号:C10-FR06、C10-FR12 +- 负责人:罗皓晨 +- 关联数据表:无 +- 当前状态:待评审 +- 用途:判断实例是否具备接收业务流量的必要依赖。 +- 方法与路径:`GET /health/ready` +- operationId:`Infrastructure_GetReadiness` +- 请求 Schema:无 +- 响应 Schema:`ReadinessStatusResponse` +- 身份与 Policy:无需认证 +- 资源归属:不适用 +- 幂等要求:只读接口,天然幂等 + +#### 请求 + +- Route 参数:无。 +- Query 参数:无。 +- Header:无必需 Header。 +- Body:无。 +- 校验规则:不接受外部传入检查目标。 + +#### 成功响应 + +- HTTP 状态:全部必需依赖可用时为 `200 OK` +- 响应 Schema:`ReadinessStatusResponse` +- 本接口不使用通用业务包装。 +- 示例: + +```json +{ + "status": "healthy", + "service": "mall-api", + "instanceId": "api-1", + "checkedAt": "2026-07-24T02:45:00Z", + "checks": [ + { + "name": "postgres", + "status": "healthy" + }, + { + "name": "redis", + "status": "healthy" + } + ] +} +``` + +#### 失败响应 + +| HTTP 状态 | 响应 | 触发条件 | |---|---|---| -| 唐宇昊 | `interface-tyh.md` | `A001`~`A100` | -| 顾欣月 | `interface-gxy.md` | `A101`~`A200` | -| 朱惠惠 | `interface-zhh.md` | `A201`~`A300` | -| 韦乾强 | `interface-wqq.md` | `A301`~`A400` | -| 张海洋 | `interface-zhy.md` | `A401`~`A500` | -| 罗皓晨 | `interface-lhc.md` | `A501`~`A600` | +| 503 | `ReadinessStatusResponse` | PostgreSQL 或当前阶段已启用且被配置为必需的依赖不可用 | + +`503` 示例: + +```json +{ + "status": "unhealthy", + "service": "mall-api", + "instanceId": "api-1", + "checkedAt": "2026-07-24T02:46:00Z", + "checks": [ + { + "name": "postgres", + "status": "unhealthy" + } + ] +} +``` + +#### 业务规则与并发 + +- PostgreSQL 始终属于就绪必需依赖。 +- Redis、RabbitMQ 和对象存储仅在当前阶段启用且配置为该实例必要依赖时参与就绪判断;未启用依赖不能错误阻塞就绪。 +- 响应不得包含连接字符串、主机、端口、异常消息、堆栈或凭据。 + +#### 缓存、事件或外部依赖 + +- 检查设置短超时,避免探针堆积拖垮实例。 +- 响应使用 `Cache-Control: no-store`。 + +#### 验证场景 + +- PostgreSQL 正常时返回 `200`。 +- PostgreSQL 不可用时返回 `503`。 +- 未启用 RabbitMQ 或对象存储时不把它们报告为失败。 +- 两个实例使用同一契约并返回不同 `instanceId`。 + +## 四、非 HTTP 契约与附录 + +### 4.1 Messaging 公共 Schema 与枚举 + +#### `MessageType` + +受控字符串枚举: + +| 值 | 含义 | +|---|---| +| `OrderCreated` | 买家订单创建成功 | +| `OrderCancelled` | 订单取消 | +| `PaymentSucceeded` | 支付成功 | +| `OrderShipped` | 商家已发货 | +| `OrderCompleted` | 订单完成 | +| `AfterSalesSubmitted` | 售后申请已提交,提醒指定商家处理 | +| `AfterSalesReviewed` | 售后审核完成,通知买家结果 | + +后续新增消息类型属于兼容性扩展。客户端必须对未知值使用“业务通知”兜底展示,不能因此白屏。 + +#### `RelatedResourceType` + +受控字符串枚举:`Order`、`Payment`、`AfterSales`。 + +#### `MessageAction` + +安全跳转描述,不包含前端内部路由字符串: + +| 字段 | 类型 | 必需 | 说明 | +|---|---|---:|---| +| `target` | string | 是 | `OrderDetail` 或 `AfterSalesDetail` | +| `resourceId` | UUID | 是 | 目标业务资源 ID;进入目标页面时仍须重新鉴权 | + +当关联资源不存在、已归档或当前用户已无权访问时,`action` 返回 `null`。 + +#### `MessageSummaryResponse` + +| 字段 | 类型 | 可空 | 说明 | +|---|---|---:|---| +| `messageId` | UUID | 否 | 消息 ID | +| `type` | `MessageType` | 否 | 消息类型 | +| `title` | string | 否 | 标题,最长 100 个字符 | +| `summary` | string | 否 | 摘要,最长 200 个字符 | +| `relatedResourceType` | `RelatedResourceType` | 是 | 关联业务类型 | +| `relatedResourceId` | UUID | 是 | 关联业务 ID | +| `action` | `MessageAction` | 是 | 安全跳转描述 | +| `isRead` | boolean | 否 | 是否已读 | +| `readAt` | UTC 时间 | 是 | 首次标记已读时间 | +| `createdAt` | UTC 时间 | 否 | 消息创建时间 | + +#### `MessageDetailResponse` + +包含 `MessageSummaryResponse` 的全部字段,并增加: + +| 字段 | 类型 | 可空 | 说明 | +|---|---|---:|---| +| `body` | string | 否 | 消息正文,最长 2000 个字符 | + +正文和摘要是消息创建时保存的历史快照,不随商品名称、订单展示文本或用户昵称变化。 + +### 4.2 SignalR 实时契约(不占 Axxx) + +#### Hub 连接 + +| 项目 | 契约 | +|---|---| +| Hub 路径 | `/hubs/messaging` | +| 鉴权 | 有效买家或商家 JWT | +| 身份来源 | 服务端认证上下文中的用户 ID 和角色 | +| 客户端订阅参数 | 无,不接受客户端传入任意 `userId`、角色或组名 | +| 多实例 | 使用 Redis Backplane | +| 事实来源 | PostgreSQL 中的 M09 消息 | + +浏览器在 WebSocket 握手限制下可通过 SignalR `accessTokenFactory` 传递令牌。服务端只允许在 `/hubs/messaging` 握手路径读取受控的 `access_token` Query,并必须在 Nginx、ASP.NET Core、Serilog 和 Trace 中完整脱敏;集成、演示和发布环境只使用 HTTPS/WSS。 + +客户端主动退出后关闭连接。非主动断线使用有限退避自动重连;初次连接和每次重连成功后调用 A503,并按需调用 A501 补查断线期间消息。 + +#### 服务端事件 `MessageCreated` + +服务端向目标认证用户的全部在线连接推送 `MessageCreated`。载荷 Schema 为 `MessageCreatedPayload`: + +| 字段 | 类型 | 可空 | 说明 | +|---|---|---:|---| +| `messageId` | UUID | 否 | 已持久化消息 ID,也是客户端去重键 | +| `type` | `MessageType` | 否 | 消息类型 | +| `title` | string | 否 | 标题 | +| `summary` | string | 否 | 摘要 | +| `relatedResourceType` | `RelatedResourceType` | 是 | 关联业务类型 | +| `relatedResourceId` | UUID | 是 | 关联业务 ID | +| `action` | `MessageAction` | 是 | 安全跳转描述 | +| `createdAt` | UTC 时间 | 否 | 服务端消息创建时间 | + +示例: + +```json +{ + "messageId": "6aa23ac8-8cf7-4cb5-8387-69f463df4b77", + "type": "OrderShipped", + "title": "订单已发货", + "summary": "你的订单已由商家发出,可进入订单详情查看。", + "relatedResourceType": "Order", + "relatedResourceId": "93c8d8e1-bdac-4a13-8ba2-0c21c327b81e", + "action": { + "target": "OrderDetail", + "resourceId": "93c8d8e1-bdac-4a13-8ba2-0c21c327b81e" + }, + "createdAt": "2026-07-24T02:30:00Z" +} +``` + +规则: + +- 只有消息数据库事务成功提交后才能推送。 +- 推送失败不回滚业务事务或消息记录,也不把消息重新标记为未生成。 +- 客户端按 `messageId` 去重轻提示;不得仅凭推送载荷修改订单、支付或售后最终状态。 +- 本期不提供客户端调用的聊天、广播、已送达回执、任意加组或按用户订阅 Hub 方法。 + +### 4.3 Messaging 错误码与跨模块待确认项 + +| 错误码 | HTTP 状态 | 含义 | +|---|---:|---| +| `MESSAGE.NOT_FOUND` | 404 | 消息不存在或不属于当前用户 | + +认证、验证、限流、依赖不可用和未知错误复用本文件第一章登记的通用错误码,不创建同义错误码。 + +#### 需要其他负责人评审的协作点 + +- Ordering、Payment 和 AfterSales 负责人需确认会触发通知的业务事实、事件 ID、业务 ID、接收用户和发生时间。 +- Identity 负责人需确认买家与商家认证身份、账号禁用和令牌失效规则可以同时约束 HTTP 与 SignalR。 +- 商家通知必须由来源模块明确指定接收账号或受控接收范围,不允许 Messaging 自行向全部商家广播私人订单或售后信息。 +- Catalog 负责人继续拥有 C07 商品缓存失效业务规则;罗皓晨只提供 Redis 与多实例缓存基础设施,不新增公开缓存控制接口。 + +#### 实现前必须补齐 + +- 创建并评审 `database-lhc.md`,登记消息表及必要的唯一约束、用户未读查询索引和关联 A501~A505。 +- 单独评审 Ordering、Payment、AfterSales 到 Messaging 的集成事件 Schema、Routing Key、Outbox/Inbox 幂等键和失败处理;这些不是 HTTP Axxx 接口。 +- 在后端脚手架建立后形成真实 OpenAPI,并保证 `operationId`、Schema、状态码和错误码与本文件一致。 +- 在测试计划中登记 X03、C06 和 C10 的分页、越权、重复已读、并发全部已读、断线重连、多标签页、多实例和健康检查场景。 + +当前文档只能证明接口契约已形成待评审草案,不能证明接口已经实现、联调或通过验收。 + +### 4.4 C03 Worker 内部契约(不占 Axxx) + +本节描述后台任务与事件处理,不是可由客户端调用的 HTTP 接口。 + +订单超时自动取消(Worker接口) + +> **说明**:C03订单超时自动取消由Worker后台任务执行,不对外提供HTTP API。接口设计记录其与外部系统的交互关系。 + +#### 业务规则 + +1. **超时时间配置**:订单超时时间通过配置项 `OrderTimeoutMinutes` 管理,默认30分钟 +2. **扫描策略**:Worker定时扫描 `PendingPayment` 状态且 `created_at + timeout < now()` 的订单 +3. **取消事务**:在同一事务内完成状态变更 `PendingPayment → Cancelled`、库存回补、写入 `cancelled_at` 和 `cancel_reason = 'TIMEOUT'` +4. **幂等保证**:使用条件更新 `WHERE status = 'PendingPayment'`,同一订单多次扫描只有一次成功 +5. **支付竞争**:与M05支付并发时,条件更新确保只有一个成功 + +#### 事件消费 + +- 消费 `OrderCreatedEvent`(由M04-01发布)触发后续超时跟踪 + +#### 事件发布 + +- 发布 `OrderCancelledEvent`(`cancel_reason = 'TIMEOUT'`)到Outbox,供给M09站内消息 + +#### 关键实现点 + +1. 扫描间隔建议 ≤ 超时时间/2 +2. 每批次处理上限100条,避免长时间锁表 +3. 失败重试3次后告警,订单保留待处理状态 +4. 多实例Worker使用 `SELECT FOR UPDATE SKIP LOCKED` 避免重复处理 + +#### 验证场景 + +1. 超时订单被自动取消,库存回补 +2. 买家在超时前支付成功,取消被跳过 +3. 并发取消与支付只有一个成功 +4. Worker重启后继续扫描,不漏扫 + +### 4.5 Catalog 与 Review 附录 + +(本人模块新增) + +以下为本人模块在《接口设计.md》通用错误码之外新增的稳定业务错误码,待汇总时并入总表: + +| 错误码 | HTTP 状态 | 含义 | +|---|---:|---| +| `CATALOG.PRODUCT_NOT_FOUND` | 404 | 商品不存在或购物端不可见 | +| `CATALOG.CATEGORY_NOT_FOUND` | 404 | 分类不存在 | +| `CATALOG.CATEGORY_DISABLED` | 409 | 分类已停用,不能用于上架/新建 | +| `CATALOG.CATEGORY_NAME_CONFLICT` | 409 | 同父级下分类名称重复 | +| `CATALOG.INVALID_PRICE_RANGE` | 400 | `minPrice > maxPrice` | +| `CATALOG.INVALID_SORT_FIELD` | 400 | 排序字段不在白名单 | +| `CATALOG.PRODUCT_VERSION_CONFLICT` | 409 | 商品并发编辑版本冲突 | +| `CATALOG.PRODUCT_HAS_ORDERS` | 409 | 存在历史订单关联,禁止物理删除 | +| `CATALOG.PRODUCT_INCOMPLETE` | 409 | 上架时必填项/主图缺失 | +| `CATALOG.IMAGE_LIMIT_EXCEEDED` | 409 | 商品图片超过 8 张 | +| `CATALOG.INVALID_IMAGE` | 415 | 图片格式或尺寸不符合要求 | +| `CATALOG.PRIMARY_IMAGE_REQUIRED` | 409 | 已上架商品不得删至无主图 | +| `REVIEW.ORDER_ITEM_NOT_FOUND` | 404 | 订单项不存在或不属于当前买家 | +| `REVIEW.ORDER_NOT_COMPLETED` | 409 | 订单未完成,不能评价 | +| `REVIEW.ALREADY_REVIEWED` | 409 | 该订单项已评价 | +| `REVIEW.INVALID_IMAGE` | 415 | 评价图片格式或尺寸不符合要求 | + +#### 五、待确认事项 + +1. `DB021`~`DB025` 编号需与本人 `database-gxy.md` 交叉确认并固定;表字段、约束、索引以数据库设计为准。 +2. A124、A142、A143 依赖 Ordering(韦乾强)提供“订单项归属 + 订单完成状态 + 是否存在订单关联”的应用契约,需在联调前确认契约形态。 +3. 商品/评价缓存失效与搜索索引同步(C04、C07)由罗皓晨主责的公共能力协作,事件与缓存 Key 以《命名规范》与架构设计为准。 +4. 购物端商品详情(A103)是否内联轻量评分汇总,最终以评审结论为准;当前设计由 A140 统一提供汇总,保持模块边界清晰。 +5. 上传体积超限(A127、A141)当前引用 `COMMON.PAYLOAD_TOO_LARGE`(413),该码属 M00 公共错误码,需由罗皓晨在《接口设计.md》1.10 通用错误码表登记后统一引用;本文件不自建 `COMMON.*` 码。 +6. A140 公开评价的 `buyerDisplayName` 为脱敏昵称,其来源与脱敏规则需与 Identity(唐宇昊)确认,评价模块只做展示不落库敏感字段。 + +#### 六、枚举附录 + +对外接口一律使用 PascalCase 字符串枚举,数据库落库使用 `lower_snake_case`;客户端必须对未知枚举值做兜底展示。 + +| 枚举 | 使用接口 | 对外值(API) | 落库值(DB,参考) | 含义 | +|---|---|---|---|---| +| 分类状态 `status` | A110、A113、A114 | `Enabled` / `Disabled` | `enabled` / `disabled` | 分类是否作为购物端筛选入口 | +| 商品状态 `status` | A120~A126 | `Draft` / `Published` / `Unpublished` | `draft` / `published` / `unpublished` | 草稿 / 已上架 / 已下架 | +| 库存状态 `stockStatus` | A102、A103 | `InStock` / `SoldOut` | 由 `stock` 计算,不落库 | 有货 / 售罄 | +| 评价资格原因 `reason` | A143 | `Eligible` / `OrderNotCompleted` / `AlreadyReviewed` / `NotOwner` | 由订单与评价状态计算,不落库 | 可评价及不可评价原因 | +| 排序方向 `sortOrder` | A102、A120、A140 | `asc` / `desc`(小写,规范 1.11.3) | — | 升序 / 降序 | + +> 落库值仅为跨文档参考,最终以 `database-gxy.md` 与实体映射为准;已删除商品在购物端按“不存在”处理,不作为对外枚举值暴露。 + +### 4.6 Payment 与 AfterSales 跨接口约束 + +#### 4.6.1 错误码统一 + +- `AUTH.*` / `RESOURCE.*` / `IDEMPOTENCY.*` / `COMMON.*` 按接口设计 1.10 节基础 +- 模块错误码:`PAYMENT.*` / `AFTER_SALES.*` / `RECONCILIATION.*` +- 同一错误场景使用同一错误码 + HTTP 状态 + +#### 4.6.2 幂等键一致性 + +涉及资金 / 状态 / 回调的接口统一: + +| 幂等范围 | 字段格式 | +|---|---| +| 客户端生成 | UUID v4 | +| 服务端 Key | `Idempotency-Key` Header | +| 储存 | DB082 / DB085 / DB088 / DB089 对应表唯一约束 | +| 保留期 | 与对应业务表相当(≥ 90 天) | + +#### 4.6.3 响应包装 + +所有成功响应统一为 `data` 包装(204 除外),按接口设计 1.7 节。 + +#### 4.6.4 失败响应 + +全部使用 `application/problem+json`,按接口设计 1.8 节。 + +#### 4.6.5 操作审计 + +- 涉及状态变更的接口(A402 / A405 / A412 / A415 / A416 / A417 / A419 / A421 / A425 / A431)在 `audit_logs` 或对应表登记 actor、at、from_status、to_status +- 涉及资金的接口在 `wallet_ledgers` 写入流水 + +--- + +### 4.7 Payment 与 AfterSales 协作范围 + +#### 4.7.1 不在个人原稿范围 + +- `database-zhy.md` 中 DB081~DB100 的字段、约束和索引由数据库设计任务单独维护,本文件只引用已确认 DBxxx。 +- 不在教师基线和需求规格之外自行扩展 C08;新增范围必须先完成需求确认。 +- 集成事件命名需要与 M00 可靠事件规范对齐,不能用外部 HTTP 接口替代内部事件。 +- `AdminOnly` Policy 名称和授权语义需要与 M00 公共鉴权约定对齐。 + +#### 4.7.2 评审清单 + +- [ ] 罗皓晨:对照接口设计 1.20 模板核对字段完整性 +- [ ] 韦乾强:核对 A401-A408 与 Ordering 的协作边界(订单状态、回调联动) +- [ ] 顾欣月:核对 A402 / A405 与 Catalog 库存联动(如有) +- [ ] 张海洋自审:核对 17 项 PAY 业务规则 + 12 项 M10 规则 + 11 项 C08 规则全部覆盖 + +#### 4.7.3 汇总与保留 + +- 六份原稿已完成首轮汇总;后续评审修正必须在同一任务中同步本文件第二、三、五章。 +- 个人文件 `interface/interface-zhy.md` 继续保留,不单独作为实现事实源。 +- 历史贡献通过 Git 记录保留 + +## 五、汇总审计与冻结条件 + +### 5.1 当前成熟度 + +本次只完成六份个人原稿的结构汇总和一致性审计,不代表 102 个接口已全部评审或可以直接编码。主文档当前整体成熟度为 **部分定义,未冻结**。 + +| 负责人 | 已登记 | 明确缺少或必须闭环 | 当前结论 | +|---|---:|---|---| +| 唐宇昊 | 23 | 缺浏览记录写入、浏览记录开关查询;A006~A014 身份范围与需求冲突 | X02 未闭环 | +| 顾欣月 | 21 | 单条评价读取需新增接口或删除现有跳转引用;图片暂存和商品创建顺序待统一 | 主体齐全,待评审 | +| 朱惠惠 | 19 | 无新增外部接口硬缺;A229/A230 应复用 Ordering,秒杀库存与订单边界待修正 | 主体齐全,边界冲突 | +| 韦乾强 | 7 | 缺买家确认收货;现有 DBxxx、幂等、状态字段和商家订单边界待修正 | F09 未闭环 | +| 张海洋 | 25 | 缺买家提交退货说明/寄回信息;内部退款与外部 HTTP 授权边界待重构 | X04 未闭环 | +| 罗皓晨 | 7 | 无新增业务 HTTP 硬缺;缺正式集成事件、DBxxx 和 OpenAPI 落地 | HTTP 主体齐全 | + +### 5.2 建议由原负责人补充的接口 + +| 建议编号 | 负责人 | 方法与路径 | operationId | 对应需求 | 必要性 | +|---|---|---|---|---|---| +| A024 | 唐宇昊 | `PUT /api/browsing-history/{productId}` | `Engagement_RecordBrowsingHistory` | M08-FR04 | 必需 | +| A025 | 唐宇昊 | `GET /api/browsing-history/settings` | `Engagement_GetBrowsingHistorySetting` | M08-FR07 | 必需 | +| A144 | 顾欣月 | `GET /api/reviews/{reviewId}` | `Review_GetReview` | A142 Location、A143 existingReviewId | 二选一:新增,或删除不可达引用 | +| A308 | 韦乾强 | `POST /api/orders/{orderId}/confirm-receipt` | `Ordering_ConfirmReceipt` | M04-04、F09 | 必需 | +| A434 | 张海洋 | `POST /api/after-sales/requests/{requestId}/return` | `AfterSales_SubmitReturn` | M10-FR11 | 必需 | + +建议编号只有对应负责人补齐完整详细定义并通过交叉评审后才正式占用;本表不能替代接口定义。 -每个个人接口文件必须: +### 5.3 冻结前必须完成 -1. 只使用本人 Axxx 编号区间,只编写本人负责模块。 -2. 先给出接口清单,再按 1.20 节模板逐个编写同编号详细定义。 -3. 接口清单至少包含编号、模块、需求编号、名称、方法、路径、`operationId`、请求/响应 Schema、鉴权、关联 DBxxx 和当前状态。 -4. `DBxxx` 只用于跨文档追踪,不向客户端暴露,也不表示接口可以跨模块直接访问该表。 -5. 个人文件是协作阶段材料,不是长期事实源。全部文件通过交叉评审后,由罗皓晨按编号汇总到本文件的统一接口清单和接口详细定义,并形成 OpenAPI 契约。 -6. 汇总完成并确认无遗漏后,在同一文档任务中删除六份个人接口文件;历史贡献通过 Git 记录保留,避免长期维护两套接口事实。 +1. 由各负责人修正本人接口的需求冲突、状态机、资源归属、错误码和跨模块边界。 +2. 将 A229/A230 改为复用 A302/A303;秒杀筛选通过 Ordering 查询契约表达,不建立第二套订单事实。 +3. 将商家后台订单能力的 Tag 和 `operationId` 统一归 Ordering;后台不是独立业务模块。 +4. 补齐 A024、A025、A308、A434,并对 A144 采用“新增接口”或“删除不可达引用”中的一种明确方案。 +5. 完成六份数据库设计,替换推断或越界的 DBxxx;当前不得按错误 DBxxx 生成 Migration。 +6. 明确 Ordering、Payment、AfterSales、Messaging 之间的 Application/Contracts 与集成事件,禁止跨模块直接读写内部表。 +7. 生成并校验真实 OpenAPI,确保每个 `operationId`、Schema、状态码、Policy 与本文件一致。 +8. 至少一名其他成员完成交叉评审后,才把对应接口状态改为“已确认”;未确认接口不得宣称已冻结。 diff --git a/eshop-project-rules-upload/AGENTS.md b/eshop-project-rules-upload/AGENTS.md index c1e8b50..85b9a34 100644 --- a/eshop-project-rules-upload/AGENTS.md +++ b/eshop-project-rules-upload/AGENTS.md @@ -121,6 +121,8 @@ 读取接口设计时,必须同时定位第一章相关通用约定、第二章目标 Axxx 清单和第三章同编号详细定义;缺少请求字段、响应、错误、鉴权或业务规则时先记录并补齐契约,不得凭清单名称猜测实现。 +六份个人接口原稿保存在 `docs/02-设计文档/interface/interface-<姓名拼音首字母>.md`,仅用于贡献和交叉评审追踪,不得作为实现事实源。`docs/02-设计文档/接口设计.md` 是实现、OpenAPI、联调和测试的唯一接口契约;修改个人原稿时必须在同一任务中同步总文档,未在总文档中标记为“已确认”的接口不得直接进入实现。 + ### 5. 前端页面与交互 涉及前端时,读取: diff --git a/eshop-project-rules-upload/document-routing.reference.md b/eshop-project-rules-upload/document-routing.reference.md index 1dd81e6..b9c360c 100644 --- a/eshop-project-rules-upload/document-routing.reference.md +++ b/eshop-project-rules-upload/document-routing.reference.md @@ -175,11 +175,12 @@ 然后读取: -1. `二、接口清单` 中的编号分配、个人文件规则和目标负责人区间。 -2. 协作阶段读取目标负责人的 `docs/02-设计文档/interface-<姓名拼音首字母>.md`;汇总完成后改读本文件中的统一清单和同编号详细定义。 -3. 实际 OpenAPI、Endpoint、DTO、调用方和测试。 +1. `二、接口清单` 中的编号分配、个人文件规则、目标负责人区间和汇总状态。 +2. 以本文件中的统一清单和同编号详细定义作为实现事实源;需要核对负责人原始设计或交叉评审记录时,再读 `docs/02-设计文档/interface/interface-<姓名拼音首字母>.md`。 +3. `五、汇总审计与冻结条件` 中目标负责人的缺口、冲突和冻结阻塞项。 +4. 实际 OpenAPI、Endpoint、DTO、调用方和测试。 -只有接口清单、没有详细 Axxx 时标记为 `部分定义`;字段、失败响应和业务规则未确认前不得直接编码。当前审计中六份 `interface-*.md` 尚未创建,主接口文档只有公共规则和分工,尚无可直接实施的具体 Axxx 契约;每次任务开始时重新检查,不永久假设此状态。 +只有接口清单、没有详细 Axxx 时标记为 `部分定义`;字段、失败响应和业务规则未确认前不得直接编码。当前六份个人原稿已保存在 `docs/02-设计文档/interface/`,主接口文档已汇总 102 个不重复 Axxx 及其详细定义,但整体仍为“部分定义,未冻结”:A024、A025、A308、A434 尚缺,A144 需要在“新增单条评价读取”与“删除不可达引用”之间作出决策,A229/A230 与 Ordering 查询边界冲突。每次任务开始时重新检查,不永久假设此状态。 ### 6.5 `docs/02-设计文档/数据库设计.md` @@ -212,18 +213,18 @@ | 模块 | 需求定位 | 架构重点 | 常见关联 | |---|---|---|---| | M00 公共基建 | M00、需求 8/9 | 架构 4/5/10/11/14/15 | 全模块组合根、公共契约 | -| M01 用户鉴权 | M01-01~03、M06-03、M08 | 架构 5/8/9 | A001~A100 待登记、账号状态、Policy | -| M02 商品 | M02-01/02、M06-01、M07、C04 | 架构 6/7.5/7.6/7.9/7.11 | A101~A200 待登记、图片、搜索、缓存失效 | -| M03 购物车 | M03-01、C01 | 架构 7.1/7.7 | A201~A300 待登记、库存与订单协作 | -| M04 订单 | M04-01~04、M06-02、C03 | 架构 7.1/7.3/7.8 | 库存、支付、Worker | -| M05 支付 | M05-01、C08 | 架构 7.2/7.12 | 订单、Outbox、对账 | +| M01 用户鉴权 | M01-01~03、M06-03、M08 | 架构 5/8/9 | A001~A023 已登记,A024/A025 缺失,账号状态、Policy | +| M02 商品 | M02-01/02、M06-01、M07、C04 | 架构 6/7.5/7.6/7.9/7.11 | A101~A103、A110~A114、A120~A128、A140~A143 已登记,A144 待决策 | +| M03 购物车 | M03-01、C01 | 架构 7.1/7.7 | A201~A208、A220~A230 已登记,A229/A230 边界冲突,库存与订单协作 | +| M04 订单 | M04-01~04、M06-02、C03 | 架构 7.1/7.3/7.8 | A301~A307 已登记,A308 缺失,库存、支付、Worker | +| M05 支付 | M05-01、C08 | 架构 7.2/7.12 | A401~A408、A421~A425、A431~A433 已登记,订单、Outbox、对账 | | M06 后台 | M06-01~03 | 架构 5/6/8 | 商品、订单、账号各自主责 | | M07 评价 | M07 | 架构 7.5/7.6 | 订单项、对象存储 | | M08 收藏历史 | M08 | 架构 7.6 | 用户隔离、商品引用 | -| M09 消息 | M09、C06 | 架构 7.4/7.6/7.10 | PostgreSQL、SignalR、事件 | -| M10 售后 | M10、C08 | 架构 7.6/7.12 | 订单项、支付退款、对账 | +| M09 消息 | M09、C06 | 架构 7.4/7.6/7.10 | A501~A505 已登记,PostgreSQL、SignalR、事件 | +| M10 售后 | M10、C08 | 架构 7.6/7.12 | A411~A419、A431~A433 已登记,A434 缺失,订单项、支付退款、对账 | | C07 缓存 | C07 | 架构 7.11 | Catalog 规则与 M00 基础设施 | -| C10 部署 | C10 | 架构 7.13/10/11/12/15 | Compose、Nginx、多实例 | +| C10 部署 | C10 | 架构 7.13/10/11/12/15 | A506/A507 已登记,Compose、Nginx、多实例 | 映射只帮助定位,不改变需求文档中的负责人和边界。 diff --git a/eshop-project-rules-upload/eshop-align-docs.SKILL.md b/eshop-project-rules-upload/eshop-align-docs.SKILL.md index a12e909..e627e43 100644 --- a/eshop-project-rules-upload/eshop-align-docs.SKILL.md +++ b/eshop-project-rules-upload/eshop-align-docs.SKILL.md @@ -40,6 +40,13 @@ description: 维护 E-Shop 教师要求、需求、架构、数据库、接口 接口清单没有对应 Axxx 详情、数据库章节只有表名、测试计划或报告仍为空白模板时,必须保留为“部分/模板/缺失”,不能为了文档看起来完整而虚构字段、统计或结论。 +## 维护接口汇总 + +- `docs/02-设计文档/接口设计.md` 是实现、OpenAPI、联调和测试的唯一接口事实源。 +- 六份 `docs/02-设计文档/interface/interface-<姓名拼音首字母>.md` 作为个人贡献原稿长期保留,用于自审和交叉评审,但不能覆盖总文档。 +- 负责人修改个人原稿时,同一任务必须同步总文档中的统一清单、同编号详细定义、需求追踪状态和未决项;不得只改个人文件。 +- 汇总时检查 Axxx、`operationId` 和“HTTP 方法 + 路径”全局唯一,并把缺少字段、状态机、鉴权、DBxxx 或跨模块契约的接口标为“部分定义”或“待交叉评审”,不得为了凑齐数量改成“已确认”。 + ## 建立一致性追踪 逐项核对: diff --git a/eshop-project-rules-upload/eshop-project-workflow.SKILL.md b/eshop-project-rules-upload/eshop-project-workflow.SKILL.md index 4e07557..fd858ed 100644 --- a/eshop-project-rules-upload/eshop-project-workflow.SKILL.md +++ b/eshop-project-rules-upload/eshop-project-workflow.SKILL.md @@ -29,6 +29,7 @@ description: 作为 E-Shop 仓库所有任务的项目级总入口,统一完 3. 将关键文档标记为 `完整定义`、`部分定义`、`模板/占位`、`实现偏离` 或 `缺失`。 4. 接口只有清单没有 Axxx 详情、数据库只有表名没有字段约束、测试文件只有模板时,先作为缺口报告,不把它们当作可实施契约或通过证据。 5. 同一事实冲突时,按教师基线、用户当前确认、真实实现与验证证据、已确认设计、模板与计划的顺序判断。 +6. 接口任务以 `docs/02-设计文档/接口设计.md` 为唯一实施契约;`docs/02-设计文档/interface/` 中的个人原稿只用于贡献追踪,修改后必须同步总文档。 ## 选择专项 Skill -- Gitee From b4c75bb1c35f7c565d36f77aeb57cbc9d28da210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=B5=B7=E6=B4=8B?= <2040454617@qq.com> Date: Fri, 24 Jul 2026 10:50:30 +0800 Subject: [PATCH 048/118] docs(interface): fix A431 boundary + add A434 buyer return-info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A431 修正:从外部 HTTP 接口改为内部应用能力(IRefundService.CreateRefundAsync) 不占 Axxx HTTP 编号;A432/A433 保留为外部查询接口 - A434 新增:买家提交退货/寄回信息(POST /api/after-sales/requests/{requestId}/return-info) 适配 M10-FR11 退货流程:买家提交物流 → 商家确认收货 → 触发内部退款 - 1.2 节 M10 售后流程接口清单新增 A434 - 1.4 节 标题改为 '退款查询 + A431 内部应用能力',表格删除 A431 HTTP 行 - 3.5 节 操作审计列表更新(A431 改为 A434;A431 内部应用能力单独说明) - 新增业务错误码:AFTER_SALES.WRONG_TYPE、AFTER_SALES.DUPLICATE_TRACKING_NUMBER Refs: 团队评审反馈 2026-07-24 --- .../interface-zhy.md" | 186 +++++++++++++----- 1 file changed, 141 insertions(+), 45 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhy.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhy.md" index 563c83a..3ab393b 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhy.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-zhy.md" @@ -51,6 +51,7 @@ | A417 | `AfterSales_ConfirmReturn` | POST | `/api/after-sales/requests/{requestId}/confirm-return` | M10-FR11 | AfterSales | MerchantOnly | 是 | | A418 | `AfterSales_ListAuditLogs` | GET | `/api/after-sales/requests/{requestId}/audit-logs` | M10-FR04 | AfterSales | BuyerOnly/MerchantOnly | 否 | | A419 | `AfterSales_RetryRefund` | POST | `/api/after-sales/requests/{requestId}/retry-refund` | M10-FR07 | AfterSales | MerchantOnly | 是 | +| A434 | `AfterSales_SubmitReturnInfo` | POST | `/api/after-sales/requests/{requestId}/return-info` | M10-FR11(扩展) | AfterSales | BuyerOnly | 是 | ### 1.3 C08 支付回调与对账(A421-A425) @@ -62,15 +63,19 @@ | A424 | `Reconciliation_ListDifferences` | GET | `/api/admin/reconciliation/batches/{batchId}/differences` | C08-FR07/FR08 | Reconciliation | AdminOnly | 否 | | A425 | `Reconciliation_ProcessDifference` | POST | `/api/admin/reconciliation/differences/{differenceId}/process` | C08-FR08 | Reconciliation | AdminOnly | 是 | -### 1.4 M10 退款入账(A431-A433) +### 1.4 M10 退款查询(A432-A433)+ A431 内部应用能力 | 编号 | operationId | 方法 | 路径 | 需求 | Tag | 鉴权 | 幂等 | |---|---|---|---|---|---|---|---| -| A431 | `Refund_Create` | POST | `/api/after-sales/requests/{requestId}/refund` | M10-FR07 | Payment | MerchantOnly(系统内部) | 是 | +| A431 | _(内部应用能力,不占 Axxx HTTP 编号)_ | — | — | M10-FR07 | Payment | 内部模块信任 | 是 | | A432 | `Refund_Get` | GET | `/api/refunds/{refundId}` | M10-FR04 | Payment | BuyerOnly/MerchantOnly | 否 | | A433 | `Refund_List` | GET | `/api/refunds` | M10-FR03 | Payment | BuyerOnly/MerchantOnly | 否 | -> **A431 触发说明**:A431 实际由 AfterSales 审核通过后系统内部调用(来源 A416),不属于买家/商家直接调用的接口;保留在 Payment 区间因关联交易事项本质是钱包入账。 +> **A431 边界说明(2026-07-24 团队评审反馈修正)**:A431 退款入账本质是**进程内应用能力**,不是 HTTP 接口。 +> - 进程内调用:`IRefundService.CreateRefundAsync(CreateRefundCommand, CancellationToken)` → `RefundResult` +> - 调用方:A416 商家审核通过(`expectRefund=true`)、A417 商家确认收货、A419 退款失败重试 +> - 详细契约见 §2 中 `A431 内部应用能力:退款入账` 节 +> - v0.1 草稿将 A431 误写为 `MerchantOnly` 外部 HTTP 接口,边界混淆;本版本改为内部契约 --- @@ -1889,21 +1894,92 @@ --- -### A431 模拟退款 +### A431 内部应用能力:退款入账(不占 Axxx HTTP 编号) -- **模块 / Tag**:Payment +> **重要修正(2026-07-24 团队评审反馈)**:A431 退款入账本质是**进程内应用能力**,不是 HTTP 接口。v0.1 草稿误写为 `MerchantOnly` 外部接口,边界混淆;本版本改为内部契约,**不占 Axxx HTTP 编号**。A432/A433 仍是外部查询接口。 + +- **模块 / Tag**:Payment(应用服务层) - **需求编号**:M10-FR07 - **负责人**:张海洋 - **关联数据表**:DB088(待评审)— `refunds`、DB081(待评审)— `wallets`、DB083(待评审)— `wallet_ledgers` +- **当前状态**:已设计(内部契约) +- **用途**:由 AfterSales 模块审核通过后(来源 A416 / A417 / A419)调用,将售后金额幂等退回买家小金库 +- **调用方式**:进程内应用服务调用(**非 HTTP**) +- **应用服务签名**:`IRefundService.CreateRefundAsync(CreateRefundCommand command, CancellationToken cancellationToken) → RefundResult` +- **命令 Schema**:`CreateRefundCommand`(公开应用能力) +- **结果 Schema**:`RefundResult` +- **身份与 Policy**:内部模块信任(无 Policy) +- **资源归属**:按传入 `requestId` / `buyerId` +- **幂等要求**:**必须支持 `IdempotencyKey`**(同 1.12.1 退款入账) + +#### 命令输入 + +- **目标申请**:`requestId`(UUID) +- **期待金额**:`expectedAmount`(decimal) +- **币种**:`currency`(默认 `CNY`) +- **幂等键**:`IdempotencyKey`(必填,UUID) + +#### 校验规则 + +- 申请状态必须为 `Refunding`(已通过 A416 / A417 触发) +- `expectedAmount` 必须等于申请计算金额 +- 同一 `IdempotencyKey` + 相同 `amount` → 返回首次结果 +- 同一 `IdempotencyKey` + 不同 `amount` → 抛 `IdempotencyKeyReusedException` + +#### 结果输出 + +- **RefundResult**: + - `refundId`:string + - `requestId`:string + - `buyerId`:string + - `amount`:decimal + - `currency`:string + - `status`:`Succeeded` / `Failed` + - `walletBalanceAfter`:decimal + +#### 业务规则与并发 + +- 钱包入账 + 退款记录 + 钱包流水 + 申请状态更新 **同事务**(按架构 §7.2) +- 退款成功后订单**保持原核心状态**(按 M10 业务规则 + 架构 §7.6) +- 退款流水必须纳入 C08 每日对账(按 M10 业务规则) +- AfterSales 不直接改钱包数据;通过 Payment 公开应用能力**幂等**退回(按架构 §7.2) + +#### 缓存、事件或外部依赖 + +- 缓存:幂等记录存在 DB +- 事件:发布 `RefundCompletedIntegrationEvent` +- 外部依赖:PostgreSQL + AfterSales 模块 + +#### 验证场景 + +- 正常:审核通过触发 → 退款成功,余额增加 +- 重复:相同 IdempotencyKey → 返回首次结果,不重复入账 +- 异常:金额不一致 → 失败回滚 +- 异常:写流水失败 → 整体事务回滚,申请状态还原 + +#### 调用方 + +- A416 商家审核通过且 `expectRefund=true` → AfterSales 异步应用服务调用 +- A417 商家确认收货 → AfterSales 异步应用服务调用 +- A419 退款失败重试 → AfterSales 异步应用服务调用 + +--- + +### A434 买家提交退货/寄回信息 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR11(扩展:买家提交退货物流) +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_audit_logs`、DB097(待评审)— `return_shipments`(快递单号登记) - **当前状态**:已设计 -- **用途**:将售后金额幂等退回买家小金库 -- **方法与路径**:`POST /api/after-sales/requests/{requestId}/refund` -- **operationId**:`Refund_Create` -- **请求 Schema**:`CreateRefundRequest` -- **响应 Schema**:`RefundDetailResponse` -- **身份与 Policy**:JWT Bearer + `MerchantOnly`(系统内部调用) -- **资源归属**:当前 merchant 授权范围内申请 -- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 退款入账) +- **用途**:买家提交退货的物流单号与快递公司(用于 `ReturnAndRefund` 类型的申请) +- **方法与路径**:`POST /api/after-sales/requests/{requestId}/return-info` +- **operationId**:`AfterSales_SubmitReturnInfo` +- **请求 Schema**:`SubmitReturnInfoRequest` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 申请 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) #### 请求 @@ -1913,36 +1989,52 @@ - **Body**: ```json { - "expectedAmount": 100.00, - "currency": "CNY" + "carrier": "SF", + "trackingNumber": "SF1234567890", + "shippedAt": "2026-07-23T08:30:00Z", + "note": "外包装完好" } ``` - **校验规则**: - - 申请归属当前 merchant(或系统内部) - - 申请状态必须为 `Refunding`(已通过 A416 / A417 触发) - - `expectedAmount` 必须等于申请计算金额 + - 申请归属当前 buyerId + - 申请类型必须为 `ReturnAndRefund` + - 申请状态必须为 `PendingReturn`(商家审核通过后、待退货) + - `carrier` 必填,枚举白名单(待命名规范扩展,如 `SF` / `YTO` / `ZTO` / `YD` 等) + - `trackingNumber` 必填,≤ 50 字符,全局唯一约束(同一快递单号不能重复提交) + - `shippedAt` 必填,ISO 8601 UTC,且 ≤ now + - `note` 选填,≤ 500 字 - `Idempotency-Key` 必填 - - 同一 Key + 相同 amount → 返回首次结果 #### 成功响应 - **HTTP 状态**:`200 OK` -- **响应 Schema**:`RefundDetailResponse` +- **响应 Schema**:`AfterSalesRequestDetailResponse` - **示例**: ```json { "code": "success", "message": "ok", "data": { - "refundId": "...", "requestId": "b9c1...", - "buyerId": "...", - "amount": 100.00, + "orderId": "3f0ed9a9-...", + "orderItemId": "5a7c...", + "type": "ReturnAndRefund", + "quantity": 1, + "calculatedAmount": 100.00, "currency": "CNY", - "status": "Succeeded", - "walletBalanceAfter": 200.50, - "createdAt": "2026-07-23T09:00:00Z", - "succeededAt": "2026-07-23T09:00:01Z" + "status": "PendingReceipt", + "returnInfo": { + "carrier": "SF", + "trackingNumber": "SF1234567890", + "shippedAt": "2026-07-23T08:30:00Z", + "note": "外包装完好" + }, + "createdAt": "2026-07-23T08:00:00Z", + "timeline": [ + { "status": "PendingReview", "at": "2026-07-23T08:00:00Z", "actor": "buyer" }, + { "status": "PendingReturn", "at": "2026-07-23T08:10:00Z", "actor": "merchant" }, + { "status": "PendingReceipt", "at": "2026-07-23T08:30:00Z", "actor": "buyer" } + ] } } ``` @@ -1951,35 +2043,38 @@ | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 400 | `COMMON.VALIDATION_FAILED` | 字段错误(trackingNumber 格式、shippedAt 未来时间) | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | -| 403 | `AUTH.FORBIDDEN` | 角色非 Merchant | -| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | -| 409 | `AFTER_SALES.INVALID_STATUS` | 申请状态非 `Refunding` | -| 409 | `PAYMENT.AMOUNT_MISMATCH` | `expectedAmount` 与申请计算金额不一致 | -| 409 | `PAYMENT.REFUND_FAILED` | 退款执行失败(写流水失败等) | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或非本人 | +| 409 | `AFTER_SALES.INVALID_STATUS` | 申请状态非 `PendingReturn` | +| 409 | `AFTER_SALES.WRONG_TYPE` | 申请类型非 `ReturnAndRefund` | +| 409 | `AFTER_SALES.DUPLICATE_TRACKING_NUMBER` | 同一快递单号已被使用 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 钱包入账 + 退款记录 + 钱包流水 + 申请状态更新 **同事务**(按架构 §7.2) -- 退款成功后订单**保持原核心状态**(按 M10 业务规则 + 架构 §7.6) -- 退款流水必须纳入 C08 每日对账(按 M10 业务规则) -- AfterSales 不直接改钱包数据;通过 Payment 公开应用能力**幂等**退回(按架构 §7.2) +- 状态条件更新:`WHERE status = 'PendingReturn' AND buyer_id = currentBuyerId AND type = 'ReturnAndRefund'` +- 提交后状态变为 `PendingReceipt` +- 快递单号全局唯一约束(同一单号不能用于多笔售后) +- `audit_log` 记录提交人与提交内容 +- 商家在 A417 确认收货后 → 触发 A431 内部应用能力退款 #### 缓存、事件或外部依赖 -- 缓存:幂等记录存在 DB -- 事件:发布 `RefundCompletedIntegrationEvent` -- 外部依赖:PostgreSQL + AfterSales 模块 +- 缓存:不缓存 +- 事件:发布 `AfterSalesReturnInfoSubmittedIntegrationEvent` +- 外部依赖:PostgreSQL #### 验证场景 -- 正常:审核通过触发 → 退款成功,余额增加 -- 重复:相同 Idempotency-Key → 返回首次结果,不重复入账 -- 异常:金额不一致 → 409 + `PAYMENT.AMOUNT_MISMATCH` -- 异常:写流水失败 → 409 + `PAYMENT.REFUND_FAILED`,申请状态回滚 +- 正常:买家提交退货物流 → 状态进入 `PendingReceipt` +- 重复:相同 Idempotency-Key → 返回首次结果,不重复写入 +- 异常:他人申请 → 404 + `RESOURCE.NOT_FOUND` +- 异常:申请类型为 `RefundOnly` → 409 + `AFTER_SALES.WRONG_TYPE` +- 异常:状态非 `PendingReturn` → 409 + `AFTER_SALES.INVALID_STATUS` +- 异常:快递单号已被使用 → 409 + `AFTER_SALES.DUPLICATE_TRACKING_NUMBER` --- @@ -2153,7 +2248,8 @@ ### 3.5 操作审计 -- 涉及状态变更的接口(A402 / A405 / A412 / A415 / A416 / A417 / A419 / A421 / A425 / A431)在 `audit_logs` 或对应表登记 actor、at、from_status、to_status +- 涉及状态变更的接口(A402 / A405 / A412 / A415 / A416 / A417 / A419 / A421 / A425 / A434)在 `audit_logs` 或对应表登记 actor、at、from_status、to_status +- A431 内部应用能力在 `refunds` + `wallet_ledgers` + `after_sales_requests` 同步登记(事务内) - 涉及资金的接口在 `wallet_ledgers` 写入流水 --- -- Gitee From eef2f43c91da3259317f026f7404fa638e309332 Mon Sep 17 00:00:00 2001 From: FISH SOUP Date: Fri, 24 Jul 2026 10:44:28 +0800 Subject: [PATCH 049/118] =?UTF-8?q?docs(api):=20=E8=A1=A5=E7=99=BB=20A024/?= =?UTF-8?q?A025=20=E5=B9=B6=E6=94=B6=E7=B4=A7=20Identity=20=E9=89=B4?= =?UTF-8?q?=E6=9D=83=E8=8C=83=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 docs/02-设计文档/interface-tyh.md 中:\n- 补登 A024 记录浏览历史与 A025 查询浏览记录开关,覆盖 X02 浏览历史读写与开关读取闭环。\n- 将 A006/A007 路径迁回 /api/users/me 资源域,避免个人资料变更混入 /api/auth 认证域。\n- 按 F03 把 A010~A014 收货地址接口鉴权收紧为 BuyerOnly,符合 'M01-03 仅买家可访问' 与 '管理员不通过本模块查看或修改买家资料' 范围。 --- .../interface/interface-tyh.md" | 143 ++++++++++++++++-- 1 file changed, 133 insertions(+), 10 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" index 6efedcf..4cf6b82 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" @@ -11,6 +11,7 @@ | 版本 | 日期 | 修改人 | 修改说明 | |---|---|---|---| | v0.1 | 2026-07-24 | 唐宇昊 | 建立 `A001`~`A023` 接口清单并补齐全部详细定义 | +| v0.2 | 2026-07-24 | 唐宇昊 | 补登 `A024` 记录浏览历史、`A025` 查询浏览记录开关;将 `A006`/`A007` 路径迁回 `/api/users/me` 资源域;按 F03 收紧 `A010`~`A014` 鉴权 Policy 为仅 `BuyerOnly` | ## 一、接口清单 @@ -20,16 +21,16 @@ | A002 | Identity | F02 | 登录 | POST | `/api/auth/login` | `Identity_Login` | `LoginRequest` | `LoginResponse` | 允许游客 | DB001 | 已定义 | | A003 | Identity | F02 | 退出当前令牌 | POST | `/api/auth/logout` | `Identity_Logout` | 无 | `LogoutResponse` | BuyerOnly / MerchantOnly / AdminOnly | DB001、DB004 | 已定义 | | A004 | Identity | F02 | 获取当前用户 | GET | `/api/auth/me` | `Identity_GetCurrentUser` | 无 | `CurrentUserResponse` | BuyerOnly / MerchantOnly / AdminOnly | DB001 | 已定义 | -| A005 | Identity | F02 | 刷新访问令牌 | POST | `/api/auth/refresh-token` | `Identity_RefreshToken` | `RefreshTokenRequest` | `LoginResponse` | 已认证用户 | DB001、DB004 | 已定义 | -| A006 | Identity | F03 | 修改手机号 | POST | `/api/auth/change-phone` | `Identity_ChangePhone` | `ChangePhoneRequest` | `CurrentUserResponse` | BuyerOnly / MerchantOnly | DB001、DB004 | 已定义 | -| A007 | Identity | F03 | 重置用户名 | POST | `/api/auth/reset-username` | `Identity_ResetUsername` | 无 | `ResetUsernameResponse` | BuyerOnly / MerchantOnly | DB001 | 已定义 | +| A005 | Identity | F02 | 刷新访问令牌 | POST | `/api/auth/refresh-token` | `Identity_RefreshToken` | `RefreshTokenRequest` | `LoginResponse` | BuyerOnly / MerchantOnly / AdminOnly | DB001、DB004 | 已定义 | +| A006 | Identity | F03 | 修改手机号 | POST | `/api/users/me/phone` | `Identity_ChangePhone` | `ChangePhoneRequest` | `CurrentUserResponse` | BuyerOnly / MerchantOnly | DB001、DB004 | 已定义 | +| A007 | Identity | F03 | 重置用户名 | POST | `/api/users/me/username/reset` | `Identity_ResetUsername` | 无 | `ResetUsernameResponse` | BuyerOnly / MerchantOnly | DB001 | 已定义 | | A008 | Identity | F03 | 获取本人资料 | GET | `/api/users/me` | `Identity_GetMyProfile` | 无 | `MyProfileResponse` | BuyerOnly / MerchantOnly | DB001 | 已定义 | | A009 | Identity | F03 | 修改本人资料 | PATCH | `/api/users/me` | `Identity_UpdateMyProfile` | `UpdateMyProfileRequest` | `MyProfileResponse` | BuyerOnly / MerchantOnly | DB001 | 已定义 | -| A010 | Identity | F03 | 我的地址列表 | GET | `/api/users/me/addresses` | `Identity_ListMyAddresses` | 无 | `AddressListResponse` | BuyerOnly / MerchantOnly | DB003 | 已定义 | -| A011 | Identity | F03 | 新增地址 | POST | `/api/users/me/addresses` | `Identity_CreateMyAddress` | `CreateAddressRequest` | `AddressResponse` | BuyerOnly / MerchantOnly | DB003 | 已定义 | -| A012 | Identity | F03 | 编辑地址 | PATCH | `/api/users/me/addresses/{addressId}` | `Identity_UpdateMyAddress` | `UpdateAddressRequest` | `AddressResponse` | BuyerOnly / MerchantOnly | DB003 | 已定义 | -| A013 | Identity | F03 | 删除地址 | DELETE | `/api/users/me/addresses/{addressId}` | `Identity_DeleteMyAddress` | 无 | 无(204) | BuyerOnly / MerchantOnly | DB003 | 已定义 | -| A014 | Identity | F03 | 设置默认地址 | POST | `/api/users/me/addresses/{addressId}/default` | `Identity_SetDefaultAddress` | 无 | `AddressResponse` | BuyerOnly / MerchantOnly | DB003 | 已定义 | +| A010 | Identity | F03 | 我的地址列表 | GET | `/api/users/me/addresses` | `Identity_ListMyAddresses` | 无 | `AddressListResponse` | BuyerOnly | DB003 | 已定义 | +| A011 | Identity | F03 | 新增地址 | POST | `/api/users/me/addresses` | `Identity_CreateMyAddress` | `CreateAddressRequest` | `AddressResponse` | BuyerOnly | DB003 | 已定义 | +| A012 | Identity | F03 | 编辑地址 | PATCH | `/api/users/me/addresses/{addressId}` | `Identity_UpdateMyAddress` | `UpdateAddressRequest` | `AddressResponse` | BuyerOnly | DB003 | 已定义 | +| A013 | Identity | F03 | 删除地址 | DELETE | `/api/users/me/addresses/{addressId}` | `Identity_DeleteMyAddress` | 无 | 无(204) | BuyerOnly | DB003 | 已定义 | +| A014 | Identity | F03 | 设置默认地址 | POST | `/api/users/me/addresses/{addressId}/default` | `Identity_SetDefaultAddress` | 无 | `AddressResponse` | BuyerOnly | DB003 | 已定义 | | A015 | Identity | F13 | 后台账号列表 | GET | `/api/admin/users` | `Identity_AdminListUsers` | 无(Query 分页/筛选) | `AdminUserListResponse` | AdminOnly | DB001 | 已定义 | | A016 | Identity | F13 | 禁用账号 | POST | `/api/admin/users/{userId}/disable` | `Identity_AdminDisableUser` | 无 | `AdminUserResponse` | AdminOnly | DB001、DB004 | 已定义 | | A017 | Identity | F13 | 启用账号 | POST | `/api/admin/users/{userId}/enable` | `Identity_AdminEnableUser` | 无 | `AdminUserResponse` | AdminOnly | DB001、DB004 | 已定义 | @@ -39,6 +40,8 @@ | A021 | Engagement | X02 | 浏览历史列表 | GET | `/api/browsing-history` | `Engagement_ListBrowsingHistory` | 无(Query 分页) | `BrowsingHistoryListResponse` | BuyerOnly | DB006 | 已定义 | | A022 | Engagement | X02 | 修改浏览记录开关 | PATCH | `/api/browsing-history/settings` | `Engagement_UpdateBrowsingHistorySetting` | `UpdateBrowsingHistorySettingRequest` | `BrowsingHistorySettingResponse` | BuyerOnly | DB006 | 已定义 | | A023 | Engagement | X02 | 清空浏览历史 | DELETE | `/api/browsing-history` | `Engagement_ClearBrowsingHistory` | 无 | 无(204) | BuyerOnly | DB006 | 已定义 | +| A024 | Engagement | X02 | 记录浏览历史 | POST | `/api/browsing-history/records` | `Engagement_RecordBrowsingHistory` | `RecordBrowsingHistoryRequest` | `BrowsingHistoryResponse` | BuyerOnly | DB006 | 已定义(v0.2 补登) | +| A025 | Engagement | X02 | 查询浏览记录开关 | GET | `/api/browsing-history/settings` | `Engagement_GetBrowsingHistorySetting` | 无 | `BrowsingHistorySettingResponse` | BuyerOnly | DB006 | 已定义(v0.2 补登) | 接口路径补充说明: @@ -374,7 +377,7 @@ RefreshTokenRequest { - 关联数据表:DB001、DB004 - 当前状态:已定义 - 用途:买家或商家修改本人手机号,提交后旧登录态全部失效并要求重新登录。 -- 方法与路径:`POST /api/auth/change-phone` +- 方法与路径:`POST /api/users/me/phone` - operationId:`Identity_ChangePhone` #### 请求 @@ -431,7 +434,7 @@ ChangePhoneRequest { - 关联数据表:DB001 - 当前状态:已定义 - 用途:用户自助重置一次用户名,重置次数用完即返回错误。 -- 方法与路径:`POST /api/auth/reset-username` +- 方法与路径:`POST /api/users/me/username/reset` - operationId:`Identity_ResetUsername` #### 请求 @@ -1305,3 +1308,123 @@ BrowsingHistorySettingResponse { - 清空本人浏览历史 → 204,后续列表为空。 - 重复清空 → 204,幂等。 + +### A024 记录浏览历史 + +- 模块 / Tag:Engagement +- 需求编号:X02、M08、M08-FR04 +- 负责人:唐宇昊 +- 关联数据表:DB006 +- 当前状态:已定义 +- 用途:买家查看商品详情时,记录或更新其最近浏览时间;同一买家同一商品只保留一条记录,由 Catalog 模块的"商品详情查询成功"或前端埋点触发。 +- 方法与路径:`POST /api/browsing-history/records` +- operationId:`Engagement_RecordBrowsingHistory` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Buyer) +- Body: + +```text +RecordBrowsingHistoryRequest { + productId: uuid // 必填 + viewedAt: string? // 可选,UTC ISO 8601;缺省时取服务端时间。客户端不得伪造未来时间 +} +``` + +- 校验规则:`productId` 必须存在且为已上架商品;字段缺失或格式错误返回字段级 ProblemDetails。 + +#### 成功响应 + +- HTTP 状态:`201 Created`(首次写入)或 `200 OK`(仅更新时间) +- 响应 Schema:`BrowsingHistoryResponse` + +```text +BrowsingHistoryResponse { + productId: uuid + viewedAt: string // UTC ISO 8601 + expired: boolean // 达到记录上限被清理时返回 true +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或格式错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 已登录用户令牌无效 | +| 403 | `ENGAGEMENT.BROWSING_HISTORY_DISABLED` | 当前买家已关闭浏览记录开关 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在或未上架 | +| 429 | `COMMON.RATE_LIMITED` | 同一买家短时间内高频记录浏览 | + +#### 业务规则与并发 + +- 若调用方为已登录买家且其浏览记录开关关闭,返回 `403 / ENGAGEMENT.BROWSING_HISTORY_DISABLED`,不写入任何记录。 +- 游客身份不持久化记录;本接口仅 BuyerOnly;前端在游客访问商品详情时引导登录,登录后再调用本接口。 +- 同一买家同一商品只保留一条记录;按 `(user_id, product_id)` 唯一约束更新 `viewed_at` 为最新值。 +- 默认单买家最多保留 200 条记录;超出时按 `viewed_at` 由小到大移除多余记录,并在响应 `expired=true` 告知。 +- 商品下架后续访问记录依旧写入;记录保留但不提供购买入口。 +- 触发方(Catalog 详情查询成功或前端埋点)不得伪造未来时间;服务端校正为 `min(viewed_at, NOW())`。 + +#### 缓存、事件或外部依赖 + +- 不缓存;不再写入 PostgreSQL。 +- 不发布集成事件。 + +#### 验证场景 + +- 已开启开关的买家查看新商品 → 201,记录新增。 +- 同一买家再次查看该商品 → 200,仅更新时间,原有过期记录不重复创建。 +- 关闭开关后调用 → 403 / `ENGAGEMENT.BROWSING_HISTORY_DISABLED`。 +- 商家或游客调用 → 403 / `AUTH.FORBIDDEN`。 +- 达到 200 条上限后再记录 → 200,且响应 `expired=true`。 +- 商品下架后调用 → 仍记录并返回 200。 + +### A025 查询浏览记录开关 + +- 模块 / Tag:Engagement +- 需求编号:X02、M08、M08-FR07 +- 负责人:唐宇昊 +- 关联数据表:DB006 +- 当前状态:已定义 +- 用途:买家查询本人"是否记录浏览历史"开关当前值,前端用于初始化控件状态。 +- 方法与路径:`GET /api/browsing-history/settings` +- operationId:`Engagement_GetBrowsingHistorySetting` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Buyer) + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`BrowsingHistorySettingResponse` + +```text +BrowsingHistorySettingResponse { + enabled: boolean + updatedAt: string +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 严格按 `user_id = current_user_id` 过滤;不存在记录时按 `enabled=true` 返回(默认开启)。 +- 与 A022 配对:GET 读取当前值,PATCH 修改值;同一资源不重复定义写入入口。 + +#### 缓存、事件或外部依赖 + +- 不缓存、不发布事件。 + +#### 验证场景 + +- 默认账号 → 200,`enabled=true`。 +- 已通过 A022 关闭过 → 200,`enabled=false`。 +- 商家账号调用 → 403。 -- Gitee From ed01b97bed216e953ac37cac5ba735e6a47c9b9d Mon Sep 17 00:00:00 2001 From: eshop-class1-group7 Date: Fri, 24 Jul 2026 10:59:29 +0800 Subject: [PATCH 050/118] =?UTF-8?q?docs(interface-gxy):=20=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E5=8F=8D=E9=A6=88=E5=A4=84=E7=90=86=20v0.2=EF=BC=88?= =?UTF-8?q?=E6=96=B0=E8=B7=AF=E5=BE=84=E8=A1=A5=20A144=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景: - 上一轮在 docs/02-设计文档/interface-gxy.md 上提交的 A144(commit e0a233e,分支 docs/interface-review-add-gxy)已确认合并,但因 9c25f26 目录重构(个人接口文档统一迁至 docs/02-设计文档/interface/) 时仅整合了 v0.1 基线,A144 未随迁;新路径下文件重新回到 v0.1。 本轮处理: - 基于 origin/dev@9de9aef 重做同一反馈处理,分支 docs/interface-review-add2-gxy。 - Catalog 清单(A101~A128)保持原样,无必缺项。 - 新增 A144 GET /api/reviews/{reviewId} -> Review_GetReview 详细定义(章节三);响应 Schema 与 A142 ReviewDetailResponse 同口径;消除 A142 Location 头与 A143 existingReviewId 的不可达引用。 - 新增错误码 REVIEW.NOT_FOUND(404),不区分「不存在」与「已下线」 以避免泄露存在性。 - DB024 / DB025 关联接口补充 A144。 - 版本号 v0.1 → v0.2,修订记录同步追加。 未修改:教师基线 docs/00-项目要求/、其他成员 interface-*.md、汇总 接口设计.md。 --- .../interface/interface-gxy.md" | 84 ++++++++++++++++++- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" index 22f7fba..c7ad8fd 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" @@ -1,6 +1,6 @@ # 接口设计(顾欣月)— Catalog、Review -> 组别:24级1班第7组 编写人:顾欣月 编写日期:2026-07-24 版本:v0.1 +> 组别:24级1班第7组 编写人:顾欣月 编写日期:2026-07-24 版本:v0.2 > 编号区间:`A101`~`A200` 负责模块:Catalog(商品目录)、Review(评价) ## 修订记录 @@ -8,6 +8,7 @@ | 版本 | 日期 | 修改人 | 修改说明 | |------|------|--------|----------| | v0.1 | 2026-07-24 | 顾欣月 | 建立并完善 Catalog、Review 接口清单、A101~A143 详细定义与枚举附录 | +| v0.2 | 2026-07-24 | 顾欣月 | 评审反馈处理:Catalog 清单(A101~A128)已完整无必缺;新增 A144 单条评价详情查询,消除 A142 Location 头与 A143 `existingReviewId` 不可达引用;同步登记 `REVIEW.NOT_FOUND` 错误码 | ## 一、说明与约定引用 @@ -25,8 +26,8 @@ | DB021 | `categories` | Catalog | A101、A110~A114 | | DB022 | `products` | Catalog | A102、A103、A120~A126 | | DB023 | `product_images` | Catalog | A103、A127、A128 | -| DB024 | `reviews` | Review | A140、A142、A143 | -| DB025 | `review_images` | Review | A141、A142 | +| DB024 | `reviews` | Review | A140、A142、A143、A144 | +| DB025 | `review_images` | Review | A141、A142、A144 | ## 二、接口清单 @@ -53,6 +54,7 @@ | A141 | Review | M07-FR03 | 上传评价图片(提交前暂存) | POST | `/api/reviews/images` | `Review_UploadReviewImage` | `multipart/form-data` | `ReviewImageResponse` | BuyerOnly | DB025 | 待评审 | | A142 | Review | M07-FR04、FR05 | 提交商品评价(幂等) | POST | `/api/reviews` | `Review_CreateReview` | `CreateReviewRequest` | `ReviewDetailResponse` | BuyerOnly | DB024、DB025 | 待评审 | | A143 | Review | M07-FR01 | 查询订单项评价资格/结果 | GET | `/api/reviews/eligibility` | `Review_GetReviewEligibility` | —(Query) | `ReviewEligibilityResponse` | BuyerOnly | DB024 | 待评审 | +| A144 | Review | M07-FR02 | 单条评价详情查询 | GET | `/api/reviews/{reviewId}` | `Review_GetReview` | —(Route) | `ReviewDetailResponse` | 游客可访问 | DB024、DB025 | 待评审 | 清单补充说明: @@ -1107,6 +1109,81 @@ - 已评价返回 `eligible=false, reason=AlreadyReviewed` 且带 `existingReviewId`;未完成返回 `OrderNotCompleted`。 +### A144 单条评价详情查询 + +- 模块 / Tag:Review +- 需求编号:M07-FR02 +- 负责人:顾欣月 +- 关联数据表:DB024 `reviews`、DB025 `review_images` +- 当前状态:待评审 +- 用途:单条评价的对外可寻址读取;服务于 A142 创建响应 `Location: /api/reviews/{reviewId}` 的 REST 约定与 A143 `existingReviewId` 跳转场景,供商品详情、订单详情等位置按需拉取单条评价。 +- 方法与路径:`GET /api/reviews/{reviewId}` +- operationId:`Review_GetReview` +- 请求 Schema:无(仅 Route) +- 响应 Schema:`ReviewDetailResponse`(与 A142 创建响应同口径) +- 身份与 Policy:游客可访问;公开评价与 A140 列表同口径,不返回买家手机号、邮箱、内部用户标识等敏感字段。 +- 资源归属:公开评价;当前买家请求时不附加任何归属校验。 +- 幂等要求:只读,天然幂等。 + +#### 请求 + +- Route 参数:`reviewId`(uuid)。 +- Header:`Accept: application/json`。 +- Body:无。 +- 校验规则:`reviewId` 必须是标准带连字符 UUID 格式;非法格式按 `400 Bad Request`(`COMMON.INVALID_UUID` 通用码)处理;记录不存在按 `404 Not Found`(`REVIEW.NOT_FOUND`)处理,不泄露存在性差异。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`ReviewDetailResponse`,含 `reviewId`、`productId`、`orderItemId`、`rating`、`content`、`images`(`imageId`、`url`、`sortOrder`)、`buyerDisplayName`(脱敏昵称,与 A140 一致)、`createdAt`。 +- 示例: + +```json +{ + "code": "success", + "message": "ok", + "data": { + "reviewId": "9c2f…", + "productId": "6f1d…", + "orderItemId": "1d3a…", + "rating": 5, + "content": "很好用", + "images": [ + { "imageId": "img1…", "url": "https://…/r1.jpg", "sortOrder": 1 } + ], + "buyerDisplayName": "用***月", + "createdAt": "2026-07-21T03:00:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.INVALID_UUID` | `reviewId` 非标准 UUID 格式 | +| 404 | `REVIEW.NOT_FOUND` | 评价不存在或已下线 | +| 500 | `COMMON.INTERNAL_ERROR` | 未处理服务端错误 | + +#### 业务规则与并发 + +- 仅返回 `status = visible` 的有效评价;与 A140 列表同口径;被运营下线的评价对游客返回 404,对买家本人可继续返回(与 A142 提交成功后状态联动由后续评审决定,当前版本对所有调用方一致返回可见评价)。 +- 不返回买家手机号、邮箱、内部用户标识;昵称按 A140 的 `buyerDisplayName` 脱敏规则展示。 +- 与 A142 创建响应的 `Location` 头严格对齐:创建成功后客户端可凭 `Location` 直接 GET 本接口获取完整评价。 +- A143 返回 `existingReviewId` 时,前端可经本接口跳转拉取评价详情;如该评价已被下线,按 404 处理并显示对应文案。 + +#### 缓存、事件或外部依赖 + +- 汇总/详情缓存与 A140 共用同一 Key 前缀;新增/修改评价经事件失效(评价创建/更新事件名以《命名规范》与架构设计为准)。 +- 不依赖 Identity、Ordering 等其他模块的应用契约;仅按 `reviewId` 主键读取 DB024 与 DB025。 + +#### 验证场景 + +- 有效 `reviewId` 返回 200 与 `ReviewDetailResponse`; +- 已下线的 `reviewId` 返回 404(`REVIEW.NOT_FOUND`),响应文案不区分"不存在"与"已下线"; +- 非法 UUID 格式返回 400(`COMMON.INVALID_UUID`); +- 不暴露买家敏感字段;与 A140 列表的 `buyerDisplayName` 脱敏结果一致。 + ## 四、错误码登记(本人模块新增) 以下为本人模块在《接口设计.md》通用错误码之外新增的稳定业务错误码,待汇总时并入总表: @@ -1129,6 +1206,7 @@ | `REVIEW.ORDER_NOT_COMPLETED` | 409 | 订单未完成,不能评价 | | `REVIEW.ALREADY_REVIEWED` | 409 | 该订单项已评价 | | `REVIEW.INVALID_IMAGE` | 415 | 评价图片格式或尺寸不符合要求 | +| `REVIEW.NOT_FOUND` | 404 | 评价不存在或已下线(A144 单条评价详情查询使用,不区分"不存在"与"已下线"以避免泄露存在性) | ## 五、待确认事项 -- Gitee From 503545686e170a3699a787fd3476848cf38d7297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9F=A6=E4=B9=BE=E5=BC=BA?= <3195306445@qq.com> Date: Fri, 24 Jul 2026 11:09:02 +0800 Subject: [PATCH 051/118] =?UTF-8?q?docs(interface):=20=E8=A1=A5=E5=85=A8A3?= =?UTF-8?q?08=E4=B9=B0=E5=AE=B6=E7=A1=AE=E8=AE=A4=E6=94=B6=E8=B4=A7?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interface-wqq.md" | 72 ++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-wqq.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-wqq.md" index 5b3f6c4..9039dfd 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-wqq.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface-wqq.md" @@ -2,7 +2,7 @@ > 负责人:韦乾强 > 模块:Ordering(订单模块)、Merchant后台订单管理 -> 接口编号范围:A301~A307 +> 接口编号范围:A301~A308 > 编写日期:2026-07-24 ## 接口清单 @@ -16,6 +16,76 @@ | A305 | Merchant | F12 | 商家查询订单列表 | GET | /api/merchant/orders | Merchant_GetOrders | - | MerchantOrderListResponse | MerchantOnly | DB001,DB003 | 部分定义 | | A306 | Merchant | F12 | 商家查询订单详情 | GET | /api/merchant/orders/{orderId} | Merchant_GetOrderById | - | MerchantOrderDetailResponse | MerchantOnly | DB001,DB003 | 部分定义 | | A307 | Merchant | F12 | 商家发货 | POST | /api/merchant/orders/{orderId}/ship | Merchant_ShipOrder | ShipOrderRequest | ShipOrderResponse | MerchantOnly | DB001 | 部分定义 | +| A308 | Ordering | F09 | 买家确认收货 | POST | /api/orders/{orderId}/confirm | Ordering_ConfirmOrder | - | ConfirmOrderResponse | BuyerOnly | DB001 | 部分定义 | + +--- + +## A308 买家确认收货 + +- **模块 / Tag**:Ordering +- **需求编号**:F09 +- **负责人**:韦乾强 +- **关联数据表**:DB001(orders) +- **当前状态**:部分定义 +- **用途**:买家确认已收到商品,将订单状态从 `Shipped` 变更为 `Completed` +- **方法与路径**:`POST /api/orders/{orderId}/confirm` +- **operationId**:`Ordering_ConfirmOrder` +- **请求Schema**:无 +- **响应Schema**:`ConfirmOrderResponse` +- **身份与Policy**:BuyerOnly +- **资源归属**:订单必须属于当前买家 +- **幂等要求**:以订单号为幂等键,重复确认返回成功 + +### 请求 + +- **Route参数**:`orderId`(必需,UUID) +- **Query参数**:无 +- **Header**:`Authorization: Bearer `(必需) +- **Body**:无 + +### 成功响应 + +- **HTTP状态**:`200 OK` +- **响应Schema**:`ConfirmOrderResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "orderId": "uuid", + "status": "Completed", + "completedAt": "2026-07-24T14:00:00Z", + "completedBy": "BUYER_CONFIRMED" + } +} +``` + +### 失败响应 + +| HTTP状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 404 | ORDER.NOT_FOUND | 订单不存在 | +| 403 | ORDER.ACCESS_DENIED | 订单不属于当前用户 | +| 409 | ORDER.INVALID_STATUS | 订单状态不允许确认收货(只有已发货可确认) | + +### 业务规则与并发 + +1. 只有 `Shipped` 状态可确认收货 +2. 使用条件更新 `WHERE status = 'Shipped'` 保证幂等 +3. 记录 `completed_at` 和 `completed_by = 'BUYER_CONFIRMED'` +4. 确认收货后触发评价入口开放(若 X01 已实现) + +### 缓存、事件或外部依赖 + +- 发布 `OrderConfirmedEvent` 到 Outbox + +### 验证场景 + +1. 正常确认收货:返回成功,状态变为 Completed +2. 重复确认:返回幂等成功 +3. 订单未发货:返回 409 +4. 跨用户确认:返回 403 --- -- Gitee From 212d4c18e9f00921f4ead0cd3a16c469d87bcbb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=83=A0=E6=83=A0?= <2205590672@qq.com> Date: Fri, 24 Jul 2026 11:10:28 +0800 Subject: [PATCH 052/118] docs(api): align seckill order queries with A302/A303 --- .../interface/interface-zhh.md" | 160 ++---------------- 1 file changed, 10 insertions(+), 150 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" index 5745c0e..760f369 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" @@ -1,7 +1,7 @@ # 个人接口文件 — 朱惠惠(Cart、Seckill) > 组别:24级1班第7组 负责人:朱惠惠(zhh) 接口编号区间:`A201`~`A300` -> 负责模块:Cart(购物车 CRUD、选中、结算预览、清空)、Seckill(商家活动维护、买家抢购下单、秒杀订单查询) +> 负责模块:Cart(购物车 CRUD、选中、结算预览、清空)、Seckill(商家活动维护、买家抢购下单;订单查询复用 Ordering 的 A302/A303) > 关联教师验收编号:F07、C01 > 当前状态:部分定义;清单已给出,详细定义按接口设计 1.20 模板补齐 > 本文件为协作与评审追踪材料;汇总后继续保留,实现、OpenAPI 和联调以 `../接口设计.md` 为准 @@ -11,6 +11,7 @@ | 版本 | 日期 | 修改人 | 修改说明 | |---|---|---|---| | v0.1 | 2026-07-24 | 朱惠惠 | 建立 `A201`~`A230` 接口清单并补齐全部详细定义 | +| v0.2 | 2026-07-24 | 朱惠惠 | 取消 A229/A230 独立契约,秒杀订单查询复用 A302/A303 | ## 一、接口清单 @@ -33,15 +34,15 @@ | A226 | Seckill | C01 | 买家秒杀活动列表 | GET | `/api/seckill-activities` | `Seckill_ListActiveActivities` | 无(Query 分页) | `SeckillActivityListResponse` | 允许游客 | DB042、DB043 | 已定义 | | A227 | Seckill | C01 | 买家秒杀活动详情 | GET | `/api/seckill-activities/{activityId}/public` | `Seckill_GetActiveActivityDetail` | 无 | `SeckillActivityDetailResponse` | 允许游客 | DB042、DB043 | 已定义 | | A228 | Seckill | C01 | 秒杀下单 | POST | `/api/seckill-orders` | `Seckill_PlaceOrder` | `PlaceSeckillOrderRequest` | `PlaceSeckillOrderResponse` | BuyerOnly | DB043、DB044、DB045 | 已定义 | -| A229 | Seckill | C01 | 买家秒杀订单列表 | GET | `/api/seckill-orders` | `Seckill_ListMyOrders` | 无(Query 分页/筛选) | `SeckillOrderListResponse` | BuyerOnly | DB044、DB045 | 已定义 | -| A230 | Seckill | C01 | 买家秒杀订单详情 | GET | `/api/seckill-orders/{orderId}` | `Seckill_GetMyOrder` | 无 | `SeckillOrderDetailResponse` | BuyerOnly | DB044、DB045 | 已定义 | +| A229 | Seckill | C01 | 买家秒杀订单列表(取消) | — | — | — | — | — | — | — | 已取消,复用 A302 | +| A230 | Seckill | C01 | 买家秒杀订单详情(取消) | — | — | — | — | — | — | — | 已取消,复用 A303 | 接口路径补充说明: - Cart 业务接口位于 `/api/cart` 前缀之下,遵守《接口设计》1.2 节小写复数 + 动宾资源原则。 -- Seckill 业务接口位于 `/api/seckill-activities`(活动)和 `/api/seckill-orders`(订单)两个根路径;商家维护入口额外加 `/api/merchant` 前缀,与公开购物端入口物理隔离。 +- Seckill 业务接口使用 `/api/seckill-activities` 活动根路径;商家维护入口额外加 `/api/merchant` 前缀,与公开购物端入口物理隔离。秒杀订单查询复用 Ordering 的 A302/A303(`/api/orders`、`/api/orders/{orderId}`)。 - `/api/cart/items` 表达购物车条目集合;`/api/cart/items/selection` 为选中状态专用子资源,避免在 GET 之上覆盖副作用。 -- `/api/seckill-orders` 与 M04 普通订单共用 `orders` / `order_items` 表与状态机,仅在订单上记录 `seckill_activity_id` 快照;不再建立平行订单接口。 +- 秒杀下单产生的订单与 M04 普通订单共用 `orders` / `order_items` 表与状态机,仅在订单上记录 `seckill_activity_id` 快照;订单查询统一复用 A302/A303,不建立平行订单接口。 - A225 商家详情与 A227 买家详情返回字段范围不同:A225 含商家内部字段(取消原因、回补策略),A227 仅返回公开可见字段。 ## 二、接口详细定义 @@ -1077,149 +1078,8 @@ PlaceSeckillOrderResponse { - 商品已下架 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 - 地址不属于当前买家 → 409 / `RESOURCE.CONFLICT`,不泄露地址存在性。 -### A229 买家秒杀订单列表 +### A229/A230 已取消:秒杀订单查询复用 A302/A303 -- 模块 / Tag:Seckill -- 需求编号:C01 -- 负责人:朱惠惠 -- 关联数据表:DB044、DB045 -- 当前状态:已定义 -- 用途:买家分页查询本人秒杀订单,支持按状态、活动和时间筛选。 -- 方法与路径:`GET /api/seckill-orders` -- operationId:`Seckill_ListMyOrders` - -#### 请求 - -- Header:`Authorization: Bearer `(必填,角色 Buyer) -- Query 参数: - - `page`(默认 1) - - `pageSize`(默认 10,上限 50) - - `status`(可选,`PendingPayment` / `Paid` / `Shipped` / `Completed` / `Cancelled`) - - `activityId`(可选,按活动过滤) - - `createdFrom`、`createdTo`(可选,时间范围) - -#### 成功响应 - -- HTTP 状态:`200 OK` -- 响应 Schema:`SeckillOrderListResponse` - -```text -SeckillOrderListResponse { - items: SeckillOrderSummaryResponse[] - page: integer - pageSize: integer - total: integer - totalPages: integer -} - -SeckillOrderSummaryResponse { - orderId: uuid - activityId: uuid - activityName: string - productId: uuid - productName: string - productImageUrl: string - quantity: integer - seckillPrice: number - totalAmount: number - status: "PendingPayment" | "Paid" | "Shipped" | "Completed" | "Cancelled" - createdAt: string - expiresAt: string // PendingPayment 时返回 -} -``` - -#### 失败响应 - -| HTTP 状态 | 业务错误码 | 触发条件 | -|---:|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 分页或筛选参数非法 | -| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | - -#### 业务规则与并发 - -- 严格按 `buyer_id = current_user_id AND seckill_activity_id IS NOT NULL` 过滤。 -- 排序默认按 `createdAt desc`;相同 `createdAt` 时按 `orderId` 稳定排序。 -- 不返回完整地址或支付敏感信息;详细快照在 A230。 - -#### 缓存、事件或外部依赖 - -- 不缓存;订单状态实时读取 DB044。 - -#### 验证场景 - -- 买家查询本人秒杀订单 → 200,仅返回与当前买家关联的记录。 -- 按活动过滤 → 200,仅返回该活动的订单。 -- 跨用户查询 → 403,不泄露他人订单。 - -### A230 买家秒杀订单详情 - -- 模块 / Tag:Seckill -- 需求编号:C01 -- 负责人:朱惠惠 -- 关联数据表:DB044、DB045 -- 当前状态:已定义 -- 用途:买家查看本人秒杀订单完整详情;包含活动快照、订单项快照、地址快照与状态时间线。 -- 方法与路径:`GET /api/seckill-orders/{orderId}` -- operationId:`Seckill_GetMyOrder` - -#### 请求 - -- Route 参数:`orderId: uuid` -- Header:`Authorization: Bearer `(必填,角色 Buyer) -- Body:无 - -#### 成功响应 - -- HTTP 状态:`200 OK` -- 响应 Schema:`SeckillOrderDetailResponse` - -```text -SeckillOrderDetailResponse { - orderId: uuid - activityId: uuid - activityName: string - productId: uuid - productName: string - productImageUrl: string - quantity: integer - seckillPrice: number - originalPrice: number // 商品原价快照 - totalAmount: number - status: "PendingPayment" | "Paid" | "Shipped" | "Completed" | "Cancelled" - addressSnapshot: AddressSnapshotResponse - timeline: OrderTimelineEntryResponse[] - paymentInfo: PaymentInfoResponse? - createdAt: string - expiresAt: string - paidAt: string? - cancelledAt: string? -} -``` - -#### 失败响应 - -| HTTP 状态 | 业务错误码 | 触发条件 | -|---:|---|---| -| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | -| 404 | `RESOURCE.NOT_FOUND` | 订单不存在、不属于当前用户或非秒杀订单 | - -#### 业务规则与并发 - -- 严格按 `order_id = :id AND buyer_id = current_user_id AND seckill_activity_id IS NOT NULL` 过滤;不满足任一条件返回 404,避免泄露订单存在性。 -- 地址快照来自下单时刻保存的 `orders.address_snapshot`,与 M04 共享字段。 -- 时间线包含创建、支付、发货、完成、取消等关键节点;时间均以 UTC 存储,前端按本地时区展示。 -- 支付信息(`paymentInfo`)仅在订单已支付后返回;支付卡号、Token 等敏感字段不出现。 - -#### 缓存、事件或外部依赖 - -- 不缓存;订单详情实时读取 DB044、DB045 与 M04 `orders`、`order_items`、`payments`。 - -#### 验证场景 - -- 买家查询本人秒杀订单 → 200,含活动快照、订单项快照、地址快照、时间线。 -- 跨用户访问 → 404,不泄露归属。 -- 已支付订单 → `paymentInfo` 返回;未支付订单不返回。 -- 已取消订单 → `cancelledAt` 与取消节点返回。 -- 普通订单(非秒杀)通过此接口访问 → 404,避免与 M04 详情接口混淆。 +- A229、A230 不再作为独立 HTTP 接口实施,保留编号仅用于历史审计,不再分配独立路径、`operationId`、请求 Schema 或响应 Schema。 +- 秒杀订单列表查询复用 A302(`GET /api/orders`),秒杀筛选通过 Ordering 查询契约表达;秒杀订单详情查询复用 A303(`GET /api/orders/{orderId}`)。 +- 订单是否为秒杀订单由共享订单事实中的 `seckill_activity_id` 等字段表达,不建立 `/api/seckill-orders` 平行查询接口。 -- Gitee From 94e252855a7cc9d4883399a23f456d18449c0567 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 13:27:11 +0800 Subject: [PATCH 053/118] =?UTF-8?q?docs(api):=20=E7=BB=BC=E5=90=88?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=A5=91=E7=BA=A6=EF=BC=9B=E5=AF=B9=E9=BD=90?= =?UTF-8?q?=E9=9C=80=E6=B1=82=E6=9E=B6=E6=9E=84=E5=B9=B6=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=B7=A8=E6=A8=A1=E5=9D=97=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 141 +- .../interface/interface-gxy.md" | 74 +- .../interface/interface-lhc.md" | 89 +- .../interface/interface-tyh.md" | 158 +- .../interface/interface-wqq.md" | 237 +- .../interface/interface-zhh.md" | 166 +- .../interface/interface-zhy.md" | 459 ++-- ...45\345\217\243\350\256\276\350\256\241.md" | 2150 ++++++++++------- ...66\346\236\204\350\256\276\350\256\241.md" | 18 +- .../document-routing.reference.md | 21 +- 10 files changed, 2007 insertions(+), 1506 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 7709cc9..7efe1b8 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -9,7 +9,7 @@ | 版本 | 日期 | 修改人 | 修改说明 | |---|---|---|---| -| v0.1 | 2026-07-22 | 全体成员(罗皓晨统稿) | 形成需求规格初稿,明确四类角色、必做功能、4 项选做、7 项挑战、验收口径和六人模块边界 | +| v0.1 | 2026-07-24 | 全体成员(罗皓晨统稿) | 形成并统一需求规格,明确四类角色、必做功能、4 项选做、7 项挑战、单店 B2C 边界、核心状态与六人模块职责 | ## 一、引言 @@ -609,7 +609,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, |---|---| | 商品模块 | 提供已上架商品的实时价格和可售库存;下架或库存变更后通知购物车重新标记失效条目 | | 订单模块 | 提交订单成功后通知购物车清理对应条目;订单回滚或取消时按本期规则保留购物车条目 | -| 秒杀模块(C01) | 在秒杀下单前使用购物车字段做实时库存校验和并发约束,避免超卖影响其余条目 | +| 秒杀模块(C01) | 秒杀立即抢购绕过购物车,由 Seckill 与 Ordering 独立完成资格、限购、库存和订单校验,不读写购物车条目 | | Cart 模块 | 校验买家身份、维护条目唯一性、执行数量上下限、服务端计价、清理失效条目、保证下单前后一致性 | 购物车数据归属只以买家 ID 为准,角色只决定接口是否可见和可写。商家和管理员不接收任何与购物车结构相关的私人通知,业务模块不允许直接读写其他模块内部的购物车实例。 @@ -656,7 +656,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, - 数据归属:所有读写按 `(买家ID, 条目ID)` 或 `(买家ID, 商品ID)` 双重过滤;条目 ID 不允许跨用户访问,操作他人购物车返回资源不存在或无权限,不泄露条目是否存在。 - 服务端为唯一事实来源:数据库为准,前端缓存只用于展示;刷新、重新登录和断线重连后均按服务端数据重新渲染。 - 时间口径:服务端统一使用 UTC 写入 `createdAt` / `updatedAt`,前端按用户时区展示。 -- 库存协作:购物车不预先占用库存;库存仅在订单模块提交订单事务中扣减;C01 秒杀路径使用购物车实时校验 + 行锁 / 条件更新,避免把购物车作为库存持有者。 +- 库存协作:购物车不预先占用库存;普通订单在提交事务中扣减库存。C01 秒杀路径绕过购物车,由 Seckill 与 Ordering 使用独立的限购、条件扣减和订单创建契约。 - 日志规范:加入失败、修改超限、删除越权、下单清理和并发回滚都记录 traceId、买家 ID、商品 ID 和原因码;不记录 Token、密码或支付敏感信息。 #### 6. 异常与边界场景 @@ -669,7 +669,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, - 下单竞态:选中条目在结算瞬间被他人抢光或下架,对应条目标记失效后返回“已更新,请重新选择”,其余条目不连带失败。 - 下单事务回滚:订单写入或库存扣减回滚,已选中的购物车条目保留,不会被错误清理。 - 订单取消或超时:买家主动取消或 C03 自动取消后,购物车本期不恢复对应条目;用户希望重新购买时需手动再次加车。 -- C01 秒杀边界:进入秒杀下单必须经过购物车实时校验;超过库存上限或商品被秒杀标记占用时返回明确原因,不影响其余非秒杀条目。 +- C01 秒杀边界:立即抢购不加入或读取购物车;超出活动库存或个人限购时由 Seckill 明确拒绝,普通购物车条目不受影响。 - 接口异常与重试:网络失败、500 和 401 等场景给出明确错误:401 引导重新登录,400 显示字段问题,5xx 提供重试入口,避免页面整页刷新或丢失购物车状态。 - 失效条目清理:用户手动删除/清空购物车时同步清理;后台不主动物理删除失效条目,便于排查;后续如需归档清理应单独定义保留期。 - 价格变动:商品改价、上下架或参与促销后,购物车再次展示时使用实时单价,金额随单价变化即时刷新,未支付不锁定价格。 @@ -687,12 +687,12 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, - 买家主动取消订单或 C03 自动取消后,对应购物车条目本期不自动恢复,符合本期范围说明。 - 并发:同一条目同时被改数量和删除,最终只出现删除结果或最新数量,两者不会同时生效导致数据错乱。 - 幂等:相同请求幂等标识在约定窗口内重复提交不重复累加数量,返回结果一致。 -- C01 衔接:进入秒杀路径前,购物车校验实时库存生效,超卖被拒绝且不影响其余非秒杀条目。 +- C01 衔接:秒杀路径独立执行活动库存和个人限购校验,超卖被拒绝且不影响普通购物车条目。 - 库存与下单协作:购物车不预留库存,订单提交事务内完成扣减;C03 超时取消正确回补库存,购物车无需联动处理。 - 性能与可用性:购物车页在 30~100 条目内常规环境下加载时间低于 2 秒;大量条目场景启用分页,禁止无上限返回。 - 日志:加车失败、并发回滚、下单清理和订单回滚均留下 traceId、买家 ID、商品 ID 与原因码;日志不包含完整 Token、密码或卡号。 - 界面反馈:列表的加载、空数据、错误、删除成功、修改成功、失效原因和“去结算”被禁用都给出明确提示;操作失败时可一键重试,不要求刷新整页或重新登录。 -- 与 C03、C01 配合稳定:超时取消、秒杀库存校验、并发提交路径下,购物车数据始终与数据库一致,前端无虚假状态。 +- 与 C03、C01 配合稳定:普通订单超时取消不恢复购物车;秒杀绕过购物车,二者均不会制造虚假购物车状态。 ### M04-01 提交订单(F08)— 韦乾强 @@ -712,7 +712,7 @@ M04-01 要求买家选择购物车中的商品和收货地址完成订单提交 | 商家 | 不得调用买家下单接口或查看买家购物车 | | 管理员 | 不得代替买家下单或查看私人购物车 | -购物车提供待结算条目,Identity 提供地址归属信息,商品模块提供实时价格与库存,订单模块通过公开边界完成协调。 +购物车提供待结算条目,Identity 提供地址归属信息并解析唯一且启用的默认商家运营账号,商品模块提供实时价格与库存,订单模块通过公开边界完成协调。本期为单店 B2C,不拆多商户子订单。 #### 3. 功能需求 @@ -721,12 +721,12 @@ M04-01 要求买家选择购物车中的商品和收货地址完成订单提交 | M04-01-FR01 | 购物车商品预校验 | 服务端接收买家提交的选择商品 ID 列表后,先校验:(1) 所有商品属于当前买家购物车;(2) 所有商品当前可售(已上架且有库存);(3) 每个商品选择的数量不超过实时库存且大于 0。预校验失败时整单失败,不进入库存扣减。 | | M04-01-FR02 | 地址归属校验 | 买家提交的收货地址 ID 必须属于当前买家(`address.user_id = current_user_id`);不属于或地址不存在时整单失败,返回明确错误提示。 | | M04-01-FR03 | 库存原子扣减 | 在同一数据库事务内使用带库存充足条件的原子更新扣减每个商品库存;任一商品扣减失败时事务整体回滚,不产生部分订单。库存扣减必须具备订单维度幂等保障,防止重复扣减。 | -| M04-01-FR04 | 订单创建 | 事务扣减库存成功后,创建订单主记录(`orders`):保存订单号(全局唯一 UUID)、买家 ID、地址快照(收件人、手机号、省市区、详细地址)、订单状态(`PendingPayment`)、订单总额(由服务端根据订单项实付金额汇总计算,不接受前端传入)、创建时间、幂等键。 | +| M04-01-FR04 | 订单创建 | 事务扣减库存成功后创建唯一订单事实,保存订单号、买家 ID、地址快照、`assignedMerchantUserId`、`PendingPayment` 状态、服务端计算总额、支付截止时间、创建时间和幂等键;客户端不得指定处理商家或订单金额。 | | M04-01-FR05 | 订单项快照 | 为每个订单项创建订单项记录(`order_items`):保存商品 ID、商品名称(快照)、商品主图 URL(快照)、成交单价(快照,下单时服务端的实时价格)、购买数量。订单项单价以创建订单时的服务端实时价格为准,不受后续商品改价影响。 | | M04-01-FR06 | 购物车清理 | 在订单事务内清理本次已下单的购物车条目;清理失败时整笔订单事务回滚,不产生订单、不扣减库存,也不丢失购物车条目。 | | M04-01-FR07 | 幂等键设计 | 买家客户端生成唯一幂等键(UUID),随下单请求一同发送;服务端以 `(user_id, idempotency_key)` 为唯一约束,同一买家同一幂等键只创建一张订单;重复提交返回首次成功创建的订单号,不重复扣库存、不重复创建订单项。 | | M04-01-FR08 | 价格服务端计算 | 订单总额和订单项实付单价均由服务端计算,不接受前端传入;商品最新价格从 `products.price` 实时读取;订单项保存快照后,商品后续改价不影响已有订单。 | -| M04-01-FR09 | 事件通知 | 订单创建成功后,通过 Outbox 机制发布 `OrderCreated` 事件,包含订单号、买家 ID、订单总额、订单项摘要,供给下游 M09 站内消息和 C03 超时取消消费。事件发布不得阻塞订单创建返回。 | +| M04-01-FR09 | 事件通知 | 订单创建成功后,通过 Outbox 发布 `OrderCreatedIntegrationEvent`,只将当前买家列为接收账号,供 M09 站内消息消费;商家待处理提醒统一由支付成功事件触发,避免重复通知。C03 直接扫描 PostgreSQL 的支付截止时间,不消费该事件保存唯一调度事实。 | #### 4. 主流程 @@ -736,10 +736,10 @@ M04-01 要求买家选择购物车中的商品和收货地址完成订单提交 4. 校验收货地址是否属于当前买家且状态正常。 5. 开启数据库事务: a. 按商品维度依次执行条件更新扣减库存(`WHERE stock >= quantity`),任一失败则整体回滚。 - b. 创建订单主记录,状态为 `PendingPayment`。 + b. 解析并保存处理商家账号,创建状态为 `PendingPayment` 的订单主记录。 c. 创建每个订单项记录,保存商品信息快照和成交单价。 d. 删除已下单的购物车条目。 - e. 写入 Outbox `OrderCreated` 事件。 + e. 写入 Outbox `OrderCreatedIntegrationEvent` 事件。 提交事务。 6. 事务提交成功后,返回订单号给客户端。 7. 客户端跳转到支付页面或收银台,等待买家操作。 @@ -755,6 +755,7 @@ M04-01 要求买家选择购物车中的商品和收货地址完成订单提交 - 地址快照保存下单时刻的完整地址文本,后续地址修改不影响已有订单。 - 商品名称/图片快照保存下单时刻的完整信息,后续商品信息修改不影响已有订单。 - 订单创建后不得通过接口修改订单金额,金额以数据库记录为准。 +- Identity 未配置唯一且启用的默认商家运营账号时拒绝创建订单,不产生无人处理的订单事实。 #### 6. 异常与边界场景 @@ -769,7 +770,9 @@ M04-01 要求买家选择购物车中的商品和收货地址完成订单提交 | 同一幂等键重复提交 | 返回首次成功的订单号,不重复扣库存 | 无感知,得到订单号 | | 并发扣减库存竞争 | 条件更新失败的事务回滚,返回库存不足错误 | 重试或减少数量 | | 数据库连接失败 | 返回服务异常,附 `traceId`,不暴露内部细节 | 稍后重试 | -| Outbox 写入失败 | 订单仍创建成功,Outbox 写入失败进入重试队列,不阻塞买家 | 无感知,消息最终补发 | +| 默认商家未配置、重复或不可用 | 整单失败并提示服务暂不可用,不扣库存、不清理购物车 | 稍后重试并由管理员修复配置 | +| Outbox 记录写入失败 | 与订单、库存和购物车变更一起回滚,当前请求失败且可安全重试 | 提示稍后重试,购物车和库存保持原状 | +| Outbox 已写入但 RabbitMQ 发布失败 | 订单保持成功,由 Worker 重试发布,不回滚业务事务 | 正常进入支付,消息稍后补发 | #### 7. 验收标准与证据 @@ -779,7 +782,7 @@ M04-01 要求买家选择购物车中的商品和收货地址完成订单提交 |---|---|---| | 1 | 进入购物车,选择一件商品,提交订单并选择有效地址 | 订单创建成功,返回订单号,购物车中该商品消失。 | | 2 | 进入订单详情,查看订单号、地址快照、订单项快照(商品名称、图片、单价、数量) | 快照内容与下单时刻一致;订单总额 = Σ(单价×数量)。 | -| 3 | 选择多件商品(不同店铺或不同分类)提交订单 | 整单成功,订单包含多个订单项,库存均正确扣减。 | +| 3 | 选择多个不同分类的商品提交订单 | 整单成功,订单包含多个订单项,库存均正确扣减。 | | 4 | 故意选择库存为 0 的商品提交订单 | 整单失败,返回"库存不足";购物车商品未被删除,库存未扣减。 | | 5 | 故意选择不属于自己的收货地址提交订单 | 整单失败,返回"收货地址无效";不暴露地址是否存在。 | | 6 | 使用同一幂等键重复提交相同订单 | 第二次返回首次成功的订单号,不重复扣库存,订单项不重复。 | @@ -804,7 +807,7 @@ M04-01 要求买家选择购物车中的商品和收货地址完成订单提交 - 能解释为何选择条件更新而非先查后改扣减库存,以及并发下的正确性保障。 - 能解释订单项快照的设计原因:为何快照能保护买家利益、为何商家改价不影响已有订单。 - 能解释幂等键如何防止重复提交, `(user_id, idempotency_key)` 唯一约束如何生效。 -- 能解释 Outbox 事件在订单创建成功后的投递关系,以及 M09/C03 如何消费 `OrderCreated` 事件。 +- 能解释 Outbox 事件在订单创建成功后的投递关系,以及 M09 如何消费 `OrderCreatedIntegrationEvent`;C03 为什么改为扫描 PostgreSQL 支付截止时间而不消费该事件保存唯一调度事实。 - 能解释购物车清理与订单创建为何必须处于同一事务,以及清理失败时如何回滚并保持购物车原状。 ### M04-02 订单列表与详情(F09)— 韦乾强 @@ -928,7 +931,7 @@ M04-02 要求买家分页查看自己的订单列表,并按订单状态筛选 #### 1. 功能目标与范围 -M04-03 要求买家取消自己创建的待支付订单,系统在单个数据库事务内完成订单状态变更(`PendingPayment` → `Cancelled`)和库存回补(将下单时扣减的库存返还给商品)。核心目标是让买家取消订单"**操作即生效、库存不回吐、重复取消无副作用**":买家点取消后不会因网络抖动或重复点击看到"取消成功了但库存没回来"的异常状态,也不会因多次点击取消按钮导致库存被多次回补。 +M04-03 要求买家取消自己创建的待支付订单,系统在单个受控事务内完成订单状态变更(`PendingPayment` → `Cancelled`)和原库存通道回补。普通订单回补 Catalog,秒杀订单回补原 Seckill 活动库存并释放买家限购额度。核心目标是让买家取消订单“操作即生效、库存不丢失、重复取消无副作用”。 本期不实现买家取消已支付订单、取消已发货订单、商家主动取消订单等横向场景。 @@ -948,11 +951,11 @@ M04-03 要求买家取消自己创建的待支付订单,系统在单个数据 | 编号 | 功能 | 详细要求 | |---|---|---| | M04-03-FR01 | 取消条件校验 | 服务端接收买家提交的取消请求后,先校验订单存在、订单属于当前买家且状态为待支付;任一条件不满足时拒绝取消。 | -| M04-03-FR02 | 库存回补事务 | 取消订单获批后,在同一数据库事务内更新订单为已取消、按订单项数量回补对应库存并记录取消时间;事务提交成功即取消成功。 | +| M04-03-FR02 | 库存回补事务 | 取消订单获批后,在同一受控事务内更新订单为已取消,并按订单项的 `orderType` 与 `seckillActivityId` 幂等回补 Catalog 普通库存或 Seckill 原活动库存;秒杀订单同时释放买家限购额度。 | | M04-03-FR03 | 幂等取消 | 以订单号为唯一标识,同一订单多次发起取消请求,第二次及之后返回首次取消的结果,不重复回补库存。已取消订单的重复取消操作返回幂等成功响应。 | | M04-03-FR04 | 取消时间记录 | 订单取消成功后,记录 `cancelled_at`(取消时间)到 `orders` 表;取消时间使用 UTC 存储,前端按用户时区展示。 | -| M04-03-FR05 | 事件通知 | 订单取消成功后,通过 Outbox 机制发布 `OrderCancelled` 事件,包含订单号、买家 ID、取消时间、商品及数量摘要,供给下游 M09 站内消息消费。事件发布不得阻塞取消操作返回。 | -| M04-03-FR06 | 库存回补校验 | 库存回补时必须校验当前库存值加上回补量不超过商品历史最大库存(若存在库存上限约束);若超过上限(理论上不应发生),记录警告日志但不阻止取消,仍将订单状态变更为已取消。 | +| M04-03-FR05 | 事件通知 | 订单取消事务写入 `OrderCancelledIntegrationEvent` Outbox,只将当前买家列为接收账号,供 M09 消费;消息发布重试不阻塞已提交的取消结果。 | +| M04-03-FR06 | 回补一致性 | 库存通道根据订单项快照确定,回补使用订单项和取消动作作为幂等边界;状态、库存、限购额度和 Outbox 任一写入失败时整笔事务回滚,不留下“已取消但库存未恢复”的部分结果。 | #### 4. 主流程 @@ -967,9 +970,9 @@ M04-03 要求买家取消自己创建的待支付订单,系统在单个数据 5. 开启数据库事务: a. 将订单状态更新为 `Cancelled`(带状态条件更新 `WHERE status = 'PendingPayment'`)。 b. 根据 `order_items` 查询该订单涉及的商品和数量。 - c. 按订单项依次回补每个商品的库存。 + c. 按订单项原通道回补 Catalog 普通库存,或回补 Seckill 活动库存并释放限购额度。 d. 记录 `cancelled_at = NOW()`。 - e. 写入 Outbox `OrderCancelled` 事件。 + e. 写入 Outbox `OrderCancelledIntegrationEvent` 事件。 提交事务。 6. 事务提交成功后,返回取消成功响应。 7. 前端刷新订单列表和详情,显示订单状态已变为"已取消"。 @@ -978,7 +981,7 @@ M04-03 要求买家取消自己创建的待支付订单,系统在单个数据 - 取消订单的本质是**库存回补 + 状态变更**在同一事务内完成,两者不可分割。 - 只有 `PendingPayment`(待支付)状态的订单可以取消;已支付、已发货、已完成、已取消订单一律拒绝取消。 -- 库存回补量 = 下单时该商品的扣减量 = `order_items` 中记录的购买数量。 +- 库存回补量等于订单项购买数量;普通订单和秒杀订单必须回到各自原库存通道,不得把秒杀配额误加到普通库存。 - 取消操作使用**条件更新**(`WHERE status = 'PendingPayment'`)防止并发取消或并发支付导致的状态冲突。 - 幂等设计以订单号为准:同一订单号只能成功取消一次,重复取消返回幂等成功。 - 取消订单后,订单仍保留在数据库中作为历史记录,不得物理删除。 @@ -997,9 +1000,9 @@ M04-03 要求买家取消自己创建的待支付订单,系统在单个数据 | 订单状态不为 `PendingPayment`(已取消) | 返回幂等成功,不报错 | 无感知,已取消 | | 并发取消(两台设备同时取消同一订单) | 只有一个请求成功执行状态变更和库存回补,另一个返回幂等成功 | 只有一个成功,不重复回补库存 | | 并发支付与取消(取消操作进行中时支付回调到达) | 取消使用条件更新 `WHERE status = 'PendingPayment'`,支付使用条件更新 `WHERE status = 'PendingPayment'`,最终只有一个操作成功 | 状态一致,不会出现"又支付又取消" | -| 库存回补时商品已被删除 | 记录警告日志,跳过该商品回补,订单仍变更为已取消 | 取消成功,库存差异由后台处理 | +| 原库存通道或商品事实异常 | 整笔取消事务回滚并返回可重试错误,记录 `traceId`,不得留下部分回补 | 提示稍后重试,订单保持原状态 | | 数据库连接失败 | 返回服务异常,附 `traceId` | 稍后重试 | -| Outbox 写入失败 | 取消操作仍成功,Outbox 写入失败进入重试队列,不阻塞买家 | 无感知,消息最终补发 | +| Outbox 记录写入失败 | 与订单、库存回补一起回滚;已有 Outbox 记录的后续发布失败由 Worker 重试 | 当前取消返回失败,可安全重试 | #### 7. 验收标准与证据 @@ -1010,7 +1013,7 @@ M04-03 要求买家取消自己创建的待支付订单,系统在单个数据 | 1 | 进入待支付订单详情,点击"取消订单" | 弹出确认对话框,要求买家确认取消操作。 | | 2 | 确认取消后,订单状态变为"已取消" | 订单列表和详情均显示状态为"已取消",显示取消时间。 | | 3 | 检查订单状态数据 | 已更新为已取消,并记录取消时间。 | -| 4 | 检查库存数据 | 库存已正确回补,回补量 = 订单项数量。 | +| 4 | 检查库存数据 | 普通/秒杀库存均回到原通道,秒杀限购额度按订单项数量释放。 | | 5 | 再次进入该已取消订单,点击"取消订单" | 返回"订单已取消"或幂等成功,不重复回补库存。 | | 6 | 对已支付订单点击"取消订单" | 返回"订单已支付,无法取消",订单状态不变,库存不变。 | | 7 | 对已发货订单点击"取消订单" | 返回"订单已发货,无法取消"。 | @@ -1020,13 +1023,13 @@ M04-03 要求买家取消自己创建的待支付订单,系统在单个数据 | 11 | 用他人账号登录,尝试取消目标买家的订单 | 返回 403 或"无权操作"。 | | 12 | 用商家/管理员身份调用取消订单接口 | 返回 403。 | | 13 | 游客(未登录)调用取消订单接口 | 返回 401 并引导登录。 | -| 14 | 取消订单后,查看商品详情页的库存 | 库存已恢复为取消前的数量(等于下单后的数量 + 订单项数量)。 | +| 14 | 取消订单后检查库存 | 普通订单恢复 Catalog 库存;秒杀订单恢复原活动库存与限购额度,不混入普通库存。 | | 15 | 取消订单后,再次尝试对该订单进行支付 | 返回"订单已取消,无法支付"。 | **验收证据与答辩要求:** - 保存取消订单成功的完整录屏,包含确认对话框、取消后状态、取消时间展示。 -- 保存取消前后库存对比截图:商品库存在下单后减少,取消后恢复。 +- 保存普通与秒杀订单取消前后库存对比:两类库存各自原路恢复,秒杀限购额度只释放一次。 - 保存重复取消测试截图:第二次取消返回幂等成功,库存未继续回补。 - 保存各状态订单取消失败的异常场景录屏(已支付、已发货、已完成、已取消),包含错误提示和订单/库存未被影响。 - 保存并发取消测试截图:两台设备同时取消,库存只回补一次。 @@ -1036,7 +1039,7 @@ M04-03 要求买家取消自己创建的待支付订单,系统在单个数据 - 能解释条件更新 `WHERE status = 'PendingPayment'` 如何保证并发安全。 - 能解释幂等设计如何防止重复取消和重复回补库存。 - 能解释取消与支付并发竞争的处理策略(C03 订单超时取消与支付的竞争也适用同一原理)。 -- 能解释 `OrderCancelled` 事件如何被 M09 消费,以及下游如何处理取消通知。 +- 能解释 `OrderCancelledIntegrationEvent` 如何被 M09 消费,以及下游如何处理取消通知。 - 能解释为何已支付/已发货/已完成订单不能取消,以及买家需要走售后流程 X04。 ### M04-04 确认收货与订单完成(F09)— 韦乾强 @@ -1221,7 +1224,7 @@ M04 提供订单金额与状态,M09接收支付结果通知,C03处理支付 | M06-01-FR08 | 删除约束 | 无历史关联时可按设计删除;已有订单关联时禁止破坏性删除并建议下架 | | M06-01-FR09 | 图片管理 | 图片上传到 S3 Compatible Object Storage;单商品最多 8 张,单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 400~4096 像素,第一张作为主图 | | M06-01-FR10 | 并发保护 | 编辑依据并发标记或条件更新防止静默覆盖;冲突时保留已填写内容并提示刷新确认 | -| M06-01-FR11 | 变更传播 | 商品事务提交后发布领域事件;缓存失效与搜索索引更新由相应处理器执行,失败可重试或重建 | +| M06-01-FR11 | 变更传播 | 商品事务提交后触发缓存失效;`pg_trgm`/GIN 数据库索引随 PostgreSQL 商品数据同步维护,不通过异步处理器复制搜索索引 | | M06-01-FR12 | 操作反馈 | 保存、上下架和删除均显示明确结果;危险操作需要确认,失败时保留可恢复的表单数据 | #### 4. 主流程 @@ -1263,7 +1266,7 @@ M04 提供订单金额与状态,M09接收支付结果通知,C03处理支付 - 下架商品不会出现在购物端列表和搜索结果,旧链接不再允许购买;历史订单快照保持可读。 - 游客、买家和管理员无法进入或调用商家写操作,后端返回正确的认证或授权结果。 - 并发编辑、图片失败、保存失败和删除受限时均有明确反馈,已填写内容不会无故丢失。 -- 商品变更后,缓存与搜索更新过程可追踪、可重试;答辩能说明商品事务与异步更新的边界。 +- 商品变更后,PostgreSQL `pg_trgm`/GIN 索引随数据同步保持一致;缓存失效过程可追踪、可重试,答辩能说明两者边界。 ### M06-02 后台订单管理(F12)— 韦乾强 @@ -1277,19 +1280,19 @@ M04 提供订单金额与状态,M09接收支付结果通知,C03处理支付 |---|---| | 游客 | 无后台订单入口,访问时要求登录 | | 买家 | 通过 M04 查看本人订单,不得调用商家发货接口 | -| 商家 | 可查询授权范围内的订单并对已支付订单发货 | +| 商家 | 只能查询 `assignedMerchantUserId` 为当前账号的订单,并对其中已支付订单发货 | | 管理员 | 本期不代替商家发货,不通过本模块查看私人订单 | -M04 拥有订单状态与快照,M05 提供已支付事实,M09向买家发送发货通知;本模块通过公开应用接口推进发货状态。 +M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实,M09 向买家发送发货通知;本模块通过 Ordering 公开应用接口推进发货状态。本期不建设商户租户、商品归属、拆单或结算模型。 #### 3. 功能需求 | 编号 | 功能 | 详细要求 | |---|---|---| -| M06-02-FR01 | 订单列表 | 按创建时间倒序分页展示订单号、买家必要摘要、金额、状态和时间 | +| M06-02-FR01 | 订单列表 | 只按当前 `assignedMerchantUserId` 过滤,按创建时间倒序分页展示订单号、买家必要摘要、金额、状态和时间 | | M06-02-FR02 | 状态筛选 | 支持按待支付、已支付、已发货、已完成和已取消等受控状态筛选 | -| M06-02-FR03 | 订单详情 | 展示订单项、地址快照、金额和状态时间,敏感字段按最小必要原则提供 | -| M06-02-FR04 | 发货处理 | 仅允许把已支付订单推进为已发货,并记录发货时间和必要说明 | +| M06-02-FR03 | 订单详情 | 展示订单项、地址快照、金额、状态时间及售后影响后的剩余可履约数量,敏感字段按最小必要原则提供 | +| M06-02-FR04 | 发货处理 | 仅允许处理已支付且不存在履约阻断的订单;部分退款后只发剩余可履约数量,并记录发货时间和必要说明 | | M06-02-FR05 | 幂等发货 | 重复提交发货不得重复改变状态或生成多条相同通知 | | M06-02-FR06 | 结果通知 | 发货事务成功后通过可靠事件触发 M09 买家通知 | | M06-02-FR07 | 操作反馈 | 查询、筛选和发货过程提供加载、空状态、成功、失败和冲突提示 | @@ -1298,14 +1301,18 @@ M04 拥有订单状态与快照,M05 提供已支付事实,M09向买家发送 1. 商家进入后台订单列表并按状态查找已支付订单。 2. 商家查看订单详情,确认订单项和收货地址快照。 -3. 商家提交发货操作,服务端校验商家权限和订单当前状态。 -4. 条件更新成功后订单变为已发货并记录时间,同时写入可靠事件。 +3. 商家提交发货操作,服务端校验商家权限、订单当前状态以及售后履约快照。 +4. 无处理中售后且仍有可履约数量时,条件更新成功后订单变为已发货,只记录剩余可履约数量并写入可靠事件。 5. 页面刷新最新状态,买家可在订单详情和站内消息中看到发货结果。 #### 5. 业务规则与权限 - 只有已支付订单可以发货,其他状态不得跳转为已发货。 +- 商家只能处理明确分配给当前账号的整单;不得按 Merchant 角色全局查看或向所有商家广播订单。 - 发货状态更新使用条件更新或等效并发保护,重复操作保持幂等。 +- 待审核、待退货、待收货、退款中或退款失败等仍可能改变履约结果的售后申请阻断发货;已拒绝或已撤销申请不阻断。 +- 已退款数量不得再次发货;部分退款后只发剩余数量,全部数量已退款时不允许发货。该规则只影响履约动作,不新增订单核心状态。 +- 提交售后与商家发货必须在各自事务中先经 Ordering 公开应用契约锁定同一订单记录并复核最新状态,锁保持到业务写入提交,避免同一数量同时进入退款和发货。 - 商家不得修改订单金额、支付记录、地址快照或订单项快照。 - 后台响应只返回履约所需数据,不暴露无关买家隐私。 - 游客、买家和管理员不得调用商家发货接口。 @@ -1317,6 +1324,8 @@ M04 拥有订单状态与快照,M05 提供已支付事实,M09向买家发送 | 订单不存在或不在授权范围 | 返回资源不存在或无权限,不泄露订单内容 | | 订单仍待支付、已取消或已完成 | 拒绝发货并展示当前状态 | | 两次并发发货 | 仅一次状态更新成功,另一次返回已处理结果 | +| 发货与售后申请并发 | 先完成的一方决定后续校验口径,不出现同一数量既退款又发货 | +| 存在处理中售后或已全额退款 | 阻断发货并显示原因;部分退款完成后仅发剩余数量 | | 发货事务失败 | 状态保持原样,页面允许安全重试 | | 通知投递暂时失败 | 发货事实保持成功,由 Outbox/Worker 重试通知 | @@ -1325,6 +1334,7 @@ M04 拥有订单状态与快照,M05 提供已支付事实,M09向买家发送 - 商家可分页查询、按状态筛选并查看授权范围内的订单详情。 - 已支付订单能够发货并变为已发货,其他状态发货均被拒绝。 - 重复和并发发货不会重复推进状态或重复产生业务结果。 +- 售后申请、部分退款和全部退款场景下,发货入口、错误提示及实际发货数量与履约快照一致。 - 游客、买家和管理员不能越权发货,敏感信息遵循最小展示原则。 - 保存列表筛选、正常发货、非法状态、重复发货和通知衔接证据。 @@ -1351,7 +1361,7 @@ M04 拥有订单状态与快照,M05 提供已支付事实,M09向买家发送 |---|---|---| | M06-03-FR01 | 用户列表 | 分页展示买家和商家账号的用户名、掩码手机号、角色、状态和注册时间 | | M06-03-FR02 | 筛选查询 | 支持按用户名、手机号、角色和状态等已确认条件筛选 | -| M06-03-FR03 | 禁用账号 | 将正常买家或商家变为禁用状态,并立即使该账号禁用前签发且尚未过期的全部令牌失效 | +| M06-03-FR03 | 禁用账号 | 将可禁用的正常买家或商家变为禁用状态,并立即使旧令牌失效;默认商家及仍有待处理业务的商家必须拒绝禁用 | | M06-03-FR04 | 启用账号 | 将禁用账号恢复为正常状态;禁用前令牌不恢复,用户必须重新登录获取新令牌 | | M06-03-FR05 | 状态幂等 | 重复禁用或重复启用返回当前结果,不产生矛盾状态 | | M06-03-FR06 | 敏感信息保护 | 手机号默认掩码,只返回管理操作所需字段 | @@ -1372,6 +1382,8 @@ M04 拥有订单状态与快照,M05 提供已支付事实,M09向买家发送 - 本模块不提供角色修改字段,禁用和启用不能改变账号角色。 - 多实例下的账号禁用和登录凭证失效结果必须一致。 - 重复禁用或启用必须保持结果一致,不得产生相互矛盾的账号状态。 +- 单店模式由 Identity 保证至多一个启用的默认商家,并由部署/种子数据保证实际存在一个;本期不通过 A016 迁移默认商家,默认账号直接禁用返回冲突。 +- 非默认商家仍有关联待履约订单、售后期限内订单、未完成售后或未结束秒杀活动时不得禁用;不自动把业务归属改给其他账号。 - 手机号默认掩码,列表不得返回密码哈希、完整 Token 或私人业务数据。 - 只有管理员 Policy 可以调用列表和状态变更接口。 @@ -1384,6 +1396,7 @@ M04 拥有订单状态与快照,M05 提供已支付事实,M09向买家发送 | 尝试修改角色或管理员账号 | 接口不接受相关字段或明确拒绝 | | 重复禁用或启用 | 返回当前状态,不重复产生副作用 | | 并发状态变更 | 仅符合当前状态的一次更新成功,其余返回最新状态 | +| 禁用默认商家或仍有待处理业务的商家 | 返回 409 和明确原因,账号及业务归属不变 | | 登录凭证失效能力暂时不可用 | 禁用操作不得返回虚假成功;无法确认登录态的受保护请求提示服务暂不可用 | #### 7. 验收标准与证据 @@ -1393,6 +1406,7 @@ M04 拥有订单状态与快照,M05 提供已支付事实,M09向买家发送 - 失效能力不可用时,无法确认登录态的受保护请求不得放行;恢复后多个服务实例必须得到一致结果。 - 买家、商家和游客不能访问管理接口,管理员账号和角色字段不能通过本模块操作。 - 重复与并发状态变更保持幂等,安全追踪记录不包含敏感凭据。 +- 默认商家和存在待处理业务的商家禁用保护可复现,普通可禁用商家仍可完成禁用/启用验收。 - 保存列表、禁用、启用、旧令牌失效、越权和角色注入拦截证据。 ### M07 商品评价与晒图(X01)— 顾欣月 @@ -1423,7 +1437,7 @@ M04 拥有订单状态与快照,M05 提供已支付事实,M09向买家发送 | M07-FR03 | 图片上传 | 单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 200~4096 像素;逐张显示上传状态,失败图片可重试或移除 | | M07-FR04 | 提交校验 | 服务端重新校验身份、订单项归属、订单状态、评分、文字、图片和重复提交 | | M07-FR05 | 幂等与唯一性 | 同一订单项只能形成一条评价;重复点击或重复请求不得新增多条记录 | -| M07-FR06 | 公开列表 | 商品详情分页展示评分、文字、图片和评价时间,不返回手机号、邮箱等敏感信息 | +| M07-FR06 | 公开列表 | 商品详情分页展示评分、文字、图片、评价时间和评价提交时形成的脱敏买家展示名快照,不返回手机号、邮箱等敏感信息 | | M07-FR07 | 评分汇总 | 根据有效评价计算总数和平均分;新增评价后结果最终更新且可追踪 | | M07-FR08 | 提交反馈 | 提交期间防止重复点击;成功后明确标记“已评价”,失败时保留文字和已上传状态 | | M07-FR09 | 内容安全 | 文字按纯文本或受控内容展示,图片经过类型和大小校验,不执行脚本或泄露存储凭据 | @@ -1443,6 +1457,7 @@ M04 拥有订单状态与快照,M05 提供已支付事实,M09向买家发送 - 同一订单项只能评价一次,数据库唯一约束或等效机制必须作为最终保障。 - 图片为可选;每条评价最多 6 张,单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 200~4096 像素;前端提示与服务端校验必须一致。 - 评价公开展示时不得返回手机号、邮箱、内部用户标识等不必要的敏感信息。 +- 公开展示名在提交评价时形成脱敏快照;用户以后修改资料不改变历史评价展示,也不得为评价列表逐条查询用户资料。 - 游客、商家和管理员只能查看公开评价,不能通过伪造订单项提交或修改评价。 #### 6. 异常与边界场景 @@ -1645,7 +1660,7 @@ M04 提供订单项与实付快照,M05 执行幂等模拟退款,M09 通知 | 编号 | 功能 | 详细要求 | |---|---|---| | M10-FR01 | 可申请判断 | 已支付、已发货订单以及完成后 7 天内的订单可以申请;根据订单项归属、状态、时限和剩余可售后数量判断资格 | -| M10-FR02 | 提交申请 | 买家按订单项选择退款或退货、填写原因和数量;退款金额由系统按实付单价和数量计算,不接受任意金额 | +| M10-FR02 | 提交申请 | 买家按订单项选择退款或退货、填写原因和数量;未发货订单只允许仅退款,退款金额由系统按实付单价和数量计算,不接受任意金额 | | M10-FR03 | 申请列表 | 买家分页查看本人申请,商家分页查看待处理及历史申请 | | M10-FR04 | 申请详情 | 展示订单项快照、实付金额、申请内容、审核意见和状态时间线 | | M10-FR05 | 商家审核 | 商家同意或拒绝待审核申请,并填写审核意见 | @@ -1681,6 +1696,8 @@ M04 提供订单项与实付快照,M05 执行幂等模拟退款,M09 通知 - 退款流水纳入 C08 每日对账,至少核对售后退款成功、钱包入账和退款流水三方一致。 - 申请提交后通知商家;审核通过、审核拒绝、待退货、退款成功和退款失败均通知买家。 - 同一订单的不同订单项可以分别申请;同一订单项也可以按未售后数量分次申请,但处理中和已退款数量不得重复占用。 +- 未发货的已支付订单只允许仅退款;已发货或在售后期限内的已完成订单才提供退货退款选项。 +- 售后申请与商家发货在同一订单行上串行复核并提交;处理中申请阻断发货,已退款数量从可履约数量中扣除,部分退款后仅发剩余数量,全部退款后不得发货。 - 售后申请和退款处理不参与 C03 待支付订单超时扫描。 - 售后页面展示状态时间线;本期不做批量审核,不接入真实退货物流平台,只记录必要退货说明。 @@ -1740,7 +1757,7 @@ C01 要求在面向买家的高并发秒杀场景下保证库存“只减不超 | C01-FR03 | 秒杀下单入口 | 买家点击“立即抢购”直接进入秒杀提交订单:必须登录、必须命中进行中的活动、可购数量必须大于 0;提交前在客户端防抖、防连点和按钮置灰,但所有可购买性校验以服务端为准。 | | C01-FR04 | 库存条件扣减 | 服务端以数据库条件更新作为秒杀库存扣减的唯一正确性手段,条件至少同时约束活动、时间窗口、状态和剩余库存;未命中更新条件时拒绝请求,不得在应用层先读后写或仅依赖 Redis 扣减。 | | C01-FR05 | 事务一致性 | 秒杀库存扣减、订单创建和秒杀订单项快照写入必须在同一数据库事务中完成;任何一步失败整事务回滚:库存不被扣减、不产生订单、不产生订单项、不产生支付前置记录。 | -| C01-FR06 | 单用户限购 | 在同一秒杀活动中,同一买家成功提交的订单数量不得超过单用户限购;超出时拒绝并返回剩余可购买数量;通过 `(activity_id, buyer_id)` 维度聚合已成功秒杀订单数量校验,而不是只校验本请求。 | +| C01-FR06 | 单用户限购 | 在同一秒杀活动中,同一买家当前有效秒杀数量不得超过限购;以 PostgreSQL 中 `(activity_id, buyer_id)` 唯一配额事实原子累加,取消成功时按订单幂等释放,不能使用普通聚合查询或 Redis 作为并发正确性边界。 | | C01-FR07 | 时间窗口校验 | 提交秒杀订单时服务端再次校验当前 UTC 时间落在活动开始和结束之间;活动开始前和结束后均不进入扣减;状态字段也参与校验,避免服务器之间时钟轻微漂移造成提早或延后成功。 | | C01-FR08 | 接口幂等 | 秒杀下单接口接受稳定的请求幂等标识,同一买家、活动和标识在约定窗口内重复提交只生成一笔订单并返回同一结果;重复请求不得重复扣减库存或创建订单。 | | C01-FR09 | 失败回退与取消 | 秒杀订单与普通订单取消(买家主动或 C03 超时取消)后的库存回补必须回补到同一活动的秒杀可售库存,不污染普通商品库存;同一笔秒杀订单不得重复回补,无论取消接口被调用多少次都只能回补一次。 | @@ -1748,7 +1765,7 @@ C01 要求在面向买家的高并发秒杀场景下保证库存“只减不超 | C01-FR11 | 流量削峰与体验保护 | 在 Nginx、API、应用层或数据库连接池执行有上限的限流,超过上限的请求被快速拒绝并返回 `429/409`;普通商品入口和秒杀入口流量隔离,秒杀高并发不得拖垮普通商品查询。 | | C01-FR12 | 日志与追踪 | 所有秒杀提交请求记录买家 ID、活动 ID、商品 ID、请求数量、抢购买结果(成功/已售罄/超限/重复/未开始/已结束)、受影响行数、订单号(成功时)和 traceId;不记录 Token、密码或支付卡号。 | | C01-FR13 | 与订单、支付、消息衔接 | 秒杀成功订单沿用 M04 提交订单后状态机、支付(M05)、商家发货(M06-02)、消息(M09)和售后(M10)流程;不需要为秒杀订单开辟独立支付通道或独立通知渠道,但需要在订单上保留“秒杀活动 ID / 秒杀价快照”便于追溯。 | -| C01-FR14 | 活动运营动作 | 商家可对草稿和已发布活动执行“取消”动作:取消后库存可被回收到普通商品库存或保留为已冻结状态,本期以“保留为冻结、不回收”实现,避免取消后被普通订单夹带走量。 | +| C01-FR14 | 活动运营动作 | 商家可对草稿、已发布和进行中活动执行“取消”动作:取消后本期保留已分配库存、不回收到普通库存,避免取消后被普通订单夹带走量;已结束或已取消活动拒绝重复状态变更。 | | C01-FR15 | 演示数据与脚本 | 提供可重复执行的压测脚本、配套数据和环境变量,并支持一键回滚到秒杀前的库存基线和订单基线。 | #### 4. 主流程 @@ -1757,7 +1774,7 @@ C01 要求在面向买家的高并发秒杀场景下保证库存“只减不超 买家发起秒杀订单请求,服务端按以下顺序校验:登录身份、目标活动存在、活动处于进行中、单用户未超限购、客户端可购买数量大于 0、当前 UTC 时间落在窗口内。任一校验失败立即返回明确原因,不进入库存扣减。 -校验通过的请求进入数据库事务:使用条件更新一次性扣减秒杀库存并累加已售数量,影响行数 = 1 才继续;接着在同一事务内写入秒杀订单和订单项快照,并写入订单支付的占位状态。事务提交成功后将订单号、库存扣减结果和活动 ID 一并返回。 +校验通过的请求进入数据库事务:使用条件更新一次性扣减秒杀库存并累加已售数量,原子占用买家限购配额,影响行数符合预期才继续;接着通过 Ordering 公开应用契约写入共享 `orders` / `order_items` 订单事实,不建立第二套秒杀订单状态机。事务提交成功后将订单号、库存扣减结果和活动 ID 一并返回。 任一步骤失败时事务整体回滚:库存未被扣减、订单未被写入、不存在半扣减或不存在的订单项;受影响请求以一致的错误码返回,前端据此刷新抢购状态。 @@ -1770,10 +1787,10 @@ C01 要求在面向买家的高并发秒杀场景下保证库存“只减不超 - 锁与事务边界:秒杀事务只覆盖秒杀库存行、订单写入和必要快照,事务应保持短小,不在事务内调用外部 HTTP、不在事务内等待用户输入;锁的粒度按活动 ID 单行,不全表扫描。 - 队列不是唯一正确性保障:消息队列仅用于活动开始消息广播、库存剩余量缓存和压测流量削峰;不允许以“队列收到请求 = 抢购成功”为业务判定,最终仍以数据库条件更新的结果为准,避免因为队列丢失、重复或乱序导致超卖或少卖。 - 幂等与去重:买家请求携带稳定的幂等标识;同一买家、活动和标识在约定窗口内重复提交只生成一笔订单。即使缺少幂等标识,也不能跳过单用户限购与重复校验。 -- 单用户限购校验口径:以 `(activity_id, buyer_id)` 维度聚合“已成功提交订单数量”,包含待支付、已支付、已取消但属于本活动的全部订单;取消订单在限购计数中是否回滚按活动规则单独定义,本期默认取消后回退名额(数量减 1),避免“取消即永久占名额”造成少卖。 +- 单用户限购校验口径:以 `(activity_id, buyer_id)` 唯一配额行记录当前占用数量;待支付、已支付订单占用名额,取消成功后按订单最多释放一次,避免并发聚合误判和“取消即永久占名额”造成少卖。 - 库存语义:`remaining` 为剩余可售,`sold` 为已售;`remaining + sold + frozen` 在活动期间恒等于初始总量;`frozen` 表示被锁定但未最终成功的请求,本期不启用 `frozen`,所有提交要么直接成功,要么立即失败。 - 时间口径:所有时间以 UTC 存储;活动状态推进依赖数据库 `now()` 或带时区的 UTC 时间,避免服务器本地时间和数据库时间漂移带来的漏洞。 -- 限流与拒绝优先级:限流 429 < 时间窗口未到/已结束 409 < 已售罄 409 < 超过单用户限购 409 < 幂等命中 200 < 业务异常 5xx;前端按状态码和错误码展示对应文案,不把 5xx 误判为“已售罄”。 +- 幂等与拒绝顺序:完成认证和固定字段校验后,先按 PostgreSQL 幂等记录处理相同 Key;同指纹直接重放首次结果,不再受当前限流、时间、库存或限购变化影响,不同指纹返回 409。只有全新 Key 才依次进入限流、时间窗口、售罄、单用户限购和其他业务校验;前端不把 5xx 误判为“已售罄”。 - 数据隔离:秒杀活动接口、订单接口和库存接口在读写上都必须按活动 ID、买家 ID 双重过滤;活动维度数据不暴露他人抢购明细,只返回当前请求可见信息。 - 日志脱敏:秒杀日志记录买家 ID、活动 ID、行为结果和 traceId;不输出完整 Token、密码或支付卡号;截图和答辩材料中订单金额、库存数据按需脱敏。 - 与 C03、C08、C10、C07 的衔接:超时取消使用秒杀库存回补通道;支付回调幂等(C08)也作用于秒杀订单;多 API 实例(C10)下秒杀入口由任一实例受理,最终一致性仍以数据库为准;缓存(C07)可用于活动列表和商品详情读取,但秒杀库存不参与缓存。 @@ -1787,7 +1804,7 @@ C01 要求在面向买家的高并发秒杀场景下保证库存“只减不超 - 单用户超限:同一买家试图抢多件超过单用户限购时,第 N+1 次请求以“超过单用户限购”拒绝,剩余可购数量随剩余库存动态变化;不允许通过更换账号或绕过前台校验绕过此限制。 - 网络与重试:HTTP 5xx、重定向超时和连接中断都可能让客户端重试,幂等键 + 条件更新保证重试不产生副作用;返回 5xx 时前端友好提示“稍后重试”,且重试仍携带原幂等键。 - 服务器时钟漂移:活动开始/结束时间以数据库 UTC 时间为权威;应用节点间时钟漂移不影响业务结果;活动开始前 1 秒到达的请求仍可能被延迟到 0.x 秒后处理,文档说明允许秒级误差。 -- 取消与回补竞争:买家在订单已支付、超时取消或主动取消时均触发秒杀库存回补;同笔订单的重复取消请求只回补一次;回补操作同样使用条件更新或唯一约束保证幂等。 +- 取消与回补竞争:买家主动取消或 C03 超时取消时回补秒杀库存并释放限购名额;已支付订单不走取消回补,后续退款/退货按 M10 处理。同笔订单的重复取消请求只回补一次。 - 活动提前取消:商家在“已发布”或“进行中”状态下取消活动,已存在订单继续按既有流程走完;未提交的请求直接拒绝;本期不回收已分配库存,避免与普通购买混淆。 - 缓存不一致:秒杀库存不进入缓存;活动列表、商品基础信息走缓存时必须按定义好的失效策略更新;活动开始/结束/售罄状态变化必须以数据库为准重新加载活动详情。 - 越权访问:买家只能查看自己的秒杀订单和抢购资格,不允许通过订单 ID、买家 ID 或活动 ID 直接访问他人的购买明细;接口不存在或无权限返回统一错误,不暴露是否命中目标记录。 @@ -2412,33 +2429,33 @@ Electron 后续原则上复用 PC Web 原型,只补充窗口、桌面导航和 ### 9.1 需求追踪矩阵 -> 本表用于把教师验收编号落实到负责人、页面、接口契约和测试用例。接口列引用《接口设计》中的 Axxx;当前 102 个已登记接口均仍处于汇总或交叉评审阶段,缺少项直接标明,不得据此宣称已经实现、冻结或验证。 +> 本表用于把教师验收编号落实到负责人、页面、接口契约和测试用例。接口列引用《接口设计》中的 Axxx;当前保留 107 个追踪编号,其中 103 个为有效 HTTP 契约,均仍处于汇总或交叉评审阶段,不得据此宣称已经实现、冻结或验证。 | 教师编号 | 模块与负责人 | 页面或操作入口 | 接口契约(Axxx) | 测试用例 | 当前状态 | |---|---|---|---|---|---| | F01 | M01-01 用户注册—唐宇昊 | 注册页 | A001 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | -| F02 | M01-02 登录与退出—唐宇昊 | 统一登录页、全端退出入口 | A002~A005 | 待测试计划登记 | 接口草案已汇总,令牌规则待统一 | -| F03 | M01-03 个人信息与地址—唐宇昊 | 买家个人中心、地址管理 | A006~A014 | 待测试计划登记 | 接口草案已汇总,身份范围待修正 | +| F02 | M01-02 登录与退出—唐宇昊 | 统一登录页、全端退出入口 | A002~A005 | 待测试计划登记 | 接口草案已汇总,待 OpenAPI 与交叉评审 | +| F03 | M01-03 个人信息与地址—唐宇昊 | 买家个人中心、地址管理 | A006~A014 | 待测试计划登记 | 已统一为买家专属,待 OpenAPI 与交叉评审 | | F04 | M02-01 商品列表与分类—顾欣月 | 购物端商品列表、分类入口 | A101、A102 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | | F05 | M02-01 商品搜索—顾欣月 | 购物端搜索框、商品列表 | A102 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | | F06 | M02-02 商品详情—顾欣月 | 商品详情页 | A103 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | | F07 | M03-01 购物车—朱惠惠 | 商品加购入口、购物车页 | A201~A208 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | -| F08 | M04-01 提交订单—韦乾强 | 购物车结算、提交订单页 | A301 | 待测试计划登记 | 部分定义,幂等与事务字段待确认 | -| F09 | M04-02/M04-03/M04-04 订单查询、取消与完成—韦乾强 | 买家订单列表、订单详情、确认收货 | A302~A304;缺 A308 | 待测试计划登记 | 缺确认收货接口,尚未闭环 | -| F10 | M05-01 模拟支付—张海洋 | 小金库、统一收银台、支付记录 | A401~A408 | 待测试计划登记 | 接口草案已汇总,支付语义待统一 | -| F11 | M06-01 后台商品管理—顾欣月 | 商家分类管理、商品管理 | A110~A114、A120~A128 | 待测试计划登记 | 接口草案已汇总,创建与图片顺序待统一 | -| F12 | M06-02 后台订单管理—韦乾强 | 商家订单列表、详情和发货入口 | A305~A307 | 待测试计划登记 | 部分定义,模块命名与状态字段待确认 | -| F13 | M06-03 后台用户管理—唐宇昊 | 管理端用户管理 | A015~A017 | 待测试计划登记 | 接口草案已汇总,重复操作语义待修正 | -| X01 | M07 商品评价与晒图—顾欣月 | 订单评价入口、商品评价区 | A140~A143;A144 待决策 | 待测试计划登记 | 单条评价读取需新增接口或删除不可达引用 | -| X02 | M08 收藏与浏览历史—唐宇昊 | 商品收藏、收藏页、浏览历史页 | A018~A023;缺 A024、A025 | 待测试计划登记 | 缺浏览记录写入与设置查询,尚未闭环 | -| X03 | M09 站内消息—罗皓晨 | 买家/商家消息中心 | A501~A505 | 待测试计划登记 | HTTP 草案已汇总,事件契约待确认 | -| X04 | M10 售后流程—张海洋 | 买家售后申请、商家售后审核 | A411~A419、A431~A433;缺 A434 | 待测试计划登记 | 缺买家退货提交接口,内部退款边界待修正 | -| C01 | 秒杀与防超卖—朱惠惠 | 秒杀活动页、商家活动管理、压测脚本 | A220~A228;订单查询复用 A302/A303 | 待挑战测试登记 | A229/A230 边界冲突,暂不实施 | -| C03 | 订单超时取消—韦乾强 | 订单倒计时、Worker 验收脚本 | 复用 A304;超时扫描为 Worker 内部契约 | 待挑战测试登记 | 无新增外部 HTTP,内部任务契约待确认 | +| F08 | M04-01 提交订单—韦乾强 | 购物车结算、提交订单页 | A301 | 待测试计划登记 | 幂等、超时和商家归属已统一,待数据库、OpenAPI 与交叉评审 | +| F09 | M04-02/M04-03/M04-04 订单查询、取消与完成—韦乾强 | 买家订单列表、订单详情、确认收货 | A302~A304、A308 | 待测试计划登记 | 接口草案已闭合,待数据库、OpenAPI 与交叉评审 | +| F10 | M05-01 模拟支付—张海洋 | 小金库、统一收银台、支付记录 | A401~A408 | 待测试计划登记 | 重复支付与幂等语义已统一,待数据库、OpenAPI 与交叉评审 | +| F11 | M06-01 后台商品管理—顾欣月 | 商家分类管理、商品管理 | A110~A114、A120~A128 | 待测试计划登记 | 创建、图片顺序与同步索引边界已统一,待数据库、OpenAPI 与交叉评审 | +| F12 | M06-02 后台订单管理—韦乾强 | 商家订单列表、详情和发货入口 | A305~A307 | 待测试计划登记 | Ordering 命名、状态与商家归属已统一,待数据库、OpenAPI 与交叉评审 | +| F13 | M06-03 后台用户管理—唐宇昊 | 管理端用户管理 | A015~A017 | 待测试计划登记 | 禁用、启用和旧令牌失效语义已统一,待数据库、OpenAPI 与交叉评审 | +| X01 | M07 商品评价与晒图—顾欣月 | 订单评价入口、商品评价区 | A140~A144 | 待测试计划登记 | 单条评价读取与图片顺序已闭合,待数据库、OpenAPI 与交叉评审 | +| X02 | M08 收藏与浏览历史—唐宇昊 | 商品收藏、收藏页、浏览历史页 | A018~A025 | 待测试计划登记 | 浏览记录写入与设置查询已闭合,待数据库、OpenAPI 与交叉评审 | +| X03 | M09 站内消息—罗皓晨 | 买家/商家消息中心 | A501~A505 | 待测试计划登记 | HTTP、集成事件和接收人边界已统一,待数据库、OpenAPI 与来源模块交叉评审 | +| X04 | M10 售后流程—张海洋 | 买家售后申请、商家售后审核 | A411~A417、A419、A432~A434;退款入账使用 Payment 应用契约 | 待测试计划登记 | A418/A431 为取消历史号,待数据库、OpenAPI 与交叉评审 | +| C01 | 秒杀与防超卖—朱惠惠 | 秒杀活动页、商家活动管理、压测脚本 | A220~A228;订单查询复用 A302/A303;生命周期为 Worker 内部契约 | 待挑战测试登记 | 原子限购、库存通道与状态推进已定义,待数据库、OpenAPI 与跨模块联调 | +| C03 | 订单超时取消—韦乾强 | 订单倒计时、Worker 验收脚本 | 复用 A304;超时扫描为 Worker 内部契约 | 待挑战测试登记 | Worker 扫描、取消复用和原库存通道回补已定义,待数据库与测试评审 | | C04 | 商品搜索进阶—顾欣月 | 商品搜索页、性能对比报告 | A102 | 待挑战测试登记 | 接口草案已汇总,待交叉评审 | -| C06 | 实时消息推送—罗皓晨 | 实时通知、连接状态、消息中心 | SignalR 契约(无 Axxx),补查复用 A501~A505 | 待挑战测试登记 | 实时与持久化边界待确认 | +| C06 | 实时消息推送—罗皓晨 | 实时通知、连接状态、消息中心 | SignalR 契约(无 Axxx),补查复用 A501~A505 | 待挑战测试登记 | 实时推送、持久化事实与断线补查边界已定义,待部署与测试评审 | | C07 | 缓存与性能优化—罗皓晨,顾欣月协作 | 首页、商品详情、压测报告 | 复用 A102、A103 | 待挑战测试登记 | 不新增业务 HTTP,缓存约定待确认 | -| C08 | 支付回调幂等与对账—张海洋 | 回调重放脚本、管理端对账 | A421~A425、A431~A433 | 待挑战测试登记 | 接口草案已汇总,回调与退款边界待修正 | +| C08 | 支付回调幂等与对账—张海洋 | 回调重放脚本、管理端对账 | A421~A425、A432~A433;退款入账使用 Payment 应用契约 | 待挑战测试登记 | A431 为取消历史号,待数据库、OpenAPI 与交叉评审 | | C10 | 容器化与负载均衡—罗皓晨 | 统一访问入口、健康与实例验证 | A506、A507 | 待部署验收登记 | 健康检查草案已汇总,待交叉评审 | 负责人补齐缺少接口并完成交叉评审后,应把对应状态更新为“已确认”;生成真实 OpenAPI 后再补充 `operationId` 校验结果。测试计划完成后,应替换为真实测试用例编号,并随需求变更同步维护状态。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" index c7ad8fd..9abcd11 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" @@ -1,14 +1,13 @@ # 接口设计(顾欣月)— Catalog、Review -> 组别:24级1班第7组 编写人:顾欣月 编写日期:2026-07-24 版本:v0.2 +> 组别:24级1班第7组 编写人:顾欣月 编写日期:2026-07-24 版本:v0.1 > 编号区间:`A101`~`A200` 负责模块:Catalog(商品目录)、Review(评价) ## 修订记录 | 版本 | 日期 | 修改人 | 修改说明 | |------|------|--------|----------| -| v0.1 | 2026-07-24 | 顾欣月 | 建立并完善 Catalog、Review 接口清单、A101~A143 详细定义与枚举附录 | -| v0.2 | 2026-07-24 | 顾欣月 | 评审反馈处理:Catalog 清单(A101~A128)已完整无必缺;新增 A144 单条评价详情查询,消除 A142 Location 头与 A143 `existingReviewId` 不可达引用;同步登记 `REVIEW.NOT_FOUND` 错误码 | +| v0.1 | 2026-07-24 | 顾欣月 | 建立并完善 Catalog、Review 接口清单、A101~A144 详细定义与枚举附录;补齐单条评价读取并统一商品、评价图片暂存顺序 | ## 一、说明与约定引用 @@ -33,9 +32,9 @@ | 编号 | 模块 | 需求编号 | 名称 | 方法 | 路径 | operationId | 请求 Schema | 响应 Schema | 鉴权/Policy | 关联 DBxxx | 当前状态 | |---|---|---|---|---|---|---|---|---|---|---|---| -| A101 | Catalog | M02-01-FR02 | 购物端有效分类列表 | GET | `/api/categories` | `Catalog_ListCategories` | —(Query) | `CategoryTreeResponse` | 游客可访问 | DB021 | 待评审 | -| A102 | Catalog | M02-01、F05、C04 | 商品分页列表/搜索/筛选/排序 | GET | `/api/products` | `Catalog_ListProducts` | —(Query) | `ProductListResponse` | 游客可访问 | DB022、DB023 | 待评审 | -| A103 | Catalog | M02-02、F06 | 购物端商品详情 | GET | `/api/products/{productId}` | `Catalog_GetProduct` | —(Route) | `ProductDetailResponse` | 游客可访问 | DB022、DB023 | 待评审 | +| A101 | Catalog | M02-01-FR02 | 购物端有效分类列表 | GET | `/api/categories` | `Catalog_ListCategories` | —(Query) | `CategoryTreeResponse` | Anonymous | DB021 | 待评审 | +| A102 | Catalog | M02-01、F05、C04 | 商品分页列表/搜索/筛选/排序 | GET | `/api/products` | `Catalog_ListProducts` | —(Query) | `ProductListResponse` | Anonymous | DB022、DB023 | 待评审 | +| A103 | Catalog | M02-02、F06 | 购物端商品详情 | GET | `/api/products/{productId}` | `Catalog_GetProduct` | —(Route) | `ProductDetailResponse` | Anonymous | DB022、DB023 | 待评审 | | A110 | Catalog | M06-01-FR01 | 后台分类列表(全状态) | GET | `/api/merchant/categories` | `Catalog_ListMerchantCategories` | —(Query) | `MerchantCategoryListResponse` | MerchantOnly | DB021 | 待评审 | | A111 | Catalog | M06-01-FR02 | 新建分类 | POST | `/api/merchant/categories` | `Catalog_CreateCategory` | `CreateCategoryRequest` | `MerchantCategoryResponse` | MerchantOnly | DB021 | 待评审 | | A112 | Catalog | M06-01-FR02 | 编辑分类 | PUT | `/api/merchant/categories/{categoryId}` | `Catalog_UpdateCategory` | `UpdateCategoryRequest` | `MerchantCategoryResponse` | MerchantOnly | DB021 | 待评审 | @@ -50,11 +49,11 @@ | A126 | Catalog | M06-01-FR07 | 商品下架 | POST | `/api/merchant/products/{productId}/unpublish` | `Catalog_UnpublishProduct` | —(Route) | `MerchantProductDetailResponse` | MerchantOnly | DB022 | 待评审 | | A127 | Catalog | M06-01-FR09 | 上传商品图片 | POST | `/api/merchant/products/{productId}/images` | `Catalog_UploadProductImage` | `multipart/form-data` | `ProductImageResponse` | MerchantOnly | DB023 | 待评审 | | A128 | Catalog | M06-01-FR09 | 删除商品图片 | DELETE | `/api/merchant/products/{productId}/images/{imageId}` | `Catalog_DeleteProductImage` | —(Route) | —(204) | MerchantOnly | DB023 | 待评审 | -| A140 | Review | M07-FR06、FR07 | 商品公开评价分页 + 评分汇总 | GET | `/api/products/{productId}/reviews` | `Review_ListProductReviews` | —(Route/Query) | `ProductReviewListResponse` | 游客可访问 | DB024、DB025 | 待评审 | +| A140 | Review | M07-FR06、FR07 | 商品公开评价分页 + 评分汇总 | GET | `/api/products/{productId}/reviews` | `Review_ListProductReviews` | —(Route/Query) | `ProductReviewListResponse` | Anonymous | DB024、DB025 | 待评审 | | A141 | Review | M07-FR03 | 上传评价图片(提交前暂存) | POST | `/api/reviews/images` | `Review_UploadReviewImage` | `multipart/form-data` | `ReviewImageResponse` | BuyerOnly | DB025 | 待评审 | | A142 | Review | M07-FR04、FR05 | 提交商品评价(幂等) | POST | `/api/reviews` | `Review_CreateReview` | `CreateReviewRequest` | `ReviewDetailResponse` | BuyerOnly | DB024、DB025 | 待评审 | | A143 | Review | M07-FR01 | 查询订单项评价资格/结果 | GET | `/api/reviews/eligibility` | `Review_GetReviewEligibility` | —(Query) | `ReviewEligibilityResponse` | BuyerOnly | DB024 | 待评审 | -| A144 | Review | M07-FR02 | 单条评价详情查询 | GET | `/api/reviews/{reviewId}` | `Review_GetReview` | —(Route) | `ReviewDetailResponse` | 游客可访问 | DB024、DB025 | 待评审 | +| A144 | Review | M07-FR06 | 单条公开评价详情查询 | GET | `/api/reviews/{reviewId}` | `Review_GetReview` | —(Route) | `PublicReviewDetailResponse` | Anonymous | DB024、DB025 | 待评审 | 清单补充说明: @@ -258,7 +257,8 @@ | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| | 400 | `COMMON.VALIDATION_FAILED` | `productId` 格式非法 | -| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在、草稿、下架或已删除(对购物端统一按不存在处理) | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | +| 404 | `CATALOG.PRODUCT_UNAVAILABLE` | 商品已下架、已删除或当前不可公开 | #### 业务规则与并发 @@ -272,7 +272,7 @@ #### 验证场景 -- 有货、售罄、下架/不存在状态可区分;下架商品详情返回 404;图片失败时前端占位不阻断其余信息。 +- 有货、售罄、下架和不存在状态可通过成功模型或稳定业务错误码区分;图片失败时前端占位不阻断其余信息。 --- @@ -599,7 +599,6 @@ | `price` | number | 是 | ≥ 0,最多两位小数 | | `stock` | integer | 是 | ≥ 0 非负整数 | | `description` | string | 否 | ≤ 2000,受控内容 | -| `imageIds` | uuid[] | 否 | 引用已通过 A127 上传的图片,≤ 8 | - 校验规则:分类须启用;价格非负;库存非负整数;图片数 ≤ 8。 @@ -620,11 +619,11 @@ #### 业务规则与并发 -- 新建默认草稿,需通过 A125 上架前完整性校验后才对购物端可见。 +- 新建默认草稿,成功取得 `productId` 后再通过 A127 上传图片,最后经 A125 完整性校验上架;A122 不接受尚无归属商品的预上传图片 ID。 #### 缓存、事件或外部依赖 -- 创建成功发布领域事件用于后续搜索索引同步(C04-FR05);索引随商品数据在同一 PostgreSQL 事务/同步流程更新。 +- `pg_trgm`/GIN 索引随 PostgreSQL 商品数据同步维护,不通过 Outbox 或 Worker 复制搜索索引;事务提交后只触发已确认的缓存失效。 #### 验证场景 @@ -650,7 +649,7 @@ #### 请求 - Route 参数:`productId`(uuid,必填)。 -- Body:`UpdateProductRequest`:字段同 A122(`name`、`categoryId`、`price`、`stock`、`description`、`imageIds`),另加必填 `version`(integer,来自 A121)。 +- Body:`UpdateProductRequest`:字段同 A122(`name`、`categoryId`、`price`、`stock`、`description`),另加必填 `version`(integer,来自 A121);图片新增和删除分别使用 A127、A128。 - 校验规则:`version` 必填;分类须启用;其余同 A122。 #### 成功响应 @@ -675,7 +674,7 @@ #### 缓存、事件或外部依赖 -- 提交后发布领域事件:失效商品详情/列表缓存并同步搜索索引(失败可重试或重建,不回滚已提交商品事务)。 +- 提交后失效商品详情/列表缓存;`pg_trgm`/GIN 索引由 PostgreSQL 随数据同步维护,不发布“同步搜索索引”事件。 #### 验证场景 @@ -771,7 +770,7 @@ #### 缓存、事件或外部依赖 -- 状态变更发布领域事件,同步失效购物端缓存与搜索索引(C04-FR05/FR06:下架商品不得因索引旧数据被重新公开)。 +- 状态变更提交后失效购物端缓存;搜索查询始终附加上架状态过滤,`pg_trgm`/GIN 索引由 PostgreSQL 同步维护。 #### 验证场景 @@ -931,7 +930,7 @@ #### 业务规则与并发 -- 只返回有效评价;不返回手机号、邮箱、内部用户标识,昵称脱敏展示。 +- 只返回有效评价;`buyerDisplayName` 直接读取评价创建时保存的脱敏展示名快照,不在列表查询中逐条调用 Identity;不返回手机号、邮箱、内部用户标识。 - 评分汇总由有效评价计算;新增评价后最终更新(可接受短暂最终一致)。 #### 缓存、事件或外部依赖 @@ -981,7 +980,7 @@ #### 业务规则与并发 -- 暂存图片归属当前买家;对象 Key 采用 `reviews/{reviewId}/{fileId}.`,`reviewId` 在 A142 提交成功后关联。未被引用的暂存图片由清理策略回收。 +- 暂存图片归属当前买家;评价尚未创建时对象 Key 使用 `review-uploads/{buyerId}/{fileId}.`,符合 `//.` 规范。A142 提交成功后只在数据库中关联 `reviewId`,不要求物理搬移对象;未被引用的暂存图片由清理策略回收。 #### 缓存、事件或外部依赖 @@ -1044,11 +1043,12 @@ - 同一订单项只能形成一条评价:唯一约束 `ux_reviews_order_item_id` 作为最终保障;重复点击/重复请求返回首次已确认结果,不新增记录。 - 订单完成状态、订单项归属由 Ordering 提供的应用契约校验,不直接改订单表。 +- 创建评价时通过 Identity 公开应用契约读取当前买家的安全展示名并完成脱敏,将结果保存为 `buyerDisplayName` 快照;没有展示名时回退到自动用户名的脱敏值。后续用户资料变化不改写历史评价快照,公开列表和详情不逐条查询 Identity。 - 提交成功后触发商品评分汇总更新(A140 汇总最终一致)。 #### 缓存、事件或外部依赖 -- 依赖 Ordering 校验订单项;依赖 M00 幂等基础设施与对象存储图片关联。 +- 依赖 Ordering 校验订单项,依赖 Identity 提供当前买家的安全展示名快照;依赖 M00 幂等基础设施与对象存储图片关联。 #### 验证场景 @@ -1081,7 +1081,7 @@ #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`ReviewEligibilityResponse`,含 `eligible`(boolean)、`reason`(枚举字符串:`Eligible`/`OrderNotCompleted`/`AlreadyReviewed`/`NotOwner`)、`existingReviewId`(uuid,可空)。 +- 响应 Schema:`ReviewEligibilityResponse`,含 `eligible`(boolean)、`reason`(枚举字符串:`Eligible`/`OrderNotCompleted`/`AlreadyReviewed`)、`existingReviewId`(uuid,可空);订单项不存在或不属于当前买家时统一返回 404,不返回 `NotOwner`。 - 示例: ```json @@ -1109,10 +1109,10 @@ - 已评价返回 `eligible=false, reason=AlreadyReviewed` 且带 `existingReviewId`;未完成返回 `OrderNotCompleted`。 -### A144 单条评价详情查询 +### A144 单条公开评价详情查询 - 模块 / Tag:Review -- 需求编号:M07-FR02 +- 需求编号:M07-FR06;同时承接 A142 `Location` 与 A143 `existingReviewId` 的可达读取 - 负责人:顾欣月 - 关联数据表:DB024 `reviews`、DB025 `review_images` - 当前状态:待评审 @@ -1120,7 +1120,7 @@ - 方法与路径:`GET /api/reviews/{reviewId}` - operationId:`Review_GetReview` - 请求 Schema:无(仅 Route) -- 响应 Schema:`ReviewDetailResponse`(与 A142 创建响应同口径) +- 响应 Schema:`PublicReviewDetailResponse` - 身份与 Policy:游客可访问;公开评价与 A140 列表同口径,不返回买家手机号、邮箱、内部用户标识等敏感字段。 - 资源归属:公开评价;当前买家请求时不附加任何归属校验。 - 幂等要求:只读,天然幂等。 @@ -1135,7 +1135,7 @@ #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`ReviewDetailResponse`,含 `reviewId`、`productId`、`orderItemId`、`rating`、`content`、`images`(`imageId`、`url`、`sortOrder`)、`buyerDisplayName`(脱敏昵称,与 A140 一致)、`createdAt`。 +- 响应 Schema:`PublicReviewDetailResponse`,含 `reviewId`、`productId`、`rating`、`content`、`images`(`imageId`、`url`、`sortOrder`)、`buyerDisplayName`(脱敏昵称,与 A140 一致)、`createdAt`;不公开 `orderItemId` 或内部买家标识。 - 示例: ```json @@ -1145,7 +1145,6 @@ "data": { "reviewId": "9c2f…", "productId": "6f1d…", - "orderItemId": "1d3a…", "rating": 5, "content": "很好用", "images": [ @@ -1162,25 +1161,25 @@ | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| | 400 | `COMMON.INVALID_UUID` | `reviewId` 非标准 UUID 格式 | -| 404 | `REVIEW.NOT_FOUND` | 评价不存在或已下线 | +| 404 | `REVIEW.NOT_FOUND` | 评价不存在 | | 500 | `COMMON.INTERNAL_ERROR` | 未处理服务端错误 | #### 业务规则与并发 -- 仅返回 `status = visible` 的有效评价;与 A140 列表同口径;被运营下线的评价对游客返回 404,对买家本人可继续返回(与 A142 提交成功后状态联动由后续评审决定,当前版本对所有调用方一致返回可见评价)。 -- 不返回买家手机号、邮箱、内部用户标识;昵称按 A140 的 `buyerDisplayName` 脱敏规则展示。 +- 本期评价创建成功后即按 A140 口径公开;本期不提供运营下线、隐藏或评价治理状态。评价不存在时返回 404。 +- 不返回买家手机号、邮箱、内部用户标识;昵称读取评价创建时保存的 `buyerDisplayName` 脱敏快照,与 A140 保持一致。 - 与 A142 创建响应的 `Location` 头严格对齐:创建成功后客户端可凭 `Location` 直接 GET 本接口获取完整评价。 -- A143 返回 `existingReviewId` 时,前端可经本接口跳转拉取评价详情;如该评价已被下线,按 404 处理并显示对应文案。 +- A143 返回 `existingReviewId` 时,前端可经本接口跳转拉取评价详情。 #### 缓存、事件或外部依赖 -- 汇总/详情缓存与 A140 共用同一 Key 前缀;新增/修改评价经事件失效(评价创建/更新事件名以《命名规范》与架构设计为准)。 +- 汇总/详情缓存与 A140 共用同一 Key 前缀;本期仅在 A142 评价创建成功后触发对应缓存失效,不定义尚未提供的评价更新事件。 - 不依赖 Identity、Ordering 等其他模块的应用契约;仅按 `reviewId` 主键读取 DB024 与 DB025。 #### 验证场景 - 有效 `reviewId` 返回 200 与 `ReviewDetailResponse`; -- 已下线的 `reviewId` 返回 404(`REVIEW.NOT_FOUND`),响应文案不区分"不存在"与"已下线"; +- 不存在的 `reviewId` 返回 404(`REVIEW.NOT_FOUND`); - 非法 UUID 格式返回 400(`COMMON.INVALID_UUID`); - 不暴露买家敏感字段;与 A140 列表的 `buyerDisplayName` 脱敏结果一致。 @@ -1190,7 +1189,8 @@ | 错误码 | HTTP 状态 | 含义 | |---|---:|---| -| `CATALOG.PRODUCT_NOT_FOUND` | 404 | 商品不存在或购物端不可见 | +| `CATALOG.PRODUCT_NOT_FOUND` | 404 | 商品不存在 | +| `CATALOG.PRODUCT_UNAVAILABLE` | 404 | 商品已下架、已删除或当前不可公开 | | `CATALOG.CATEGORY_NOT_FOUND` | 404 | 分类不存在 | | `CATALOG.CATEGORY_DISABLED` | 409 | 分类已停用,不能用于上架/新建 | | `CATALOG.CATEGORY_NAME_CONFLICT` | 409 | 同父级下分类名称重复 | @@ -1206,16 +1206,14 @@ | `REVIEW.ORDER_NOT_COMPLETED` | 409 | 订单未完成,不能评价 | | `REVIEW.ALREADY_REVIEWED` | 409 | 该订单项已评价 | | `REVIEW.INVALID_IMAGE` | 415 | 评价图片格式或尺寸不符合要求 | -| `REVIEW.NOT_FOUND` | 404 | 评价不存在或已下线(A144 单条评价详情查询使用,不区分"不存在"与"已下线"以避免泄露存在性) | +| `REVIEW.NOT_FOUND` | 404 | 评价不存在 | ## 五、待确认事项 1. `DB021`~`DB025` 编号需与本人 `database-gxy.md` 交叉确认并固定;表字段、约束、索引以数据库设计为准。 2. A124、A142、A143 依赖 Ordering(韦乾强)提供“订单项归属 + 订单完成状态 + 是否存在订单关联”的应用契约,需在联调前确认契约形态。 -3. 商品/评价缓存失效与搜索索引同步(C04、C07)由罗皓晨主责的公共能力协作,事件与缓存 Key 以《命名规范》与架构设计为准。 -4. 购物端商品详情(A103)是否内联轻量评分汇总,最终以评审结论为准;当前设计由 A140 统一提供汇总,保持模块边界清晰。 -5. 上传体积超限(A127、A141)当前引用 `COMMON.PAYLOAD_TOO_LARGE`(413),该码属 M00 公共错误码,需由罗皓晨在《接口设计.md》1.10 通用错误码表登记后统一引用;本文件不自建 `COMMON.*` 码。 -6. A140 公开评价的 `buyerDisplayName` 为脱敏昵称,其来源与脱敏规则需与 Identity(唐宇昊)确认,评价模块只做展示不落库敏感字段。 +3. 罗皓晨只协作提供 C07 缓存基础设施与 Key 规范;Catalog 负责人维护缓存失效时机及 C04 的 PostgreSQL `pg_trgm`/GIN 查询与索引设计。 +4. 上传体积超限(A127、A141)统一引用总《接口设计》1.10 已登记的 `COMMON.PAYLOAD_TOO_LARGE`(413);本文件不重复自建 `COMMON.*` 错误码。 ## 六、枚举附录 @@ -1226,7 +1224,7 @@ | 分类状态 `status` | A110、A113、A114 | `Enabled` / `Disabled` | `enabled` / `disabled` | 分类是否作为购物端筛选入口 | | 商品状态 `status` | A120~A126 | `Draft` / `Published` / `Unpublished` | `draft` / `published` / `unpublished` | 草稿 / 已上架 / 已下架 | | 库存状态 `stockStatus` | A102、A103 | `InStock` / `SoldOut` | 由 `stock` 计算,不落库 | 有货 / 售罄 | -| 评价资格原因 `reason` | A143 | `Eligible` / `OrderNotCompleted` / `AlreadyReviewed` / `NotOwner` | 由订单与评价状态计算,不落库 | 可评价及不可评价原因 | +| 评价资格原因 `reason` | A143 | `Eligible` / `OrderNotCompleted` / `AlreadyReviewed` | 由订单与评价状态计算,不落库 | 可评价及不可评价原因 | | 排序方向 `sortOrder` | A102、A120、A140 | `asc` / `desc`(小写,规范 1.11.3) | — | 升序 / 降序 | > 落库值仅为跨文档参考,最终以 `database-gxy.md` 与实体映射为准;已删除商品在购物端按“不存在”处理,不作为对外枚举值暴露。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" index fb2b8a2..4fbfaa5 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" @@ -5,6 +5,13 @@ > 接口编号范围:`A501`~`A600` > 当前状态:待交叉评审 > 编写日期:2026-07-24 +> 版本:v0.1 + +## 修订记录 + +| 版本 | 日期 | 修改人 | 修改说明 | +|---|---|---|---| +| v0.1 | 2026-07-24 | 罗皓晨 | 建立并完善 A501~A507 消息与健康检查接口,并登记 Messaging 集成事件、实时推送和断线补偿契约 | ## 一、范围与设计结论 @@ -23,13 +30,15 @@ | 编号 | 模块 | 需求编号 | 名称 | 方法 | 路径 | operationId | 请求 Schema | 响应 Schema | 鉴权 | 关联 DBxxx | 状态 | |---|---|---|---|---|---|---|---|---|---|---|---| -| A501 | Messaging | X03-FR03 | 查询本人消息列表 | GET | `/api/messages` | `Messaging_ListMessages` | Query 参数 | `MessageListResponse` | 买家或商家 JWT | 待 `database-lhc.md` 确认 | 待评审 | -| A502 | Messaging | X03-FR04 | 查询本人消息详情 | GET | `/api/messages/{messageId}` | `Messaging_GetMessage` | Route 参数 | `MessageDetailResponse` | 买家或商家 JWT | 待 `database-lhc.md` 确认 | 待评审 | -| A503 | Messaging | X03-FR05 | 查询本人未读消息数 | GET | `/api/messages/unread-count` | `Messaging_GetUnreadCount` | 无 | `UnreadMessageCountResponse` | 买家或商家 JWT | 待 `database-lhc.md` 确认 | 待评审 | -| A504 | Messaging | X03-FR06 | 标记本人单条消息已读 | POST | `/api/messages/{messageId}/read` | `Messaging_MarkMessageRead` | Route 参数 | `MarkMessageReadResponse` | 买家或商家 JWT | 待 `database-lhc.md` 确认 | 待评审 | -| A505 | Messaging | X03-FR07 | 标记本人当前消息全部已读 | POST | `/api/messages/read-all` | `Messaging_MarkAllMessagesRead` | 无 | `MarkAllMessagesReadResponse` | 买家或商家 JWT | 待 `database-lhc.md` 确认 | 待评审 | -| A506 | M00 | C10-FR06 | API 存活检查 | GET | `/health/live` | `Infrastructure_GetLiveness` | 无 | `HealthStatusResponse` | 无 | 无 | 待评审 | -| A507 | M00 | C10-FR06 | API 就绪检查 | GET | `/health/ready` | `Infrastructure_GetReadiness` | 无 | `ReadinessStatusResponse` | 无 | 无 | 待评审 | +| A501 | Messaging | X03-FR03 | 查询本人消息列表 | GET | `/api/messages` | `Messaging_ListMessages` | Query 参数 | `MessageListResponse` | BuyerOnly / MerchantOnly | 待 `database-lhc.md` 确认 | 待评审 | +| A502 | Messaging | X03-FR04 | 查询本人消息详情 | GET | `/api/messages/{messageId}` | `Messaging_GetMessage` | Route 参数 | `MessageDetailResponse` | BuyerOnly / MerchantOnly | 待 `database-lhc.md` 确认 | 待评审 | +| A503 | Messaging | X03-FR05 | 查询本人未读消息数 | GET | `/api/messages/unread-count` | `Messaging_GetUnreadCount` | 无 | `UnreadMessageCountResponse` | BuyerOnly / MerchantOnly | 待 `database-lhc.md` 确认 | 待评审 | +| A504 | Messaging | X03-FR06 | 标记本人单条消息已读 | POST | `/api/messages/{messageId}/read` | `Messaging_MarkMessageRead` | Route 参数 | `MarkMessageReadResponse` | BuyerOnly / MerchantOnly | 待 `database-lhc.md` 确认 | 待评审 | +| A505 | Messaging | X03-FR07 | 标记本人当前消息全部已读 | POST | `/api/messages/read-all` | `Messaging_MarkAllMessagesRead` | 无 | `MarkAllMessagesReadResponse` | BuyerOnly / MerchantOnly | 待 `database-lhc.md` 确认 | 待评审 | +| A506 | Infrastructure | C10-FR06 | API 存活检查 | GET | `/health/live` | `Infrastructure_GetLiveness` | 无 | `HealthStatusResponse` | Anonymous | 无 | 待评审 | +| A507 | Infrastructure | C10-FR06 | API 就绪检查 | GET | `/health/ready` | `Infrastructure_GetReadiness` | 无 | `ReadinessStatusResponse` | Anonymous | 无 | 待评审 | + +`BuyerOnly / MerchantOnly` 表示满足其中任一角色策略即可,要求有效 JWT。管理员不因此获得查看用户私人消息的权限;所有查询和写操作还必须按当前 `sub` 过滤消息归属。 ## 三、公共 Schema 与枚举 @@ -46,6 +55,10 @@ | `OrderCompleted` | 订单完成 | | `AfterSalesSubmitted` | 售后申请已提交,提醒指定商家处理 | | `AfterSalesReviewed` | 售后审核完成,通知买家结果 | +| `AfterSalesPendingReturn` | 退货申请审核通过,提醒买家寄回商品 | +| `AfterSalesReturnSubmitted` | 买家已提交寄回信息,提醒指定商家处理 | +| `RefundSucceeded` | 售后退款成功并已退回小金库 | +| `RefundFailed` | 售后退款失败,可在售后详情查看或重试 | 后续新增消息类型属于兼容性扩展。客户端必须对未知值使用“业务通知”兜底展示,不能因此白屏。 @@ -59,7 +72,7 @@ | 字段 | 类型 | 必需 | 说明 | |---|---|---:|---| -| `target` | string | 是 | `OrderDetail` 或 `AfterSalesDetail` | +| `target` | string | 是 | `OrderDetail`、`PaymentDetail` 或 `AfterSalesDetail` | | `resourceId` | UUID | 是 | 目标业务资源 ID;进入目标页面时仍须重新鉴权 | 当关联资源不存在、已归档或当前用户已无权访问时,`action` 返回 `null`。 @@ -103,7 +116,7 @@ - operationId:`Messaging_ListMessages` - 请求 Schema:Query 参数 - 响应 Schema:`MessageListResponse` -- 身份与 Policy:有效 JWT;仅买家或商家 +- 身份与 Policy:`BuyerOnly / MerchantOnly` - 资源归属:接收用户必须等于当前认证用户;服务端不接收 `userId` - 幂等要求:只读接口,天然幂等 @@ -204,7 +217,7 @@ - operationId:`Messaging_GetMessage` - 请求 Schema:Route 参数 - 响应 Schema:`MessageDetailResponse` -- 身份与 Policy:有效 JWT;仅买家或商家 +- 身份与 Policy:`BuyerOnly / MerchantOnly` - 资源归属:消息接收用户必须等于当前认证用户 - 幂等要求:只读接口,天然幂等 @@ -283,7 +296,7 @@ - operationId:`Messaging_GetUnreadCount` - 请求 Schema:无 - 响应 Schema:`UnreadMessageCountResponse` -- 身份与 Policy:有效 JWT;仅买家或商家 +- 身份与 Policy:`BuyerOnly / MerchantOnly` - 资源归属:只统计当前认证用户 - 幂等要求:只读接口,天然幂等 @@ -345,7 +358,7 @@ - operationId:`Messaging_MarkMessageRead` - 请求 Schema:Route 参数 - 响应 Schema:`MarkMessageReadResponse` -- 身份与 Policy:有效 JWT;仅买家或商家 +- 身份与 Policy:`BuyerOnly / MerchantOnly` - 资源归属:消息接收用户必须等于当前认证用户 - 幂等要求:同一用户对同一消息重复调用返回相同首次 `readAt` @@ -412,7 +425,7 @@ - operationId:`Messaging_MarkAllMessagesRead` - 请求 Schema:无 - 响应 Schema:`MarkAllMessagesReadResponse` -- 身份与 Policy:有效 JWT;仅买家或商家 +- 身份与 Policy:`BuyerOnly / MerchantOnly` - 资源归属:只更新当前认证用户 - 幂等要求:没有新的未读消息时重复调用返回 `markedCount = 0` @@ -613,14 +626,52 @@ - 未启用 RabbitMQ 或对象存储时不把它们报告为失败。 - 两个实例使用同一契约并返回不同 `instanceId`。 -## 五、SignalR 实时契约(不占 Axxx 编号) +## 五、非 HTTP 集成与实时契约(不占 Axxx 编号) + +### 5.1 业务模块到 Messaging 的集成事件 -### 5.1 Hub 连接 +Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封向 Messaging 提交已经发生且已提交的业务事实: + +| 字段 | 类型 | 必需 | 说明 | +|---|---|---:|---| +| `messageId` | UUID | 是 | 全局唯一业务消息 ID,也是 Inbox 第一去重键 | +| `type` | string | 是 | 下表白名单值 | +| `schemaVersion` | string | 是 | 当前固定 `v1` | +| `occurredAt` | UTC 时间 | 是 | 业务事实发生时间 | +| `recipients` | array | 是 | 至少 1 项;每项包含 `userId` 和 `role`(`Buyer`/`Merchant`),明确列出接收账号,不允许按全角色广播 | +| `aggregateId` | UUID | 是 | 订单、支付或售后申请 ID | +| `correlationId` | string | 是 | 跨请求与消息链路追踪标识,与当前 Trace 关联但不暴露内部实现 | +| `data` | object | 是 | 仅包含生成标题、摘要、正文与安全跳转所需的最小业务快照 | + +事件登记与消息映射: + +| 来源模块 | `type` | Routing Key | 精确接收账号来源 | `MessageType` | `data` 最小字段 | 默认跳转 | +|---|---|---|---|---|---|---| +| Ordering | `OrderCreatedIntegrationEvent` | `ordering.order.created.v1` | 订单 `buyerId` | `OrderCreated` | `orderId`、`orderNo`、`totalAmount` | `OrderDetail` | +| Ordering | `OrderCancelledIntegrationEvent` | `ordering.order.cancelled.v1` | 订单 `buyerId` | `OrderCancelled` | `orderId`、`orderNo`、`cancelReason` | `OrderDetail` | +| Payment | `OrderPaidIntegrationEvent` | `payment.order.paid.v1` | 订单 `buyerId`、`assignedMerchantUserId` | `PaymentSucceeded` | `orderId`、`paymentId`、`amount` | 买家 `PaymentDetail`;商家 `OrderDetail` | +| Ordering | `OrderShippedIntegrationEvent` | `ordering.order.shipped.v1` | 订单 `buyerId` | `OrderShipped` | `orderId`、`orderNo`、`shippedAt` | `OrderDetail` | +| Ordering | `OrderCompletedIntegrationEvent` | `ordering.order.completed.v1` | 订单 `buyerId` | `OrderCompleted` | `orderId`、`orderNo`、`completedAt`、`completedBy` | `OrderDetail` | +| AfterSales | `AfterSalesApplicationSubmittedIntegrationEvent` | `after-sales.request.submitted.v1` | 申请 `buyerId`、订单 `assignedMerchantUserId` | `AfterSalesSubmitted` | `requestId`、`orderId`、`type` | `AfterSalesDetail` | +| AfterSales | `AfterSalesApplicationAuditedIntegrationEvent` | `after-sales.request.audited.v1` | 申请 `buyerId` | `AfterSalesReviewed` 或 `AfterSalesPendingReturn` | `requestId`、`decision`、`status` | `AfterSalesDetail` | +| AfterSales | `AfterSalesReturnInfoSubmittedIntegrationEvent` | `after-sales.return-info.submitted.v1` | 订单 `assignedMerchantUserId` | `AfterSalesReturnSubmitted` | `requestId`、`status` | `AfterSalesDetail` | +| Payment | `RefundCompletedIntegrationEvent` | `payment.refund.completed.v1` | 申请 `buyerId` | `RefundSucceeded` | `requestId`、`refundId`、`amount` | `AfterSalesDetail` | +| AfterSales | `RefundFailedIntegrationEvent` | `after-sales.refund.failed.v1` | 申请 `buyerId`、订单 `assignedMerchantUserId` | `RefundFailed` | `requestId`、`failureCode` | `AfterSalesDetail` | + +传输与幂等规则: + +- 事件发布到 `eshop.events` Exchange;Routing Key 使用上表固定值,新增事件仍遵守 `...v1`。 +- 来源模块在业务事务中写 Outbox;Worker 发布 RabbitMQ;Messaging 在保存消息的同一事务中写 Inbox。 +- Messaging 对 `recipients` 逐项生成消息,并以 `(messageId, recipientUserId, MessageType)` 建唯一约束;重复投递返回已处理结果,不重复生成消息或未读数。 +- `data` 不包含完整手机号、地址、支付凭证、JWT、密码或内部前端路由;消息文案由 Messaging 按每项接收人的 `role` 选择模板。 +- 消息保存成功后才触发 SignalR;RabbitMQ 或实时推送失败不回滚已经提交的来源业务事实。 + +### 5.2 Hub 连接 | 项目 | 契约 | |---|---| | Hub 路径 | `/hubs/messaging` | -| 鉴权 | 有效买家或商家 JWT | +| 鉴权 | `BuyerOnly / MerchantOnly`(满足其中任一) | | 身份来源 | 服务端认证上下文中的用户 ID 和角色 | | 客户端订阅参数 | 无,不接受客户端传入任意 `userId`、角色或组名 | | 多实例 | 使用 Redis Backplane | @@ -630,7 +681,7 @@ 客户端主动退出后关闭连接。非主动断线使用有限退避自动重连;初次连接和每次重连成功后调用 A503,并按需调用 A501 补查断线期间消息。 -### 5.2 服务端事件 `MessageCreated` +### 5.3 服务端事件 `MessageCreated` 服务端向目标认证用户的全部在线连接推送 `MessageCreated`。载荷 Schema 为 `MessageCreatedPayload`: @@ -682,7 +733,7 @@ ### 7.1 需要其他负责人评审的协作点 -- Ordering、Payment 和 AfterSales 负责人需确认会触发通知的业务事实、事件 ID、业务 ID、接收用户和发生时间。 +- Ordering、Payment 和 AfterSales 负责人需评审 5.1 的事件触发时机、接收账号和最小字段映射;字段和 Routing Key 不得在实现中另起一套。 - Identity 负责人需确认买家与商家认证身份、账号禁用和令牌失效规则可以同时约束 HTTP 与 SignalR。 - 商家通知必须由来源模块明确指定接收账号或受控接收范围,不允许 Messaging 自行向全部商家广播私人订单或售后信息。 - Catalog 负责人继续拥有 C07 商品缓存失效业务规则;罗皓晨只提供 Redis 与多实例缓存基础设施,不新增公开缓存控制接口。 @@ -690,7 +741,7 @@ ### 7.2 实现前必须补齐 - 创建并评审 `database-lhc.md`,登记消息表及必要的唯一约束、用户未读查询索引和关联 A501~A505。 -- 单独评审 Ordering、Payment、AfterSales 到 Messaging 的集成事件 Schema、Routing Key、Outbox/Inbox 幂等键和失败处理;这些不是 HTTP Axxx 接口。 +- 由 Ordering、Payment、AfterSales 负责人确认 5.1 已登记的事件映射和接收账号;这些契约不占 HTTP Axxx 编号。 - 在后端脚手架建立后形成真实 OpenAPI,并保证 `operationId`、Schema、状态码和错误码与本文件一致。 - 在测试计划中登记 X03、C06 和 C10 的分页、越权、重复已读、并发全部已读、断线重连、多标签页、多实例和健康检查场景。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" index 4cf6b82..f474216 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" @@ -3,29 +3,28 @@ > 组别:24级1班第7组 负责人:唐宇昊(tyh) 接口编号区间:`A001`~`A100` > 负责模块:Identity(注册、登录退出、JWT、用户资料、收货地址、后台账号治理)、Engagement(收藏、浏览历史) > 关联教师验收编号:F01、F02、F03、F13、X02 -> 当前状态:部分定义;清单已给出,详细定义按接口设计 1.20 模板补齐 +> 当前状态:部分定义;清单与详细定义已完成,待数据库、OpenAPI 和交叉评审 > 本文件为协作与评审追踪材料;汇总后继续保留,实现、OpenAPI 和联调以 `../接口设计.md` 为准 ## 修订记录 | 版本 | 日期 | 修改人 | 修改说明 | |---|---|---|---| -| v0.1 | 2026-07-24 | 唐宇昊 | 建立 `A001`~`A023` 接口清单并补齐全部详细定义 | -| v0.2 | 2026-07-24 | 唐宇昊 | 补登 `A024` 记录浏览历史、`A025` 查询浏览记录开关;将 `A006`/`A007` 路径迁回 `/api/users/me` 资源域;按 F03 收紧 `A010`~`A014` 鉴权 Policy 为仅 `BuyerOnly` | +| v0.1 | 2026-07-24 | 唐宇昊 | 建立并完善 `A001`~`A025` 接口清单与详细定义;补齐浏览记录写入和设置查询,统一 F03 买家权限、令牌撤销及账号状态幂等语义 | ## 一、接口清单 | 编号 | 模块 | 需求编号 | 名称 | 方法 | 路径 | operationId | 请求 Schema | 响应 Schema | 鉴权 Policy | 关联 DBxxx | 当前状态 | |---|---|---|---|---|---|---|---|---|---|---|---| -| A001 | Identity | F01 | 买家注册 | POST | `/api/auth/register` | `Identity_RegisterUser` | `RegisterUserRequest` | `RegisteredUserResponse` | 允许游客 | DB001 | 已定义 | -| A002 | Identity | F02 | 登录 | POST | `/api/auth/login` | `Identity_Login` | `LoginRequest` | `LoginResponse` | 允许游客 | DB001 | 已定义 | +| A001 | Identity | F01 | 买家注册 | POST | `/api/auth/register` | `Identity_RegisterUser` | `RegisterUserRequest` | `RegisteredUserResponse` | Anonymous | DB001 | 已定义 | +| A002 | Identity | F02 | 登录 | POST | `/api/auth/login` | `Identity_Login` | `LoginRequest` | `LoginResponse` | Anonymous | DB001 | 已定义 | | A003 | Identity | F02 | 退出当前令牌 | POST | `/api/auth/logout` | `Identity_Logout` | 无 | `LogoutResponse` | BuyerOnly / MerchantOnly / AdminOnly | DB001、DB004 | 已定义 | | A004 | Identity | F02 | 获取当前用户 | GET | `/api/auth/me` | `Identity_GetCurrentUser` | 无 | `CurrentUserResponse` | BuyerOnly / MerchantOnly / AdminOnly | DB001 | 已定义 | -| A005 | Identity | F02 | 刷新访问令牌 | POST | `/api/auth/refresh-token` | `Identity_RefreshToken` | `RefreshTokenRequest` | `LoginResponse` | BuyerOnly / MerchantOnly / AdminOnly | DB001、DB004 | 已定义 | -| A006 | Identity | F03 | 修改手机号 | POST | `/api/users/me/phone` | `Identity_ChangePhone` | `ChangePhoneRequest` | `CurrentUserResponse` | BuyerOnly / MerchantOnly | DB001、DB004 | 已定义 | -| A007 | Identity | F03 | 重置用户名 | POST | `/api/users/me/username/reset` | `Identity_ResetUsername` | 无 | `ResetUsernameResponse` | BuyerOnly / MerchantOnly | DB001 | 已定义 | -| A008 | Identity | F03 | 获取本人资料 | GET | `/api/users/me` | `Identity_GetMyProfile` | 无 | `MyProfileResponse` | BuyerOnly / MerchantOnly | DB001 | 已定义 | -| A009 | Identity | F03 | 修改本人资料 | PATCH | `/api/users/me` | `Identity_UpdateMyProfile` | `UpdateMyProfileRequest` | `MyProfileResponse` | BuyerOnly / MerchantOnly | DB001 | 已定义 | +| A005 | Identity | F02 | 刷新访问令牌 | POST | `/api/auth/refresh-token` | `Identity_RefreshToken` | `RefreshTokenRequest` | `LoginResponse` | 有效刷新令牌 | DB001、DB004 | 已定义 | +| A006 | Identity | F03 | 修改手机号 | POST | `/api/users/me/phone` | `Identity_ChangePhone` | `ChangePhoneRequest` | `CurrentUserResponse` | BuyerOnly | DB001、DB004 | 已定义 | +| A007 | Identity | F03 | 重置用户名 | POST | `/api/users/me/username/reset` | `Identity_ResetUsername` | 无 | `ResetUsernameResponse` | BuyerOnly | DB001 | 已定义 | +| A008 | Identity | F03 | 获取本人资料 | GET | `/api/users/me` | `Identity_GetMyProfile` | 无 | `MyProfileResponse` | BuyerOnly | DB001 | 已定义 | +| A009 | Identity | F03 | 修改本人资料 | PATCH | `/api/users/me` | `Identity_UpdateMyProfile` | `UpdateMyProfileRequest` | `MyProfileResponse` | BuyerOnly | DB001 | 已定义 | | A010 | Identity | F03 | 我的地址列表 | GET | `/api/users/me/addresses` | `Identity_ListMyAddresses` | 无 | `AddressListResponse` | BuyerOnly | DB003 | 已定义 | | A011 | Identity | F03 | 新增地址 | POST | `/api/users/me/addresses` | `Identity_CreateMyAddress` | `CreateAddressRequest` | `AddressResponse` | BuyerOnly | DB003 | 已定义 | | A012 | Identity | F03 | 编辑地址 | PATCH | `/api/users/me/addresses/{addressId}` | `Identity_UpdateMyAddress` | `UpdateAddressRequest` | `AddressResponse` | BuyerOnly | DB003 | 已定义 | @@ -40,8 +39,8 @@ | A021 | Engagement | X02 | 浏览历史列表 | GET | `/api/browsing-history` | `Engagement_ListBrowsingHistory` | 无(Query 分页) | `BrowsingHistoryListResponse` | BuyerOnly | DB006 | 已定义 | | A022 | Engagement | X02 | 修改浏览记录开关 | PATCH | `/api/browsing-history/settings` | `Engagement_UpdateBrowsingHistorySetting` | `UpdateBrowsingHistorySettingRequest` | `BrowsingHistorySettingResponse` | BuyerOnly | DB006 | 已定义 | | A023 | Engagement | X02 | 清空浏览历史 | DELETE | `/api/browsing-history` | `Engagement_ClearBrowsingHistory` | 无 | 无(204) | BuyerOnly | DB006 | 已定义 | -| A024 | Engagement | X02 | 记录浏览历史 | POST | `/api/browsing-history/records` | `Engagement_RecordBrowsingHistory` | `RecordBrowsingHistoryRequest` | `BrowsingHistoryResponse` | BuyerOnly | DB006 | 已定义(v0.2 补登) | -| A025 | Engagement | X02 | 查询浏览记录开关 | GET | `/api/browsing-history/settings` | `Engagement_GetBrowsingHistorySetting` | 无 | `BrowsingHistorySettingResponse` | BuyerOnly | DB006 | 已定义(v0.2 补登) | +| A024 | Engagement | X02 | 记录浏览历史 | POST | `/api/browsing-history/records` | `Engagement_RecordBrowsingHistory` | `RecordBrowsingHistoryRequest` | `BrowsingHistoryResponse` | BuyerOnly | DB006 | 已定义 | +| A025 | Engagement | X02 | 查询浏览记录开关 | GET | `/api/browsing-history/settings` | `Engagement_GetBrowsingHistorySetting` | 无 | `BrowsingHistorySettingResponse` | BuyerOnly | DB006 | 已定义 | 接口路径补充说明: @@ -88,7 +87,6 @@ RegisterUserRequest { #### 成功响应 - HTTP 状态:`201 Created` -- Response Header:`Location: /api/users/me` - 响应 Schema:`RegisteredUserResponse` ```text @@ -115,14 +113,14 @@ RegisteredUserResponse { #### 业务规则与并发 - 用户名生成规则:`u_` + 8 位不易混淆字符(去除 0/O/1/I/L),最多重试 3 次;最终不重复。 -- 密码使用可靠哈希算法(如 Argon2id)保存;明文密码、确认密码和哈希结果均不得出现在响应、日志或 ProblemDetails 中。 +- 密码使用可靠的自适应哈希保存,具体算法按系统架构与实现统一确定;明文密码、确认密码和哈希结果均不得出现在响应、日志或 ProblemDetails 中。 - 手机号唯一性由 PostgreSQL 唯一约束 `ux_users_phone` 保证;并发注册同一手机号时仅一笔成功,其余返回 `409 / AUTH.PHONE_ALREADY_REGISTERED`。 - 公开注册固定产出 Buyer;客户端传入的角色字段被忽略,且不被任何后续接口读取。 #### 缓存、事件或外部依赖 - 不缓存、不发布集成事件。 -- 成功后建议客户端调用 `A004 GetCurrentUser` 校验登录态恢复。 +- 注册成功不自动签发令牌;客户端应引导用户调用 A002 登录,取得令牌后才能调用 A004 等受保护接口。 #### 验证场景 @@ -203,12 +201,12 @@ CurrentUserResponse { - 账号不存在和密码错误统一返回 `401 / AUTH.INVALID_CREDENTIALS`,不泄露账号是否存在。 - 禁用账号返回 `403 / AUTH.ACCOUNT_DISABLED` 并明确说明联系管理员。 - 签发的 JWT 至少包含 `sub`、`role`、`jti`、过期时间与 `tokenVersion`;签名、Issuer、Audience、过期、撤销、账号状态、版本号任一校验失败即拒绝。 -- 刷新令牌与访问令牌通过受控 Redis 列表记录 `jti`,实现多实例撤销共享。 +- 访问令牌和刷新令牌都包含独立 `jti`;仅退出、刷新轮换或账号禁用时把相应 `jti` 加入撤销集合,刚签发的有效令牌不得写入撤销集合。 - 当令牌服务或 Redis 撤销校验不可用时,宁可拒绝登录也不放过无法确认的请求(`503 / AUTH.TOKEN_SERVICE_UNAVAILABLE`)。 #### 缓存、事件或外部依赖 -- 登录成功后向 Redis 写入撤销/版本共享:`auth:revoked:{jti}` 与 `auth:user:{userId}:tokenVersion`。 +- 登录成功后登记当前账号的 `tokenVersion` 和刷新令牌会话;`auth:revoked:*` 只保存已经撤销的令牌,不登记新签发令牌。 - 不发布集成事件;用户级会话不持久化到数据库。 #### 验证场景 @@ -300,8 +298,7 @@ LogoutResponse { |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少访问令牌 | | 401 | `AUTH.TOKEN_EXPIRED` | 访问令牌已过期 | -| 401 | `AUTH.TOKEN_REVOKED` | 访问令牌已退出或账号版本失效 | -| 403 | `AUTH.ACCOUNT_DISABLED` | 账号被禁用 | +| 401 | `AUTH.TOKEN_REVOKED` | 访问令牌已退出、账号已禁用或令牌版本失效 | | 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态校验不可用 | #### 业务规则与并发 @@ -350,15 +347,14 @@ RefreshTokenRequest { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 缺少刷新令牌 | -| 401 | `AUTH.TOKEN_REVOKED` | 刷新令牌已撤销 | +| 401 | `AUTH.TOKEN_REVOKED` | 刷新令牌已撤销、账号已禁用或令牌版本失效 | | 401 | `AUTH.TOKEN_EXPIRED` | 刷新令牌已过期 | -| 403 | `AUTH.ACCOUNT_DISABLED` | 账号被禁用 | | 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态共享不可用 | #### 业务规则与并发 - 旧刷新令牌随新令牌签发一起撤销,避免长期重放。 -- 访问令牌与刷新令牌均加入撤销集合。 +- 旧刷新令牌加入撤销集合;新访问令牌和新刷新令牌保持有效,不得误写入撤销集合。 #### 缓存、事件或外部依赖 @@ -376,7 +372,7 @@ RefreshTokenRequest { - 负责人:唐宇昊 - 关联数据表:DB001、DB004 - 当前状态:已定义 -- 用途:买家或商家修改本人手机号,提交后旧登录态全部失效并要求重新登录。 +- 用途:买家修改本人手机号,提交后修改前签发的全部登录态失效并要求重新登录。 - 方法与路径:`POST /api/users/me/phone` - operationId:`Identity_ChangePhone` @@ -404,7 +400,7 @@ ChangePhoneRequest { | 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或新手机号格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 401 | `AUTH.INVALID_CREDENTIALS` | 当前密码错误 | -| 403 | `AUTH.ACCOUNT_DISABLED` | 账号已被禁用 | +| 401 | `AUTH.TOKEN_REVOKED` | 账号已禁用或访问令牌版本失效 | | 409 | `AUTH.PHONE_ALREADY_REGISTERED` | 新手机号已被他人使用 | | 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态共享不可用,无法签发新令牌 | @@ -416,7 +412,7 @@ ChangePhoneRequest { #### 缓存、事件或外部依赖 -- Redis Key:`auth:user:{userId}:tokenVersion`。 +- Redis Key:`auth:token-version:{userId}`。 - 不发布集成事件。 #### 验证场景 @@ -459,6 +455,7 @@ ResetUsernameResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 409 | `AUTH.USERNAME_RESET_EXHAUSTED` | 当前账号已使用过一次自助重置 | | 409 | `AUTH.USERNAME_GENERATION_RETRY_EXHAUSTED` | 新用户名生成冲突且超过重试上限 | | 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 服务暂不可用 | @@ -485,7 +482,7 @@ ResetUsernameResponse { - 负责人:唐宇昊 - 关联数据表:DB001 - 当前状态:已定义 -- 用途:买家或商家查看本人资料;不返回内部审计或登录态字段。 +- 用途:买家查看本人资料;不返回内部审计或登录态字段。 - 方法与路径:`GET /api/users/me` - operationId:`Identity_GetMyProfile` @@ -505,7 +502,9 @@ MyProfileResponse { username: string phoneMasked: string avatarUrl: string - role: "Buyer" | "Merchant" + displayName: string? + bio: string? + role: "Buyer" canResetUsername: boolean // 是否仍可自助重置用户名 createdAt: string } @@ -516,7 +515,7 @@ MyProfileResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 403 | `AUTH.ACCOUNT_DISABLED` | 账号已被禁用 | +| 401 | `AUTH.TOKEN_REVOKED` | 账号已禁用或访问令牌版本失效 | | 404 | `RESOURCE.NOT_FOUND` | 当前用户记录不存在 | #### 业务规则与并发 @@ -531,8 +530,8 @@ MyProfileResponse { #### 验证场景 - 已登录买家 → 200。 -- 已禁用账号 → 403 / `AUTH.ACCOUNT_DISABLED`。 -- 商家账号登录后同样可调用,但 `role` 为 `Merchant`。 +- 已禁用账号的旧令牌 → 401 / `AUTH.TOKEN_REVOKED`。 +- 商家或管理员调用 → 403 / `AUTH.FORBIDDEN`。 ### A009 修改本人资料 @@ -541,7 +540,7 @@ MyProfileResponse { - 负责人:唐宇昊 - 关联数据表:DB001 - 当前状态:已定义 -- 用途:买家或商家维护本人展示资料;手机号与用户名变更走专门接口,本接口不接受这两类字段。 +- 用途:买家维护本人展示资料;手机号与用户名变更走专门接口,本接口不接受这两类字段。 - 方法与路径:`PATCH /api/users/me` - operationId:`Identity_UpdateMyProfile` @@ -554,7 +553,6 @@ MyProfileResponse { UpdateMyProfileRequest { displayName?: string // 可选,昵称或展示名 bio?: string // 可选,简介,0~200 字 - avatarUrl?: string // 可选;本期不支持自定义头像上传,仅允许系统默认 } ``` @@ -569,8 +567,8 @@ UpdateMyProfileRequest { |---:|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 字段长度或格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 403 | `AUTH.ACCOUNT_DISABLED` | 账号已被禁用 | -| 409 | `COMMON.VALIDATION_FAILED` | 不接受修改 `phone`、`username`、`role`、`status`、`userId` | +| 401 | `AUTH.TOKEN_REVOKED` | 账号已禁用或访问令牌版本失效 | +| 400 | `COMMON.VALIDATION_FAILED` | 请求包含不允许修改的 `phone`、`username`、`role`、`status`、`userId` 或 `avatarUrl` | #### 业务规则与并发 @@ -594,7 +592,7 @@ UpdateMyProfileRequest { - 负责人:唐宇昊 - 关联数据表:DB003 - 当前状态:已定义 -- 用途:买家或商家分页查询本人收货地址,标记默认地址。 +- 用途:买家分页查询本人收货地址,标记默认地址。 - 方法与路径:`GET /api/users/me/addresses` - operationId:`Identity_ListMyAddresses` @@ -623,6 +621,7 @@ AddressListResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 400 | `COMMON.VALIDATION_FAILED` | 分页参数非法 | #### 业务规则与并发 @@ -646,7 +645,7 @@ AddressListResponse { - 负责人:唐宇昊 - 关联数据表:DB003 - 当前状态:已定义 -- 用途:买家或商家新增收货地址。 +- 用途:买家新增收货地址。 - 方法与路径:`POST /api/users/me/addresses` - operationId:`Identity_CreateMyAddress` @@ -694,6 +693,8 @@ AddressResponse { |---:|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 409 | `IDENTITY.ADDRESS_LIMIT_REACHED` | 本人地址数量已达到 20 条上限 | #### 业务规则与并发 @@ -716,7 +717,7 @@ AddressResponse { - 负责人:唐宇昊 - 关联数据表:DB003 - 当前状态:已定义 -- 用途:买家或商家编辑本人地址;非本人地址返回 404。 +- 用途:买家编辑本人地址;非本人地址返回 404。 - 方法与路径:`PATCH /api/users/me/addresses/{addressId}` - operationId:`Identity_UpdateMyAddress` @@ -737,6 +738,7 @@ AddressResponse { |---:|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `RESOURCE.NOT_FOUND` | 地址不存在或不属于当前用户 | #### 业务规则与并发 @@ -760,7 +762,7 @@ AddressResponse { - 负责人:唐宇昊 - 关联数据表:DB003 - 当前状态:已定义 -- 用途:买家或商家删除本人地址;默认地址被删除时不自动指定其他地址。 +- 用途:买家删除本人地址;默认地址被删除时不自动指定其他地址。 - 方法与路径:`DELETE /api/users/me/addresses/{addressId}` - operationId:`Identity_DeleteMyAddress` @@ -778,13 +780,12 @@ AddressResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 404 | `RESOURCE.NOT_FOUND` | 地址不存在或不属于当前用户 | -| 409 | `IDENTITY.ADDRESS_IN_USE_BY_ORDER` | 该地址被未完成订单引用,需要先迁移或完成订单 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | #### 业务规则与并发 - 删除默认地址后不自动指定其他默认地址;下单时由买家明确确认。 -- 幂等:已删除地址再次删除返回 204,不报错。 +- 幂等:本人地址不存在、已删除或不属于当前买家时统一返回 204,不泄露地址是否存在或归属;历史订单使用地址快照,不阻止删除当前地址记录。 #### 缓存、事件或外部依赖 @@ -794,6 +795,7 @@ AddressResponse { - 删除非默认地址 → 204,列表更新。 - 删除默认地址 → 204,列表无默认地址标记。 +- 重复删除或传入他人地址 ID → 204,不泄露资源归属。 ### A014 设置默认地址 @@ -802,7 +804,7 @@ AddressResponse { - 负责人:唐宇昊 - 关联数据表:DB003 - 当前状态:已定义 -- 用途:买家或商家将本人某条地址设为默认;同一用户最多一个默认地址。 +- 用途:买家将本人某条地址设为默认;同一用户最多一个默认地址。 - 方法与路径:`POST /api/users/me/addresses/{addressId}/default` - operationId:`Identity_SetDefaultAddress` @@ -821,6 +823,7 @@ AddressResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `RESOURCE.NOT_FOUND` | 地址不存在或不属于当前用户 | #### 业务规则与并发 @@ -885,6 +888,7 @@ AdminUserListResponse { - 永远不返回管理员账号;过滤条件 `role IN ('Buyer','Merchant')`。 - 列表响应只返回管理操作所需字段;不返回密码哈希、内部审计、登录态。 - 手机号使用掩码 `138****8888` 形式。 +- `AdminUserResponse.isDefaultMerchant` 仅用于说明单店默认运营账号及禁用按钮原因;买家固定为 `false`。 #### 缓存、事件或外部依赖 @@ -911,13 +915,7 @@ AdminUserListResponse { - Route 参数:`userId: uuid` - Header:`Authorization: Bearer `(必填,角色 Admin) -- Body: - -```text -DisableUserRequest { - reason?: string // 可选,0~200 字 -} -``` +- Body:无。 #### 成功响应 @@ -930,6 +928,7 @@ AdminUserResponse { username: string phoneMasked: string role: "Buyer" | "Merchant" + isDefaultMerchant: boolean status: "Active" | "Disabled" updatedAt: string } @@ -939,28 +938,32 @@ AdminUserResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是管理员 | | 404 | `RESOURCE.NOT_FOUND` | 目标账号不存在或不是买家/商家 | -| 409 | `RESOURCE.CONFLICT` | 当前账号已处于禁用状态 | +| 409 | `IDENTITY.DEFAULT_MERCHANT_PROTECTED` | 目标是本期唯一默认商家运营账号,不允许直接禁用 | +| 409 | `IDENTITY.MERCHANT_HAS_ACTIVE_WORK` | 非默认商家仍有关联待履约订单、售后窗口/申请或未结束秒杀活动 | | 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态共享不可用 | #### 业务规则与并发 -- 条件更新:`UPDATE users SET status='Disabled', token_version=token_version+1 WHERE user_id=:uid AND role IN ('Buyer','Merchant') AND status='Active'`;影响行数为 0 时按 409 处理。 -- 禁用成功后通过 `auth:user:{userId}:tokenVersion` 提升版本号;Redis 中保留的令牌记录按版本失效。 +- 条件更新:仅当目标为 `Active` 时改为 `Disabled` 并提升 `tokenVersion`;目标已是 `Disabled` 时返回当前禁用结果,不再次提升版本号。 +- Identity 必须配置且最多只能有一个 `isDefaultMerchant=true` 的启用商家账号;该账号负责普通订单默认归属,本期 A016 不提供默认账号迁移能力,因此直接禁用返回 409。 +- 禁用其他商家前,通过 Ordering、AfterSales、Seckill 公开应用契约确认不存在待支付/待履约订单、仍在售后期限内的订单、未完成售后申请或未结束活动;存在时拒绝禁用,不自动改写历史归属。 +- 禁用成功后通过 `auth:token-version:{userId}` 提升版本号;Redis 中保留的令牌记录按版本失效。 - 状态变更可追踪:操作人、目标账号、原状态、新状态、时间、`traceId` 写入结构化日志;不写入通用操作审计。 #### 缓存、事件或外部依赖 -- Redis Key:`auth:user:{userId}:tokenVersion`。 +- Redis Key:`auth:token-version:{userId}`。 #### 验证场景 - 禁用正常买家 → 200,旧令牌 401 / `AUTH.TOKEN_REVOKED`。 -- 重复禁用 → 409 / `RESOURCE.CONFLICT`。 +- 重复禁用 → 200,返回当前禁用状态。 - 禁用管理员账号 → 404。 +- 禁用默认商家 → 409 / `IDENTITY.DEFAULT_MERCHANT_PROTECTED`。 +- 禁用仍有待处理业务的非默认商家 → 409 / `IDENTITY.MERCHANT_HAS_ACTIVE_WORK`。 - 禁用过程中 Redis 撤销不可用 → 503,不返回虚假成功。 ### A017 启用账号 @@ -992,11 +995,10 @@ AdminUserResponse { | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是管理员 | | 404 | `RESOURCE.NOT_FOUND` | 目标账号不存在或不是买家/商家 | -| 409 | `RESOURCE.CONFLICT` | 当前账号已处于正常状态 | #### 业务规则与并发 -- 条件更新:`status='Active'`,影响行数为 0 时按 409 处理。 +- 条件更新:仅当目标为 `Disabled` 时改为 `Active`;目标已是 `Active` 时返回当前正常结果。 - 启用不改变 `tokenVersion`;禁用前签发的旧令牌仍不可用,需重新登录。 #### 缓存、事件或外部依赖 @@ -1006,7 +1008,7 @@ AdminUserResponse { #### 验证场景 - 启用已禁用账号 → 200,旧令牌仍 401 / `AUTH.TOKEN_REVOKED`;新登录可用。 -- 启用正常账号 → 409 / `RESOURCE.CONFLICT`。 +- 启用正常账号 → 200,返回当前正常状态。 ### A018 收藏列表 @@ -1241,12 +1243,7 @@ UpdateBrowsingHistorySettingRequest { - HTTP 状态:`200 OK` - 响应 Schema:`BrowsingHistorySettingResponse` -```text -BrowsingHistorySettingResponse { - enabled: boolean - updatedAt: string -} -``` +- 字段定义复用 A022 的同名响应 Schema,本接口不重复定义第二份结构。 #### 失败响应 @@ -1316,7 +1313,7 @@ BrowsingHistorySettingResponse { - 负责人:唐宇昊 - 关联数据表:DB006 - 当前状态:已定义 -- 用途:买家查看商品详情时,记录或更新其最近浏览时间;同一买家同一商品只保留一条记录,由 Catalog 模块的"商品详情查询成功"或前端埋点触发。 +- 用途:买家成功打开已上架商品详情后,由前端显式记录或更新最近浏览时间;A103 商品详情 GET 本身不产生写入副作用。 - 方法与路径:`POST /api/browsing-history/records` - operationId:`Engagement_RecordBrowsingHistory` @@ -1328,7 +1325,6 @@ BrowsingHistorySettingResponse { ```text RecordBrowsingHistoryRequest { productId: uuid // 必填 - viewedAt: string? // 可选,UTC ISO 8601;缺省时取服务端时间。客户端不得伪造未来时间 } ``` @@ -1336,14 +1332,15 @@ RecordBrowsingHistoryRequest { #### 成功响应 -- HTTP 状态:`201 Created`(首次写入)或 `200 OK`(仅更新时间) +- HTTP 状态:`200 OK` - 响应 Schema:`BrowsingHistoryResponse` ```text BrowsingHistoryResponse { productId: uuid - viewedAt: string // UTC ISO 8601 - expired: boolean // 达到记录上限被清理时返回 true + recorded: boolean // 开关关闭时为 false + viewedAt: string? // recorded=true 时返回服务端生成的 UTC 时间 + trimmedCount: integer // 因超过上限而移除的最早记录数 } ``` @@ -1353,32 +1350,31 @@ BrowsingHistoryResponse { |---:|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 已登录用户令牌无效 | -| 403 | `ENGAGEMENT.BROWSING_HISTORY_DISABLED` | 当前买家已关闭浏览记录开关 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在或未上架 | | 429 | `COMMON.RATE_LIMITED` | 同一买家短时间内高频记录浏览 | #### 业务规则与并发 -- 若调用方为已登录买家且其浏览记录开关关闭,返回 `403 / ENGAGEMENT.BROWSING_HISTORY_DISABLED`,不写入任何记录。 +- 浏览记录开关关闭时返回 `200`、`recorded=false`,不写入记录;这属于用户偏好,不是权限错误。 - 游客身份不持久化记录;本接口仅 BuyerOnly;前端在游客访问商品详情时引导登录,登录后再调用本接口。 -- 同一买家同一商品只保留一条记录;按 `(user_id, product_id)` 唯一约束更新 `viewed_at` 为最新值。 -- 默认单买家最多保留 200 条记录;超出时按 `viewed_at` 由小到大移除多余记录,并在响应 `expired=true` 告知。 -- 商品下架后续访问记录依旧写入;记录保留但不提供购买入口。 -- 触发方(Catalog 详情查询成功或前端埋点)不得伪造未来时间;服务端校正为 `min(viewed_at, NOW())`。 +- 同一买家同一商品只保留一条记录;按 `(user_id, product_id)` 唯一约束写入或更新,`viewedAt` 始终取服务端时间。 +- 默认单买家最多保留 200 条记录;超出时按 `viewed_at` 由小到大移除多余记录,并在 `trimmedCount` 返回本次清理数量。 +- 新写入仅接受当前已上架商品;商品后来下架时保留既有历史记录,并由列表标记为不可购买。 #### 缓存、事件或外部依赖 -- 不缓存;不再写入 PostgreSQL。 +- 不缓存;浏览记录写入 PostgreSQL,商品存在性通过 Catalog 公开应用契约校验,不直接读取 Catalog 内部表。 - 不发布集成事件。 #### 验证场景 -- 已开启开关的买家查看新商品 → 201,记录新增。 -- 同一买家再次查看该商品 → 200,仅更新时间,原有过期记录不重复创建。 -- 关闭开关后调用 → 403 / `ENGAGEMENT.BROWSING_HISTORY_DISABLED`。 +- 已开启开关的买家查看新商品 → 200,`recorded=true`,记录新增。 +- 同一买家再次查看该商品 → 200,仅更新时间,不重复创建。 +- 关闭开关后调用 → 200,`recorded=false`,数据库不新增或更新记录。 - 商家或游客调用 → 403 / `AUTH.FORBIDDEN`。 -- 达到 200 条上限后再记录 → 200,且响应 `expired=true`。 -- 商品下架后调用 → 仍记录并返回 200。 +- 达到 200 条上限后再记录 → 200,`trimmedCount` 大于 0。 +- 商品不存在或已下架后调用 → 404;已有历史记录仍保留。 ### A025 查询浏览记录开关 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" index 79f1ec2..3287ccb 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" @@ -1,23 +1,30 @@ # 韦乾强 - 订单模块接口详细定义 > 负责人:韦乾强 -> 模块:Ordering(订单模块)、Merchant后台订单管理 +> 模块:Ordering(含买家订单与商家订单管理) > 接口编号范围:A301~A308 > 编写日期:2026-07-24 +> 版本:v0.1 > 本文件为协作与评审追踪材料;汇总后继续保留,实现、OpenAPI 和联调以 `../接口设计.md` 为准 +## 修订记录 + +| 版本 | 日期 | 修改人 | 修改说明 | +|---|---|---|---| +| v0.1 | 2026-07-24 | 韦乾强 | 建立并完善 A301~A308 订单接口;统一 Ordering 模块命名、确认收货、秒杀订单查询复用与幂等规则 | + ## 接口清单 | 编号 | 模块 | 需求编号 | 名称 | 方法 | 路径 | operationId | 请求Schema | 响应Schema | 鉴权 | 关联DB | 状态 | |---|---|---|---|---|---|---|---|---|---|---|---|---| -| A301 | Ordering | F08 | 提交订单 | POST | /api/orders | Ordering_CreateOrder | CreateOrderRequest | CreateOrderResponse | BuyerOnly | DB001,DB003 | 部分定义 | -| A302 | Ordering | F09 | 查询订单列表 | GET | /api/orders | Ordering_GetOrders | - | OrderListResponse | BuyerOnly | DB001 | 部分定义 | -| A303 | Ordering | F09 | 查询订单详情 | GET | /api/orders/{orderId} | Ordering_GetOrderById | - | OrderDetailResponse | BuyerOnly | DB001,DB003 | 部分定义 | -| A304 | Ordering | F09 | 取消订单 | POST | /api/orders/{orderId}/cancel | Ordering_CancelOrder | - | CancelOrderResponse | BuyerOnly | DB001,DB003 | 部分定义 | -| A305 | Merchant | F12 | 商家查询订单列表 | GET | /api/merchant/orders | Merchant_GetOrders | - | MerchantOrderListResponse | MerchantOnly | DB001,DB003 | 部分定义 | -| A306 | Merchant | F12 | 商家查询订单详情 | GET | /api/merchant/orders/{orderId} | Merchant_GetOrderById | - | MerchantOrderDetailResponse | MerchantOnly | DB001,DB003 | 部分定义 | -| A307 | Merchant | F12 | 商家发货 | POST | /api/merchant/orders/{orderId}/ship | Merchant_ShipOrder | ShipOrderRequest | ShipOrderResponse | MerchantOnly | DB001 | 部分定义 | -| A308 | Ordering | F09 | 买家确认收货 | POST | /api/orders/{orderId}/confirm | Ordering_ConfirmOrder | - | ConfirmOrderResponse | BuyerOnly | DB001 | 部分定义 | +| A301 | Ordering | F08 | 提交订单 | POST | /api/orders | Ordering_CreateOrder | CreateOrderRequest | CreateOrderResponse | BuyerOnly | DB061,DB062 | 部分定义 | +| A302 | Ordering | F09、C01 | 查询订单列表 | GET | /api/orders | Ordering_ListOrders | - | OrderListResponse | BuyerOnly | DB061,DB062 | 部分定义 | +| A303 | Ordering | F09、C01 | 查询订单详情 | GET | /api/orders/{orderId} | Ordering_GetOrder | - | OrderDetailResponse | BuyerOnly | DB061,DB062 | 部分定义 | +| A304 | Ordering | F09 | 取消订单 | POST | /api/orders/{orderId}/cancel | Ordering_CancelOrder | - | CancelOrderResponse | BuyerOnly | DB061,DB062 | 部分定义 | +| A305 | Ordering | F12 | 商家查询订单列表 | GET | /api/merchant/orders | Ordering_ListMerchantOrders | - | MerchantOrderListResponse | MerchantOnly | DB061,DB062 | 部分定义 | +| A306 | Ordering | F12 | 商家查询订单详情 | GET | /api/merchant/orders/{orderId} | Ordering_GetMerchantOrder | - | MerchantOrderDetailResponse | MerchantOnly | DB061,DB062 | 部分定义 | +| A307 | Ordering | F12 | 商家发货 | POST | /api/merchant/orders/{orderId}/ship | Ordering_ShipOrder | ShipOrderRequest | ShipOrderResponse | MerchantOnly | DB061 | 部分定义 | +| A308 | Ordering | F09 | 买家确认收货 | POST | /api/orders/{orderId}/confirm-receipt | Ordering_ConfirmReceipt | - | ConfirmReceiptResponse | BuyerOnly | DB061 | 部分定义 | --- @@ -26,13 +33,13 @@ - **模块 / Tag**:Ordering - **需求编号**:F09 - **负责人**:韦乾强 -- **关联数据表**:DB001(orders) +- **关联数据表**:DB061(orders) - **当前状态**:部分定义 - **用途**:买家确认已收到商品,将订单状态从 `Shipped` 变更为 `Completed` -- **方法与路径**:`POST /api/orders/{orderId}/confirm` -- **operationId**:`Ordering_ConfirmOrder` +- **方法与路径**:`POST /api/orders/{orderId}/confirm-receipt` +- **operationId**:`Ordering_ConfirmReceipt` - **请求Schema**:无 -- **响应Schema**:`ConfirmOrderResponse` +- **响应Schema**:`ConfirmReceiptResponse` - **身份与Policy**:BuyerOnly - **资源归属**:订单必须属于当前买家 - **幂等要求**:以订单号为幂等键,重复确认返回成功 @@ -47,7 +54,7 @@ ### 成功响应 - **HTTP状态**:`200 OK` -- **响应Schema**:`ConfirmOrderResponse` +- **响应Schema**:`ConfirmReceiptResponse` - **示例**: ```json { @@ -66,20 +73,21 @@ | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| +| 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 404 | ORDER.NOT_FOUND | 订单不存在 | | 403 | ORDER.ACCESS_DENIED | 订单不属于当前用户 | | 409 | ORDER.INVALID_STATUS | 订单状态不允许确认收货(只有已发货可确认) | ### 业务规则与并发 -1. 只有 `Shipped` 状态可确认收货 -2. 使用条件更新 `WHERE status = 'Shipped'` 保证幂等 -3. 记录 `completed_at` 和 `completed_by = 'BUYER_CONFIRMED'` -4. 确认收货后触发评价入口开放(若 X01 已实现) +1. `Shipped` 状态执行首次确认;已是 `Completed` 且 `completedBy = BUYER_CONFIRMED` 时返回现有 `200` 结果。 +2. 已由 Worker 自动完成或处于其他不允许状态时返回 409;条件更新为 0 后读取现状再区分幂等重放与状态冲突。 +3. 使用条件更新 `WHERE status = 'Shipped'` 防止重复副作用,记录 `completed_at` 和 `completed_by = 'BUYER_CONFIRMED'`。 +4. 确认收货后触发评价入口开放(若 X01 已实现)。 ### 缓存、事件或外部依赖 -- 发布 `OrderConfirmedEvent` 到 Outbox +- 发布统一的 `OrderCompletedIntegrationEvent` 到 Outbox;买家确认和 Worker 自动完成共用同一“订单已完成”事实 ### 验证场景 @@ -95,7 +103,7 @@ - **模块 / Tag**:Ordering - **需求编号**:F08 - **负责人**:韦乾强 -- **关联数据表**:DB001(orders)、DB003(order_items) +- **关联数据表**:DB061(orders)、DB062(order_items) - **当前状态**:部分定义 - **用途**:买家选择购物车商品和收货地址提交订单,系统原子扣减库存并返回订单号 - **方法与路径**:`POST /api/orders` @@ -118,14 +126,13 @@ ```json { "addressId": "uuid", - "cartItemIds": ["uuid"], - "idempotencyKey": "uuid" + "cartItemIds": ["uuid"] } ``` - **校验规则**: - `addressId`:必填,UUID格式,必须属于当前买家 - `cartItemIds`:必填,非空数组,每个元素为UUID格式 - - `idempotencyKey`:必填,UUID格式 + - `Idempotency-Key` 只从 Header 读取,Body 不重复传递 ### 成功响应 @@ -141,7 +148,8 @@ "orderNo": "ORD20260724001", "totalAmount": 299.00, "status": "PendingPayment", - "createdAt": "2026-07-24T10:00:00Z" + "createdAt": "2026-07-24T10:00:00Z", + "expiresAt": "2026-07-24T10:30:00Z" } } ``` @@ -153,9 +161,12 @@ | 400 | ORDER.INVALID_PARAM | 参数格式错误 | | 400 | ORDER.EMPTY_CART_ITEMS | 购物车商品列表为空 | | 400 | ORDER.INVALID_ADDRESS | 收货地址无效或不归属当前用户 | +| 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | +| 403 | AUTH.FORBIDDEN | 当前账号不是买家 | | 409 | ORDER.STOCK_INSUFFICIENT | 商品库存不足 | | 409 | ORDER.ITEM_NOT_AVAILABLE | 商品已下架或不可售 | -| 409 | ORDER.IDEMPOTENT_CONFLICT | 幂等键重复,返回原订单 | +| 409 | IDEMPOTENCY.KEY_REUSED | 同一幂等键被用于不同请求内容 | +| 503 | ORDER.DEFAULT_MERCHANT_UNAVAILABLE | Identity 未能解析唯一且启用的默认商家运营账号 | ### 业务规则与并发 @@ -163,11 +174,12 @@ 2. 库存扣减使用条件更新 `WHERE stock >= quantity`,避免超卖 3. 订单金额由服务端计算,不接受客户端传入 4. 订单项保存商品名称、图片、单价快照 +5. 本期是单店 B2C,不拆多商户子订单;普通订单创建时通过 Identity 公开应用契约解析唯一且启用的默认商家运营账号,并把 `assignedMerchantUserId` 保存为订单处理与通知归属。未配置、配置重复或账号不可用时整单失败,不创建无人处理的订单。 ### 缓存、事件或外部依赖 -- 发布 `OrderCreatedEvent` 到 Outbox -- 依赖 DB001(orders)、DB003(order_items)、DB004(products) +- 发布 `OrderCreatedIntegrationEvent` 到 Outbox +- 订单事实写入 DB061 `orders`、DB062 `order_items`;地址、购物车、商品、库存和默认商家运营账号通过对应模块公开应用契约协作,不直接访问其他模块内部表 ### 验证场景 @@ -183,11 +195,11 @@ - **模块 / Tag**:Ordering - **需求编号**:F09 - **负责人**:韦乾强 -- **关联数据表**:DB001(orders) +- **关联数据表**:DB061(orders)、DB062(order_items) - **当前状态**:部分定义 -- **用途**:买家分页查询自己的订单列表,支持按状态筛选 +- **用途**:买家分页查询自己的普通或秒杀订单,支持按状态、订单来源、秒杀活动和创建时间筛选 - **方法与路径**:`GET /api/orders` -- **operationId**:`Ordering_GetOrders` +- **operationId**:`Ordering_ListOrders` - **请求Schema**:无 - **响应Schema**:`OrderListResponse` - **身份与Policy**:BuyerOnly @@ -201,6 +213,9 @@ - `page`(可选,默认1):页码 - `pageSize`(可选,默认10,上限50):每页条数 - `status`(可选):筛选订单状态,`PendingPayment`/`Paid`/`Shipped`/`Completed`/`Cancelled` + - `orderType`(可选):`Normal` / `Seckill` + - `seckillActivityId`(可选,UUID):按秒杀活动筛选;传入时 `orderType` 固定按 `Seckill` 处理 + - `createdFrom`、`createdTo`(可选,UTC ISO 8601):创建时间范围 - **Header**:`Authorization: Bearer `(必需) - **Body**:无 @@ -218,15 +233,19 @@ { "orderId": "uuid", "orderNo": "ORD20260724001", + "orderType": "Seckill", + "seckillActivityId": "uuid", + "seckillActivityName": "暑期秒杀", "status": "PendingPayment", "totalAmount": 299.00, "createdAt": "2026-07-24T10:00:00Z", + "expiresAt": "2026-07-24T10:30:00Z", "itemSummary": "商品A x1,商品B x2" } ], "page": 1, "pageSize": 10, - "totalCount": 25, + "total": 25, "totalPages": 3 } } @@ -237,11 +256,14 @@ | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| | 400 | ORDER.INVALID_PAGE_PARAM | 分页参数非法 | +| 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | +| 403 | AUTH.FORBIDDEN | 当前账号不是买家 | ### 业务规则与并发 -1. 订单按创建时间倒序排列 -2. `itemSummary`最多展示3个商品名称,多的显示"+X件" +1. 订单按 `createdAt desc, orderId desc` 稳定排序。 +2. `itemSummary` 最多展示 3 个商品名称,多的显示“+X件”。 +3. A229 已取消;秒杀订单列表由本接口通过 `orderType=Seckill` 或 `seckillActivityId` 查询,不建立第二套订单查询事实。 ### 缓存、事件或外部依赖 @@ -260,11 +282,11 @@ - **模块 / Tag**:Ordering - **需求编号**:F09 - **负责人**:韦乾强 -- **关联数据表**:DB001(orders)、DB003(order_items) +- **关联数据表**:DB061(orders)、DB062(order_items) - **当前状态**:部分定义 - **用途**:买家查看单个订单的完整详情 - **方法与路径**:`GET /api/orders/{orderId}` -- **operationId**:`Ordering_GetOrderById` +- **operationId**:`Ordering_GetOrder` - **请求Schema**:无 - **响应Schema**:`OrderDetailResponse` - **身份与Policy**:BuyerOnly @@ -290,9 +312,17 @@ "data": { "orderId": "uuid", "orderNo": "ORD20260724001", + "orderType": "Seckill", + "seckill": { + "activityId": "uuid", + "activityName": "暑期秒杀", + "originalUnitPrice": 399.00, + "seckillUnitPrice": 199.00 + }, "status": "PendingPayment", "totalAmount": 299.00, "createdAt": "2026-07-24T10:00:00Z", + "expiresAt": "2026-07-24T10:30:00Z", "addressSnapshot": { "receiverName": "张三", "phone": "138****8888", @@ -312,8 +342,7 @@ } ], "statusHistory": [ - {"status": "PendingPayment", "time": "2026-07-24T10:00:00Z"}, - {"status": "Paid", "time": "2026-07-24T10:05:00Z"} + {"status": "PendingPayment", "time": "2026-07-24T10:00:00Z"} ], "availableActions": ["cancel"] } @@ -324,14 +353,16 @@ | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| +| 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 404 | ORDER.NOT_FOUND | 订单不存在 | | 403 | ORDER.ACCESS_DENIED | 订单不属于当前用户 | ### 业务规则与并发 -1. 订单项为快照,包含下单时的商品名称、图片、单价 -2. 地址为快照,包含下单时的收货信息 -3. `availableActions`根据当前状态展示可执行操作 +1. 订单项为快照,包含下单时的商品名称、图片和成交单价。 +2. 地址为快照,包含下单时的收货信息。 +3. 普通订单 `orderType=Normal` 且 `seckill=null`;秒杀订单返回活动 ID、活动名称、原价和秒杀价快照,承接已取消的 A230。 +4. `availableActions` 根据当前状态展示可执行操作。 ### 缓存、事件或外部依赖 @@ -350,7 +381,7 @@ - **模块 / Tag**:Ordering - **需求编号**:F09 - **负责人**:韦乾强 -- **关联数据表**:DB001(orders)、DB003(order_items)、DB004(products) +- **关联数据表**:DB061(orders)、DB062(order_items) - **当前状态**:部分定义 - **用途**:买家取消自己待支付的订单,触发库存回补 - **方法与路径**:`POST /api/orders/{orderId}/cancel` @@ -390,21 +421,22 @@ | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| +| 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 404 | ORDER.NOT_FOUND | 订单不存在 | | 403 | ORDER.ACCESS_DENIED | 订单不属于当前用户 | -| 409 | ORDER.INVALID_STATUS | 订单状态不允许取消(已支付/已发货/已完成/已取消) | +| 409 | ORDER.INVALID_STATUS | 订单状态不允许取消(已支付/已发货/已完成);已取消返回现有成功结果 | ### 业务规则与并发 -1. 只有 `PendingPayment` 状态可取消 -2. 取消与库存回补在同一事务内完成 -3. 使用条件更新 `WHERE status = 'PendingPayment'` 保证幂等 -4. `cancelReason` 记录为 `BUYER_CANCELLED` +1. `PendingPayment` 状态执行取消;订单已经是 `Cancelled` 时返回现有 `200` 结果,不重复回补库存;其他状态返回 409。 +2. 取消与库存回补通过公开应用契约处于同一受控事务:普通订单回补 Catalog,秒杀订单按 `seckillActivityId` 回补 Seckill 原活动库存并释放对应限购名额。 +3. 使用 `WHERE status = 'PendingPayment'` 条件更新和库存侧 `(orderId, orderItemId, reason)` 唯一幂等键,保证并发时最多取消和回补一次。 +4. `cancelReason` 记录为 `BUYER_CANCELLED`;C03 超时取消复用同一取消用例,仅将原因改为 `TIMEOUT`。 ### 缓存、事件或外部依赖 -- 发布 `OrderCancelledEvent` 到 Outbox -- 库存回补操作 DB004(products) +- 发布 `OrderCancelledIntegrationEvent` 到 Outbox +- 库存回补根据订单库存通道调用 Catalog 或 Seckill 公开应用契约,不直接修改其他模块内部表 ### 验证场景 @@ -417,18 +449,18 @@ ## A305 商家查询订单列表 -- **模块 / Tag**:Merchant +- **模块 / Tag**:Ordering - **需求编号**:F12 - **负责人**:韦乾强 -- **关联数据表**:DB001(orders)、DB003(order_items) +- **关联数据表**:DB061(orders)、DB062(order_items) - **当前状态**:部分定义 -- **用途**:商家分页查询本店订单,支持按状态筛选 +- **用途**:商家分页查询分配给当前运营账号的订单,支持按状态筛选 - **方法与路径**:`GET /api/merchant/orders` -- **operationId**:`Merchant_GetOrders` +- **operationId**:`Ordering_ListMerchantOrders` - **请求Schema**:无 - **响应Schema**:`MerchantOrderListResponse` - **身份与Policy**:MerchantOnly -- **资源归属**:只返回当前商家的订单 +- **资源归属**:只返回 `assignedMerchantUserId` 等于当前账号的订单 - **幂等要求**:GET请求天然幂等 ### 请求 @@ -437,7 +469,7 @@ - **Query参数**: - `page`(可选,默认1):页码 - `pageSize`(可选,默认10,上限50):每页条数 - - `status`(可选):筛选订单状态 + - `status`(可选):`PendingPayment` / `Paid` / `Shipped` / `Completed` / `Cancelled` - **Header**:`Authorization: Bearer `(必需) - **Body**:无 @@ -464,7 +496,7 @@ ], "page": 1, "pageSize": 10, - "totalCount": 15, + "total": 15, "totalPages": 2 } } @@ -475,10 +507,12 @@ | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| | 400 | ORDER.INVALID_PAGE_PARAM | 分页参数非法 | +| 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | +| 403 | AUTH.FORBIDDEN | 当前账号不是商家 | ### 业务规则与并发 -1. 只返回与当前商家商品相关的订单 +1. 只返回 `assignedMerchantUserId = currentUserId` 的订单;本项目不按商户租户拆分商品或结算。 2. 订单按创建时间倒序排列 ### 缓存、事件或外部依赖 @@ -494,18 +528,18 @@ ## A306 商家查询订单详情 -- **模块 / Tag**:Merchant +- **模块 / Tag**:Ordering - **需求编号**:F12 - **负责人**:韦乾强 -- **关联数据表**:DB001(orders)、DB003(order_items) +- **关联数据表**:DB061(orders)、DB062(order_items) - **当前状态**:部分定义 -- **用途**:商家查看本店订单的完整详情 +- **用途**:商家查看分配给当前运营账号的订单详情 - **方法与路径**:`GET /api/merchant/orders/{orderId}` -- **operationId**:`Merchant_GetOrderById` +- **operationId**:`Ordering_GetMerchantOrder` - **请求Schema**:无 - **响应Schema**:`MerchantOrderDetailResponse` - **身份与Policy**:MerchantOnly -- **资源归属**:订单必须属于当前商家的商品 +- **资源归属**:订单的 `assignedMerchantUserId` 必须等于当前账号 - **幂等要求**:GET请求天然幂等 ### 请求 @@ -542,14 +576,21 @@ }, "items": [ { + "orderItemId": "uuid", "productId": "uuid", "productName": "商品A", "imageUrl": "https://...", "unitPrice": 199.00, "quantity": 1, + "refundedQuantity": 0, + "fulfillableQuantity": 1, "subtotal": 199.00 } ], + "fulfillment": { + "state": "ReadyToShip", + "blockReason": null + }, "availableActions": ["ship"] } } @@ -559,40 +600,43 @@ | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| +| 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 404 | ORDER.NOT_FOUND | 订单不存在 | -| 403 | ORDER.ACCESS_DENIED | 订单不归属当前商家 | +| 403 | ORDER.ACCESS_DENIED | 订单未分配给当前商家运营账号 | ### 业务规则与并发 -1. 只返回与当前商家商品相关的订单项 -2. `availableActions`根据当前状态展示可执行操作 +1. 只返回分配给当前运营账号的整单及其订单项,不把一个订单拆成多商户子订单。 +2. `fulfillment.state` 是根据订单状态和 AfterSales 履约快照得到的展示字段,可取 `ReadyToShip`、`BlockedByAfterSales`、`PartiallyRefunded`、`FullyRefunded`、`Shipped`、`Completed`;它不是新的订单核心状态。 +3. `refundedQuantity` 与 `fulfillableQuantity` 由售后终态数量计算;存在处理中售后或无剩余可履约数量时,`availableActions` 不返回 `ship`。 ### 缓存、事件或外部依赖 -无 +- 通过 AfterSales 公开应用契约查询当前订单的履约阻断状态和各订单项累计已退款数量;不直接读取售后内部表。 ### 验证场景 1. 正常查询:返回完整订单详情 2. 订单不存在:返回404 3. 跨商家访问:返回403 +4. 存在处理中售后或部分退款:返回可理解的履约状态和准确剩余数量,不错误展示发货入口 --- ## A307 商家发货 -- **模块 / Tag**:Merchant +- **模块 / Tag**:Ordering - **需求编号**:F12 - **负责人**:韦乾强 -- **关联数据表**:DB001(orders) +- **关联数据表**:DB061(orders)、DB062(order_items) - **当前状态**:部分定义 - **用途**:商家对已支付订单执行发货操作 - **方法与路径**:`POST /api/merchant/orders/{orderId}/ship` -- **operationId**:`Merchant_ShipOrder` +- **operationId**:`Ordering_ShipOrder` - **请求Schema**:`ShipOrderRequest` - **响应Schema**:`ShipOrderResponse` - **身份与Policy**:MerchantOnly -- **资源归属**:订单必须属于当前商家的商品 +- **资源归属**:订单的 `assignedMerchantUserId` 必须等于当前账号 - **幂等要求**:以订单号为幂等键,重复发货返回成功 ### 请求 @@ -625,7 +669,13 @@ "status": "Shipped", "shippedAt": "2026-07-24T12:00:00Z", "expressCompany": "顺丰速运", - "trackingNo": "SF1234567890" + "trackingNo": "SF1234567890", + "shippedItems": [ + { + "orderItemId": "uuid", + "quantity": 1 + } + ] } } ``` @@ -634,19 +684,26 @@ | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| +| 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 404 | ORDER.NOT_FOUND | 订单不存在 | -| 403 | ORDER.ACCESS_DENIED | 订单不归属当前商家 | +| 403 | ORDER.ACCESS_DENIED | 订单未分配给当前商家运营账号 | | 409 | ORDER.INVALID_STATUS | 订单状态不允许发货(只有已支付可发货) | +| 409 | ORDER.AFTER_SALES_IN_PROGRESS | 订单存在会影响履约的处理中售后申请 | +| 409 | ORDER.NO_FULFILLABLE_ITEMS | 全部订单项均已退款,没有剩余可发货数量 | ### 业务规则与并发 -1. 只有 `Paid` 状态可发货 -2. 使用条件更新 `WHERE status = 'Paid'` 保证幂等 -3. 记录发货时间、物流公司和物流单号 +1. `Paid` 状态执行首次发货;已经是 `Shipped` 且物流公司、单号与首次请求一致时返回现有 `200` 结果。 +2. 已是 `Shipped` 但物流载荷不同,或处于其他不允许状态时返回 409;条件更新为 0 后必须读取现状再判定,不能把所有重复请求都当错误。 +3. 使用条件更新 `WHERE status = 'Paid'` 防止重复副作用,并记录发货时间、物流公司和物流单号。 +4. 发货前通过 AfterSales 公开应用契约取得履约快照。`PendingReview`、`PendingReturn`、`PendingReceipt`、`Refunding`、`RefundFailed` 等仍可能改变履约结果的申请阻断发货;`Rejected`、`Cancelled` 不阻断。 +5. `Refunded` 数量从原购买数量中扣除;仍有剩余数量时只发出剩余可履约数量,并在响应 `shippedItems` 中返回实际发货明细;全部数量均已退款时拒绝发货。 +6. A307 与 A412 提交售后必须先通过 Ordering 公开应用契约在同一 PostgreSQL 事务中锁定同一 `orders` 行并取得最新履约快照,锁保持到各自业务写入提交;禁止“先查询、后另开事务更新”。若发货先提交,售后按已发货规则重新判断;若售后申请先提交,发货必须看到占用结果并按上述规则处理。 ### 缓存、事件或外部依赖 -- 发布 `OrderShippedEvent` 到 Outbox +- 发布 `OrderShippedIntegrationEvent` 到 Outbox +- 在事务内锁定 Ordering 订单行后调用 AfterSales 的履约查询公开应用契约;Ordering 不直接读取或修改售后内部表。 ### 验证场景 @@ -654,6 +711,8 @@ 2. 重复发货:返回幂等成功 3. 订单未支付:返回409 4. 跨商家发货:返回403 +5. 存在处理中售后:返回409且订单仍为Paid +6. 部分退款完成:仅发出剩余数量;全部退款完成:返回无可履约商品 --- @@ -665,17 +724,17 @@ 1. **超时时间配置**:订单超时时间通过配置项 `OrderTimeoutMinutes` 管理,默认30分钟 2. **扫描策略**:Worker定时扫描 `PendingPayment` 状态且 `created_at + timeout < now()` 的订单 -3. **取消事务**:在同一事务内完成状态变更 `PendingPayment → Cancelled`、库存回补、写入 `cancelled_at` 和 `cancel_reason = 'TIMEOUT'` +3. **取消事务**:复用 A304 的内部取消用例,在同一受控事务内完成 `PendingPayment → Cancelled`、按普通/秒杀原通道回补库存并释放秒杀限购名额、写入 `cancelled_at` 和 `cancel_reason = 'TIMEOUT'` 4. **幂等保证**:使用条件更新 `WHERE status = 'PendingPayment'`,同一订单多次扫描只有一次成功 5. **支付竞争**:与M05支付并发时,条件更新确保只有一个成功 -### 事件消费 +### 任务触发 -- 消费 `OrderCreatedEvent`(由M04-01发布)触发后续超时跟踪 +- `Mall.Worker` 周期扫描 `expiresAt <= now` 且仍为 `PendingPayment` 的订单;不依赖进程内定时器或消费“订单创建”事件保存唯一任务事实 ### 事件发布 -- 发布 `OrderCancelledEvent`(`cancel_reason = 'TIMEOUT'`)到Outbox,供给M09站内消息 +- 发布 `OrderCancelledIntegrationEvent`(`cancelReason = 'TIMEOUT'`)到 Outbox,供 M09 站内消息消费 ### 关键实现点 @@ -686,7 +745,25 @@ ### 验证场景 -1. 超时订单被自动取消,库存回补 +1. 普通与秒杀超时订单均被自动取消并回补原库存通道 2. 买家在超时前支付成功,取消被跳过 3. 并发取消与支付只有一个成功 4. Worker重启后继续扫描,不漏扫 + +## M04-04 发货超时自动完成(Worker 内部契约) + +> 本任务由 `Mall.Worker` 执行,不提供 HTTP API,也不占用 Axxx 编号;买家主动确认仍使用 A308。 + +### 业务规则 + +1. 周期扫描 `Shipped` 且 `shippedAt + 7 days <= now()` 的订单。 +2. 复用 A308 的完成订单用例,使用 `WHERE status = 'Shipped'` 条件更新为 `Completed`,并记录 `completedAt`、`completedBy = 'Auto'`。 +3. 成功后只写一次 `OrderCompletedIntegrationEvent` Outbox;与买家主动确认并发时仅一个条件更新成功,失败方读取并返回当前终态,不重复发布事件。 +4. 订单事实和发货时间均来自 PostgreSQL;Worker 重启后继续扫描,不依赖进程内定时器保存唯一任务事实。 + +### 验证场景 + +1. 发货满 7 天且仍为 `Shipped` 的订单被自动推进为 `Completed`。 +2. 买家在 Worker 扫描前主动确认后,Worker 跳过该订单。 +3. 多实例 Worker 与 A308 并发时只产生一次完成状态和一条完成事件。 +4. Worker 重启后继续扫描,不漏掉已到期订单。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" index 760f369..0e82710 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" @@ -3,15 +3,14 @@ > 组别:24级1班第7组 负责人:朱惠惠(zhh) 接口编号区间:`A201`~`A300` > 负责模块:Cart(购物车 CRUD、选中、结算预览、清空)、Seckill(商家活动维护、买家抢购下单;订单查询复用 Ordering 的 A302/A303) > 关联教师验收编号:F07、C01 -> 当前状态:部分定义;清单已给出,详细定义按接口设计 1.20 模板补齐 +> 当前状态:部分定义;清单与详细定义已完成,待数据库、OpenAPI 和交叉评审 > 本文件为协作与评审追踪材料;汇总后继续保留,实现、OpenAPI 和联调以 `../接口设计.md` 为准 ## 修订记录 | 版本 | 日期 | 修改人 | 修改说明 | |---|---|---|---| -| v0.1 | 2026-07-24 | 朱惠惠 | 建立 `A201`~`A230` 接口清单并补齐全部详细定义 | -| v0.2 | 2026-07-24 | 朱惠惠 | 取消 A229/A230 独立契约,秒杀订单查询复用 A302/A303 | +| v0.1 | 2026-07-24 | 朱惠惠 | 建立并完善 `A201`~`A230` 接口清单与详细定义;A229/A230 作为历史占号取消,秒杀订单查询复用 A302/A303 | ## 一、接口清单 @@ -26,14 +25,14 @@ | A207 | Cart | F07 | 清空购物车 | DELETE | `/api/cart` | `Cart_Clear` | 无 | 无(204) | BuyerOnly | DB041 | 已定义 | | A208 | Cart | F07 | 获取结算预览 | GET | `/api/cart/checkout-preview` | `Cart_GetCheckoutPreview` | 无(Query 可选 `cartItemIds`) | `CheckoutPreviewResponse` | BuyerOnly | DB041 | 已定义 | | A220 | Seckill | C01 | 商家创建秒杀活动 | POST | `/api/merchant/seckill-activities` | `Seckill_CreateActivity` | `CreateSeckillActivityRequest` | `SeckillActivityResponse` | MerchantOnly | DB042、DB043 | 已定义 | -| A221 | Seckill | C01 | 商家更新秒杀活动 | PATCH | `/api/seckill-activities/{activityId}` | `Seckill_UpdateActivity` | `UpdateSeckillActivityRequest` | `SeckillActivityResponse` | MerchantOnly | DB042、DB043 | 已定义 | -| A222 | Seckill | C01 | 商家发布秒杀活动 | POST | `/api/seckill-activities/{activityId}/publish` | `Seckill_PublishActivity` | 无 | `SeckillActivityResponse` | MerchantOnly | DB042、DB043 | 已定义 | -| A223 | Seckill | C01 | 商家取消秒杀活动 | POST | `/api/seckill-activities/{activityId}/cancel` | `Seckill_CancelActivity` | `CancelSeckillActivityRequest` | `SeckillActivityResponse` | MerchantOnly | DB042、DB043 | 已定义 | +| A221 | Seckill | C01 | 商家更新秒杀活动 | PATCH | `/api/merchant/seckill-activities/{activityId}` | `Seckill_UpdateActivity` | `UpdateSeckillActivityRequest` | `SeckillActivityResponse` | MerchantOnly | DB042、DB043 | 已定义 | +| A222 | Seckill | C01 | 商家发布秒杀活动 | POST | `/api/merchant/seckill-activities/{activityId}/publish` | `Seckill_PublishActivity` | 无 | `SeckillActivityResponse` | MerchantOnly | DB042、DB043 | 已定义 | +| A223 | Seckill | C01 | 商家取消秒杀活动 | POST | `/api/merchant/seckill-activities/{activityId}/cancel` | `Seckill_CancelActivity` | `CancelSeckillActivityRequest` | `SeckillActivityResponse` | MerchantOnly | DB042、DB043 | 已定义 | | A224 | Seckill | C01 | 商家秒杀活动列表 | GET | `/api/merchant/seckill-activities` | `Seckill_ListMerchantActivities` | 无(Query 分页/筛选) | `SeckillActivityListResponse` | MerchantOnly | DB042、DB043 | 已定义 | -| A225 | Seckill | C01 | 商家秒杀活动详情 | GET | `/api/seckill-activities/{activityId}` | `Seckill_GetMerchantActivityDetail` | 无 | `SeckillActivityDetailResponse` | MerchantOnly | DB042、DB043、DB044 | 已定义 | -| A226 | Seckill | C01 | 买家秒杀活动列表 | GET | `/api/seckill-activities` | `Seckill_ListActiveActivities` | 无(Query 分页) | `SeckillActivityListResponse` | 允许游客 | DB042、DB043 | 已定义 | -| A227 | Seckill | C01 | 买家秒杀活动详情 | GET | `/api/seckill-activities/{activityId}/public` | `Seckill_GetActiveActivityDetail` | 无 | `SeckillActivityDetailResponse` | 允许游客 | DB042、DB043 | 已定义 | -| A228 | Seckill | C01 | 秒杀下单 | POST | `/api/seckill-orders` | `Seckill_PlaceOrder` | `PlaceSeckillOrderRequest` | `PlaceSeckillOrderResponse` | BuyerOnly | DB043、DB044、DB045 | 已定义 | +| A225 | Seckill | C01 | 商家秒杀活动详情 | GET | `/api/merchant/seckill-activities/{activityId}` | `Seckill_GetMerchantActivityDetail` | 无 | `SeckillActivityDetailResponse` | MerchantOnly | DB042、DB043 | 已定义 | +| A226 | Seckill | C01 | 买家秒杀活动列表 | GET | `/api/seckill-activities` | `Seckill_ListActiveActivities` | 无(Query 分页) | `SeckillActivityListResponse` | Anonymous | DB042、DB043 | 已定义 | +| A227 | Seckill | C01 | 买家秒杀活动详情 | GET | `/api/seckill-activities/{activityId}` | `Seckill_GetActiveActivityDetail` | 无 | `SeckillActivityDetailResponse` | Anonymous | DB042、DB043 | 已定义 | +| A228 | Seckill | C01 | 秒杀下单 | POST | `/api/seckill-orders` | `Seckill_PlaceOrder` | `PlaceSeckillOrderRequest` | `PlaceSeckillOrderResponse` | BuyerOnly | DB043、DB044、DB061、DB062 | 已定义 | | A229 | Seckill | C01 | 买家秒杀订单列表(取消) | — | — | — | — | — | — | — | 已取消,复用 A302 | | A230 | Seckill | C01 | 买家秒杀订单详情(取消) | — | — | — | — | — | — | — | 已取消,复用 A303 | @@ -43,7 +42,7 @@ - Seckill 业务接口使用 `/api/seckill-activities` 活动根路径;商家维护入口额外加 `/api/merchant` 前缀,与公开购物端入口物理隔离。秒杀订单查询复用 Ordering 的 A302/A303(`/api/orders`、`/api/orders/{orderId}`)。 - `/api/cart/items` 表达购物车条目集合;`/api/cart/items/selection` 为选中状态专用子资源,避免在 GET 之上覆盖副作用。 - 秒杀下单产生的订单与 M04 普通订单共用 `orders` / `order_items` 表与状态机,仅在订单上记录 `seckill_activity_id` 快照;订单查询统一复用 A302/A303,不建立平行订单接口。 -- A225 商家详情与 A227 买家详情返回字段范围不同:A225 含商家内部字段(取消原因、回补策略),A227 仅返回公开可见字段。 +- A225 商家详情使用 `/api/merchant` 前缀并返回经营字段;A227 使用公开路径且仅返回公开可见字段。 ## 二、接口详细定义 @@ -113,7 +112,7 @@ CartItemResponse { | 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品已下架或被禁用 | | 409 | `CART.QUANTITY_EXCEEDS_STOCK` | 累加后数量超过商品实时可售库存,返回当前最大允许值 | | 429 | `COMMON.RATE_LIMITED` | 触发限流 | -| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Redis 幂等存储或商品服务暂时不可用 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 商品服务或数据库暂时不可用 | | 500 | `COMMON.INTERNAL_ERROR` | 未处理的服务端错误 | #### 业务规则与并发 @@ -127,7 +126,7 @@ CartItemResponse { #### 缓存、事件或外部依赖 - 不缓存购物车条目;商品价格、库存与上下架状态由 Catalog 模块实时提供。 -- 幂等键记录写入 Redis:`cart:idempotency:{userId}:{key}`,TTL = 5 分钟。 +- 幂等请求指纹与首次结果和购物车变更在同一 PostgreSQL 事务中保存;Redis 只能作为可丢失的读取加速,不承担唯一幂等事实。 - 不发布集成事件。 #### 验证场景 @@ -396,7 +395,7 @@ UpdateCartItemSelectionRequest { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`CartListResponse`(同 A206,按当前选中状态返回完整购物车) +- 响应 Schema:`CartListResponse`(同 A202,按当前选中状态返回完整购物车) #### 失败响应 @@ -410,7 +409,7 @@ UpdateCartItemSelectionRequest { #### 业务规则与并发 - 全选/反选按 `buyer_id = current_user_id` 过滤;失效条目保持未选中,不被强制选中。 -- `SetExplicit` 仅修改 `cartItemIds` 中属于当前买家的条目;他人条目被忽略并计入 `skippedCount`(由响应 `selectedCount`/`availableSelectedCount` 体现)。 +- `SetExplicit` 仅修改 `cartItemIds` 中属于当前买家的条目;不存在或不属于当前买家的 ID 统一忽略,响应不返回数量或明细,避免暴露资源归属。 - 单条切换并发安全:服务端使用条件更新 `WHERE cart_item_id = :id AND buyer_id = current_user_id`。 - 选中状态保存在服务端;前端刷新或重新登录后状态保留。 @@ -424,7 +423,7 @@ UpdateCartItemSelectionRequest { - 反选 → 200,所有可用条目 `isSelected=false`。 - 单选切换某条目 → 200,仅该条目 `isSelected` 变更。 - 尝试选中失效条目 → 409 / `CART.ITEM_UNAVAILABLE`,不修改状态。 -- 跨用户 ID 提交 → 仅本人条目被修改,他人条目被忽略。 +- 跨用户 ID 提交 → 仅本人条目被修改,其他 ID 被静默忽略且响应不泄露数量或明细。 ### A207 清空购物车 @@ -494,9 +493,9 @@ UpdateCartItemSelectionRequest { ```text CheckoutPreviewResponse { items: CartItemResponse[] // 当前可用于结算的条目 - unavailableItems: CartItemResponse[] // 失效条目(不下单但提示买家) + unavailableItems: CartItemResponse[] // 仅当前买家本次选中的失效条目 totalAmount: number // 服务端按实时单价计算的总额 - availableForCheckout: boolean // 是否有至少一条可结算条目 + availableForCheckout: boolean // 选中项非空且全部可结算时为 true } ``` @@ -511,9 +510,9 @@ CheckoutPreviewResponse { #### 业务规则与并发 - 不传 `cartItemIds` 时按 `isSelected=true AND buyer_id = current_user_id` 过滤。 -- 传入 `cartItemIds` 时取交集;不在本人购物车或失效条目归入 `unavailableItems`。 +- 传入 `cartItemIds` 时先与当前买家购物车取交集;不存在或不属于当前买家的 ID 被忽略且不返回任何明细。只有当前买家的失效条目进入 `unavailableItems`。 - `totalAmount` 由服务端实时计算并返回;前端不得自行覆盖金额。 -- 返回 `availableForCheckout=false` 时前端禁用提交订单按钮。 +- 只有选中项非空且全部有效时 `availableForCheckout=true`;只要存在失效项就返回 `false`,前端提示取消勾选或删除失效项后重试。 #### 缓存、事件或外部依赖 @@ -522,9 +521,9 @@ CheckoutPreviewResponse { #### 验证场景 -- 选中 2 条可用 + 1 条失效 → `items=2`、`unavailableItems=1`、`availableForCheckout=true`。 +- 选中 2 条可用 + 1 条失效 → `items=2`、`unavailableItems=1`、`availableForCheckout=false`。 - 全部失效 → `items=[]`、`availableForCheckout=false`,前端禁用提交。 -- 传入他人 `cartItemId` → 归入 `unavailableItems`,不报错也不泄露归属。 +- 传入他人 `cartItemId` → 该 ID 被忽略,不进入任何响应数组,不泄露是否存在或归属。 ### A220 商家创建秒杀活动 @@ -544,12 +543,12 @@ CheckoutPreviewResponse { ```text CreateSeckillActivityRequest { - productId: uuid // 必填,必须是当前商家已上架商品 + productId: uuid // 必填,必须是平台内已上架商品 activityName: string // 必填,1~50 字 seckillPrice: number // 必填,>0 且 < 商品当前上架价 totalStock: integer // 必填,1 ≤ totalStock ≤ 商品当前可售库存 perBuyerLimit: integer // 必填,1 ≤ perBuyerLimit ≤ totalStock - startAt: string // 必填,UTC ISO 8601,≥ now() + 5min + startAt: string // 必填,UTC ISO 8601,≥ now() endAt: string // 必填,UTC ISO 8601,> startAt 且 ≤ startAt + 30d } ``` @@ -557,7 +556,7 @@ CreateSeckillActivityRequest { #### 成功响应 - HTTP 状态:`201 Created` -- Response Header:`Location: /api/seckill-activities/{activityId}` +- Response Header:`Location: /api/merchant/seckill-activities/{activityId}` - 响应 Schema:`SeckillActivityResponse` ```text @@ -587,21 +586,21 @@ SeckillActivityResponse { | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是商家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | -| 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品未上架或不属于当前商家 | +| 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品未上架 | | 409 | `SECKILL.STOCK_EXCEEDS_AVAILABLE` | `totalStock` 超过商品当前可售库存 | -| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 商品服务或分布式锁不可用 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 应用能力或数据库暂时不可用 | #### 业务规则与并发 - 同一商品同一时间段(`startAt`、`endAt` 与已存在活动存在重叠)不允许重复创建;重叠返回 `409 / SECKILL.TIME_WINDOW_CONFLICT`。 - `seckillPrice < originalPrice` 由服务端校验;不接受等于或高于原价的秒杀活动。 -- `startAt ≥ now() + 5min` 避免立刻开始的发布影响压测一致性。 -- 创建活动时同步在 `seckill_inventory`(DB043)写入 `remainingStock=totalStock`、`soldCount=0`、`frozenCount=0`;两者在同一事务。 -- 商品归属:仅当 `product.owner_merchant_id = current_user_id` 才允许创建;越权访问返回 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 +- `startAt ≥ now()`;可以创建立即开始的活动,服务端发布时按当前时间决定进入 `Published` 或直接进入 `Ongoing`。 +- 创建活动时同步在 `seckill_inventory`(DB043)写入计划配额 `remainingStock=totalStock`、`soldCount=0`、`frozenCount=0`;`Draft` 不对买家开放,真正的普通库存划转只在 A222 发布事务中完成。 +- 本项目是单一 B2C 平台,不按商户租户隔离商品;活动记录 `createdByMerchantUserId` 用于操作归属与秒杀订单商家分配。 #### 缓存、事件或外部依赖 -- 活动创建后向 Redis 写入分布式锁 Key:`lock:seckill:activity:create:{productId}`,事务结束释放。 +- 同商品时间窗口冲突在 PostgreSQL 事务内按商品加锁并复核,不能依赖 Redis 锁作为唯一正确性边界。 - 不缓存、不发布集成事件。 #### 验证场景 @@ -609,9 +608,9 @@ SeckillActivityResponse { - 合法参数创建 → 201,状态 `Draft`,库存=总量。 - `totalStock` 超过商品库存 → 409 / `SECKILL.STOCK_EXCEEDS_AVAILABLE`。 - `seckillPrice ≥ originalPrice` → 400 / `COMMON.VALIDATION_FAILED`。 -- `startAt < now() + 5min` → 400 / `COMMON.VALIDATION_FAILED`。 +- `startAt < now()` → 400 / `COMMON.VALIDATION_FAILED`。 - 时间窗口与已存在活动重叠 → 409 / `SECKILL.TIME_WINDOW_CONFLICT`。 -- 尝试绑定他人商品 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 +- 尝试绑定未上架商品 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 ### A221 商家更新秒杀活动 @@ -620,8 +619,8 @@ SeckillActivityResponse { - 负责人:朱惠惠 - 关联数据表:DB042、DB043 - 当前状态:已定义 -- 用途:商家在 `Draft` 或 `Scheduled` 状态下更新秒杀活动参数;`Ongoing`/`Finished`/`Cancelled` 状态不允许修改。 -- 方法与路径:`PATCH /api/seckill-activities/{activityId}` +- 用途:商家在 `Draft` 或 `Published` 状态下更新尚未开始的秒杀活动参数;`Ongoing`/`Ended`/`Cancelled` 状态不允许修改。 +- 方法与路径:`PATCH /api/merchant/seckill-activities/{activityId}` - operationId:`Seckill_UpdateActivity` #### 请求 @@ -636,7 +635,7 @@ UpdateSeckillActivityRequest { seckillPrice?: number // 可选 totalStock?: integer // 可选;只能调大或保持;不得小于已售数量 perBuyerLimit?: integer // 可选 - startAt?: string // 可选;不得早于 now() + 5min + startAt?: string // 可选;不得早于 now() endAt?: string // 可选 } ``` @@ -654,15 +653,16 @@ UpdateSeckillActivityRequest { | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | | 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | -| 409 | `SECKILL.INVALID_STATUS` | 活动已 `Ongoing`/`Finished`/`Cancelled` | +| 409 | `SECKILL.INVALID_STATUS` | 活动已 `Ongoing`/`Ended`/`Cancelled` | | 409 | `SECKILL.STOCK_BELOW_SOLD` | `totalStock` 小于已售数量 | | 409 | `SECKILL.TIME_WINDOW_CONFLICT` | 与其他活动时间窗口重叠 | #### 业务规则与并发 -- 仅允许在 `Draft` 或 `Scheduled` 状态更新;状态字段由 `status='Draft' OR status='Scheduled'` 条件更新保证。 +- 仅允许在 `Draft` 或尚未开始的 `Published` 状态更新;状态字段由 `status IN ('Draft','Published') AND start_at > now()` 条件更新保证。 +- `totalStock` 只允许在 `Draft` 修改;发布后库存配额已经从 Catalog 普通库存划转,不通过本接口调整。 - `totalStock` 只允许调大或保持;调整后必须满足 `remainingStock + soldCount + frozenCount = totalStock`。 -- 修改后 `startAt` 与 `endAt` 必须保持 `startAt ≥ now() + 5min` 与 `endAt > startAt`。 +- 修改后 `startAt` 与 `endAt` 必须保持 `startAt ≥ now()` 与 `endAt > startAt`。 #### 缓存、事件或外部依赖 @@ -682,8 +682,8 @@ UpdateSeckillActivityRequest { - 负责人:朱惠惠 - 关联数据表:DB042、DB043 - 当前状态:已定义 -- 用途:商家将 `Draft` 状态活动提交审核后立即变为 `Scheduled`;系统按 `startAt` 自动推进到 `Ongoing`。 -- 方法与路径:`POST /api/seckill-activities/{activityId}/publish` +- 用途:商家直接发布 `Draft` 活动;本期没有审核流程,服务端按数据库当前时间决定立即进入 `Ongoing` 或先进入 `Published`,后续按 `endAt` 推进到 `Ended`。 +- 方法与路径:`POST /api/merchant/seckill-activities/{activityId}/publish` - operationId:`Seckill_PublishActivity` #### 请求 @@ -695,7 +695,7 @@ UpdateSeckillActivityRequest { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`SeckillActivityResponse`(`status="Scheduled"`) +- 响应 Schema:`SeckillActivityResponse`(`startAt <= databaseNow` 时 `status="Ongoing"`,否则 `status="Published"`) #### 失败响应 @@ -705,14 +705,16 @@ UpdateSeckillActivityRequest { | 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | | 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | | 409 | `SECKILL.INVALID_STATUS` | 活动已发布或已结束 | +| 409 | `SECKILL.TIME_WINDOW_EXPIRED` | 数据库当前时间已经达到或超过 `endAt`,该草稿活动不能再发布 | | 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品已下架,禁止发布 | -| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 缓存写入失败 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 库存划转或数据库暂时不可用 | #### 业务规则与并发 -- 条件更新:`UPDATE ... SET status='Scheduled' WHERE activity_id=:id AND status='Draft' AND owner_merchant_id=:mid`;影响行数为 0 时按 409 处理。 -- 商品已下架时拒绝发布;商家需先恢复上架。 -- 发布成功后刷新 Redis 缓存并预热活动详情 Key;Worker 按 `startAt` 自动推进到 `Ongoing`。 +- 发布事务先读取一次数据库当前时间;若 `now() >= endAt`,立即返回 `SECKILL.TIME_WINDOW_EXPIRED`,不得调用 Catalog 或划转库存。时间有效时,再通过 Catalog 公开应用契约按 `activityId` 幂等地把 `totalStock` 从普通可售库存划转为秒杀配额,并按同一数据库时间把活动从 `Draft` 条件更新为 `Ongoing`(`startAt <= now() < endAt`)或 `Published`(`now() < startAt`);任一步失败整体回滚,不产生双份可售库存。 +- 发布、更新和取消均按 `created_by_merchant_user_id = current_user_id` 校验活动操作归属;这只约束活动创建人,不引入多商户商品租户。 +- 商品已下架或普通可售库存不足时拒绝发布;商家修正商品或活动库存后重试。 +- 事务提交后尽力失效并预热 Redis 活动缓存;缓存失败只影响性能并进入重试,不否定已提交的发布结果。Worker 只需把尚未开始的 `Published` 按 `startAt` 推进为 `Ongoing`,并把到期的 `Ongoing` 推进为 `Ended`。 #### 缓存、事件或外部依赖 @@ -720,7 +722,8 @@ UpdateSeckillActivityRequest { #### 验证场景 -- 草稿活动发布 → 200,状态 `Scheduled`。 +- 未来开始的草稿活动发布 → 200 + `Published`;立即开始的草稿活动发布 → 200 + `Ongoing`;两者普通库存与秒杀配额总量均守恒。 +- 已超过 `endAt` 的草稿活动发布 → 409 / `SECKILL.TIME_WINDOW_EXPIRED`,普通库存和秒杀配额均不变化。 - 重复发布 → 409 / `SECKILL.INVALID_STATUS`。 - 商品已下架 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 @@ -731,8 +734,8 @@ UpdateSeckillActivityRequest { - 负责人:朱惠惠 - 关联数据表:DB042、DB043 - 当前状态:已定义 -- 用途:商家取消 `Draft` / `Scheduled` / `Ongoing` 状态活动;取消后入口立即失效,已存在秒杀订单按既有流程走完;本期不回收已分配库存。 -- 方法与路径:`POST /api/seckill-activities/{activityId}/cancel` +- 用途:商家取消 `Draft` / `Published` / `Ongoing` 状态活动;取消后入口立即失效,已存在秒杀订单按既有流程走完;本期按 C01-FR14 保留已分配库存,不回收到普通库存。 +- 方法与路径:`POST /api/merchant/seckill-activities/{activityId}/cancel` - operationId:`Seckill_CancelActivity` #### 请求 @@ -760,11 +763,11 @@ CancelSeckillActivityRequest { | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | | 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | -| 409 | `SECKILL.INVALID_STATUS` | 活动已 `Finished` 或已 `Cancelled` | +| 409 | `SECKILL.INVALID_STATUS` | 活动已 `Ended` 或已 `Cancelled` | #### 业务规则与并发 -- 条件更新:`status IN ('Draft','Scheduled','Ongoing') → 'Cancelled'`;影响行数为 0 时按 409 处理。 +- 条件更新:`status IN ('Draft','Published','Ongoing') → 'Cancelled'`;影响行数为 0 时按 409 处理。 - 取消时 `remainingStock` 保留为冻结状态,不自动回收到普通商品库存。 - 已存在秒杀订单沿用 M04 状态机;C03 超时取消时回补到原秒杀库存通道。 @@ -795,7 +798,7 @@ CancelSeckillActivityRequest { - Query 参数: - `page`(默认 1) - `pageSize`(默认 10,上限 50) - - `status`(可选,可多值:`Draft` / `Scheduled` / `Ongoing` / `Finished` / `Cancelled`) + - `status`(可选,可多值:`Draft` / `Published` / `Ongoing` / `Ended` / `Cancelled`) - `keyword`(可选,对活动名称做模糊匹配) - `startFrom`、`startTo`(可选,时间范围) @@ -824,7 +827,7 @@ SeckillActivityListResponse { #### 业务规则与并发 -- 严格按 `owner_merchant_id = current_user_id` 过滤;不允许查询他人活动。 +- 严格按 `created_by_merchant_user_id = current_user_id` 过滤;创建人是活动操作归属,不代表商品租户隔离。 - 排序默认按 `startAt desc`;相同 `startAt` 时按 `activityId` 稳定排序。 #### 缓存、事件或外部依赖 @@ -842,10 +845,10 @@ SeckillActivityListResponse { - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 -- 关联数据表:DB042、DB043、DB044 +- 关联数据表:DB042、DB043 - 当前状态:已定义 - 用途:商家查看本人秒杀活动详情;包含库存、已售、单用户限购、订单统计与取消原因等内部字段。 -- 方法与路径:`GET /api/seckill-activities/{activityId}` +- 方法与路径:`GET /api/merchant/seckill-activities/{activityId}` - operationId:`Seckill_GetMerchantActivityDetail` #### 请求 @@ -885,8 +888,8 @@ SeckillOrderStatsResponse { #### 业务规则与并发 -- 严格按 `owner_merchant_id = current_user_id` 过滤;跨商家访问返回 404,避免泄露活动存在性。 -- 订单统计来自 DB044(`seckill_orders`,与 M04 `orders` 共享事实库,通过 `seckill_activity_id` 关联)。 +- 严格按 `created_by_merchant_user_id = current_user_id` 过滤;非创建人访问返回 404,避免泄露活动存在性。 +- 订单统计通过 Ordering 公开查询契约取得;Seckill 不读取 Ordering 内部订单表,也不维护平行订单事实。 - `cancelReason` 与 `cancelledAt` 仅在 `status=Cancelled` 时返回。 #### 缓存、事件或外部依赖 @@ -921,7 +924,7 @@ SeckillOrderStatsResponse { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`SeckillActivityListResponse`(仅公开字段,`status` 仅返回 `Scheduled` / `Ongoing`) +- 响应 Schema:`SeckillActivityListResponse`(仅公开字段,`status` 仅返回 `Published` / `Ongoing`) #### 失败响应 @@ -931,7 +934,7 @@ SeckillOrderStatsResponse { #### 业务规则与并发 -- 仅返回 `status IN ('Scheduled','Ongoing')` 的活动;已结束、已取消或草稿活动不公开。 +- 仅返回 `status IN ('Published','Ongoing')` 的活动;已结束、已取消或草稿活动不公开。 - 排序默认按 `startAt asc`(即将开始优先),相同 `startAt` 时按 `activityId` 稳定排序。 - 公开响应中 `remainingStock` 不返回具体数字,仅返回 `isSoldOut` 布尔;具体剩余库存通过 A227 查询。 @@ -953,7 +956,7 @@ SeckillOrderStatsResponse { - 关联数据表:DB042、DB043 - 当前状态:已定义 - 用途:游客和买家查看秒杀活动详情;返回公开字段、商品基础信息与抢购入口。 -- 方法与路径:`GET /api/seckill-activities/{activityId}/public` +- 方法与路径:`GET /api/seckill-activities/{activityId}` - operationId:`Seckill_GetActiveActivityDetail` #### 请求 @@ -976,7 +979,7 @@ SeckillOrderStatsResponse { #### 业务规则与并发 -- 仅返回 `status IN ('Scheduled','Ongoing')` 的活动;其他状态返回 410。 +- 仅返回 `status IN ('Published','Ongoing')` 的活动;其他状态返回 410。 - 已登录买家响应额外包含 `currentBuyerOrderCount`、`currentBuyerRemaining`(用于限购提示),按 `(activity_id, buyer_id)` 实时统计。 #### 缓存、事件或外部依赖 @@ -994,7 +997,7 @@ SeckillOrderStatsResponse { - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 -- 关联数据表:DB043、DB044、DB045 +- 关联数据表:DB043;订单事实由 Ordering 的 DB061、DB062 持有 - 当前状态:已定义 - 用途:买家抢购秒杀商品;服务端以数据库条件更新扣减秒杀库存、创建订单与秒杀订单项快照;事务保证不超卖、不少卖、不产生孤立记录。 - 方法与路径:`POST /api/seckill-orders` @@ -1016,7 +1019,7 @@ PlaceSeckillOrderRequest { #### 成功响应 - HTTP 状态:`201 Created` -- Response Header:`Location: /api/seckill-orders/{orderId}` +- Response Header:`Location: /api/orders/{orderId}`(A303) - 响应 Schema:`PlaceSeckillOrderResponse` ```text @@ -1055,18 +1058,20 @@ PlaceSeckillOrderResponse { #### 业务规则与并发 - 秒杀下单为高风险操作,必须使用 `Idempotency-Key`;缺失时返回 400 / `COMMON.VALIDATION_FAILED`。 +- 完成认证和固定请求字段校验后,先读取 PostgreSQL 幂等记录:相同 Key + 相同请求指纹直接重放首次结果,不再经过限流、时间、库存或限购校验;相同 Key + 不同指纹立即返回 `409 / IDEMPOTENCY.KEY_REUSED`。只有全新 Key 才进入后续可变业务校验。 - 同一数据库事务内顺序: 1. 按 `UPDATE seckill_inventory SET remaining = remaining - :qty, sold = sold + :qty, updated_at = now() WHERE activity_id = :aid AND status='Ongoing' AND start_at <= now() AND end_at > now() AND remaining >= :qty` 条件扣减秒杀库存;影响行数为 0 时整体事务回滚。 - 2. 校验 `(activity_id, buyer_id)` 维度已下单数量(含 `PendingPayment`、`Paid`、`Cancelled`)+ 本次 `quantity` 不超过 `perBuyerLimit`;超出时事务回滚。 - 3. 写入 `seckill_orders`(DB044,`orderId = order.id`)与 `seckill_order_items`(DB045,含 `seckillPrice` 快照与 `originalPrice`)。 - 4. 写入 Outbox `SeckillOrderCreated` 事件。 + 2. 在 DB044 `seckill_buyer_quotas` 对 `(activity_id, buyer_id)` 建唯一配额行,使用原子 UPSERT/条件更新保证 `purchased_quantity + :qty <= perBuyerLimit`;普通聚合查询或 Redis 不能作为限购正确性边界。取消成功按 `orderId` 幂等释放一次对应数量。 + 3. 通过 Ordering 公开应用契约创建 DB061 `orders` 与 DB062 `order_items` 的共享订单事实,保存 `seckillActivityId`、秒杀价、原价快照,并把活动的 `createdByMerchantUserId` 固定为该订单的 `assignedMerchantUserId`;Seckill 不建立第二套订单状态机。 + 4. Ordering 在同一受控事务中写入 `OrderCreatedIntegrationEvent` Outbox;Seckill 不再另造平行的订单创建事件。 - 不写入普通商品库存;`products.stock` 不受秒杀下单影响。 -- 失败优先级:限流 429 < 未开始 / 已结束 409 < 售罄 409 < 超过单用户限购 409 < 幂等键复用 409 < 业务异常 5xx。 +- 新请求的拒绝顺序为限流、时间窗口、库存、单用户限购及其他业务异常;已命中的幂等重放或 Key 冲突在这些可变校验之前处理。 #### 缓存、事件或外部依赖 -- Redis:`lock:seckill:order:{activityId}`(细粒度互斥,避免活动行成为热点)、`cache:seckill:activity:{activityId}`(事务成功后失效)、`cart:idempotency:{userId}:{key}`(幂等记录,TTL 24 小时)。 -- Outbox:`SeckillOrderCreated`,由 M09 站内消息与 C03 超时取消消费。 +- PostgreSQL:幂等请求指纹、首次结果、秒杀库存条件扣减和共享订单创建处于同一受控事务;相同 Key 重放首次结果,不依赖 Redis 保存唯一事实。 +- Redis:仅用于入口限流和活动元数据缓存;失败时按本接口的 429/503 降级规则处理,不参与库存与幂等正确性。 +- Outbox:Ordering 发布 `OrderCreatedIntegrationEvent` 供 M09 消费;C03 不消费订单创建事件,只按 Ordering 的 `expiresAt` 周期扫描待支付订单。 #### 验证场景 @@ -1083,3 +1088,26 @@ PlaceSeckillOrderResponse { - A229、A230 不再作为独立 HTTP 接口实施,保留编号仅用于历史审计,不再分配独立路径、`operationId`、请求 Schema 或响应 Schema。 - 秒杀订单列表查询复用 A302(`GET /api/orders`),秒杀筛选通过 Ordering 查询契约表达;秒杀订单详情查询复用 A303(`GET /api/orders/{orderId}`)。 - 订单是否为秒杀订单由共享订单事实中的 `seckill_activity_id` 等字段表达,不建立 `/api/seckill-orders` 平行查询接口。 + +## C01 秒杀活动生命周期 Worker 内部契约 + +> 本任务由 `Mall.Worker` 执行,不提供 HTTP API,也不占用 Axxx 编号。 + +### 状态推进 + +1. 周期扫描 `Published` 且 `startAt <= now()` 的活动,使用条件更新推进为 `Ongoing`。 +2. 周期扫描 `Ongoing` 且 `endAt <= now()` 的活动,使用条件更新推进为 `Ended`。 +3. 每次状态成功变化后触发对应活动列表和详情缓存失效;PostgreSQL 仍是活动状态与库存事实来源。 +4. `Draft` 和 `Cancelled` 不由 Worker 自动推进;A223 取消与 Worker 竞争时,只有一个条件更新成功。 + +### 幂等与恢复 + +- 多实例 Worker 使用小批量扫描与 `FOR UPDATE SKIP LOCKED`(或等价条件更新)避免重复处理;重复扫描已经到达目标状态的活动无副作用。 +- 进程重启后继续以数据库时间字段扫描,不依赖内存定时器保存唯一任务事实。 +- 单条失败记录 `activityId`、目标状态与 `traceId` 后重试,不阻塞同批次其他活动。 + +### 验证场景 + +- 已发布活动到达开始时间后可通过 A228 抢购;结束时间后 A228 返回活动已结束。 +- 取消与自动开始并发时,最终只出现 `Cancelled` 或 `Ongoing` 中一个合法结果。 +- Worker 重启或多实例重复扫描不会重复推进、重复失效缓存或改写库存。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" index 2ac59d0..715e701 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" @@ -1,9 +1,10 @@ # 张海洋个人接口文件(A401-A500) -> **模块**:Payment / AfterSales / Reconciliation +> **模块**:Payment(含支付对账) / AfterSales > **负责人**:张海洋(zhy) > **范围**:M05-01 模拟支付 + M10 售后流程 + C08 支付回调幂等与对账 > **创建日期**:2026-07-24 +> **版本**:v0.1 > **当前状态**:已汇总到主文档,个人文件继续保留用于贡献与评审追踪;实现、OpenAPI 和联调以 `../接口设计.md` 为准 > **关联规范**:`docs/02-设计文档/接口设计.md` v0.1 + `docs/02-设计文档/命名规范.md` + `docs/02-设计文档/Git团队协作流程.md` > **关联根命名空间**:`Mall.Modules.Payment`、`Mall.Modules.AfterSales` @@ -11,13 +12,19 @@ --- +## 修订记录 + +| 版本 | 日期 | 修改人 | 修改说明 | +|---|---|---|---| +| v0.1 | 2026-07-24 | 张海洋 | 建立并完善 A401~A434 支付、售后与对账接口;取消 A418/A431 历史 HTTP 编号,并补齐退货信息 A434 与退款应用契约 | + ## 0. 阅读须知 1. 每个接口按《接口设计.md》1.20 节模板逐项填写。 2. 所有路径遵循 `/api//...`,小写 + 复数 + kebab-case,多个单词用连字符。 3. JSON 字段统一 `camelCase`;Schema 名称统一 `PascalCase` + 用途后缀。 4. `operationId` 统一 `_`,全小写 PascalCase 拼接。 -5. 业务错误码格式:`PAYMENT.` / `AFTER_SALES.` / `RECONCILIATION.`,全大写下划线。 +5. 业务错误码格式:`PAYMENT.` / `AFTER_SALES.`;支付对账错误统一使用 `PAYMENT.RECONCILIATION_`,仍归属 Payment 模块,不建立独立 Reconciliation 业务模块。 6. 涉及资金、状态、回调的接口强制 `Idempotency-Key`(按 1.12.1)。 7. 字段同时承担数据库来源的,在"关联数据表"标注推断;正式评审以 `database-zhy.md` 为准。 @@ -49,33 +56,33 @@ | A415 | `AfterSales_CancelRequest` | POST | `/api/after-sales/requests/{requestId}/cancel` | M10-FR10 | AfterSales | BuyerOnly | 是 | | A416 | `AfterSales_AuditRequest` | POST | `/api/after-sales/requests/{requestId}/audit` | M10-FR05 | AfterSales | MerchantOnly | 是 | | A417 | `AfterSales_ConfirmReturn` | POST | `/api/after-sales/requests/{requestId}/confirm-return` | M10-FR11 | AfterSales | MerchantOnly | 是 | -| A418 | `AfterSales_ListAuditLogs` | GET | `/api/after-sales/requests/{requestId}/audit-logs` | M10-FR04 | AfterSales | BuyerOnly/MerchantOnly | 否 | +| A418 | — | — | — | M10-FR04 | AfterSales | — | —(已取消,状态时间线并入 A414) | | A419 | `AfterSales_RetryRefund` | POST | `/api/after-sales/requests/{requestId}/retry-refund` | M10-FR07 | AfterSales | MerchantOnly | 是 | -| A434 | `AfterSales_SubmitReturnInfo` | POST | `/api/after-sales/requests/{requestId}/return-info` | M10-FR11(扩展) | AfterSales | BuyerOnly | 是 | +| A434 | `AfterSales_SubmitReturnInfo` | POST | `/api/after-sales/requests/{requestId}/return-info` | M10-FR11 | AfterSales | BuyerOnly | 是 | ### 1.3 C08 支付回调与对账(A421-A425) | 编号 | operationId | 方法 | 路径 | 需求 | Tag | 鉴权 | 幂等 | |---|---|---|---|---|---|---|---| -| A421 | `Payment_ReceiveCallback` | POST | `/api/payment/callbacks` | C08-FR01~FR05 | Payment | Service(模拟渠道) | 是 | -| A422 | `Reconciliation_ListBatches` | GET | `/api/admin/reconciliation/batches` | C08-FR06 | Reconciliation | AdminOnly | 否 | -| A423 | `Reconciliation_GetBatch` | GET | `/api/admin/reconciliation/batches/{batchId}` | C08-FR06 | Reconciliation | AdminOnly | 否 | -| A424 | `Reconciliation_ListDifferences` | GET | `/api/admin/reconciliation/batches/{batchId}/differences` | C08-FR07/FR08 | Reconciliation | AdminOnly | 否 | -| A425 | `Reconciliation_ProcessDifference` | POST | `/api/admin/reconciliation/differences/{differenceId}/process` | C08-FR08 | Reconciliation | AdminOnly | 是 | +| A421 | `Payment_ReceiveCallback` | POST | `/api/payment/callbacks` | C08-FR01~FR05 | Payment | HMAC 签名(模拟渠道) | 是 | +| A422 | `Payment_ListReconciliationBatches` | GET | `/api/admin/reconciliation/batches` | C08-FR06 | Payment | AdminOnly | 否 | +| A423 | `Payment_GetReconciliationBatch` | GET | `/api/admin/reconciliation/batches/{batchId}` | C08-FR06 | Payment | AdminOnly | 否 | +| A424 | `Payment_ListReconciliationDifferences` | GET | `/api/admin/reconciliation/batches/{batchId}/differences` | C08-FR07/FR08 | Payment | AdminOnly | 否 | +| A425 | `Payment_ProcessReconciliationDifference` | POST | `/api/admin/reconciliation/differences/{differenceId}/process` | C08-FR08 | Payment | AdminOnly | 是 | -### 1.4 M10 退款查询(A432-A433)+ A431 内部应用能力 +### 1.4 M10 退款查询(A432-A433)及 A431 历史取消编号 | 编号 | operationId | 方法 | 路径 | 需求 | Tag | 鉴权 | 幂等 | |---|---|---|---|---|---|---|---| -| A431 | _(内部应用能力,不占 Axxx HTTP 编号)_ | — | — | M10-FR07 | Payment | 内部模块信任 | 是 | -| A432 | `Refund_Get` | GET | `/api/refunds/{refundId}` | M10-FR04 | Payment | BuyerOnly/MerchantOnly | 否 | -| A433 | `Refund_List` | GET | `/api/refunds` | M10-FR03 | Payment | BuyerOnly/MerchantOnly | 否 | +| A431 | — | — | — | M10-FR07 | Payment | — | — | +| A432 | `Payment_GetRefund` | GET | `/api/refunds/{refundId}` | M10-FR04 | Payment | BuyerOnly/MerchantOnly | 否 | +| A433 | `Payment_ListRefunds` | GET | `/api/refunds` | M10-FR03 | Payment | BuyerOnly/MerchantOnly | 否 | -> **A431 边界说明(2026-07-24 团队评审反馈修正)**:A431 退款入账本质是**进程内应用能力**,不是 HTTP 接口。 +> **A431 边界说明**:A431 原拟用于退款 HTTP 端点,现已取消并仅保留历史编号。退款入账改由不占 Axxx 的 Payment 进程内应用契约完成。 > - 进程内调用:`IRefundService.CreateRefundAsync(CreateRefundCommand, CancellationToken)` → `RefundResult` -> - 调用方:A416 商家审核通过(`expectRefund=true`)、A417 商家确认收货、A419 退款失败重试 -> - 详细契约见 §2 中 `A431 内部应用能力:退款入账` 节 -> - v0.1 草稿将 A431 误写为 `MerchantOnly` 外部 HTTP 接口,边界混淆;本版本改为内部契约 +> - 调用方:A416 仅退款审核通过、A417 商家确认收货、A419 退款失败重试 +> - 详细契约见 §2 中 `A431 已取消:退款 HTTP 改为 Payment 应用契约` 节 +> - A431 不再分配 HTTP 路径或 `operationId`,也不作为内部契约编号继续使用 --- @@ -234,7 +241,7 @@ #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:发布 `WalletTopupSucceededIntegrationEvent`(待罗皓晨 M00 集成事件规范确认) +- 事件:本期没有已确认的跨模块消费者,不为模拟充值单独发布集成事件。 - 外部依赖:PostgreSQL #### 验证场景 @@ -342,7 +349,7 @@ - **模块 / Tag**:Payment - **需求编号**:M05-01-FR05 - **负责人**:张海洋 -- **关联数据表**:DB084(待评审)— `orders`(只读,用于查询订单金额/状态) +- **关联数据表**:Payment 不拥有订单表;订单金额、归属和状态通过 Ordering 公开应用契约读取 - **当前状态**:已设计 - **用途**:进入支付前的订单金额、应付、钱包余额、可用渠道聚合查询 - **方法与路径**:`GET /api/payment/checkout/{orderId}` @@ -362,7 +369,7 @@ - **校验规则**: - `orderId` UUID 格式 - 订单归属当前 buyerId - - 订单状态为 `PendingPayment`(否则 409 + `PAYMENT.ORDER_NOT_PAYABLE`) + - `PendingPayment` 返回可支付收银台;已支付返回现有支付结果摘要;已取消或其他不可支付且无成功支付记录的状态返回 409 #### 成功响应 @@ -377,6 +384,8 @@ "orderId": "3f0ed9a9-...", "orderAmount": 199.00, "paidAmount": 0.00, + "orderStatus": "PendingPayment", + "canPay": true, "currency": "CNY", "walletBalance": 100.50, "insufficient": true, @@ -393,14 +402,14 @@ | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 订单不存在或非本人 | -| 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 订单状态非 `PendingPayment` | +| 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 订单已取消或处于其他不可支付且无成功支付记录的状态 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 - 不修改订单或钱包状态,纯查询 - 余额、订单金额、应付以服务端实时值(按 PAY-R01) -- 订单已支付 → 返回 `PAID` 状态但 `CheckoutResponse` 仍可读 +- 订单已支付 → 返回 `200`、`canPay=false`、`orderStatus=Paid` 和现有支付结果摘要,便于用户确认支付结果;已取消订单返回 409 #### 缓存、事件或外部依赖 @@ -413,7 +422,7 @@ - 正常:订单本人 + `PendingPayment` + 余额不足 → 返回 `insufficient=true` - 正常:订单本人 + `PendingPayment` + 余额充足 → 返回 `insufficient=false` - 异常:他人订单 → 404 + `RESOURCE.NOT_FOUND` -- 异常:订单已支付 → 409 + `PAYMENT.ORDER_NOT_PAYABLE` +- 正常:订单已支付 → 200,`canPay=false`,不再显示支付按钮 --- @@ -422,7 +431,7 @@ - **模块 / Tag**:Payment - **需求编号**:M05-01-FR05~FR09 - **负责人**:张海洋 -- **关联数据表**:DB081(待评审)— `wallets`、DB083(待评审)— `wallet_ledgers`、DB085(待评审)— `payments`、DB084(待评审)— `orders` +- **关联数据表**:DB081(待评审)— `wallets`、DB083(待评审)— `wallet_ledgers`、DB085(待评审)— `payments`;订单状态通过 Ordering 公开应用契约协作 - **当前状态**:已设计 - **用途**:从买家钱包扣款并完成订单支付 - **方法与路径**:`POST /api/payment/orders/{orderId}/pay` @@ -481,7 +490,6 @@ | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 订单不存在或非本人 | | 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 订单状态非 `PendingPayment` 或已取消 | -| 409 | `PAYMENT.ALREADY_PAID` | 订单已支付成功(幂等命中首次结果) | | 409 | `PAYMENT.AMOUNT_MISMATCH` | `expectedAmount` 与订单金额不一致 | | 409 | `PAYMENT.INSUFFICIENT_BALANCE` | 钱包余额不足 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | @@ -493,21 +501,21 @@ - 钱包条件扣减 + 钱包流水 + 支付记录 + 订单状态 + Outbox **同一事务**(按 PAY-R06) - 与 C03 订单超时取消通过 `WHERE order.status = 'PendingPayment'` 条件竞争,唯一胜出(按 PAY-R05) - 余额不得为负(条件更新 + CHECK 约束)(按 PAY-R02) -- 成功提交后写入 `Idempotency-Key` 记录,相同 Key + 相同 amount + 相同 orderId → 返回首次结果 -- 重复支付请求返回原成功结果,不重复写入或重复发布事件(按 PAY-R11) +- 成功提交后写入 `Idempotency-Key` 记录,相同 Key + 相同 amount + 相同 orderId → 返回首次结果。 +- 同一订单已经支付成功时,无论请求使用原 Key 还是新的 Key,均返回既有 `200 PaymentResultResponse`,不重复扣款、写流水或发布事件;只有同一 Key 被用于不同请求内容时返回 `IDEMPOTENCY.KEY_REUSED`。 #### 缓存、事件或外部依赖 -- 缓存:写入幂等结果到 `Idempotency-Key` 存储(DB 或 Redis) +- 缓存:资金幂等结果持久化在 PostgreSQL,不以 Redis 作为唯一事实 - 事件:发布 `OrderPaidIntegrationEvent`(架构 §7.4 已确定第一条集成事件) -- 外部依赖:PostgreSQL + Ordering 模块 `orders` 表 +- 外部依赖:PostgreSQL + Ordering 公开应用契约;不得直接依赖 Ordering 内部 DbContext 或仓储 #### 验证场景 - 正常:订单 `PendingPayment` + 余额充足 + 金额一致 → 200 + `PaymentResultResponse` - 重复:相同 Idempotency-Key → 返回首次结果,不重复扣款 - 异常:余额不足 → 409 + `PAYMENT.INSUFFICIENT_BALANCE` -- 异常:订单已支付 → 409 + `PAYMENT.ALREADY_PAID` +- 重复:订单已支付 → 200,返回既有支付结果,不重复扣款 - 异常:订单已取消 → 409 + `PAYMENT.ORDER_NOT_PAYABLE` - 异常:金额不一致 → 409 + `PAYMENT.AMOUNT_MISMATCH` - 并发:与 C03 同时操作 → 唯一胜出,败方 409 + `PAYMENT.ORDER_NOT_PAYABLE` @@ -739,7 +747,7 @@ - **模块 / Tag**:AfterSales - **需求编号**:M10-FR01 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB084(待评审)— `orders` +- **关联数据表**:DB086(待评审)— `after_sales_requests`;订单项归属、状态与实付快照通过 Ordering 公开应用契约查询 - **当前状态**:已设计 - **用途**:预检指定订单项是否可申请售后 - **方法与路径**:`GET /api/after-sales/eligibility` @@ -778,7 +786,7 @@ "reason": null, "maxRefundableAmount": 100.00, "maxRefundableQuantity": 1, - "availableTypes": ["RefundOnly", "ReturnAndRefund"], + "availableTypes": ["RefundOnly"], "deadlineAt": "2026-07-30T08:30:00Z" } } @@ -791,25 +799,26 @@ | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 订单或订单项不存在 | -| 409 | `AFTER_SALES.NOT_ELIGIBLE` | 订单未支付、已发货超期、不可售后状态 | #### 业务规则与并发 - 仅返回当前 buyerId 订单的可申请性 - 退款金额上限 = 实付单价 × 剩余可售后数量(M10 业务规则) -- 可申请类型根据订单状态决定:已支付/已发货 → RefundOnly;已发货 + 确认收货后 → ReturnAndRefund +- 不符合业务条件时仍返回 `200`、`eligible=false` 和稳定 `reason`,便于页面直接展示原因;订单项不存在或不属于当前买家仍统一返回 404。 +- 可申请类型根据订单状态、履约情况和剩余可售后数量计算,不由客户端推断。 +- 未发货的 `Paid` 订单只允许 `RefundOnly`;`Shipped` 或在售后期限内的 `Completed` 订单可以按资格返回 `RefundOnly`、`ReturnAndRefund`。 #### 缓存、事件或外部依赖 - 缓存:不缓存 - 事件:无 -- 外部依赖:PostgreSQL + Orders 模块 +- 外部依赖:PostgreSQL + Ordering 公开应用契约 #### 验证场景 - 正常:已支付订单 → 返回可申请 -- 异常:订单未支付 → 409 + `AFTER_SALES.NOT_ELIGIBLE` -- 异常:完成 > 7 天 → 409 + `AFTER_SALES.NOT_ELIGIBLE` +- 订单未支付 → 200,`eligible=false`,返回不可申请原因。 +- 完成超过 7 天 → 200,`eligible=false`,返回超期原因。 --- @@ -841,18 +850,16 @@ "orderItemId": "5a7c...", "type": "RefundOnly", "quantity": 1, - "reason": "DAMAGED", - "reasonNote": "外包装破损", - "evidence": ["https://...", "https://..."] + "reason": "Damaged", + "reasonNote": "外包装破损" } ``` - **校验规则**: - `orderId` / `orderItemId` 必填,UUID 格式 - `type` 枚举:`RefundOnly` / `ReturnAndRefund` - `quantity` 整数 ≥ 1 且 ≤ 剩余可售后数量 - - `reason` 枚举白名单(待 6.2 M10 业务规则定义) - - `reasonNote` 选填,≤ 500 字 - - `evidence` 选填,最多 9 张图 URL + - `reason` 枚举:`Damaged` / `QualityIssue` / `WrongItem` / `NotAsDescribed` / `Other` + - `reasonNote` ≤ 500 字;`reason=Other` 时必填,其他原因时选填 - 退款金额由 `quantity × 实付单价` 后端计算(按 M10 业务规则"不接受任意金额") - `Idempotency-Key` 必填 @@ -873,9 +880,8 @@ "quantity": 1, "calculatedAmount": 100.00, "currency": "CNY", - "reason": "DAMAGED", + "reason": "Damaged", "reasonNote": "外包装破损", - "evidence": ["https://..."], "status": "PendingReview", "createdAt": "2026-07-23T08:30:00Z" } @@ -891,8 +897,7 @@ | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 订单或订单项不存在 | | 409 | `AFTER_SALES.NOT_ELIGIBLE` | 订单不满足售后条件 | -| 409 | `AFTER_SALES.AMOUNT_EXCEEDS_PAID` | 申请数量超过剩余可售后数量 | -| 409 | `AFTER_SALES.DUPLICATE_APPLICATION` | 同一订单项已有"待审核"申请 | +| 409 | `AFTER_SALES.QUANTITY_EXCEEDS_AVAILABLE` | 申请数量超过该订单项剩余可售后数量,或并发申请已占用额度 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | @@ -901,20 +906,20 @@ - 退款金额由服务端计算(M10 规则:"不接受任意金额") - 申请数量不得超过剩余可售后数量(防重复申请) - 状态写入 `PendingReview`(M10 状态机) -- 同一 buyerId 同一订单项已有 `PendingReview` → 拒绝重复申请 +- 同一订单项可按剩余数量分次申请;仅处理中和已退款数量占用额度,不因存在另一笔 `PendingReview` 就整项禁止申请。 +- A412 必须与 A307 共用 Ordering 提供的订单级变更契约:在同一 PostgreSQL 事务中锁定目标 `orders` 行、读取最新履约状态并保持到售后申请写入提交,禁止查询后另开事务插入。申请先提交时占用数量进入履约快照;发货先提交时,本请求按已发货后的类型和库存规则重新校验。 #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:发布 `AfterSalesApplicationSubmittedIntegrationEvent`(待 M00 集成事件确认) -- 外部依赖:PostgreSQL +- 事件:发布已在 Messaging 契约登记的 `AfterSalesApplicationSubmittedIntegrationEvent`。 +- 外部依赖:PostgreSQL + Ordering 公开应用契约 #### 验证场景 - 正常:订单项可申请 → 201 + 详情 - 重复:相同 Idempotency-Key → 返回首次结果 -- 异常:申请数量 > 剩余可售后 → 409 + `AFTER_SALES.AMOUNT_EXCEEDS_PAID` -- 异常:订单项已有 PendingReview → 409 + `AFTER_SALES.DUPLICATE_APPLICATION` +- 异常:申请数量超过剩余可售后数量,或并发申请已先占用额度 → 409 + `AFTER_SALES.QUANTITY_EXCEEDS_AVAILABLE` --- @@ -931,7 +936,7 @@ - **请求 Schema**:`ListAfterSalesRequestsQuery` - **响应 Schema**:`AfterSalesRequestListResponse` - **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` -- **资源归属**:当前 buyerId 申请 / 当前 merchant 授权范围申请 +- **资源归属**:买家只看本人申请;商家只看关联订单 `assignedMerchantUserId` 等于当前账号的申请 - **幂等要求**:GET 天然幂等 #### 请求 @@ -945,7 +950,7 @@ - **Header**:`Authorization: Bearer ` - **Body**:(无) - **校验规则**: - - 买家仅看本人申请;商家仅看授权范围内申请 + - 买家仅看本人申请;商家仅看关联订单分配给当前运营账号的申请 - 标准分页 + 时间范围 + 枚举白名单 #### 成功响应 @@ -990,7 +995,7 @@ #### 业务规则与并发 -- 商家视图按 `merchantId` 过滤订单范围 +- 商家视图按关联订单的 `assignedMerchantUserId = currentUserId` 过滤 - 默认排序 `createdAt desc, requestId desc` #### 缓存、事件或外部依赖 @@ -1012,7 +1017,7 @@ - **模块 / Tag**:AfterSales - **需求编号**:M10-FR04 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_audit_logs` +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_status_histories` - **当前状态**:已设计 - **用途**:查询单条售后申请的详细信息与状态时间线 - **方法与路径**:`GET /api/after-sales/requests/{requestId}` @@ -1020,7 +1025,7 @@ - **请求 Schema**:(无) - **响应 Schema**:`AfterSalesRequestDetailResponse` - **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` -- **资源归属**:当前 buyerId 应用 / 当前 merchant 授权范围内 +- **资源归属**:买家只能查看本人申请;商家只能查看关联订单分配给当前账号的申请 - **幂等要求**:GET 天然幂等 #### 请求 @@ -1050,9 +1055,8 @@ "quantity": 1, "calculatedAmount": 100.00, "currency": "CNY", - "reason": "DAMAGED", + "reason": "Damaged", "reasonNote": "外包装破损", - "evidence": ["https://..."], "status": "PendingReview", "createdAt": "2026-07-23T08:30:00Z", "timeline": [ @@ -1073,7 +1077,7 @@ #### 业务规则与并发 -- 时间线读 `after_sales_audit_logs` 表(按 DB087 推断) +- 时间线读取模块内的 `after_sales_status_histories`(DB087 待数据库设计确认),不读取通用操作审计 - 不返回内部审计字段(如 merchant 内部 ID) #### 缓存、事件或外部依赖 @@ -1142,12 +1146,12 @@ - 审核通过后不允许撤销(M10 业务规则) - 状态条件更新:`WHERE status = 'PendingReview' AND buyer_id = currentBuyerId` -- 撤销后保留 `audit_log` 记录 +- 撤销后保留领域状态历史 #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:发布 `AfterSalesApplicationCancelledIntegrationEvent` +- 事件:本期没有已确认的跨模块消费者,撤销事实保存在售后状态时间线,不单独发布集成事件。 - 外部依赖:PostgreSQL #### 验证场景 @@ -1163,7 +1167,7 @@ - **模块 / Tag**:AfterSales - **需求编号**:M10-FR05 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_audit_logs` +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_status_histories` - **当前状态**:已设计 - **用途**:商家同意或拒绝售后申请 - **方法与路径**:`POST /api/after-sales/requests/{requestId}/audit` @@ -1171,7 +1175,7 @@ - **请求 Schema**:`AuditAfterSalesRequest` - **响应 Schema**:`AfterSalesRequestDetailResponse` - **身份与 Policy**:JWT Bearer + `MerchantOnly` -- **资源归属**:当前 merchant 授权范围内申请 +- **资源归属**:关联订单的 `assignedMerchantUserId` 必须等于当前商家账号 - **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) #### 请求 @@ -1183,22 +1187,20 @@ ```json { "decision": "Approve", - "auditNote": "同意申请", - "expectRefund": true + "auditNote": "同意申请" } ``` - **校验规则**: - - 申请归属当前 merchant + - 申请关联订单分配给当前商家账号 - 申请状态必须为 `PendingReview` - `decision` 枚举:`Approve` / `Reject` - - `expectRefund=true` 表示审核通过后系统将自动触发退款(A431) - `Idempotency-Key` 必填 #### 成功响应 - **HTTP 状态**:`200 OK` - **响应 Schema**:`AfterSalesRequestDetailResponse` -- **示例**:(同 A412 详情,status 变为 `PendingReturn` 或 `Refunding`) +- **示例**:(拒绝时 `status=Rejected`;退货退款审核通过时 `status=PendingReturn`;仅退款全部子操作成功时 `status=Refunded`) #### 失败响应 @@ -1210,25 +1212,31 @@ | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | | 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `PendingReview` | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 503 | `AFTER_SALES.REFUND_FAILED` | 审核已通过,但同步退款或必要库存回补失败;申请已记录为 `RefundFailed` | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 - 商家不能修改买家原始申请内容(业务规则) -- 状态条件更新:`WHERE status = 'PendingReview' AND merchant_id = currentMerchantId` -- 审核通过后若 `expectRefund=true` → 异步触发 A431 退款 -- `audit_log` 记录审核人与审核意见 +- 状态条件更新同时校验关联订单 `assigned_merchant_user_id = currentUserId` +- 审核结果由申请类型决定,客户端不能通过布尔字段选择是否退款: + - `ReturnAndRefund` 审核通过只进入 `PendingReturn`,等待 A434 和 A417,不在审核时退款或回补库存。 + - `RefundOnly` 审核通过后以 `Refunding` 作为事务内过渡并同步调用 Payment 退款应用契约;若 Ordering 快照表明订单仍为 `Paid` 且未发货,还必须按普通/秒杀原通道调用 Catalog 或 Seckill 库存回补契约。全部成功后本次 HTTP 返回 `Refunded`。 + - `RefundOnly` 对 `Shipped` 或 `Completed` 订单只退款、不回补库存。 +- 退款、必要的库存回补与售后终态通过公开应用契约加入同一受控数据库事务;任一步失败均不留下部分资金/库存结果,并在独立失败记录中把申请置为 `RefundFailed` 供 A419 重试。 +- 售后状态历史记录审核人、审核意见和状态变化;这是领域时间线,不是未选择的通用后台操作日志。 #### 缓存、事件或外部依赖 - 缓存:不缓存 - 事件:发布 `AfterSalesApplicationAuditedIntegrationEvent` -- 外部依赖:PostgreSQL + Payment 模块(通过应用能力调用 A431) +- 外部依赖:PostgreSQL + Ordering 售后快照 + Payment 退款应用契约 + Catalog/Seckill 库存回补应用契约 #### 验证场景 -- 正常:商家 Approve → 状态进入 `PendingReturn` 或 `Refunding` +- 正常:商家 Approve → 退货退款进入 `PendingReturn`;仅退款同步完成后返回 `Refunded` - 正常:商家 Reject → 状态进入 `Rejected` +- 异常:退款或必要库存回补失败 → 503 + `AFTER_SALES.REFUND_FAILED`,详情可查询到 `RefundFailed` - 异常:买家角色调用 → 403 + `AUTH.FORBIDDEN` - 异常:他人商家申请 → 404 + `RESOURCE.NOT_FOUND` - 异常:状态已非 `PendingReview` → 409 + `AFTER_SALES.INVALID_STATUS` @@ -1240,7 +1248,7 @@ - **模块 / Tag**:AfterSales - **需求编号**:M10-FR11 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_audit_logs` +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_status_histories` - **当前状态**:已设计 - **用途**:商家确认收到退货,触发退款流程 - **方法与路径**:`POST /api/after-sales/requests/{requestId}/confirm-return` @@ -1248,7 +1256,7 @@ - **请求 Schema**:`ConfirmReturnRequest` - **响应 Schema**:`AfterSalesRequestDetailResponse` - **身份与 Policy**:JWT Bearer + `MerchantOnly` -- **资源归属**:当前 merchant 授权范围内申请 +- **资源归属**:关联订单的 `assignedMerchantUserId` 必须等于当前商家账号 - **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) #### 请求 @@ -1264,17 +1272,17 @@ } ``` - **校验规则**: - - 申请归属当前 merchant + - 申请关联订单分配给当前商家账号 - 申请类型必须为 `ReturnAndRefund` - 申请状态必须为 `PendingReceipt` - - `receivedQuantity` ∈ [1, 申请数量] + - 本期不支持部分收货,`receivedQuantity` 必须等于申请数量 - `Idempotency-Key` 必填 #### 成功响应 - **HTTP 状态**:`200 OK` - **响应 Schema**:`AfterSalesRequestDetailResponse` -- **示例**:(同 A412 详情,status 变为 `Refunding`) +- **示例**:(同 A412 详情,全部子操作成功后 `status=Refunded`) #### 失败响应 @@ -1286,107 +1294,38 @@ | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | | 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `PendingReceipt` | | 409 | `AFTER_SALES.WRONG_TYPE` | 申请类型非 `ReturnAndRefund` | +| 409 | `AFTER_SALES.RETURN_QUANTITY_MISMATCH` | 收货数量与申请数量不一致 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 503 | `AFTER_SALES.REFUND_FAILED` | 已确认收货,但同步退款或库存回补失败;申请已记录为 `RefundFailed` | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 状态条件更新:`WHERE status = 'PendingReceipt' AND merchant_id = currentMerchantId` -- 确认收到后异步触发 A431 退款 -- 库存按退货数量回补(按 M10 业务规则"已发货或已完成订单仅在退货且商家确认收货后按退货数量回补") +- 状态条件更新同时校验关联订单 `assigned_merchant_user_id = currentUserId` +- 确认收到后以 `Refunding` 作为事务内过渡;根据 Ordering 快照的 `orderType` 与 `seckillActivityId`,通过公开应用契约幂等回补 Catalog 普通库存或 Seckill 原活动库存,并同步执行 Payment 退款。HTTP 成功时已经进入 `Refunded`。 +- 库存回补数量等于整笔申请数量;本期不拆分部分收货或部分退款。仅退货退款在 A417 回补,已发货/已完成订单的仅退款不回补。 +- 回补、退款和售后终态加入同一受控数据库事务:全部成功后转为 `Refunded`;任一步失败不保留部分结果,并在独立失败记录中置为 `RefundFailed`,不得回滚成“从未确认收货”。 #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:发布 `AfterSalesReturnConfirmedIntegrationEvent` + 库存回补事件 -- 外部依赖:PostgreSQL + Payment(A431)+ Inventory +- 事件:Payment 退款应用契约只发布一次 `RefundCompletedIntegrationEvent`;收货确认与库存回补不另发没有消费者的事件。 +- 外部依赖:PostgreSQL + Payment 退款应用契约 + Catalog/Seckill 库存回补应用契约 #### 验证场景 -- 正常:商家确认退货 → 状态进入 `Refunding`,触发退款 +- 正常:商家确认退货 → 正确库存通道回补、退款成功,状态进入 `Refunded` - 异常:申请类型为 `RefundOnly` → 409 + `AFTER_SALES.WRONG_TYPE` - 异常:状态非 `PendingReceipt` → 409 + `AFTER_SALES.INVALID_STATUS` +- 异常:收货数量与申请数量不一致 → 409 + `AFTER_SALES.RETURN_QUANTITY_MISMATCH` --- -### A418 审核日志 - -- **模块 / Tag**:AfterSales -- **需求编号**:M10-FR04 -- **负责人**:张海洋 -- **关联数据表**:DB087(待评审)— `after_sales_audit_logs` -- **当前状态**:已设计 -- **用途**:查询申请审核日志 -- **方法与路径**:`GET /api/after-sales/requests/{requestId}/audit-logs` -- **operationId**:`AfterSales_ListAuditLogs` -- **请求 Schema**:`ListAuditLogsQuery` -- **响应 Schema**:`AuditLogListResponse` -- **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` -- **资源归属**:当前 buyerId 申请 / 当前 merchant 授权范围内 -- **幂等要求**:GET 天然幂等 - -#### 请求 - -- **Route 参数**:`requestId`(UUID) -- **Query 参数**:`page` / `pageSize` / `sortBy` / `sortOrder` -- **Header**:`Authorization: Bearer ` -- **Body**:(无) -- **校验规则**: - - 申请归属当前 buyerId 或当前 merchant - -#### 成功响应 +### A418 已取消:状态时间线并入 A414 -- **HTTP 状态**:`200 OK` -- **响应 Schema**:`AuditLogListResponse` -- **示例**: -```json -{ - "code": "success", - "message": "ok", - "data": { - "items": [ - { - "logId": "...", - "action": "Submitted", - "actor": "buyer", - "fromStatus": null, - "toStatus": "PendingReview", - "note": null, - "at": "2026-07-23T08:30:00Z" - } - ], - "page": 1, - "pageSize": 10, - "total": 1, - "totalPages": 1 - } -} -``` - -#### 失败响应 - -| HTTP 状态 | 业务错误码 | 触发条件 | -|---|---|---| -| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | -| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer / Merchant | -| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | -| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | - -#### 业务规则与并发 - -- 默认排序 `at asc, logId asc`(按时间顺序) -- 不返回内部审计字段(如 `merchant_internal_id`) - -#### 缓存、事件或外部依赖 - -- 缓存:不缓存 -- 事件:无 -- 外部依赖:PostgreSQL - -#### 验证场景 - -- 正常:本人申请 → 返回审核日志 -- 异常:他人申请 → 404 + `RESOURCE.NOT_FOUND` +- A418 不再作为独立 HTTP 接口实施,编号保留用于历史审计,不再分配路径、`operationId`、请求或响应 Schema。 +- M10-FR04 所需的申请、审核与退款状态时间线由 A414 售后申请详情统一返回,避免同一页面重复请求两套等价数据。 +- 本期不建设通用后台操作日志;AfterSales 只保存满足业务展示与状态追踪所需的领域状态历史。 --- @@ -1403,7 +1342,7 @@ - **请求 Schema**:`RetryRefundRequest` - **响应 Schema**:`AfterSalesRequestDetailResponse` - **身份与 Policy**:JWT Bearer + `MerchantOnly` -- **资源归属**:当前 merchant 授权范围内申请 +- **资源归属**:关联订单的 `assignedMerchantUserId` 必须等于当前商家账号 - **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) #### 请求 @@ -1418,7 +1357,7 @@ } ``` - **校验规则**: - - 申请归属当前 merchant + - 申请关联订单分配给当前商家账号 - 申请状态必须为 `RefundFailed` - `Idempotency-Key` 必填 @@ -1426,7 +1365,7 @@ - **HTTP 状态**:`200 OK` - **响应 Schema**:`AfterSalesRequestDetailResponse` -- **示例**:(同 A412 详情,status 变为 `Refunding`) +- **示例**:(同 A412 详情,重试成功后 `status=Refunded`) #### 失败响应 @@ -1437,22 +1376,25 @@ | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | | 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `RefundFailed` | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 503 | `AFTER_SALES.REFUND_FAILED` | 本次退款或必要库存回补再次失败;申请仍为 `RefundFailed` | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 状态条件更新:`WHERE status = 'RefundFailed' AND merchant_id = currentMerchantId` -- 重试时异步触发 A431 退款 +- 状态条件更新同时校验关联订单 `assigned_merchant_user_id = currentUserId` +- 重试时以 `Refunding` 作为事务内过渡,按申请中尚未完成的退款/库存结果复用同一组业务幂等键;普通库存、秒杀库存和资金入账均不得重复。HTTP 成功时返回 `Refunded`。 +- 全部子操作成功后转为 `Refunded`;任一步再次失败时仍为 `RefundFailed`,并保留安全错误码供排查。 #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:发布 `AfterSalesRefundRetriedIntegrationEvent` -- 外部依赖:PostgreSQL + Payment(A431) +- 事件:重试动作本身不发布集成事件;最终只按结果发布 `RefundCompletedIntegrationEvent` 或由 AfterSales 形成 `RefundFailedIntegrationEvent`。 +- 外部依赖:PostgreSQL + Ordering 售后快照 + Payment 退款应用契约 + Catalog/Seckill 库存回补应用契约 #### 验证场景 -- 正常:商家对 `RefundFailed` 重试 → 状态进入 `Refunding` +- 正常:商家对 `RefundFailed` 重试 → 同步完成并返回 `Refunded` +- 异常:再次失败 → 503 + `AFTER_SALES.REFUND_FAILED`,状态保持 `RefundFailed` - 异常:状态非 `RefundFailed` → 409 + `AFTER_SALES.INVALID_STATUS` --- @@ -1462,14 +1404,14 @@ - **模块 / Tag**:Payment - **需求编号**:C08-FR01 / FR02 / FR03 / FR04 / FR05 - **负责人**:张海洋 -- **关联数据表**:DB089(待评审)— `payment_callbacks`、DB085(待评审)— `payments`、DB084(待评审)— `orders` +- **关联数据表**:DB089(待评审)— `payment_callbacks`、DB085(待评审)— `payments`;订单状态通过 Ordering 公开应用契约协作 - **当前状态**:已设计 - **用途**:接收模拟支付渠道的回调,更新支付与订单状态 - **方法与路径**:`POST /api/payment/callbacks` - **operationId**:`Payment_ReceiveCallback` - **请求 Schema**:`PaymentCallbackRequest` - **响应 Schema**:`PaymentCallbackResponse` -- **身份与 Policy**:内部服务级鉴权(Mock Channel Service,签名验证) +- **身份与 Policy**:模拟渠道 HMAC 签名鉴权(无用户 JWT) - **资源归属**:N/A(系统级) - **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 支付回调) @@ -1477,7 +1419,7 @@ - **Route 参数**:(无) - **Query 参数**:(无) -- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`X-Callback-Signature: `、`Content-Type: application/json` +- **Header**:`Idempotency-Key: `(必填)、`X-Callback-Signature: `(必填)、`Content-Type: application/json` - **Body**: ```json { @@ -1502,6 +1444,7 @@ - **HTTP 状态**:`200 OK` - **响应 Schema**:`PaymentCallbackResponse` +- **状态枚举**:`Processed`(正常处理或幂等重放)/ `RecordedForReconciliation`(迟到成功已登记对账差异) - **示例**: ```json { @@ -1521,10 +1464,8 @@ |---|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | | 401 | `PAYMENT.CALLBACK_INVALID_SIGNATURE` | 签名验证失败 | -| 401 | `AUTH.UNAUTHENTICATED` | 缺少服务 JWT | -| 409 | `PAYMENT.CALLBACK_DUPLICATE` | 同一 `callbackId` 重复到达 | -| 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 已取消订单收到迟到成功回调 → 进入对账差异 | -| 422 | `PAYMENT.CALLBACK_AMOUNT_MISMATCH` | 回调金额与订单金额不一致 | +| 409 | `PAYMENT.CALLBACK_AMOUNT_MISMATCH` | 回调金额与订单金额不一致 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一回调幂等键被用于不同请求内容 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 @@ -1533,12 +1474,12 @@ - 同事务:支付记录 + 订单状态 + Inbox/处理记录 + Outbox(按 C08-FR05) - 重复回调返回首次结果,不重复记账 - 乱序:按订单当前状态 + 事件时间决定接受/忽略/登记差异 -- 已取消订单收到迟到成功回调 → **进入对账差异**,不得直接改已支付(按 C08 业务规则) +- 已取消订单收到迟到成功回调时,原子登记对账差异并返回 `200`;`PaymentCallbackResponse.status=RecordedForReconciliation`,不得直接把订单改为 `Paid`。已成功受理的回调不以非 2xx 诱发渠道重复重试。 #### 缓存、事件或外部依赖 - 缓存:幂等记录存在 DB(不依赖 Redis) -- 事件:发布 `PaymentCallbackProcessedIntegrationEvent` / `OrderPaidIntegrationEvent`(按结果) +- 事件:仅实际完成支付时发布 `OrderPaidIntegrationEvent`;重复回调和登记对账差异不发布没有消费者的处理事件。 - 外部依赖:PostgreSQL + Ordering 模块 #### 验证场景 @@ -1546,21 +1487,21 @@ - 正常:未处理过的回调 → 处理成功 - 重复:相同 `callbackId` → 返回首次结果,不重复处理 - 异常:签名错误 → 401 + `PAYMENT.CALLBACK_INVALID_SIGNATURE` -- 异常:金额不一致 → 422 + `PAYMENT.CALLBACK_AMOUNT_MISMATCH` -- 异常:已取消订单收到 Success 回调 → 进入对账差异状态,订单不直接改 `Paid` +- 异常:金额不一致 → 409 + `PAYMENT.CALLBACK_AMOUNT_MISMATCH` +- 边界:已取消订单收到 Success 回调 → 200 + `RecordedForReconciliation`,订单不直接改 `Paid` --- ### A422 对账批次列表 -- **模块 / Tag**:Reconciliation +- **模块 / Tag**:Payment - **需求编号**:C08-FR06 - **负责人**:张海洋 - **关联数据表**:DB090(待评审)— `reconciliation_batches` - **当前状态**:已设计 - **用途**:分页查询每日对账批次 - **方法与路径**:`GET /api/admin/reconciliation/batches` -- **operationId**:`Reconciliation_ListBatches` +- **operationId**:`Payment_ListReconciliationBatches` - **请求 Schema**:`ListBatchesQuery` - **响应 Schema**:`ReconciliationBatchListResponse` - **身份与 Policy**:JWT Bearer + `AdminOnly` @@ -1640,14 +1581,14 @@ ### A423 对账批次详情 -- **模块 / Tag**:Reconciliation +- **模块 / Tag**:Payment - **需求编号**:C08-FR06 - **负责人**:张海洋 - **关联数据表**:DB090(待评审)— `reconciliation_batches`、DB091(待评审)— `reconciliation_differences` - **当前状态**:已设计 - **用途**:查询单批对账详情 - **方法与路径**:`GET /api/admin/reconciliation/batches/{batchId}` -- **operationId**:`Reconciliation_GetBatch` +- **operationId**:`Payment_GetReconciliationBatch` - **请求 Schema**:(无) - **响应 Schema**:`ReconciliationBatchDetailResponse` - **身份与 Policy**:JWT Bearer + `AdminOnly` @@ -1682,7 +1623,7 @@ "differenceCount": 2, "status": "HasDifferences", "summary": { - "byType": { "MissingPayment": 1, "AmountMismatch": 1 } + "byType": { "PaymentSucceededOrderNotUpdated": 1, "RefundAmountMismatch": 1 } }, "createdAt": "2026-07-23T01:00:00Z" } @@ -1695,7 +1636,7 @@ |---|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Admin | -| 404 | `RECONCILIATION.BATCH_NOT_FOUND` | 批次不存在 | +| 404 | `PAYMENT.RECONCILIATION_BATCH_NOT_FOUND` | 批次不存在 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 @@ -1711,20 +1652,20 @@ #### 验证场景 - 正常:管理员查询 → 返回详情 -- 异常:批次不存在 → 404 + `RECONCILIATION.BATCH_NOT_FOUND` +- 异常:批次不存在 → 404 + `PAYMENT.RECONCILIATION_BATCH_NOT_FOUND` --- ### A424 差异列表 -- **模块 / Tag**:Reconciliation +- **模块 / Tag**:Payment - **需求编号**:C08-FR07 / FR08 - **负责人**:张海洋 - **关联数据表**:DB091(待评审)— `reconciliation_differences` - **当前状态**:已设计 - **用途**:分页查询某批次的所有差异 - **方法与路径**:`GET /api/admin/reconciliation/batches/{batchId}/differences` -- **operationId**:`Reconciliation_ListDifferences` +- **operationId**:`Payment_ListReconciliationDifferences` - **请求 Schema**:`ListDifferencesQuery` - **响应 Schema**:`ReconciliationDifferenceListResponse` - **身份与 Policy**:JWT Bearer + `AdminOnly` @@ -1735,7 +1676,7 @@ - **Route 参数**:`batchId`(UUID) - **Query 参数**: - - `type`(可选):`MissingPayment` / `AmountMismatch` / `DuplicateRefund` / `LateCallback` 等 + - `type`(可选):从下方已确认差异类型中选择 - `status`(可选):`Pending` / `InProgress` / `Resolved` - 标准分页 + 排序 - **Header**:`Authorization: Bearer ` @@ -1758,7 +1699,7 @@ { "differenceId": "...", "batchId": "...", - "type": "LateCallback", + "type": "LateSuccessCallback", "orderId": "3f0ed9a9-...", "paymentId": "8d2e9d11-...", "callbackId": "5a8e...", @@ -1781,16 +1722,18 @@ |---|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Admin | -| 404 | `RECONCILIATION.BATCH_NOT_FOUND` | 批次不存在 | +| 404 | `PAYMENT.RECONCILIATION_BATCH_NOT_FOUND` | 批次不存在 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 差异类型至少识别(按 C08-FR07): - - `LateCallback`:已取消订单收到迟到成功回调 - - `MissingPayment`:订单已支付但缺支付流水 - - `AmountMismatch`:支付/退款金额不一致 - - `DuplicateRefund`:退款重复 +- 差异类型至少识别: + - `PaymentSucceededOrderNotUpdated`:支付成功但订单未更新 + - `OrderPaidPaymentMissing`:订单已支付但缺支付记录或流水 + - `LateSuccessCallback`:已取消订单收到迟到成功回调 + - `RefundSucceededWalletCreditMissing`:售后退款成功但小金库未入账 + - `DuplicateWalletCredit`:同一退款发生重复入账 + - `RefundAmountMismatch`:退款记录、钱包流水或入账金额不一致 - 状态管理(按 C08-FR08):`Pending` / `InProgress` / `Resolved` #### 缓存、事件或外部依赖 @@ -1802,20 +1745,20 @@ #### 验证场景 - 正常:管理员查询 → 返回差异列表 -- 异常:批次不存在 → 404 + `RECONCILIATION.BATCH_NOT_FOUND` +- 异常:批次不存在 → 404 + `PAYMENT.RECONCILIATION_BATCH_NOT_FOUND` --- ### A425 差异处理 -- **模块 / Tag**:Reconciliation +- **模块 / Tag**:Payment - **需求编号**:C08-FR08 - **负责人**:张海洋 - **关联数据表**:DB091(待评审)— `reconciliation_differences` - **当前状态**:已设计 - **用途**:管理员处理对账差异并标记状态 - **方法与路径**:`POST /api/admin/reconciliation/differences/{differenceId}/process` -- **operationId**:`Reconciliation_ProcessDifference` +- **operationId**:`Payment_ProcessReconciliationDifference` - **请求 Schema**:`ProcessDifferenceRequest` - **响应 Schema**:`ReconciliationDifferenceDetailResponse` - **身份与 Policy**:JWT Bearer + `AdminOnly` @@ -1836,7 +1779,7 @@ ``` - **校验规则**: - `differenceId` 必填 - - `action` 枚举:`MarkInProgress` / `MarkResolved` / `MarkIgnored` + - `action` 枚举:`MarkInProgress` / `MarkResolved` - 当前状态必须为 `Pending`(`MarkInProgress`)或 `InProgress`(`MarkResolved`) - `resolutionNote` 必填,≤ 1000 字 - `Idempotency-Key` 必填 @@ -1853,7 +1796,7 @@ "data": { "differenceId": "...", "batchId": "...", - "type": "LateCallback", + "type": "LateSuccessCallback", "status": "Resolved", "resolutionNote": "确认为模拟渠道测试回调,已通知商家", "resolvedAt": "2026-07-23T03:00:00Z", @@ -1869,8 +1812,8 @@ | 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Admin | -| 404 | `RECONCILIATION.DIFFERENCE_NOT_FOUND` | 差异不存在 | -| 409 | `RECONCILIATION.DIFFERENCE_ALREADY_PROCESSED` | 状态非法 | +| 404 | `PAYMENT.RECONCILIATION_DIFFERENCE_NOT_FOUND` | 差异不存在 | +| 409 | `PAYMENT.RECONCILIATION_DIFFERENCE_ALREADY_PROCESSED` | 状态非法 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | @@ -1883,20 +1826,20 @@ #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:发布 `ReconciliationDifferenceProcessedIntegrationEvent` +- 事件:处理结果保存在对账差异记录中;本期没有已确认的跨模块消费者,不单独发布集成事件。 - 外部依赖:PostgreSQL #### 验证场景 - 正常:管理员 MarkResolved → 状态进入 `Resolved` -- 异常:状态已为 `Resolved` → 409 + `RECONCILIATION.DIFFERENCE_ALREADY_PROCESSED` +- 异常:状态已为 `Resolved` → 409 + `PAYMENT.RECONCILIATION_DIFFERENCE_ALREADY_PROCESSED` - 异常:买家调用 → 403 + `AUTH.FORBIDDEN` --- -### A431 内部应用能力:退款入账(不占 Axxx HTTP 编号) +### A431 已取消:退款 HTTP 改为 Payment 应用契约 -> **重要修正(2026-07-24 团队评审反馈)**:A431 退款入账本质是**进程内应用能力**,不是 HTTP 接口。v0.1 草稿误写为 `MerchantOnly` 外部接口,边界混淆;本版本改为内部契约,**不占 Axxx HTTP 编号**。A432/A433 仍是外部查询接口。 +> A431 是已取消的 HTTP 历史追踪编号,不再分配路径或 `operationId`。以下内容定义其替代方案 `IRefundService`,该应用契约不使用 Axxx;A432/A433 仍是外部查询接口。 - **模块 / Tag**:Payment(应用服务层) - **需求编号**:M10-FR07 @@ -1909,20 +1852,22 @@ - **命令 Schema**:`CreateRefundCommand`(公开应用能力) - **结果 Schema**:`RefundResult` - **身份与 Policy**:内部模块信任(无 Policy) -- **资源归属**:按传入 `requestId` / `buyerId` +- **资源归属**:AfterSales 先校验申请状态与归属;Payment 再按本模块持有的原支付事实校验买家、币种和可退款上限 - **幂等要求**:**必须支持 `IdempotencyKey`**(同 1.12.1 退款入账) #### 命令输入 - **目标申请**:`requestId`(UUID) +- **原支付**:`paymentId`(UUID) +- **收款买家**:`buyerId`(UUID) - **期待金额**:`expectedAmount`(decimal) - **币种**:`currency`(默认 `CNY`) - **幂等键**:`IdempotencyKey`(必填,UUID) #### 校验规则 -- 申请状态必须为 `Refunding`(已通过 A416 / A417 触发) -- `expectedAmount` 必须等于申请计算金额 +- AfterSales 调用前必须已把申请推进到 `Refunding`;Payment 不读取或修改 AfterSales 内部表。 +- `paymentId`、`buyerId` 与 Payment 持有的原支付事实必须一致,`expectedAmount` 不得超过剩余可退款金额。 - 同一 `IdempotencyKey` + 相同 `amount` → 返回首次结果 - 同一 `IdempotencyKey` + 不同 `amount` → 抛 `IdempotencyKeyReusedException` @@ -1939,7 +1884,8 @@ #### 业务规则与并发 -- 钱包入账 + 退款记录 + 钱包流水 + 申请状态更新 **同事务**(按架构 §7.2) +- Payment 只操作本模块的钱包、退款、流水和 Outbox;由 AfterSales 编排时,这些写入加入同一受控 PostgreSQL 事务,但 Payment 不直接更新 `after_sales_requests`。 +- AfterSales 拥有申请状态并协调必要的库存回补:全部子操作成功后转为 `Refunded`;事务失败后用独立失败记录保留 `RefundFailed`,供 A419 安全重试。 - 退款成功后订单**保持原核心状态**(按 M10 业务规则 + 架构 §7.6) - 退款流水必须纳入 C08 每日对账(按 M10 业务规则) - AfterSales 不直接改钱包数据;通过 Payment 公开应用能力**幂等**退回(按架构 §7.2) @@ -1947,30 +1893,30 @@ #### 缓存、事件或外部依赖 - 缓存:幂等记录存在 DB -- 事件:发布 `RefundCompletedIntegrationEvent` -- 外部依赖:PostgreSQL + AfterSales 模块 +- 事件:成功发布一次只包含申请买家接收人的 `RefundCompletedIntegrationEvent`;失败由 AfterSales 保存 `RefundFailed` 状态并形成包含买家与订单处理商家的通知事实 +- 外部依赖:PostgreSQL;AfterSales 仅通过本公开应用契约调用 #### 验证场景 - 正常:审核通过触发 → 退款成功,余额增加 - 重复:相同 IdempotencyKey → 返回首次结果,不重复入账 -- 异常:金额不一致 → 失败回滚 -- 异常:写流水失败 → 整体事务回滚,申请状态还原 +- 异常:金额不一致 → Payment 事务不入账,AfterSales 保存 `RefundFailed` +- 异常:写流水失败 → Payment 事务整体回滚,AfterSales 保存 `RefundFailed` #### 调用方 -- A416 商家审核通过且 `expectRefund=true` → AfterSales 异步应用服务调用 -- A417 商家确认收货 → AfterSales 异步应用服务调用 -- A419 退款失败重试 → AfterSales 异步应用服务调用 +- A416 仅退款审核通过 → AfterSales 进程内调用 +- A417 商家确认收到退货 → AfterSales 进程内调用 +- A419 退款失败重试 → AfterSales 进程内调用 --- ### A434 买家提交退货/寄回信息 - **模块 / Tag**:AfterSales -- **需求编号**:M10-FR11(扩展:买家提交退货物流) +- **需求编号**:M10-FR11 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_audit_logs`、DB097(待评审)— `return_shipments`(快递单号登记) +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_status_histories`;退货信息作为申请从属数据由数据库设计确认是否单独建表 - **当前状态**:已设计 - **用途**:买家提交退货的物流单号与快递公司(用于 `ReturnAndRefund` 类型的申请) - **方法与路径**:`POST /api/after-sales/requests/{requestId}/return-info` @@ -1999,9 +1945,9 @@ - 申请归属当前 buyerId - 申请类型必须为 `ReturnAndRefund` - 申请状态必须为 `PendingReturn`(商家审核通过后、待退货) - - `carrier` 必填,枚举白名单(待命名规范扩展,如 `SF` / `YTO` / `ZTO` / `YD` 等) - - `trackingNumber` 必填,≤ 50 字符,全局唯一约束(同一快递单号不能重复提交) - - `shippedAt` 必填,ISO 8601 UTC,且 ≤ now + - `carrier` 必填,1~50 字;本期不接入真实物流平台,允许填写“其他”及实际承运方名称 + - `trackingNumber` 必填,1~50 字符 + - `shippedAt` 选填,ISO 8601 UTC 且不得晚于当前时间;缺省时使用服务端提交时间 - `note` 选填,≤ 500 字 - `Idempotency-Key` 必填 @@ -2049,7 +1995,6 @@ | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或非本人 | | 409 | `AFTER_SALES.INVALID_STATUS` | 申请状态非 `PendingReturn` | | 409 | `AFTER_SALES.WRONG_TYPE` | 申请类型非 `ReturnAndRefund` | -| 409 | `AFTER_SALES.DUPLICATE_TRACKING_NUMBER` | 同一快递单号已被使用 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | @@ -2057,9 +2002,9 @@ - 状态条件更新:`WHERE status = 'PendingReturn' AND buyer_id = currentBuyerId AND type = 'ReturnAndRefund'` - 提交后状态变为 `PendingReceipt` -- 快递单号全局唯一约束(同一单号不能用于多笔售后) -- `audit_log` 记录提交人与提交内容 -- 商家在 A417 确认收货后 → 触发 A431 内部应用能力退款 +- 同一包裹可以承载同一订单的多笔退货申请,不对快递单号施加不符合现实的全局唯一约束;重复提交由申请状态和 `Idempotency-Key` 控制。 +- 领域状态历史记录提交人、必要退货摘要和状态变化 +- 商家在 A417 确认收货后 → 触发 Payment 退款应用契约 #### 缓存、事件或外部依赖 @@ -2074,7 +2019,6 @@ - 异常:他人申请 → 404 + `RESOURCE.NOT_FOUND` - 异常:申请类型为 `RefundOnly` → 409 + `AFTER_SALES.WRONG_TYPE` - 异常:状态非 `PendingReturn` → 409 + `AFTER_SALES.INVALID_STATUS` -- 异常:快递单号已被使用 → 409 + `AFTER_SALES.DUPLICATE_TRACKING_NUMBER` --- @@ -2087,11 +2031,11 @@ - **当前状态**:已设计 - **用途**:查询单笔退款详情 - **方法与路径**:`GET /api/refunds/{refundId}` -- **operationId**:`Refund_Get` +- **operationId**:`Payment_GetRefund` - **请求 Schema**:(无) - **响应 Schema**:`RefundDetailResponse` - **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` -- **资源归属**:当前 buyerId 退款 / 当前 merchant 授权范围 +- **资源归属**:买家只看本人退款;商家只看关联售后订单分配给当前账号的退款 - **幂等要求**:GET 天然幂等 #### 请求 @@ -2102,12 +2046,26 @@ - **Body**:(无) - **校验规则**: - `refundId` UUID 格式 - - 资源归属当前 buyerId 或当前 merchant + - 资源归属当前买家,或关联售后订单分配给当前商家账号 #### 成功响应 - **HTTP 状态**:`200 OK` -- **响应 Schema**:`RefundDetailResponse`(同 A431) +- **响应 Schema**:`RefundDetailResponse`,独立于内部 `RefundResult`: + +```text +RefundDetailResponse { + refundId: uuid + requestId: uuid + paymentId: uuid + amount: decimal + currency: "CNY" + status: "Succeeded" | "Failed" + createdAt: string + completedAt: string? + failureMessage: string? +} +``` #### 失败响应 @@ -2144,18 +2102,18 @@ - **当前状态**:已设计 - **用途**:分页查询退款记录 - **方法与路径**:`GET /api/refunds` -- **operationId**:`Refund_List` +- **operationId**:`Payment_ListRefunds` - **请求 Schema**:`ListRefundsQuery` - **响应 Schema**:`RefundListResponse` - **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` -- **资源归属**:当前 buyerId 退款 / 当前 merchant 授权范围 +- **资源归属**:买家只看本人退款;商家只看关联售后订单分配给当前账号的退款 - **幂等要求**:GET 天然幂等 #### 请求 - **Route 参数**:(无) - **Query 参数**: - - `status`(可选):`Pending` / `Succeeded` / `Failed` + - `status`(可选):`Succeeded` / `Failed` - `createdFrom` / `createdTo`(可选):时间范围 - 标准分页 + 排序 - **Header**:`Authorization: Bearer ` @@ -2203,7 +2161,7 @@ #### 业务规则与并发 -- 买家视图按 `buyerId` 过滤;商家视图按授权范围过滤 +- 买家视图按 `buyerId` 过滤;商家视图按关联订单 `assignedMerchantUserId` 过滤 - 默认排序 `createdAt desc, refundId desc` #### 缓存、事件或外部依赖 @@ -2219,12 +2177,31 @@ --- +### C08 每日对账 Worker 内部契约 + +> 本任务由 `Mall.Worker` 执行,不提供 HTTP API,也不占用 Axxx 编号;A422~A425 只负责查询和处理已生成的对账事实。 + +#### 业务规则 + +1. 按 UTC 自然日生成前一日对账批次,日期范围采用左闭右开;同一范围建立唯一约束,重复执行复用同一批次。 +2. 对比 Payment 的支付、退款、钱包流水与 Ordering 的订单支付状态,生成匹配数量和稳定差异记录;不得通过直接改库隐藏差异。 +3. 批次与差异写入 PostgreSQL;只有批次完整核对成功后才标记 `Matched` 或 `HasDifferences`。 +4. 任务失败保持可重试状态并记录 `traceId`;重试不会生成第二个矛盾批次或重复差异。 + +#### 验证场景 + +- 同一日期任务重复执行只得到一个批次。 +- “支付成功但订单未更新”、迟到成功回调、退款与流水不一致均生成可由 A424/A425 处理的差异。 +- Worker 中途失败后重试可完成原批次,已登记差异不重复。 + +--- + ## 3. 跨接口的一致性约束 ### 3.1 错误码统一 - `AUTH.*` / `RESOURCE.*` / `IDEMPOTENCY.*` / `COMMON.*` 按接口设计 1.10 节基础 -- 模块错误码:`PAYMENT.*` / `AFTER_SALES.*` / `RECONCILIATION.*` +- 模块错误码:`PAYMENT.*` / `AFTER_SALES.*` - 同一错误场景使用同一错误码 + HTTP 状态 ### 3.2 幂等键一致性 @@ -2248,8 +2225,8 @@ ### 3.5 操作审计 -- 涉及状态变更的接口(A402 / A405 / A412 / A415 / A416 / A417 / A419 / A421 / A425 / A434)在 `audit_logs` 或对应表登记 actor、at、from_status、to_status -- A431 内部应用能力在 `refunds` + `wallet_ledgers` + `after_sales_requests` 同步登记(事务内) +- 涉及售后状态变更的接口(A412 / A415 / A416 / A417 / A419 / A434)在 AfterSales 领域状态历史中登记 actor、at、fromStatus、toStatus;支付与对账操作只写各自业务流水或处理记录,不建设通用后台操作日志 +- Payment 退款应用契约只操作 Payment 拥有的 `refunds`、`wallet_ledgers` 与 Outbox;可加入 AfterSales 编排的受控共享事务,但各模块仍只通过公开契约写自己的表 - 涉及资金的接口在 `wallet_ledgers` 写入流水 --- diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" index 1dd3161..0aa4eee 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" @@ -7,7 +7,7 @@ | 版本 | 日期 | 修改人 | 修改说明 | |------|------|--------|----------| -| v0.1 | 2026-07-24 | 罗皓晨、各模块负责人 | 建立通用约定,汇总六份个人接口原稿、102 个 Axxx 清单与详细定义,并登记冻结阻塞项 | +| v0.1 | 2026-07-24 | 罗皓晨、各模块负责人 | 建立通用约定,综合六份最新个人接口原稿,形成 107 个追踪编号、103 个有效 HTTP 契约及非 HTTP 协作边界,并更新冻结条件 | ## 一、通用约定 @@ -149,7 +149,10 @@ #### 1.6.2 Policy 与资源授权 -- 角色策略至少包括 `BuyerOnly`、`MerchantOnly`、`AdminOnly`。 +- 角色策略保持 `BuyerOnly`、`MerchantOnly`、`AdminOnly`;接口表中以 `/` 连接多个策略时表示一个角色授权要求接受其中任一角色,不得在 ASP.NET Core 中叠加多个 Policy 导致逻辑变成 AND。 +- 公开接口在清单中统一标记为 `Anonymous`,对应 OpenAPI `security: []`,不要求 JWT;`Anonymous` 只表示未强制认证,不是持久化角色或授权 Policy。 +- 刷新访问令牌以有效刷新令牌作为认证凭据,不要求访问令牌仍在有效期内;刷新令牌本身必须校验过期、撤销、账号状态和账号令牌版本。 +- 模拟支付回调不使用用户 JWT,通过 `X-Callback-Signature` 的 HMAC 签名认证渠道,请求幂等仍由 `Idempotency-Key` 与 PostgreSQL 唯一约束保证。 - 已认证但角色不满足 Policy 时返回 `403 Forbidden`。 - 角色正确不代表可以访问任意资源。订单、地址、购物车、消息、评价、售后等接口还必须校验资源归属和业务数据范围。 - 管理员权限只覆盖已明确的平台治理能力,不自动获得查看任意用户私人订单、地址、钱包或消息的权限。 @@ -284,7 +287,7 @@ CATALOG.PRODUCT_NOT_FOUND CART.ITEM_NOT_AVAILABLE ORDER.INVALID_STATUS PAYMENT.INSUFFICIENT_BALANCE -AFTER_SALES.DUPLICATE_APPLICATION +AFTER_SALES.INVALID_STATUS ``` 规则: @@ -299,6 +302,8 @@ AFTER_SALES.DUPLICATE_APPLICATION |---|---:|---| | `COMMON.VALIDATION_FAILED` | 400 | 请求字段校验失败 | | `COMMON.MALFORMED_JSON` | 400 | JSON 无法解析 | +| `COMMON.INVALID_UUID` | 400 | 路径或查询参数不是标准 UUID | +| `COMMON.PAYLOAD_TOO_LARGE` | 413 | 请求体或上传文件超过限制 | | `COMMON.UNSUPPORTED_MEDIA_TYPE` | 415 | 请求格式不支持 | | `COMMON.RATE_LIMITED` | 429 | 请求过于频繁 | | `COMMON.DEPENDENCY_UNAVAILABLE` | 503 | 当前操作依赖的服务不可用 | @@ -306,7 +311,9 @@ AFTER_SALES.DUPLICATE_APPLICATION | `AUTH.UNAUTHENTICATED` | 401 | 缺少有效认证信息 | | `AUTH.TOKEN_EXPIRED` | 401 | Token 已过期 | | `AUTH.TOKEN_REVOKED` | 401 | Token 已撤销或账号版本失效 | +| `AUTH.ACCOUNT_DISABLED` | 403 | 账号已被禁用 | | `AUTH.FORBIDDEN` | 403 | 当前身份无权执行操作 | +| `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 503 | 令牌撤销状态或必要认证依赖不可用 | | `RESOURCE.NOT_FOUND` | 404 | 目标资源不存在 | | `RESOURCE.CONFLICT` | 409 | 当前资源状态与操作冲突 | | `IDEMPOTENCY.KEY_REUSED` | 409 | 同一幂等键被用于不同请求内容 | @@ -540,33 +547,33 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 | 负责人 | 负责模块 | 接口编号范围 | |---|---|---| -| 唐宇昊 | Identity、Engagement | `A001`~`A100` | -| 顾欣月 | Catalog、Review | `A101`~`A200` | -| 朱惠惠 | Cart、Seckill | `A201`~`A300` | -| 韦乾强 | Ordering | `A301`~`A400` | -| 张海洋 | Payment、AfterSales | `A401`~`A500` | -| 罗皓晨 | Messaging、M00 公共 HTTP 接口 | `A501`~`A600` | +| 唐宇昊 | Identity、Engagement | `A001~A100` | +| 顾欣月 | Catalog、Review | `A101~A200` | +| 朱惠惠 | Cart、Seckill | `A201~A300` | +| 韦乾强 | Ordering | `A301~A400` | +| 张海洋 | Payment、AfterSales | `A401~A500` | +| 罗皓晨 | Messaging、M00 公共 HTTP 接口 | `A501~A600` | 编号规则: -1. 一个 HTTP 方法与路径组合占用一个接口编号;同一路径使用不同方法时分别编号。 -2. 编号合入 `dev` 后保持稳定。接口重命名但业务含义不变时保留编号;废弃接口保留原编号并标记“已废弃”,不得复用。 -3. SignalR 事件、领域事件、集成事件、Redis Key、RabbitMQ 资源和对象存储路径不占用 Axxx。 +1. 一个有效 HTTP 方法与路径组合占用一个接口编号;同一路径使用不同方法时分别编号。 +2. 编号合入 `dev` 后保持稳定。接口重命名但业务含义不变时保留编号;取消、废弃或转为内部契约后仍保留原编号,且不得复用。 +3. SignalR 事件、领域事件、集成事件、内部应用契约、Redis Key、RabbitMQ 资源和对象存储路径不新增 Axxx;A431 已取消并仅保留历史编号。 4. 不得为了占满区间提前设计无需求依据的接口。 -5. 只有本文件中同时具备清单、同编号详细定义且状态为“已确认”的接口,才可作为实现与 OpenAPI 事实源。 +5. 只有本文件中同时具备清单、同编号详细定义且状态为“已确认”的有效 HTTP 接口,才可作为实现与 OpenAPI 的冻结事实源。 ### 2.2 个人接口文件与保留规则 六份个人原稿统一保存在 `docs/02-设计文档/interface/`: -| 负责人 | 个人接口文件 | 已登记数量 | 接口编号范围 | -|---|---|---:|---| -| 唐宇昊 | [`interface-tyh.md`](interface/interface-tyh.md) | 23 | `A001`~`A100` | -| 顾欣月 | [`interface-gxy.md`](interface/interface-gxy.md) | 21 | `A101`~`A200` | -| 朱惠惠 | [`interface-zhh.md`](interface/interface-zhh.md) | 19 | `A201`~`A300` | -| 韦乾强 | [`interface-wqq.md`](interface/interface-wqq.md) | 7 | `A301`~`A400` | -| 张海洋 | [`interface-zhy.md`](interface/interface-zhy.md) | 25 | `A401`~`A500` | -| 罗皓晨 | [`interface-lhc.md`](interface/interface-lhc.md) | 7 | `A501`~`A600` | +| 负责人 | 个人接口文件 | 追踪编号 | 有效 HTTP | 接口编号范围 | +|---|---|---:|---:|---| +| 唐宇昊 | [`interface-tyh.md`](interface/interface-tyh.md) | 25 | 25 | `A001~A100` | +| 顾欣月 | [`interface-gxy.md`](interface/interface-gxy.md) | 22 | 22 | `A101~A200` | +| 朱惠惠 | [`interface-zhh.md`](interface/interface-zhh.md) | 19 | 17 | `A201~A300` | +| 韦乾强 | [`interface-wqq.md`](interface/interface-wqq.md) | 8 | 8 | `A301~A400` | +| 张海洋 | [`interface-zhy.md`](interface/interface-zhy.md) | 26 | 24 | `A401~A500` | +| 罗皓晨 | [`interface-lhc.md`](interface/interface-lhc.md) | 7 | 7 | `A501~A600` | 保留与同步规则: @@ -574,7 +581,7 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 2. 本文件是实现、OpenAPI、联调和测试的唯一接口事实源;个人原稿与本文件冲突时,不得直接按个人原稿编码。 3. 负责人修正个人原稿时,必须在同一任务中同步本文件的统一清单和同编号详细定义;只修改个人原稿不构成契约变更完成。 4. 总文档不得掩盖个人原稿中的缺口。尚未确认的字段、状态、跨模块边界或 DBxxx 必须标记为“部分定义”或“待交叉评审”。 -5. 当前共汇总 102 个不重复编号;编号、`operationId` 和“方法 + 路径”未发现全局重复,但这不代表全部接口已经冻结。 +5. 当前共保留 107 个不重复 Axxx 追踪编号,其中 103 个是有效 HTTP 契约;A229、A230、A418、A431 均为已取消历史编号。有效接口的编号、`operationId` 和“方法 + 路径”未发现全局重复。 ### 2.3 统一接口登记 @@ -582,20 +589,20 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 | 编号 | 模块/Tag | 需求 | 名称 | 方法 | 路径 | operationId | 鉴权 | 汇总状态 | |---|---|---|---|---|---|---|---|---| -| A001 | Identity | F01 | 买家注册 | POST | `/api/auth/register` | `Identity_RegisterUser` | 允许游客 | 待交叉评审 | -| A002 | Identity | F02 | 登录 | POST | `/api/auth/login` | `Identity_Login` | 允许游客 | 待交叉评审 | +| A001 | Identity | F01 | 买家注册 | POST | `/api/auth/register` | `Identity_RegisterUser` | Anonymous | 待交叉评审 | +| A002 | Identity | F02 | 登录 | POST | `/api/auth/login` | `Identity_Login` | Anonymous | 待交叉评审 | | A003 | Identity | F02 | 退出当前令牌 | POST | `/api/auth/logout` | `Identity_Logout` | BuyerOnly / MerchantOnly / AdminOnly | 待交叉评审 | | A004 | Identity | F02 | 获取当前用户 | GET | `/api/auth/me` | `Identity_GetCurrentUser` | BuyerOnly / MerchantOnly / AdminOnly | 待交叉评审 | -| A005 | Identity | F02 | 刷新访问令牌 | POST | `/api/auth/refresh-token` | `Identity_RefreshToken` | 已认证用户 | 待交叉评审 | -| A006 | Identity | F03 | 修改手机号 | POST | `/api/auth/change-phone` | `Identity_ChangePhone` | BuyerOnly / MerchantOnly | 待交叉评审 | -| A007 | Identity | F03 | 重置用户名 | POST | `/api/auth/reset-username` | `Identity_ResetUsername` | BuyerOnly / MerchantOnly | 待交叉评审 | -| A008 | Identity | F03 | 获取本人资料 | GET | `/api/users/me` | `Identity_GetMyProfile` | BuyerOnly / MerchantOnly | 待交叉评审 | -| A009 | Identity | F03 | 修改本人资料 | PATCH | `/api/users/me` | `Identity_UpdateMyProfile` | BuyerOnly / MerchantOnly | 待交叉评审 | -| A010 | Identity | F03 | 我的地址列表 | GET | `/api/users/me/addresses` | `Identity_ListMyAddresses` | BuyerOnly / MerchantOnly | 待交叉评审 | -| A011 | Identity | F03 | 新增地址 | POST | `/api/users/me/addresses` | `Identity_CreateMyAddress` | BuyerOnly / MerchantOnly | 待交叉评审 | -| A012 | Identity | F03 | 编辑地址 | PATCH | `/api/users/me/addresses/{addressId}` | `Identity_UpdateMyAddress` | BuyerOnly / MerchantOnly | 待交叉评审 | -| A013 | Identity | F03 | 删除地址 | DELETE | `/api/users/me/addresses/{addressId}` | `Identity_DeleteMyAddress` | BuyerOnly / MerchantOnly | 待交叉评审 | -| A014 | Identity | F03 | 设置默认地址 | POST | `/api/users/me/addresses/{addressId}/default` | `Identity_SetDefaultAddress` | BuyerOnly / MerchantOnly | 待交叉评审 | +| A005 | Identity | F02 | 刷新访问令牌 | POST | `/api/auth/refresh-token` | `Identity_RefreshToken` | 有效刷新令牌 | 待交叉评审 | +| A006 | Identity | F03 | 修改手机号 | POST | `/api/users/me/phone` | `Identity_ChangePhone` | BuyerOnly | 待交叉评审 | +| A007 | Identity | F03 | 重置用户名 | POST | `/api/users/me/username/reset` | `Identity_ResetUsername` | BuyerOnly | 待交叉评审 | +| A008 | Identity | F03 | 获取本人资料 | GET | `/api/users/me` | `Identity_GetMyProfile` | BuyerOnly | 待交叉评审 | +| A009 | Identity | F03 | 修改本人资料 | PATCH | `/api/users/me` | `Identity_UpdateMyProfile` | BuyerOnly | 待交叉评审 | +| A010 | Identity | F03 | 我的地址列表 | GET | `/api/users/me/addresses` | `Identity_ListMyAddresses` | BuyerOnly | 待交叉评审 | +| A011 | Identity | F03 | 新增地址 | POST | `/api/users/me/addresses` | `Identity_CreateMyAddress` | BuyerOnly | 待交叉评审 | +| A012 | Identity | F03 | 编辑地址 | PATCH | `/api/users/me/addresses/{addressId}` | `Identity_UpdateMyAddress` | BuyerOnly | 待交叉评审 | +| A013 | Identity | F03 | 删除地址 | DELETE | `/api/users/me/addresses/{addressId}` | `Identity_DeleteMyAddress` | BuyerOnly | 待交叉评审 | +| A014 | Identity | F03 | 设置默认地址 | POST | `/api/users/me/addresses/{addressId}/default` | `Identity_SetDefaultAddress` | BuyerOnly | 待交叉评审 | | A015 | Identity | F13 | 后台账号列表 | GET | `/api/admin/users` | `Identity_AdminListUsers` | AdminOnly | 待交叉评审 | | A016 | Identity | F13 | 禁用账号 | POST | `/api/admin/users/{userId}/disable` | `Identity_AdminDisableUser` | AdminOnly | 待交叉评审 | | A017 | Identity | F13 | 启用账号 | POST | `/api/admin/users/{userId}/enable` | `Identity_AdminEnableUser` | AdminOnly | 待交叉评审 | @@ -605,14 +612,16 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 | A021 | Engagement | X02 | 浏览历史列表 | GET | `/api/browsing-history` | `Engagement_ListBrowsingHistory` | BuyerOnly | 待交叉评审 | | A022 | Engagement | X02 | 修改浏览记录开关 | PATCH | `/api/browsing-history/settings` | `Engagement_UpdateBrowsingHistorySetting` | BuyerOnly | 待交叉评审 | | A023 | Engagement | X02 | 清空浏览历史 | DELETE | `/api/browsing-history` | `Engagement_ClearBrowsingHistory` | BuyerOnly | 待交叉评审 | +| A024 | Engagement | X02 | 记录浏览历史 | POST | `/api/browsing-history/records` | `Engagement_RecordBrowsingHistory` | BuyerOnly | 待交叉评审 | +| A025 | Engagement | X02 | 查询浏览记录开关 | GET | `/api/browsing-history/settings` | `Engagement_GetBrowsingHistorySetting` | BuyerOnly | 待交叉评审 | #### 顾欣月(A101~A200) | 编号 | 模块/Tag | 需求 | 名称 | 方法 | 路径 | operationId | 鉴权 | 汇总状态 | |---|---|---|---|---|---|---|---|---| -| A101 | Catalog | M02-01-FR02 | 购物端有效分类列表 | GET | `/api/categories` | `Catalog_ListCategories` | 游客可访问 | 待交叉评审 | -| A102 | Catalog | M02-01、F05、C04 | 商品分页列表/搜索/筛选/排序 | GET | `/api/products` | `Catalog_ListProducts` | 游客可访问 | 待交叉评审 | -| A103 | Catalog | M02-02、F06 | 购物端商品详情 | GET | `/api/products/{productId}` | `Catalog_GetProduct` | 游客可访问 | 待交叉评审 | +| A101 | Catalog | M02-01-FR02 | 购物端有效分类列表 | GET | `/api/categories` | `Catalog_ListCategories` | Anonymous | 待交叉评审 | +| A102 | Catalog | M02-01、F05、C04 | 商品分页列表/搜索/筛选/排序 | GET | `/api/products` | `Catalog_ListProducts` | Anonymous | 待交叉评审 | +| A103 | Catalog | M02-02、F06 | 购物端商品详情 | GET | `/api/products/{productId}` | `Catalog_GetProduct` | Anonymous | 待交叉评审 | | A110 | Catalog | M06-01-FR01 | 后台分类列表(全状态) | GET | `/api/merchant/categories` | `Catalog_ListMerchantCategories` | MerchantOnly | 待交叉评审 | | A111 | Catalog | M06-01-FR02 | 新建分类 | POST | `/api/merchant/categories` | `Catalog_CreateCategory` | MerchantOnly | 待交叉评审 | | A112 | Catalog | M06-01-FR02 | 编辑分类 | PUT | `/api/merchant/categories/{categoryId}` | `Catalog_UpdateCategory` | MerchantOnly | 待交叉评审 | @@ -627,10 +636,11 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 | A126 | Catalog | M06-01-FR07 | 商品下架 | POST | `/api/merchant/products/{productId}/unpublish` | `Catalog_UnpublishProduct` | MerchantOnly | 待交叉评审 | | A127 | Catalog | M06-01-FR09 | 上传商品图片 | POST | `/api/merchant/products/{productId}/images` | `Catalog_UploadProductImage` | MerchantOnly | 待交叉评审 | | A128 | Catalog | M06-01-FR09 | 删除商品图片 | DELETE | `/api/merchant/products/{productId}/images/{imageId}` | `Catalog_DeleteProductImage` | MerchantOnly | 待交叉评审 | -| A140 | Review | M07-FR06、FR07 | 商品公开评价分页 + 评分汇总 | GET | `/api/products/{productId}/reviews` | `Review_ListProductReviews` | 游客可访问 | 待交叉评审 | +| A140 | Review | M07-FR06、FR07 | 商品公开评价分页 + 评分汇总 | GET | `/api/products/{productId}/reviews` | `Review_ListProductReviews` | Anonymous | 待交叉评审 | | A141 | Review | M07-FR03 | 上传评价图片(提交前暂存) | POST | `/api/reviews/images` | `Review_UploadReviewImage` | BuyerOnly | 待交叉评审 | | A142 | Review | M07-FR04、FR05 | 提交商品评价(幂等) | POST | `/api/reviews` | `Review_CreateReview` | BuyerOnly | 待交叉评审 | | A143 | Review | M07-FR01 | 查询订单项评价资格/结果 | GET | `/api/reviews/eligibility` | `Review_GetReviewEligibility` | BuyerOnly | 待交叉评审 | +| A144 | Review | M07-FR06 | 单条公开评价详情查询 | GET | `/api/reviews/{reviewId}` | `Review_GetReview` | Anonymous | 待交叉评审 | #### 朱惠惠(A201~A300) @@ -645,28 +655,29 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 | A207 | Cart | F07 | 清空购物车 | DELETE | `/api/cart` | `Cart_Clear` | BuyerOnly | 待交叉评审 | | A208 | Cart | F07 | 获取结算预览 | GET | `/api/cart/checkout-preview` | `Cart_GetCheckoutPreview` | BuyerOnly | 待交叉评审 | | A220 | Seckill | C01 | 商家创建秒杀活动 | POST | `/api/merchant/seckill-activities` | `Seckill_CreateActivity` | MerchantOnly | 待交叉评审 | -| A221 | Seckill | C01 | 商家更新秒杀活动 | PATCH | `/api/seckill-activities/{activityId}` | `Seckill_UpdateActivity` | MerchantOnly | 待交叉评审 | -| A222 | Seckill | C01 | 商家发布秒杀活动 | POST | `/api/seckill-activities/{activityId}/publish` | `Seckill_PublishActivity` | MerchantOnly | 待交叉评审 | -| A223 | Seckill | C01 | 商家取消秒杀活动 | POST | `/api/seckill-activities/{activityId}/cancel` | `Seckill_CancelActivity` | MerchantOnly | 待交叉评审 | +| A221 | Seckill | C01 | 商家更新秒杀活动 | PATCH | `/api/merchant/seckill-activities/{activityId}` | `Seckill_UpdateActivity` | MerchantOnly | 待交叉评审 | +| A222 | Seckill | C01 | 商家发布秒杀活动 | POST | `/api/merchant/seckill-activities/{activityId}/publish` | `Seckill_PublishActivity` | MerchantOnly | 待交叉评审 | +| A223 | Seckill | C01 | 商家取消秒杀活动 | POST | `/api/merchant/seckill-activities/{activityId}/cancel` | `Seckill_CancelActivity` | MerchantOnly | 待交叉评审 | | A224 | Seckill | C01 | 商家秒杀活动列表 | GET | `/api/merchant/seckill-activities` | `Seckill_ListMerchantActivities` | MerchantOnly | 待交叉评审 | -| A225 | Seckill | C01 | 商家秒杀活动详情 | GET | `/api/seckill-activities/{activityId}` | `Seckill_GetMerchantActivityDetail` | MerchantOnly | 待交叉评审 | -| A226 | Seckill | C01 | 买家秒杀活动列表 | GET | `/api/seckill-activities` | `Seckill_ListActiveActivities` | 允许游客 | 待交叉评审 | -| A227 | Seckill | C01 | 买家秒杀活动详情 | GET | `/api/seckill-activities/{activityId}/public` | `Seckill_GetActiveActivityDetail` | 允许游客 | 待交叉评审 | +| A225 | Seckill | C01 | 商家秒杀活动详情 | GET | `/api/merchant/seckill-activities/{activityId}` | `Seckill_GetMerchantActivityDetail` | MerchantOnly | 待交叉评审 | +| A226 | Seckill | C01 | 买家秒杀活动列表 | GET | `/api/seckill-activities` | `Seckill_ListActiveActivities` | Anonymous | 待交叉评审 | +| A227 | Seckill | C01 | 买家秒杀活动详情 | GET | `/api/seckill-activities/{activityId}` | `Seckill_GetActiveActivityDetail` | Anonymous | 待交叉评审 | | A228 | Seckill | C01 | 秒杀下单 | POST | `/api/seckill-orders` | `Seckill_PlaceOrder` | BuyerOnly | 待交叉评审 | -| A229 | Seckill | C01 | 买家秒杀订单列表 | GET | `/api/seckill-orders` | `Seckill_ListMyOrders` | BuyerOnly | 边界冲突,暂不实施 | -| A230 | Seckill | C01 | 买家秒杀订单详情 | GET | `/api/seckill-orders/{orderId}` | `Seckill_GetMyOrder` | BuyerOnly | 边界冲突,暂不实施 | +| A229 | Seckill | C01 | 买家秒杀订单列表(取消) | — | — | — | — | 已取消,历史占号 | +| A230 | Seckill | C01 | 买家秒杀订单详情(取消) | — | — | — | — | 已取消,历史占号 | #### 韦乾强(A301~A400) | 编号 | 模块/Tag | 需求 | 名称 | 方法 | 路径 | operationId | 鉴权 | 汇总状态 | |---|---|---|---|---|---|---|---|---| | A301 | Ordering | F08 | 提交订单 | POST | `/api/orders` | `Ordering_CreateOrder` | BuyerOnly | 部分定义 | -| A302 | Ordering | F09 | 查询订单列表 | GET | `/api/orders` | `Ordering_GetOrders` | BuyerOnly | 部分定义 | -| A303 | Ordering | F09 | 查询订单详情 | GET | `/api/orders/{orderId}` | `Ordering_GetOrderById` | BuyerOnly | 部分定义 | +| A302 | Ordering | F09、C01 | 查询订单列表 | GET | `/api/orders` | `Ordering_ListOrders` | BuyerOnly | 部分定义 | +| A303 | Ordering | F09、C01 | 查询订单详情 | GET | `/api/orders/{orderId}` | `Ordering_GetOrder` | BuyerOnly | 部分定义 | | A304 | Ordering | F09 | 取消订单 | POST | `/api/orders/{orderId}/cancel` | `Ordering_CancelOrder` | BuyerOnly | 部分定义 | -| A305 | Merchant | F12 | 商家查询订单列表 | GET | `/api/merchant/orders` | `Merchant_GetOrders` | MerchantOnly | 部分定义 | -| A306 | Merchant | F12 | 商家查询订单详情 | GET | `/api/merchant/orders/{orderId}` | `Merchant_GetOrderById` | MerchantOnly | 部分定义 | -| A307 | Merchant | F12 | 商家发货 | POST | `/api/merchant/orders/{orderId}/ship` | `Merchant_ShipOrder` | MerchantOnly | 部分定义 | +| A305 | Ordering | F12 | 商家查询订单列表 | GET | `/api/merchant/orders` | `Ordering_ListMerchantOrders` | MerchantOnly | 部分定义 | +| A306 | Ordering | F12 | 商家查询订单详情 | GET | `/api/merchant/orders/{orderId}` | `Ordering_GetMerchantOrder` | MerchantOnly | 部分定义 | +| A307 | Ordering | F12 | 商家发货 | POST | `/api/merchant/orders/{orderId}/ship` | `Ordering_ShipOrder` | MerchantOnly | 部分定义 | +| A308 | Ordering | F09 | 买家确认收货 | POST | `/api/orders/{orderId}/confirm-receipt` | `Ordering_ConfirmReceipt` | BuyerOnly | 部分定义 | #### 张海洋(A401~A500) @@ -687,39 +698,41 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 | A415 | AfterSales | M10-FR10 | 撤销申请 | POST | `/api/after-sales/requests/{requestId}/cancel` | `AfterSales_CancelRequest` | BuyerOnly | 待交叉评审 | | A416 | AfterSales | M10-FR05 | 商家审核 | POST | `/api/after-sales/requests/{requestId}/audit` | `AfterSales_AuditRequest` | MerchantOnly | 待交叉评审 | | A417 | AfterSales | M10-FR11 | 商家确认退货 | POST | `/api/after-sales/requests/{requestId}/confirm-return` | `AfterSales_ConfirmReturn` | MerchantOnly | 待交叉评审 | -| A418 | AfterSales | M10-FR04 | 审核日志 | GET | `/api/after-sales/requests/{requestId}/audit-logs` | `AfterSales_ListAuditLogs` | BuyerOnly/MerchantOnly | 待交叉评审 | +| A418 | AfterSales | M10-FR04 | 已取消:状态时间线并入 A414 | — | — | — | — | 已取消,历史占号 | | A419 | AfterSales | M10-FR07 | 退款失败重试 | POST | `/api/after-sales/requests/{requestId}/retry-refund` | `AfterSales_RetryRefund` | MerchantOnly | 待交叉评审 | -| A421 | Payment | C08-FR01~FR05 | 接收支付回调 | POST | `/api/payment/callbacks` | `Payment_ReceiveCallback` | Service(模拟渠道) | 待交叉评审 | -| A422 | Reconciliation | C08-FR06 | 对账批次列表 | GET | `/api/admin/reconciliation/batches` | `Reconciliation_ListBatches` | AdminOnly | 待交叉评审 | -| A423 | Reconciliation | C08-FR06 | 对账批次详情 | GET | `/api/admin/reconciliation/batches/{batchId}` | `Reconciliation_GetBatch` | AdminOnly | 待交叉评审 | -| A424 | Reconciliation | C08-FR07/FR08 | 差异列表 | GET | `/api/admin/reconciliation/batches/{batchId}/differences` | `Reconciliation_ListDifferences` | AdminOnly | 待交叉评审 | -| A425 | Reconciliation | C08-FR08 | 差异处理 | POST | `/api/admin/reconciliation/differences/{differenceId}/process` | `Reconciliation_ProcessDifference` | AdminOnly | 待交叉评审 | -| A431 | Payment | M10-FR07 | 模拟退款 | POST | `/api/after-sales/requests/{requestId}/refund` | `Refund_Create` | MerchantOnly(系统内部) | 待交叉评审 | -| A432 | Payment | M10-FR04 | 退款详情 | GET | `/api/refunds/{refundId}` | `Refund_Get` | BuyerOnly/MerchantOnly | 待交叉评审 | -| A433 | Payment | M10-FR03 | 退款列表 | GET | `/api/refunds` | `Refund_List` | BuyerOnly/MerchantOnly | 待交叉评审 | +| A421 | Payment | C08-FR01~FR05 | 接收支付回调 | POST | `/api/payment/callbacks` | `Payment_ReceiveCallback` | HMAC 签名(模拟渠道) | 待交叉评审 | +| A422 | Payment | C08-FR06 | 对账批次列表 | GET | `/api/admin/reconciliation/batches` | `Payment_ListReconciliationBatches` | AdminOnly | 待交叉评审 | +| A423 | Payment | C08-FR06 | 对账批次详情 | GET | `/api/admin/reconciliation/batches/{batchId}` | `Payment_GetReconciliationBatch` | AdminOnly | 待交叉评审 | +| A424 | Payment | C08-FR07/FR08 | 差异列表 | GET | `/api/admin/reconciliation/batches/{batchId}/differences` | `Payment_ListReconciliationDifferences` | AdminOnly | 待交叉评审 | +| A425 | Payment | C08-FR08 | 差异处理 | POST | `/api/admin/reconciliation/differences/{differenceId}/process` | `Payment_ProcessReconciliationDifference` | AdminOnly | 待交叉评审 | +| A431 | Payment | M10-FR07 | 已取消:退款 HTTP 改为 Payment 应用契约 | — | — | — | — | 已取消,历史占号 | +| A432 | Payment | M10-FR04 | 退款详情 | GET | `/api/refunds/{refundId}` | `Payment_GetRefund` | BuyerOnly/MerchantOnly | 待交叉评审 | +| A433 | Payment | M10-FR03 | 退款列表 | GET | `/api/refunds` | `Payment_ListRefunds` | BuyerOnly/MerchantOnly | 待交叉评审 | +| A434 | AfterSales | M10-FR11 | 买家提交退货/寄回信息 | POST | `/api/after-sales/requests/{requestId}/return-info` | `AfterSales_SubmitReturnInfo` | BuyerOnly | 待交叉评审 | #### 罗皓晨(A501~A600) | 编号 | 模块/Tag | 需求 | 名称 | 方法 | 路径 | operationId | 鉴权 | 汇总状态 | |---|---|---|---|---|---|---|---|---| -| A501 | Messaging | X03-FR03 | 查询本人消息列表 | GET | `/api/messages` | `Messaging_ListMessages` | 买家或商家 JWT | 待交叉评审 | -| A502 | Messaging | X03-FR04 | 查询本人消息详情 | GET | `/api/messages/{messageId}` | `Messaging_GetMessage` | 买家或商家 JWT | 待交叉评审 | -| A503 | Messaging | X03-FR05 | 查询本人未读消息数 | GET | `/api/messages/unread-count` | `Messaging_GetUnreadCount` | 买家或商家 JWT | 待交叉评审 | -| A504 | Messaging | X03-FR06 | 标记本人单条消息已读 | POST | `/api/messages/{messageId}/read` | `Messaging_MarkMessageRead` | 买家或商家 JWT | 待交叉评审 | -| A505 | Messaging | X03-FR07 | 标记本人当前消息全部已读 | POST | `/api/messages/read-all` | `Messaging_MarkAllMessagesRead` | 买家或商家 JWT | 待交叉评审 | -| A506 | M00 | C10-FR06 | API 存活检查 | GET | `/health/live` | `Infrastructure_GetLiveness` | 无 | 待交叉评审 | -| A507 | M00 | C10-FR06 | API 就绪检查 | GET | `/health/ready` | `Infrastructure_GetReadiness` | 无 | 待交叉评审 | +| A501 | Messaging | X03-FR03 | 查询本人消息列表 | GET | `/api/messages` | `Messaging_ListMessages` | BuyerOnly / MerchantOnly | 待交叉评审 | +| A502 | Messaging | X03-FR04 | 查询本人消息详情 | GET | `/api/messages/{messageId}` | `Messaging_GetMessage` | BuyerOnly / MerchantOnly | 待交叉评审 | +| A503 | Messaging | X03-FR05 | 查询本人未读消息数 | GET | `/api/messages/unread-count` | `Messaging_GetUnreadCount` | BuyerOnly / MerchantOnly | 待交叉评审 | +| A504 | Messaging | X03-FR06 | 标记本人单条消息已读 | POST | `/api/messages/{messageId}/read` | `Messaging_MarkMessageRead` | BuyerOnly / MerchantOnly | 待交叉评审 | +| A505 | Messaging | X03-FR07 | 标记本人当前消息全部已读 | POST | `/api/messages/read-all` | `Messaging_MarkAllMessagesRead` | BuyerOnly / MerchantOnly | 待交叉评审 | +| A506 | Infrastructure | C10-FR06 | API 存活检查 | GET | `/health/live` | `Infrastructure_GetLiveness` | Anonymous | 待交叉评审 | +| A507 | Infrastructure | C10-FR06 | API 就绪检查 | GET | `/health/ready` | `Infrastructure_GetReadiness` | Anonymous | 待交叉评审 | ## 三、统一接口详细定义 -以下内容按 Axxx 编号区间汇总。每段开头保留个人原稿来源;汇总状态以第二章和第五章为准,原稿内的“已定义/已设计”不能替代交叉评审。 - -> 来源:[interface-tyh.md](interface/interface-tyh.md)。已完成结构汇总,但仍须处理身份范围、浏览历史写入和错误语义后才能冻结。 +本章只收录 103 个有效 HTTP 契约。已取消编号和内部应用契约统一放在第四章,避免被误实现为公开端点。 -> 每个接口按《接口设计》1.20 节模板补齐。Schema 名称遵守 OpenAPI 7.2 节:PascalCase + 用途后缀;`operationId` 使用 `_`;路径参数使用单数对象 + `Id`。 +> 来源:[`interface-tyh.md`](interface/interface-tyh.md)。F03 已统一为买家专属;A024/A025 已闭合浏览记录写入与设置查询,仍待 DBxxx、OpenAPI 和交叉评审。 ### A001 买家注册 +- 请求 Schema:RegisterUserRequest +- 身份与 Policy:Anonymous + - 模块 / Tag:Identity - 需求编号:F01、M01-01 - 负责人:唐宇昊 @@ -753,7 +766,6 @@ RegisterUserRequest { #### 成功响应 - HTTP 状态:`201 Created` -- Response Header:`Location: /api/users/me` - 响应 Schema:`RegisteredUserResponse` ```text @@ -780,14 +792,14 @@ RegisteredUserResponse { #### 业务规则与并发 - 用户名生成规则:`u_` + 8 位不易混淆字符(去除 0/O/1/I/L),最多重试 3 次;最终不重复。 -- 密码使用可靠哈希算法(如 Argon2id)保存;明文密码、确认密码和哈希结果均不得出现在响应、日志或 ProblemDetails 中。 +- 密码使用可靠的自适应哈希保存,具体算法按系统架构与实现统一确定;明文密码、确认密码和哈希结果均不得出现在响应、日志或 ProblemDetails 中。 - 手机号唯一性由 PostgreSQL 唯一约束 `ux_users_phone` 保证;并发注册同一手机号时仅一笔成功,其余返回 `409 / AUTH.PHONE_ALREADY_REGISTERED`。 - 公开注册固定产出 Buyer;客户端传入的角色字段被忽略,且不被任何后续接口读取。 #### 缓存、事件或外部依赖 - 不缓存、不发布集成事件。 -- 成功后建议客户端调用 `A004 GetCurrentUser` 校验登录态恢复。 +- 注册成功不自动签发令牌;客户端应引导用户调用 A002 登录,取得令牌后才能调用 A004 等受保护接口。 #### 验证场景 @@ -800,6 +812,9 @@ RegisteredUserResponse { ### A002 登录 +- 请求 Schema:LoginRequest +- 身份与 Policy:Anonymous + - 模块 / Tag:Identity - 需求编号:F02、M01-02 - 负责人:唐宇昊 @@ -868,12 +883,12 @@ CurrentUserResponse { - 账号不存在和密码错误统一返回 `401 / AUTH.INVALID_CREDENTIALS`,不泄露账号是否存在。 - 禁用账号返回 `403 / AUTH.ACCOUNT_DISABLED` 并明确说明联系管理员。 - 签发的 JWT 至少包含 `sub`、`role`、`jti`、过期时间与 `tokenVersion`;签名、Issuer、Audience、过期、撤销、账号状态、版本号任一校验失败即拒绝。 -- 刷新令牌与访问令牌通过受控 Redis 列表记录 `jti`,实现多实例撤销共享。 +- 访问令牌和刷新令牌都包含独立 `jti`;仅退出、刷新轮换或账号禁用时把相应 `jti` 加入撤销集合,刚签发的有效令牌不得写入撤销集合。 - 当令牌服务或 Redis 撤销校验不可用时,宁可拒绝登录也不放过无法确认的请求(`503 / AUTH.TOKEN_SERVICE_UNAVAILABLE`)。 #### 缓存、事件或外部依赖 -- 登录成功后向 Redis 写入撤销/版本共享:`auth:revoked:{jti}` 与 `auth:user:{userId}:tokenVersion`。 +- 登录成功后登记当前账号的 `tokenVersion` 和刷新令牌会话;`auth:revoked:*` 只保存已经撤销的令牌,不登记新签发令牌。 - 不发布集成事件;用户级会话不持久化到数据库。 #### 验证场景 @@ -886,6 +901,9 @@ CurrentUserResponse { ### A003 退出当前令牌 +- 请求 Schema:无 +- 身份与 Policy:BuyerOnly / MerchantOnly / AdminOnly + - 模块 / Tag:Identity - 需求编号:F02、M01-02 - 负责人:唐宇昊 @@ -938,6 +956,9 @@ LogoutResponse { ### A004 获取当前用户 +- 请求 Schema:无 +- 身份与 Policy:BuyerOnly / MerchantOnly / AdminOnly + - 模块 / Tag:Identity - 需求编号:F02、M01-02 - 负责人:唐宇昊 @@ -965,8 +986,7 @@ LogoutResponse { |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少访问令牌 | | 401 | `AUTH.TOKEN_EXPIRED` | 访问令牌已过期 | -| 401 | `AUTH.TOKEN_REVOKED` | 访问令牌已退出或账号版本失效 | -| 403 | `AUTH.ACCOUNT_DISABLED` | 账号被禁用 | +| 401 | `AUTH.TOKEN_REVOKED` | 访问令牌已退出、账号已禁用或令牌版本失效 | | 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态校验不可用 | #### 业务规则与并发 @@ -986,6 +1006,9 @@ LogoutResponse { ### A005 刷新访问令牌 +- 请求 Schema:RefreshTokenRequest +- 身份与 Policy:有效刷新令牌 + - 模块 / Tag:Identity - 需求编号:F02、M01-02 - 负责人:唐宇昊 @@ -1015,15 +1038,14 @@ RefreshTokenRequest { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 缺少刷新令牌 | -| 401 | `AUTH.TOKEN_REVOKED` | 刷新令牌已撤销 | +| 401 | `AUTH.TOKEN_REVOKED` | 刷新令牌已撤销、账号已禁用或令牌版本失效 | | 401 | `AUTH.TOKEN_EXPIRED` | 刷新令牌已过期 | -| 403 | `AUTH.ACCOUNT_DISABLED` | 账号被禁用 | | 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态共享不可用 | #### 业务规则与并发 - 旧刷新令牌随新令牌签发一起撤销,避免长期重放。 -- 访问令牌与刷新令牌均加入撤销集合。 +- 旧刷新令牌加入撤销集合;新访问令牌和新刷新令牌保持有效,不得误写入撤销集合。 #### 缓存、事件或外部依赖 @@ -1036,13 +1058,16 @@ RefreshTokenRequest { ### A006 修改手机号 +- 请求 Schema:ChangePhoneRequest +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Identity - 需求编号:F03、M01-03 - 负责人:唐宇昊 - 关联数据表:DB001、DB004 - 当前状态:待交叉评审 -- 用途:买家或商家修改本人手机号,提交后旧登录态全部失效并要求重新登录。 -- 方法与路径:`POST /api/auth/change-phone` +- 用途:买家修改本人手机号,提交后修改前签发的全部登录态失效并要求重新登录。 +- 方法与路径:`POST /api/users/me/phone` - operationId:`Identity_ChangePhone` #### 请求 @@ -1069,7 +1094,7 @@ ChangePhoneRequest { | 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或新手机号格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 401 | `AUTH.INVALID_CREDENTIALS` | 当前密码错误 | -| 403 | `AUTH.ACCOUNT_DISABLED` | 账号已被禁用 | +| 401 | `AUTH.TOKEN_REVOKED` | 账号已禁用或访问令牌版本失效 | | 409 | `AUTH.PHONE_ALREADY_REGISTERED` | 新手机号已被他人使用 | | 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态共享不可用,无法签发新令牌 | @@ -1081,7 +1106,7 @@ ChangePhoneRequest { #### 缓存、事件或外部依赖 -- Redis Key:`auth:user:{userId}:tokenVersion`。 +- Redis Key:`auth:token-version:{userId}`。 - 不发布集成事件。 #### 验证场景 @@ -1093,13 +1118,16 @@ ChangePhoneRequest { ### A007 重置用户名 +- 请求 Schema:无 +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Identity - 需求编号:F03、M01-03 - 负责人:唐宇昊 - 关联数据表:DB001 - 当前状态:待交叉评审 - 用途:用户自助重置一次用户名,重置次数用完即返回错误。 -- 方法与路径:`POST /api/auth/reset-username` +- 方法与路径:`POST /api/users/me/username/reset` - operationId:`Identity_ResetUsername` #### 请求 @@ -1124,6 +1152,7 @@ ResetUsernameResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 409 | `AUTH.USERNAME_RESET_EXHAUSTED` | 当前账号已使用过一次自助重置 | | 409 | `AUTH.USERNAME_GENERATION_RETRY_EXHAUSTED` | 新用户名生成冲突且超过重试上限 | | 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 服务暂不可用 | @@ -1145,12 +1174,15 @@ ResetUsernameResponse { ### A008 获取本人资料 +- 请求 Schema:无 +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Identity - 需求编号:F03、M01-03 - 负责人:唐宇昊 - 关联数据表:DB001 - 当前状态:待交叉评审 -- 用途:买家或商家查看本人资料;不返回内部审计或登录态字段。 +- 用途:买家查看本人资料;不返回内部审计或登录态字段。 - 方法与路径:`GET /api/users/me` - operationId:`Identity_GetMyProfile` @@ -1170,7 +1202,9 @@ MyProfileResponse { username: string phoneMasked: string avatarUrl: string - role: "Buyer" | "Merchant" + displayName: string? + bio: string? + role: "Buyer" canResetUsername: boolean // 是否仍可自助重置用户名 createdAt: string } @@ -1181,7 +1215,7 @@ MyProfileResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 403 | `AUTH.ACCOUNT_DISABLED` | 账号已被禁用 | +| 401 | `AUTH.TOKEN_REVOKED` | 账号已禁用或访问令牌版本失效 | | 404 | `RESOURCE.NOT_FOUND` | 当前用户记录不存在 | #### 业务规则与并发 @@ -1196,17 +1230,20 @@ MyProfileResponse { #### 验证场景 - 已登录买家 → 200。 -- 已禁用账号 → 403 / `AUTH.ACCOUNT_DISABLED`。 -- 商家账号登录后同样可调用,但 `role` 为 `Merchant`。 +- 已禁用账号的旧令牌 → 401 / `AUTH.TOKEN_REVOKED`。 +- 商家或管理员调用 → 403 / `AUTH.FORBIDDEN`。 ### A009 修改本人资料 +- 请求 Schema:UpdateMyProfileRequest +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Identity - 需求编号:F03、M01-03 - 负责人:唐宇昊 - 关联数据表:DB001 - 当前状态:待交叉评审 -- 用途:买家或商家维护本人展示资料;手机号与用户名变更走专门接口,本接口不接受这两类字段。 +- 用途:买家维护本人展示资料;手机号与用户名变更走专门接口,本接口不接受这两类字段。 - 方法与路径:`PATCH /api/users/me` - operationId:`Identity_UpdateMyProfile` @@ -1219,7 +1256,6 @@ MyProfileResponse { UpdateMyProfileRequest { displayName?: string // 可选,昵称或展示名 bio?: string // 可选,简介,0~200 字 - avatarUrl?: string // 可选;本期不支持自定义头像上传,仅允许系统默认 } ``` @@ -1234,8 +1270,8 @@ UpdateMyProfileRequest { |---:|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 字段长度或格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 403 | `AUTH.ACCOUNT_DISABLED` | 账号已被禁用 | -| 409 | `COMMON.VALIDATION_FAILED` | 不接受修改 `phone`、`username`、`role`、`status`、`userId` | +| 401 | `AUTH.TOKEN_REVOKED` | 账号已禁用或访问令牌版本失效 | +| 400 | `COMMON.VALIDATION_FAILED` | 请求包含不允许修改的 `phone`、`username`、`role`、`status`、`userId` 或 `avatarUrl` | #### 业务规则与并发 @@ -1254,12 +1290,15 @@ UpdateMyProfileRequest { ### A010 我的地址列表 +- 请求 Schema:无 +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Identity - 需求编号:F03、M01-03 - 负责人:唐宇昊 - 关联数据表:DB003 - 当前状态:待交叉评审 -- 用途:买家或商家分页查询本人收货地址,标记默认地址。 +- 用途:买家分页查询本人收货地址,标记默认地址。 - 方法与路径:`GET /api/users/me/addresses` - operationId:`Identity_ListMyAddresses` @@ -1288,6 +1327,7 @@ AddressListResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 400 | `COMMON.VALIDATION_FAILED` | 分页参数非法 | #### 业务规则与并发 @@ -1306,12 +1346,15 @@ AddressListResponse { ### A011 新增地址 +- 请求 Schema:CreateAddressRequest +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Identity - 需求编号:F03、M01-03 - 负责人:唐宇昊 - 关联数据表:DB003 - 当前状态:待交叉评审 -- 用途:买家或商家新增收货地址。 +- 用途:买家新增收货地址。 - 方法与路径:`POST /api/users/me/addresses` - operationId:`Identity_CreateMyAddress` @@ -1359,6 +1402,8 @@ AddressResponse { |---:|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 409 | `IDENTITY.ADDRESS_LIMIT_REACHED` | 本人地址数量已达到 20 条上限 | #### 业务规则与并发 @@ -1376,12 +1421,15 @@ AddressResponse { ### A012 编辑地址 +- 请求 Schema:UpdateAddressRequest +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Identity - 需求编号:F03、M01-03 - 负责人:唐宇昊 - 关联数据表:DB003 - 当前状态:待交叉评审 -- 用途:买家或商家编辑本人地址;非本人地址返回 404。 +- 用途:买家编辑本人地址;非本人地址返回 404。 - 方法与路径:`PATCH /api/users/me/addresses/{addressId}` - operationId:`Identity_UpdateMyAddress` @@ -1402,6 +1450,7 @@ AddressResponse { |---:|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `RESOURCE.NOT_FOUND` | 地址不存在或不属于当前用户 | #### 业务规则与并发 @@ -1420,12 +1469,16 @@ AddressResponse { ### A013 删除地址 +- 请求 Schema:无 +- 响应 Schema:无(204) +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Identity - 需求编号:F03、M01-03 - 负责人:唐宇昊 - 关联数据表:DB003 - 当前状态:待交叉评审 -- 用途:买家或商家删除本人地址;默认地址被删除时不自动指定其他地址。 +- 用途:买家删除本人地址;默认地址被删除时不自动指定其他地址。 - 方法与路径:`DELETE /api/users/me/addresses/{addressId}` - operationId:`Identity_DeleteMyAddress` @@ -1443,13 +1496,12 @@ AddressResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 404 | `RESOURCE.NOT_FOUND` | 地址不存在或不属于当前用户 | -| 409 | `IDENTITY.ADDRESS_IN_USE_BY_ORDER` | 该地址被未完成订单引用,需要先迁移或完成订单 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | #### 业务规则与并发 - 删除默认地址后不自动指定其他默认地址;下单时由买家明确确认。 -- 幂等:已删除地址再次删除返回 204,不报错。 +- 幂等:本人地址不存在、已删除或不属于当前买家时统一返回 204,不泄露地址是否存在或归属;历史订单使用地址快照,不阻止删除当前地址记录。 #### 缓存、事件或外部依赖 @@ -1459,15 +1511,19 @@ AddressResponse { - 删除非默认地址 → 204,列表更新。 - 删除默认地址 → 204,列表无默认地址标记。 +- 重复删除或传入他人地址 ID → 204,不泄露资源归属。 ### A014 设置默认地址 +- 请求 Schema:无 +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Identity - 需求编号:F03、M01-03 - 负责人:唐宇昊 - 关联数据表:DB003 - 当前状态:待交叉评审 -- 用途:买家或商家将本人某条地址设为默认;同一用户最多一个默认地址。 +- 用途:买家将本人某条地址设为默认;同一用户最多一个默认地址。 - 方法与路径:`POST /api/users/me/addresses/{addressId}/default` - operationId:`Identity_SetDefaultAddress` @@ -1486,6 +1542,7 @@ AddressResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `RESOURCE.NOT_FOUND` | 地址不存在或不属于当前用户 | #### 业务规则与并发 @@ -1503,6 +1560,9 @@ AddressResponse { ### A015 后台账号列表 +- 请求 Schema:无(Query 分页/筛选) +- 身份与 Policy:AdminOnly + - 模块 / Tag:Identity - 需求编号:F13、M06-03 - 负责人:唐宇昊 @@ -1550,6 +1610,7 @@ AdminUserListResponse { - 永远不返回管理员账号;过滤条件 `role IN ('Buyer','Merchant')`。 - 列表响应只返回管理操作所需字段;不返回密码哈希、内部审计、登录态。 - 手机号使用掩码 `138****8888` 形式。 +- `AdminUserResponse.isDefaultMerchant` 仅用于说明单店默认运营账号及禁用按钮原因;买家固定为 `false`。 #### 缓存、事件或外部依赖 @@ -1563,6 +1624,9 @@ AdminUserListResponse { ### A016 禁用账号 +- 请求 Schema:无 +- 身份与 Policy:AdminOnly + - 模块 / Tag:Identity - 需求编号:F13、M06-03 - 负责人:唐宇昊 @@ -1576,13 +1640,7 @@ AdminUserListResponse { - Route 参数:`userId: uuid` - Header:`Authorization: Bearer `(必填,角色 Admin) -- Body: - -```text -DisableUserRequest { - reason?: string // 可选,0~200 字 -} -``` +- Body:无。 #### 成功响应 @@ -1595,6 +1653,7 @@ AdminUserResponse { username: string phoneMasked: string role: "Buyer" | "Merchant" + isDefaultMerchant: boolean status: "Active" | "Disabled" updatedAt: string } @@ -1604,32 +1663,39 @@ AdminUserResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是管理员 | | 404 | `RESOURCE.NOT_FOUND` | 目标账号不存在或不是买家/商家 | -| 409 | `RESOURCE.CONFLICT` | 当前账号已处于禁用状态 | +| 409 | `IDENTITY.DEFAULT_MERCHANT_PROTECTED` | 目标是本期唯一默认商家运营账号,不允许直接禁用 | +| 409 | `IDENTITY.MERCHANT_HAS_ACTIVE_WORK` | 非默认商家仍有关联待履约订单、售后窗口/申请或未结束秒杀活动 | | 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态共享不可用 | #### 业务规则与并发 -- 条件更新:`UPDATE users SET status='Disabled', token_version=token_version+1 WHERE user_id=:uid AND role IN ('Buyer','Merchant') AND status='Active'`;影响行数为 0 时按 409 处理。 -- 禁用成功后通过 `auth:user:{userId}:tokenVersion` 提升版本号;Redis 中保留的令牌记录按版本失效。 +- 条件更新:仅当目标为 `Active` 时改为 `Disabled` 并提升 `tokenVersion`;目标已是 `Disabled` 时返回当前禁用结果,不再次提升版本号。 +- Identity 必须配置且最多只能有一个 `isDefaultMerchant=true` 的启用商家账号;该账号负责普通订单默认归属,本期 A016 不提供默认账号迁移能力,因此直接禁用返回 409。 +- 禁用其他商家前,通过 Ordering、AfterSales、Seckill 公开应用契约确认不存在待支付/待履约订单、仍在售后期限内的订单、未完成售后申请或未结束活动;存在时拒绝禁用,不自动改写历史归属。 +- 禁用成功后通过 `auth:token-version:{userId}` 提升版本号;Redis 中保留的令牌记录按版本失效。 - 状态变更可追踪:操作人、目标账号、原状态、新状态、时间、`traceId` 写入结构化日志;不写入通用操作审计。 #### 缓存、事件或外部依赖 -- Redis Key:`auth:user:{userId}:tokenVersion`。 +- Redis Key:`auth:token-version:{userId}`。 #### 验证场景 - 禁用正常买家 → 200,旧令牌 401 / `AUTH.TOKEN_REVOKED`。 -- 重复禁用 → 409 / `RESOURCE.CONFLICT`。 +- 重复禁用 → 200,返回当前禁用状态。 - 禁用管理员账号 → 404。 +- 禁用默认商家 → 409 / `IDENTITY.DEFAULT_MERCHANT_PROTECTED`。 +- 禁用仍有待处理业务的非默认商家 → 409 / `IDENTITY.MERCHANT_HAS_ACTIVE_WORK`。 - 禁用过程中 Redis 撤销不可用 → 503,不返回虚假成功。 ### A017 启用账号 +- 请求 Schema:无 +- 身份与 Policy:AdminOnly + - 模块 / Tag:Identity - 需求编号:F13、M06-03 - 负责人:唐宇昊 @@ -1657,11 +1723,10 @@ AdminUserResponse { | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是管理员 | | 404 | `RESOURCE.NOT_FOUND` | 目标账号不存在或不是买家/商家 | -| 409 | `RESOURCE.CONFLICT` | 当前账号已处于正常状态 | #### 业务规则与并发 -- 条件更新:`status='Active'`,影响行数为 0 时按 409 处理。 +- 条件更新:仅当目标为 `Disabled` 时改为 `Active`;目标已是 `Active` 时返回当前正常结果。 - 启用不改变 `tokenVersion`;禁用前签发的旧令牌仍不可用,需重新登录。 #### 缓存、事件或外部依赖 @@ -1671,10 +1736,13 @@ AdminUserResponse { #### 验证场景 - 启用已禁用账号 → 200,旧令牌仍 401 / `AUTH.TOKEN_REVOKED`;新登录可用。 -- 启用正常账号 → 409 / `RESOURCE.CONFLICT`。 +- 启用正常账号 → 200,返回当前正常状态。 ### A018 收藏列表 +- 请求 Schema:无(Query 分页) +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Engagement - 需求编号:X02、M08 - 负责人:唐宇昊 @@ -1729,6 +1797,9 @@ FavoriteListResponse { ### A019 收藏商品 +- 请求 Schema:AddFavoriteRequest +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Engagement - 需求编号:X02、M08 - 负责人:唐宇昊 @@ -1788,6 +1859,10 @@ FavoriteResponse { ### A020 取消收藏 +- 请求 Schema:无 +- 响应 Schema:无(204) +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Engagement - 需求编号:X02、M08 - 负责人:唐宇昊 @@ -1828,6 +1903,9 @@ FavoriteResponse { ### A021 浏览历史列表 +- 请求 Schema:无(Query 分页) +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Engagement - 需求编号:X02、M08 - 负责人:唐宇昊 @@ -1881,6 +1959,9 @@ BrowsingHistoryListResponse { ### A022 修改浏览记录开关 +- 请求 Schema:UpdateBrowsingHistorySettingRequest +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Engagement - 需求编号:X02、M08 - 负责人:唐宇昊 @@ -1906,12 +1987,7 @@ UpdateBrowsingHistorySettingRequest { - HTTP 状态:`200 OK` - 响应 Schema:`BrowsingHistorySettingResponse` -```text -BrowsingHistorySettingResponse { - enabled: boolean - updatedAt: string -} -``` +- 字段定义复用 A022 的同名响应 Schema,本接口不重复定义第二份结构。 #### 失败响应 @@ -1936,6 +2012,10 @@ BrowsingHistorySettingResponse { ### A023 清空浏览历史 +- 请求 Schema:无 +- 响应 Schema:无(204) +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Engagement - 需求编号:X02、M08 - 负责人:唐宇昊 @@ -1974,9 +2054,132 @@ BrowsingHistorySettingResponse { - 清空本人浏览历史 → 204,后续列表为空。 - 重复清空 → 204,幂等。 ---- +### A024 记录浏览历史 + +- 请求 Schema:RecordBrowsingHistoryRequest +- 身份与 Policy:BuyerOnly + +- 模块 / Tag:Engagement +- 需求编号:X02、M08、M08-FR04 +- 负责人:唐宇昊 +- 关联数据表:DB006 +- 当前状态:待交叉评审 +- 用途:买家成功打开已上架商品详情后,由前端显式记录或更新最近浏览时间;A103 商品详情 GET 本身不产生写入副作用。 +- 方法与路径:`POST /api/browsing-history/records` +- operationId:`Engagement_RecordBrowsingHistory` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Buyer) +- Body: + +```text +RecordBrowsingHistoryRequest { + productId: uuid // 必填 +} +``` + +- 校验规则:`productId` 必须存在且为已上架商品;字段缺失或格式错误返回字段级 ProblemDetails。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`BrowsingHistoryResponse` + +```text +BrowsingHistoryResponse { + productId: uuid + recorded: boolean // 开关关闭时为 false + viewedAt: string? // recorded=true 时返回服务端生成的 UTC 时间 + trimmedCount: integer // 因超过上限而移除的最早记录数 +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或格式错误 | +| 401 | `AUTH.UNAUTHENTICATED` | 已登录用户令牌无效 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在或未上架 | +| 429 | `COMMON.RATE_LIMITED` | 同一买家短时间内高频记录浏览 | + +#### 业务规则与并发 + +- 浏览记录开关关闭时返回 `200`、`recorded=false`,不写入记录;这属于用户偏好,不是权限错误。 +- 游客身份不持久化记录;本接口仅 BuyerOnly;前端在游客访问商品详情时引导登录,登录后再调用本接口。 +- 同一买家同一商品只保留一条记录;按 `(user_id, product_id)` 唯一约束写入或更新,`viewedAt` 始终取服务端时间。 +- 默认单买家最多保留 200 条记录;超出时按 `viewed_at` 由小到大移除多余记录,并在 `trimmedCount` 返回本次清理数量。 +- 新写入仅接受当前已上架商品;商品后来下架时保留既有历史记录,并由列表标记为不可购买。 + +#### 缓存、事件或外部依赖 + +- 不缓存;浏览记录写入 PostgreSQL,商品存在性通过 Catalog 公开应用契约校验,不直接读取 Catalog 内部表。 +- 不发布集成事件。 + +#### 验证场景 + +- 已开启开关的买家查看新商品 → 200,`recorded=true`,记录新增。 +- 同一买家再次查看该商品 → 200,仅更新时间,不重复创建。 +- 关闭开关后调用 → 200,`recorded=false`,数据库不新增或更新记录。 +- 商家或游客调用 → 403 / `AUTH.FORBIDDEN`。 +- 达到 200 条上限后再记录 → 200,`trimmedCount` 大于 0。 +- 商品不存在或已下架后调用 → 404;已有历史记录仍保留。 + +### A025 查询浏览记录开关 + +- 请求 Schema:无 +- 身份与 Policy:BuyerOnly + +- 模块 / Tag:Engagement +- 需求编号:X02、M08、M08-FR07 +- 负责人:唐宇昊 +- 关联数据表:DB006 +- 当前状态:待交叉评审 +- 用途:买家查询本人"是否记录浏览历史"开关当前值,前端用于初始化控件状态。 +- 方法与路径:`GET /api/browsing-history/settings` +- operationId:`Engagement_GetBrowsingHistorySetting` + +#### 请求 + +- Header:`Authorization: Bearer `(必填,角色 Buyer) + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`BrowsingHistorySettingResponse` + +```text +BrowsingHistorySettingResponse { + enabled: boolean + updatedAt: string +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | + +#### 业务规则与并发 + +- 严格按 `user_id = current_user_id` 过滤;不存在记录时按 `enabled=true` 返回(默认开启)。 +- 与 A022 配对:GET 读取当前值,PATCH 修改值;同一资源不重复定义写入入口。 + +#### 缓存、事件或外部依赖 -> 来源:[interface-gxy.md](interface/interface-gxy.md)。已完成结构汇总,并已将合写接口拆成独立 Axxx 小节;图片暂存和评价详情引用仍须评审。 +- 不缓存、不发布事件。 + +#### 验证场景 + +- 默认账号 → 200,`enabled=true`。 +- 已通过 A022 关闭过 → 200,`enabled=false`。 +- 商家账号调用 → 403。 + +> 来源:[`interface-gxy.md`](interface/interface-gxy.md)。A144 已补齐单条公开评价读取;商品与评价图片顺序、公开字段和 PostgreSQL 搜索边界已统一,仍待 DBxxx、OpenAPI 和交叉评审。 ### A101 购物端有效分类列表 @@ -1984,7 +2187,7 @@ BrowsingHistorySettingResponse { - 需求编号:M02-01-FR02 - 负责人:顾欣月 - 关联数据表:DB021 `categories` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:为购物端商品筛选提供当前启用的分类,供列表页分类入口使用。 - 方法与路径:`GET /api/categories` - operationId:`Catalog_ListCategories` @@ -2047,7 +2250,7 @@ BrowsingHistorySettingResponse { - 需求编号:M02-01(F04、F05)、C04 - 负责人:顾欣月 - 关联数据表:DB022 `products`、DB023 `product_images` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:购物端商品发现入口,支持分页、分类筛选、关键词模糊/分词搜索、价格区间、仅看有货与白名单排序,翻页保持条件。 - 方法与路径:`GET /api/products` - operationId:`Catalog_ListProducts` @@ -2130,7 +2333,7 @@ BrowsingHistorySettingResponse { - 需求编号:M02-02(F06) - 负责人:顾欣月 - 关联数据表:DB022 `products`、DB023 `product_images` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:展示已上架商品的名称、图片、描述、当前价格、库存与分类,供购买决策。 - 方法与路径:`GET /api/products/{productId}` - operationId:`Catalog_GetProduct` @@ -2172,7 +2375,8 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| | 400 | `COMMON.VALIDATION_FAILED` | `productId` 格式非法 | -| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在、草稿、下架或已删除(对购物端统一按不存在处理) | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | +| 404 | `CATALOG.PRODUCT_UNAVAILABLE` | 商品已下架、已删除或当前不可公开 | #### 业务规则与并发 @@ -2186,7 +2390,7 @@ BrowsingHistorySettingResponse { #### 验证场景 -- 有货、售罄、下架/不存在状态可区分;下架商品详情返回 404;图片失败时前端占位不阻断其余信息。 +- 有货、售罄、下架和不存在状态可通过成功模型或稳定业务错误码区分;图片失败时前端占位不阻断其余信息。 --- @@ -2196,7 +2400,7 @@ BrowsingHistorySettingResponse { - 需求编号:M06-01-FR01 - 负责人:顾欣月 - 关联数据表:DB021 `categories` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:商家维护商品时查看全部(含停用)分类及层级、排序与启停状态。 - 方法与路径:`GET /api/merchant/categories` - operationId:`Catalog_ListMerchantCategories` @@ -2247,7 +2451,7 @@ BrowsingHistorySettingResponse { - 需求编号:M06-01-FR02 - 负责人:顾欣月 - 关联数据表:DB021 `categories` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:商家新增分类。 - 方法与路径:`POST /api/merchant/categories` - operationId:`Catalog_CreateCategory` @@ -2301,7 +2505,7 @@ BrowsingHistorySettingResponse { - 需求编号:M06-01-FR02 - 负责人:顾欣月 - 关联数据表:DB021 `categories` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:商家修改分类名称、父级、排序。 - 方法与路径:`PUT /api/merchant/categories/{categoryId}` - operationId:`Catalog_UpdateCategory` @@ -2351,7 +2555,7 @@ BrowsingHistorySettingResponse { - 需求编号:M06-01-FR02 - 负责人:顾欣月 - 关联数据表:DB021 `categories` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:启用分类,使其可以重新作为购物端筛选入口和商品上架分类。 - 方法与路径:`POST /api/merchant/categories/{categoryId}/enable` - operationId:`Catalog_EnableCategory` @@ -2397,7 +2601,7 @@ BrowsingHistorySettingResponse { - 需求编号:M06-01-FR03 - 负责人:顾欣月 - 关联数据表:DB021 `categories` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:停用分类,使其退出购物端筛选,并替代破坏性删除。 - 方法与路径:`POST /api/merchant/categories/{categoryId}/disable` - operationId:`Catalog_DisableCategory` @@ -2444,7 +2648,7 @@ BrowsingHistorySettingResponse { - 需求编号:M06-01-FR04 - 负责人:顾欣月 - 关联数据表:DB022 `products` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:商家按关键词、分类、上下架状态分页查询本方商品,展示价格、库存与状态。 - 方法与路径:`GET /api/merchant/products` - operationId:`Catalog_ListMerchantProducts` @@ -2489,11 +2693,13 @@ BrowsingHistorySettingResponse { ### A121 后台商品详情 +- 请求 Schema:—(Route) + - 模块 / Tag:Catalog - 需求编号:M06-01-FR04、FR06 - 负责人:顾欣月 - 关联数据表:DB022 `products`、DB023 `product_images` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:商家编辑前获取商品完整信息(含并发版本号 `version`)。 - 方法与路径:`GET /api/merchant/products/{productId}` - operationId:`Catalog_GetMerchantProduct` @@ -2539,7 +2745,7 @@ BrowsingHistorySettingResponse { - 需求编号:M06-01-FR05 - 负责人:顾欣月 - 关联数据表:DB022 `products` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:商家录入商品基础信息,创建为草稿状态。 - 方法与路径:`POST /api/merchant/products` - operationId:`Catalog_CreateProduct` @@ -2559,7 +2765,6 @@ BrowsingHistorySettingResponse { | `price` | number | 是 | ≥ 0,最多两位小数 | | `stock` | integer | 是 | ≥ 0 非负整数 | | `description` | string | 否 | ≤ 2000,受控内容 | -| `imageIds` | uuid[] | 否 | 引用已通过 A127 上传的图片,≤ 8 | - 校验规则:分类须启用;价格非负;库存非负整数;图片数 ≤ 8。 @@ -2580,11 +2785,11 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 -- 新建默认草稿,需通过 A125 上架前完整性校验后才对购物端可见。 +- 新建默认草稿,成功取得 `productId` 后再通过 A127 上传图片,最后经 A125 完整性校验上架;A122 不接受尚无归属商品的预上传图片 ID。 #### 缓存、事件或外部依赖 -- 创建成功发布领域事件用于后续搜索索引同步(C04-FR05);索引随商品数据在同一 PostgreSQL 事务/同步流程更新。 +- `pg_trgm`/GIN 索引随 PostgreSQL 商品数据同步维护,不通过 Outbox 或 Worker 复制搜索索引;事务提交后只触发已确认的缓存失效。 #### 验证场景 @@ -2598,7 +2803,7 @@ BrowsingHistorySettingResponse { - 需求编号:M06-01-FR06、M06-01-FR10 - 负责人:顾欣月 - 关联数据表:DB022 `products` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:修改允许变更的商品信息,使用并发标记防止静默覆盖。 - 方法与路径:`PUT /api/merchant/products/{productId}` - operationId:`Catalog_UpdateProduct` @@ -2610,7 +2815,7 @@ BrowsingHistorySettingResponse { #### 请求 - Route 参数:`productId`(uuid,必填)。 -- Body:`UpdateProductRequest`:字段同 A122(`name`、`categoryId`、`price`、`stock`、`description`、`imageIds`),另加必填 `version`(integer,来自 A121)。 +- Body:`UpdateProductRequest`:字段同 A122(`name`、`categoryId`、`price`、`stock`、`description`),另加必填 `version`(integer,来自 A121);图片新增和删除分别使用 A127、A128。 - 校验规则:`version` 必填;分类须启用;其余同 A122。 #### 成功响应 @@ -2635,7 +2840,7 @@ BrowsingHistorySettingResponse { #### 缓存、事件或外部依赖 -- 提交后发布领域事件:失效商品详情/列表缓存并同步搜索索引(失败可重试或重建,不回滚已提交商品事务)。 +- 提交后失效商品详情/列表缓存;`pg_trgm`/GIN 索引由 PostgreSQL 随数据同步维护,不发布“同步搜索索引”事件。 #### 验证场景 @@ -2649,7 +2854,7 @@ BrowsingHistorySettingResponse { - 需求编号:M06-01-FR08 - 负责人:顾欣月 - 关联数据表:DB022 `products` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:无历史订单关联时删除商品;有关联时禁止破坏性删除并建议下架。 - 方法与路径:`DELETE /api/merchant/products/{productId}` - operationId:`Catalog_DeleteProduct` @@ -2696,7 +2901,7 @@ BrowsingHistorySettingResponse { - 需求编号:M06-01-FR07 - 负责人:顾欣月 - 关联数据表:DB022 `products` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:完成商品销售前校验并将商品设为已上架。 - 方法与路径:`POST /api/merchant/products/{productId}/publish` - operationId:`Catalog_PublishProduct` @@ -2730,7 +2935,7 @@ BrowsingHistorySettingResponse { #### 缓存、事件或外部依赖 -- 上架成功后发布领域事件,并失效购物端缓存与搜索索引。 +- 上架成功后失效购物端缓存;`pg_trgm`/GIN 索引由 PostgreSQL 随商品数据同步维护。 #### 验证场景 @@ -2744,7 +2949,7 @@ BrowsingHistorySettingResponse { - 需求编号:M06-01-FR07 - 负责人:顾欣月 - 关联数据表:DB022 `products` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:停止商品销售,使购物端列表、详情和搜索不再公开该商品。 - 方法与路径:`POST /api/merchant/products/{productId}/unpublish` - operationId:`Catalog_UnpublishProduct` @@ -2776,7 +2981,7 @@ BrowsingHistorySettingResponse { #### 缓存、事件或外部依赖 -- 下架成功后发布领域事件,并失效购物端缓存与搜索索引;旧索引不得重新公开商品。 +- 下架成功后失效购物端缓存;公开查询始终过滤商品状态,PostgreSQL 同步索引不会重新公开下架商品。 #### 验证场景 @@ -2790,7 +2995,7 @@ BrowsingHistorySettingResponse { - 需求编号:M06-01-FR09 - 负责人:顾欣月 - 关联数据表:DB023 `product_images` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:为商品上传图片到 S3 兼容对象存储,返回图片记录。 - 方法与路径:`POST /api/merchant/products/{productId}/images` - operationId:`Catalog_UploadProductImage` @@ -2843,7 +3048,7 @@ BrowsingHistorySettingResponse { - 需求编号:M06-01-FR09 - 负责人:顾欣月 - 关联数据表:DB023 `product_images` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:删除某张商品图片。 - 方法与路径:`DELETE /api/merchant/products/{productId}/images/{imageId}` - operationId:`Catalog_DeleteProductImage` @@ -2890,7 +3095,7 @@ BrowsingHistorySettingResponse { - 需求编号:M07-FR06、M07-FR07 - 负责人:顾欣月 - 关联数据表:DB024 `reviews`、DB025 `review_images` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:商品详情页分页展示公开评价与评分汇总(总数、平均分、星级分布)。 - 方法与路径:`GET /api/products/{productId}/reviews` - operationId:`Review_ListProductReviews` @@ -2936,7 +3141,7 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 -- 只返回有效评价;不返回手机号、邮箱、内部用户标识,昵称脱敏展示。 +- 只返回有效评价;`buyerDisplayName` 直接读取评价创建时保存的脱敏展示名快照,不在列表查询中逐条调用 Identity;不返回手机号、邮箱、内部用户标识。 - 评分汇总由有效评价计算;新增评价后最终更新(可接受短暂最终一致)。 #### 缓存、事件或外部依赖 @@ -2955,7 +3160,7 @@ BrowsingHistorySettingResponse { - 需求编号:M07-FR03 - 负责人:顾欣月 - 关联数据表:DB025 `review_images` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:买家在提交评价前逐张上传晒图,返回图片标识供 A142 引用。 - 方法与路径:`POST /api/reviews/images` - operationId:`Review_UploadReviewImage` @@ -2986,7 +3191,7 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 -- 暂存图片归属当前买家;对象 Key 采用 `reviews/{reviewId}/{fileId}.`,`reviewId` 在 A142 提交成功后关联。未被引用的暂存图片由清理策略回收。 +- 暂存图片归属当前买家;评价尚未创建时对象 Key 使用 `review-uploads/{buyerId}/{fileId}.`,符合 `//.` 规范。A142 提交成功后只在数据库中关联 `reviewId`,不要求物理搬移对象;未被引用的暂存图片由清理策略回收。 #### 缓存、事件或外部依赖 @@ -3004,7 +3209,7 @@ BrowsingHistorySettingResponse { - 需求编号:M07-FR04、M07-FR05 - 负责人:顾欣月 - 关联数据表:DB024 `reviews`、DB025 `review_images` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:买家对本人已完成订单项提交一次评分、文字与可选图片评价。 - 方法与路径:`POST /api/reviews` - operationId:`Review_CreateReview` @@ -3049,11 +3254,12 @@ BrowsingHistorySettingResponse { - 同一订单项只能形成一条评价:唯一约束 `ux_reviews_order_item_id` 作为最终保障;重复点击/重复请求返回首次已确认结果,不新增记录。 - 订单完成状态、订单项归属由 Ordering 提供的应用契约校验,不直接改订单表。 +- 创建评价时通过 Identity 公开应用契约读取当前买家的安全展示名并完成脱敏,将结果保存为 `buyerDisplayName` 快照;没有展示名时回退到自动用户名的脱敏值。后续用户资料变化不改写历史评价快照,公开列表和详情不逐条查询 Identity。 - 提交成功后触发商品评分汇总更新(A140 汇总最终一致)。 #### 缓存、事件或外部依赖 -- 依赖 Ordering 校验订单项;依赖 M00 幂等基础设施与对象存储图片关联。 +- 依赖 Ordering 校验订单项,依赖 Identity 提供当前买家的安全展示名快照;依赖 M00 幂等基础设施与对象存储图片关联。 #### 验证场景 @@ -3067,7 +3273,7 @@ BrowsingHistorySettingResponse { - 需求编号:M07-FR01 - 负责人:顾欣月 - 关联数据表:DB024 `reviews` -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:买家订单详情判断某订单项是否可评价、是否已评价,用于显示“评价商品”入口。 - 方法与路径:`GET /api/reviews/eligibility` - operationId:`Review_GetReviewEligibility` @@ -3086,7 +3292,7 @@ BrowsingHistorySettingResponse { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`ReviewEligibilityResponse`,含 `eligible`(boolean)、`reason`(枚举字符串:`Eligible`/`OrderNotCompleted`/`AlreadyReviewed`/`NotOwner`)、`existingReviewId`(uuid,可空)。 +- 响应 Schema:`ReviewEligibilityResponse`,含 `eligible`(boolean)、`reason`(枚举字符串:`Eligible`/`OrderNotCompleted`/`AlreadyReviewed`)、`existingReviewId`(uuid,可空);订单项不存在或不属于当前买家时统一返回 404,不返回 `NotOwner`。 - 示例: ```json @@ -3114,14 +3320,87 @@ BrowsingHistorySettingResponse { - 已评价返回 `eligible=false, reason=AlreadyReviewed` 且带 `existingReviewId`;未完成返回 `OrderNotCompleted`。 ---- +### A144 单条公开评价详情查询 + +- 模块 / Tag:Review +- 需求编号:M07-FR06;同时承接 A142 `Location` 与 A143 `existingReviewId` 的可达读取 +- 负责人:顾欣月 +- 关联数据表:DB024 `reviews`、DB025 `review_images` +- 当前状态:待交叉评审 +- 用途:单条评价的对外可寻址读取;服务于 A142 创建响应 `Location: /api/reviews/{reviewId}` 的 REST 约定与 A143 `existingReviewId` 跳转场景,供商品详情、订单详情等位置按需拉取单条评价。 +- 方法与路径:`GET /api/reviews/{reviewId}` +- operationId:`Review_GetReview` +- 请求 Schema:无(仅 Route) +- 响应 Schema:`PublicReviewDetailResponse` +- 身份与 Policy:游客可访问;公开评价与 A140 列表同口径,不返回买家手机号、邮箱、内部用户标识等敏感字段。 +- 资源归属:公开评价;当前买家请求时不附加任何归属校验。 +- 幂等要求:只读,天然幂等。 + +#### 请求 + +- Route 参数:`reviewId`(uuid)。 +- Header:`Accept: application/json`。 +- Body:无。 +- 校验规则:`reviewId` 必须是标准带连字符 UUID 格式;非法格式按 `400 Bad Request`(`COMMON.INVALID_UUID` 通用码)处理;记录不存在按 `404 Not Found`(`REVIEW.NOT_FOUND`)处理,不泄露存在性差异。 + +#### 成功响应 + +- HTTP 状态:`200 OK` +- 响应 Schema:`PublicReviewDetailResponse`,含 `reviewId`、`productId`、`rating`、`content`、`images`(`imageId`、`url`、`sortOrder`)、`buyerDisplayName`(脱敏昵称,与 A140 一致)、`createdAt`;不公开 `orderItemId` 或内部买家标识。 +- 示例: + +```json +{ + "code": "success", + "message": "ok", + "data": { + "reviewId": "9c2f…", + "productId": "6f1d…", + "rating": 5, + "content": "很好用", + "images": [ + { "imageId": "img1…", "url": "https://…/r1.jpg", "sortOrder": 1 } + ], + "buyerDisplayName": "用***月", + "createdAt": "2026-07-21T03:00:00Z" + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.INVALID_UUID` | `reviewId` 非标准 UUID 格式 | +| 404 | `REVIEW.NOT_FOUND` | 评价不存在 | +| 500 | `COMMON.INTERNAL_ERROR` | 未处理服务端错误 | + +#### 业务规则与并发 + +- 本期评价创建成功后即按 A140 口径公开;本期不提供运营下线、隐藏或评价治理状态。评价不存在时返回 404。 +- 不返回买家手机号、邮箱、内部用户标识;昵称读取评价创建时保存的 `buyerDisplayName` 脱敏快照,与 A140 保持一致。 +- 与 A142 创建响应的 `Location` 头严格对齐:创建成功后客户端可凭 `Location` 直接 GET 本接口获取完整评价。 +- A143 返回 `existingReviewId` 时,前端可经本接口跳转拉取评价详情。 -> 来源:[interface-zhh.md](interface/interface-zhh.md)。已完成结构汇总;A229、A230 与 Ordering 查询边界冲突,当前明确标记为暂不实施。 +#### 缓存、事件或外部依赖 + +- 汇总/详情缓存与 A140 共用同一 Key 前缀;本期仅在 A142 评价创建成功后触发对应缓存失效,不定义尚未提供的评价更新事件。 +- 不依赖 Identity、Ordering 等其他模块的应用契约;仅按 `reviewId` 主键读取 DB024 与 DB025。 + +#### 验证场景 -> 每个接口按《接口设计》1.20 节模板补齐。Schema 名称遵守 OpenAPI 7.2 节:PascalCase + 用途后缀;`operationId` 使用 `_`;路径参数使用单数对象 + `Id`。 +- 有效 `reviewId` 返回 200 与 `ReviewDetailResponse`; +- 不存在的 `reviewId` 返回 404(`REVIEW.NOT_FOUND`); +- 非法 UUID 格式返回 400(`COMMON.INVALID_UUID`); +- 不暴露买家敏感字段;与 A140 列表的 `buyerDisplayName` 脱敏结果一致。 + +> 来源:[`interface-zhh.md`](interface/interface-zhh.md)。A229/A230 已取消并由 A302/A303 承接秒杀订单查询;活动、库存与 Ordering 创建契约仍待数据库设计和联调确认。 ### A201 加入购物车 +- 请求 Schema:AddCartItemRequest +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Cart - 需求编号:F07、M03-01 - 负责人:朱惠惠 @@ -3184,7 +3463,7 @@ CartItemResponse { | 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品已下架或被禁用 | | 409 | `CART.QUANTITY_EXCEEDS_STOCK` | 累加后数量超过商品实时可售库存,返回当前最大允许值 | | 429 | `COMMON.RATE_LIMITED` | 触发限流 | -| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Redis 幂等存储或商品服务暂时不可用 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 商品服务或数据库暂时不可用 | | 500 | `COMMON.INTERNAL_ERROR` | 未处理的服务端错误 | #### 业务规则与并发 @@ -3198,7 +3477,7 @@ CartItemResponse { #### 缓存、事件或外部依赖 - 不缓存购物车条目;商品价格、库存与上下架状态由 Catalog 模块实时提供。 -- 幂等键记录写入 Redis:`cart:idempotency:{userId}:{key}`,TTL = 5 分钟。 +- 幂等请求指纹与首次结果和购物车变更在同一 PostgreSQL 事务中保存;Redis 只能作为可丢失的读取加速,不承担唯一幂等事实。 - 不发布集成事件。 #### 验证场景 @@ -3213,6 +3492,9 @@ CartItemResponse { ### A202 查看购物车 +- 请求 Schema:无(Query 分页/筛选) +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Cart - 需求编号:F07、M03-01 - 负责人:朱惠惠 @@ -3281,6 +3563,9 @@ CartListResponse { ### A203 修改购物车条目数量 +- 请求 Schema:UpdateCartItemQuantityRequest +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Cart - 需求编号:F07、M03-01 - 负责人:朱惠惠 @@ -3339,6 +3624,10 @@ UpdateCartItemQuantityRequest { ### A204 删除购物车条目 +- 请求 Schema:无 +- 响应 Schema:无(204) +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Cart - 需求编号:F07、M03-01 - 负责人:朱惠惠 @@ -3384,6 +3673,9 @@ UpdateCartItemQuantityRequest { ### A205 批量删除购物车条目 +- 请求 Schema:BatchRemoveCartItemsRequest +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Cart - 需求编号:F07、M03-01 - 负责人:朱惠惠 @@ -3442,6 +3734,9 @@ BatchRemoveCartItemsResponse { ### A206 修改选中状态(全选/反选/单选) +- 请求 Schema:UpdateCartItemSelectionRequest +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Cart - 需求编号:F07、M03-01 - 负责人:朱惠惠 @@ -3467,7 +3762,7 @@ UpdateCartItemSelectionRequest { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`CartListResponse`(同 A206,按当前选中状态返回完整购物车) +- 响应 Schema:`CartListResponse`(同 A202,按当前选中状态返回完整购物车) #### 失败响应 @@ -3481,7 +3776,7 @@ UpdateCartItemSelectionRequest { #### 业务规则与并发 - 全选/反选按 `buyer_id = current_user_id` 过滤;失效条目保持未选中,不被强制选中。 -- `SetExplicit` 仅修改 `cartItemIds` 中属于当前买家的条目;他人条目被忽略并计入 `skippedCount`(由响应 `selectedCount`/`availableSelectedCount` 体现)。 +- `SetExplicit` 仅修改 `cartItemIds` 中属于当前买家的条目;不存在或不属于当前买家的 ID 统一忽略,响应不返回数量或明细,避免暴露资源归属。 - 单条切换并发安全:服务端使用条件更新 `WHERE cart_item_id = :id AND buyer_id = current_user_id`。 - 选中状态保存在服务端;前端刷新或重新登录后状态保留。 @@ -3495,10 +3790,14 @@ UpdateCartItemSelectionRequest { - 反选 → 200,所有可用条目 `isSelected=false`。 - 单选切换某条目 → 200,仅该条目 `isSelected` 变更。 - 尝试选中失效条目 → 409 / `CART.ITEM_UNAVAILABLE`,不修改状态。 -- 跨用户 ID 提交 → 仅本人条目被修改,他人条目被忽略。 +- 跨用户 ID 提交 → 仅本人条目被修改,其他 ID 被静默忽略且响应不泄露数量或明细。 ### A207 清空购物车 +- 请求 Schema:无 +- 响应 Schema:无(204) +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Cart - 需求编号:F07、M03-01 - 负责人:朱惠惠 @@ -3542,6 +3841,9 @@ UpdateCartItemSelectionRequest { ### A208 获取结算预览 +- 请求 Schema:无(Query 可选 `cartItemIds`) +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Cart - 需求编号:F07、M03-01 - 负责人:朱惠惠 @@ -3565,9 +3867,9 @@ UpdateCartItemSelectionRequest { ```text CheckoutPreviewResponse { items: CartItemResponse[] // 当前可用于结算的条目 - unavailableItems: CartItemResponse[] // 失效条目(不下单但提示买家) + unavailableItems: CartItemResponse[] // 仅当前买家本次选中的失效条目 totalAmount: number // 服务端按实时单价计算的总额 - availableForCheckout: boolean // 是否有至少一条可结算条目 + availableForCheckout: boolean // 选中项非空且全部可结算时为 true } ``` @@ -3582,9 +3884,9 @@ CheckoutPreviewResponse { #### 业务规则与并发 - 不传 `cartItemIds` 时按 `isSelected=true AND buyer_id = current_user_id` 过滤。 -- 传入 `cartItemIds` 时取交集;不在本人购物车或失效条目归入 `unavailableItems`。 +- 传入 `cartItemIds` 时先与当前买家购物车取交集;不存在或不属于当前买家的 ID 被忽略且不返回任何明细。只有当前买家的失效条目进入 `unavailableItems`。 - `totalAmount` 由服务端实时计算并返回;前端不得自行覆盖金额。 -- 返回 `availableForCheckout=false` 时前端禁用提交订单按钮。 +- 只有选中项非空且全部有效时 `availableForCheckout=true`;只要存在失效项就返回 `false`,前端提示取消勾选或删除失效项后重试。 #### 缓存、事件或外部依赖 @@ -3593,12 +3895,15 @@ CheckoutPreviewResponse { #### 验证场景 -- 选中 2 条可用 + 1 条失效 → `items=2`、`unavailableItems=1`、`availableForCheckout=true`。 +- 选中 2 条可用 + 1 条失效 → `items=2`、`unavailableItems=1`、`availableForCheckout=false`。 - 全部失效 → `items=[]`、`availableForCheckout=false`,前端禁用提交。 -- 传入他人 `cartItemId` → 归入 `unavailableItems`,不报错也不泄露归属。 +- 传入他人 `cartItemId` → 该 ID 被忽略,不进入任何响应数组,不泄露是否存在或归属。 ### A220 商家创建秒杀活动 +- 请求 Schema:CreateSeckillActivityRequest +- 身份与 Policy:MerchantOnly + - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 @@ -3615,12 +3920,12 @@ CheckoutPreviewResponse { ```text CreateSeckillActivityRequest { - productId: uuid // 必填,必须是当前商家已上架商品 + productId: uuid // 必填,必须是平台内已上架商品 activityName: string // 必填,1~50 字 seckillPrice: number // 必填,>0 且 < 商品当前上架价 totalStock: integer // 必填,1 ≤ totalStock ≤ 商品当前可售库存 perBuyerLimit: integer // 必填,1 ≤ perBuyerLimit ≤ totalStock - startAt: string // 必填,UTC ISO 8601,≥ now() + 5min + startAt: string // 必填,UTC ISO 8601,≥ now() endAt: string // 必填,UTC ISO 8601,> startAt 且 ≤ startAt + 30d } ``` @@ -3628,7 +3933,7 @@ CreateSeckillActivityRequest { #### 成功响应 - HTTP 状态:`201 Created` -- Response Header:`Location: /api/seckill-activities/{activityId}` +- Response Header:`Location: /api/merchant/seckill-activities/{activityId}` - 响应 Schema:`SeckillActivityResponse` ```text @@ -3658,21 +3963,21 @@ SeckillActivityResponse { | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是商家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | -| 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品未上架或不属于当前商家 | +| 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品未上架 | | 409 | `SECKILL.STOCK_EXCEEDS_AVAILABLE` | `totalStock` 超过商品当前可售库存 | -| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 商品服务或分布式锁不可用 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 应用能力或数据库暂时不可用 | #### 业务规则与并发 - 同一商品同一时间段(`startAt`、`endAt` 与已存在活动存在重叠)不允许重复创建;重叠返回 `409 / SECKILL.TIME_WINDOW_CONFLICT`。 - `seckillPrice < originalPrice` 由服务端校验;不接受等于或高于原价的秒杀活动。 -- `startAt ≥ now() + 5min` 避免立刻开始的发布影响压测一致性。 -- 创建活动时同步在 `seckill_inventory`(DB043)写入 `remainingStock=totalStock`、`soldCount=0`、`frozenCount=0`;两者在同一事务。 -- 商品归属:仅当 `product.owner_merchant_id = current_user_id` 才允许创建;越权访问返回 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 +- `startAt ≥ now()`;可以创建立即开始的活动,服务端发布时按当前时间决定进入 `Published` 或直接进入 `Ongoing`。 +- 创建活动时同步在 `seckill_inventory`(DB043)写入计划配额 `remainingStock=totalStock`、`soldCount=0`、`frozenCount=0`;`Draft` 不对买家开放,真正的普通库存划转只在 A222 发布事务中完成。 +- 本项目是单一 B2C 平台,不按商户租户隔离商品;活动记录 `createdByMerchantUserId` 用于操作归属与秒杀订单商家分配。 #### 缓存、事件或外部依赖 -- 活动创建后向 Redis 写入分布式锁 Key:`lock:seckill:activity:create:{productId}`,事务结束释放。 +- 同商品时间窗口冲突在 PostgreSQL 事务内按商品加锁并复核,不能依赖 Redis 锁作为唯一正确性边界。 - 不缓存、不发布集成事件。 #### 验证场景 @@ -3680,19 +3985,22 @@ SeckillActivityResponse { - 合法参数创建 → 201,状态 `Draft`,库存=总量。 - `totalStock` 超过商品库存 → 409 / `SECKILL.STOCK_EXCEEDS_AVAILABLE`。 - `seckillPrice ≥ originalPrice` → 400 / `COMMON.VALIDATION_FAILED`。 -- `startAt < now() + 5min` → 400 / `COMMON.VALIDATION_FAILED`。 +- `startAt < now()` → 400 / `COMMON.VALIDATION_FAILED`。 - 时间窗口与已存在活动重叠 → 409 / `SECKILL.TIME_WINDOW_CONFLICT`。 -- 尝试绑定他人商品 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 +- 尝试绑定未上架商品 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 ### A221 商家更新秒杀活动 +- 请求 Schema:UpdateSeckillActivityRequest +- 身份与 Policy:MerchantOnly + - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 - 关联数据表:DB042、DB043 - 当前状态:待交叉评审 -- 用途:商家在 `Draft` 或 `Scheduled` 状态下更新秒杀活动参数;`Ongoing`/`Finished`/`Cancelled` 状态不允许修改。 -- 方法与路径:`PATCH /api/seckill-activities/{activityId}` +- 用途:商家在 `Draft` 或 `Published` 状态下更新尚未开始的秒杀活动参数;`Ongoing`/`Ended`/`Cancelled` 状态不允许修改。 +- 方法与路径:`PATCH /api/merchant/seckill-activities/{activityId}` - operationId:`Seckill_UpdateActivity` #### 请求 @@ -3707,7 +4015,7 @@ UpdateSeckillActivityRequest { seckillPrice?: number // 可选 totalStock?: integer // 可选;只能调大或保持;不得小于已售数量 perBuyerLimit?: integer // 可选 - startAt?: string // 可选;不得早于 now() + 5min + startAt?: string // 可选;不得早于 now() endAt?: string // 可选 } ``` @@ -3725,15 +4033,16 @@ UpdateSeckillActivityRequest { | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | | 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | -| 409 | `SECKILL.INVALID_STATUS` | 活动已 `Ongoing`/`Finished`/`Cancelled` | +| 409 | `SECKILL.INVALID_STATUS` | 活动已 `Ongoing`/`Ended`/`Cancelled` | | 409 | `SECKILL.STOCK_BELOW_SOLD` | `totalStock` 小于已售数量 | | 409 | `SECKILL.TIME_WINDOW_CONFLICT` | 与其他活动时间窗口重叠 | #### 业务规则与并发 -- 仅允许在 `Draft` 或 `Scheduled` 状态更新;状态字段由 `status='Draft' OR status='Scheduled'` 条件更新保证。 +- 仅允许在 `Draft` 或尚未开始的 `Published` 状态更新;状态字段由 `status IN ('Draft','Published') AND start_at > now()` 条件更新保证。 +- `totalStock` 只允许在 `Draft` 修改;发布后库存配额已经从 Catalog 普通库存划转,不通过本接口调整。 - `totalStock` 只允许调大或保持;调整后必须满足 `remainingStock + soldCount + frozenCount = totalStock`。 -- 修改后 `startAt` 与 `endAt` 必须保持 `startAt ≥ now() + 5min` 与 `endAt > startAt`。 +- 修改后 `startAt` 与 `endAt` 必须保持 `startAt ≥ now()` 与 `endAt > startAt`。 #### 缓存、事件或外部依赖 @@ -3748,13 +4057,16 @@ UpdateSeckillActivityRequest { ### A222 商家发布秒杀活动 +- 请求 Schema:无 +- 身份与 Policy:MerchantOnly + - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 - 关联数据表:DB042、DB043 - 当前状态:待交叉评审 -- 用途:商家将 `Draft` 状态活动提交审核后立即变为 `Scheduled`;系统按 `startAt` 自动推进到 `Ongoing`。 -- 方法与路径:`POST /api/seckill-activities/{activityId}/publish` +- 用途:商家直接发布 `Draft` 活动;本期没有审核流程,服务端按数据库当前时间决定立即进入 `Ongoing` 或先进入 `Published`,后续按 `endAt` 推进到 `Ended`。 +- 方法与路径:`POST /api/merchant/seckill-activities/{activityId}/publish` - operationId:`Seckill_PublishActivity` #### 请求 @@ -3766,7 +4078,7 @@ UpdateSeckillActivityRequest { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`SeckillActivityResponse`(`status="Scheduled"`) +- 响应 Schema:`SeckillActivityResponse`(`startAt <= databaseNow` 时 `status="Ongoing"`,否则 `status="Published"`) #### 失败响应 @@ -3776,14 +4088,16 @@ UpdateSeckillActivityRequest { | 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | | 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | | 409 | `SECKILL.INVALID_STATUS` | 活动已发布或已结束 | +| 409 | `SECKILL.TIME_WINDOW_EXPIRED` | 数据库当前时间已经达到或超过 `endAt`,该草稿活动不能再发布 | | 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品已下架,禁止发布 | -| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 缓存写入失败 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 库存划转或数据库暂时不可用 | #### 业务规则与并发 -- 条件更新:`UPDATE ... SET status='Scheduled' WHERE activity_id=:id AND status='Draft' AND owner_merchant_id=:mid`;影响行数为 0 时按 409 处理。 -- 商品已下架时拒绝发布;商家需先恢复上架。 -- 发布成功后刷新 Redis 缓存并预热活动详情 Key;Worker 按 `startAt` 自动推进到 `Ongoing`。 +- 发布事务先读取一次数据库当前时间;若 `now() >= endAt`,立即返回 `SECKILL.TIME_WINDOW_EXPIRED`,不得调用 Catalog 或划转库存。时间有效时,再通过 Catalog 公开应用契约按 `activityId` 幂等地把 `totalStock` 从普通可售库存划转为秒杀配额,并按同一数据库时间把活动从 `Draft` 条件更新为 `Ongoing`(`startAt <= now() < endAt`)或 `Published`(`now() < startAt`);任一步失败整体回滚,不产生双份可售库存。 +- 发布、更新和取消均按 `created_by_merchant_user_id = current_user_id` 校验活动操作归属;这只约束活动创建人,不引入多商户商品租户。 +- 商品已下架或普通可售库存不足时拒绝发布;商家修正商品或活动库存后重试。 +- 事务提交后尽力失效并预热 Redis 活动缓存;缓存失败只影响性能并进入重试,不否定已提交的发布结果。Worker 只需把尚未开始的 `Published` 按 `startAt` 推进为 `Ongoing`,并把到期的 `Ongoing` 推进为 `Ended`。 #### 缓存、事件或外部依赖 @@ -3791,19 +4105,23 @@ UpdateSeckillActivityRequest { #### 验证场景 -- 草稿活动发布 → 200,状态 `Scheduled`。 +- 未来开始的草稿活动发布 → 200 + `Published`;立即开始的草稿活动发布 → 200 + `Ongoing`;两者普通库存与秒杀配额总量均守恒。 +- 已超过 `endAt` 的草稿活动发布 → 409 / `SECKILL.TIME_WINDOW_EXPIRED`,普通库存和秒杀配额均不变化。 - 重复发布 → 409 / `SECKILL.INVALID_STATUS`。 - 商品已下架 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 ### A223 商家取消秒杀活动 +- 请求 Schema:CancelSeckillActivityRequest +- 身份与 Policy:MerchantOnly + - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 - 关联数据表:DB042、DB043 - 当前状态:待交叉评审 -- 用途:商家取消 `Draft` / `Scheduled` / `Ongoing` 状态活动;取消后入口立即失效,已存在秒杀订单按既有流程走完;本期不回收已分配库存。 -- 方法与路径:`POST /api/seckill-activities/{activityId}/cancel` +- 用途:商家取消 `Draft` / `Published` / `Ongoing` 状态活动;取消后入口立即失效,已存在秒杀订单按既有流程走完;本期按 C01-FR14 保留已分配库存,不回收到普通库存。 +- 方法与路径:`POST /api/merchant/seckill-activities/{activityId}/cancel` - operationId:`Seckill_CancelActivity` #### 请求 @@ -3831,11 +4149,11 @@ CancelSeckillActivityRequest { | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | | 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | -| 409 | `SECKILL.INVALID_STATUS` | 活动已 `Finished` 或已 `Cancelled` | +| 409 | `SECKILL.INVALID_STATUS` | 活动已 `Ended` 或已 `Cancelled` | #### 业务规则与并发 -- 条件更新:`status IN ('Draft','Scheduled','Ongoing') → 'Cancelled'`;影响行数为 0 时按 409 处理。 +- 条件更新:`status IN ('Draft','Published','Ongoing') → 'Cancelled'`;影响行数为 0 时按 409 处理。 - 取消时 `remainingStock` 保留为冻结状态,不自动回收到普通商品库存。 - 已存在秒杀订单沿用 M04 状态机;C03 超时取消时回补到原秒杀库存通道。 @@ -3851,6 +4169,9 @@ CancelSeckillActivityRequest { ### A224 商家秒杀活动列表 +- 请求 Schema:无(Query 分页/筛选) +- 身份与 Policy:MerchantOnly + - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 @@ -3866,7 +4187,7 @@ CancelSeckillActivityRequest { - Query 参数: - `page`(默认 1) - `pageSize`(默认 10,上限 50) - - `status`(可选,可多值:`Draft` / `Scheduled` / `Ongoing` / `Finished` / `Cancelled`) + - `status`(可选,可多值:`Draft` / `Published` / `Ongoing` / `Ended` / `Cancelled`) - `keyword`(可选,对活动名称做模糊匹配) - `startFrom`、`startTo`(可选,时间范围) @@ -3895,7 +4216,7 @@ SeckillActivityListResponse { #### 业务规则与并发 -- 严格按 `owner_merchant_id = current_user_id` 过滤;不允许查询他人活动。 +- 严格按 `created_by_merchant_user_id = current_user_id` 过滤;创建人是活动操作归属,不代表商品租户隔离。 - 排序默认按 `startAt desc`;相同 `startAt` 时按 `activityId` 稳定排序。 #### 缓存、事件或外部依赖 @@ -3910,13 +4231,16 @@ SeckillActivityListResponse { ### A225 商家秒杀活动详情 +- 请求 Schema:无 +- 身份与 Policy:MerchantOnly + - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 -- 关联数据表:DB042、DB043、DB044 +- 关联数据表:DB042、DB043 - 当前状态:待交叉评审 - 用途:商家查看本人秒杀活动详情;包含库存、已售、单用户限购、订单统计与取消原因等内部字段。 -- 方法与路径:`GET /api/seckill-activities/{activityId}` +- 方法与路径:`GET /api/merchant/seckill-activities/{activityId}` - operationId:`Seckill_GetMerchantActivityDetail` #### 请求 @@ -3956,8 +4280,8 @@ SeckillOrderStatsResponse { #### 业务规则与并发 -- 严格按 `owner_merchant_id = current_user_id` 过滤;跨商家访问返回 404,避免泄露活动存在性。 -- 订单统计来自 DB044(`seckill_orders`,与 M04 `orders` 共享事实库,通过 `seckill_activity_id` 关联)。 +- 严格按 `created_by_merchant_user_id = current_user_id` 过滤;非创建人访问返回 404,避免泄露活动存在性。 +- 订单统计通过 Ordering 公开查询契约取得;Seckill 不读取 Ordering 内部订单表,也不维护平行订单事实。 - `cancelReason` 与 `cancelledAt` 仅在 `status=Cancelled` 时返回。 #### 缓存、事件或外部依赖 @@ -3972,6 +4296,9 @@ SeckillOrderStatsResponse { ### A226 买家秒杀活动列表 +- 请求 Schema:无(Query 分页) +- 身份与 Policy:Anonymous + - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 @@ -3992,7 +4319,7 @@ SeckillOrderStatsResponse { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`SeckillActivityListResponse`(仅公开字段,`status` 仅返回 `Scheduled` / `Ongoing`) +- 响应 Schema:`SeckillActivityListResponse`(仅公开字段,`status` 仅返回 `Published` / `Ongoing`) #### 失败响应 @@ -4002,7 +4329,7 @@ SeckillOrderStatsResponse { #### 业务规则与并发 -- 仅返回 `status IN ('Scheduled','Ongoing')` 的活动;已结束、已取消或草稿活动不公开。 +- 仅返回 `status IN ('Published','Ongoing')` 的活动;已结束、已取消或草稿活动不公开。 - 排序默认按 `startAt asc`(即将开始优先),相同 `startAt` 时按 `activityId` 稳定排序。 - 公开响应中 `remainingStock` 不返回具体数字,仅返回 `isSoldOut` 布尔;具体剩余库存通过 A227 查询。 @@ -4018,13 +4345,16 @@ SeckillOrderStatsResponse { ### A227 买家秒杀活动详情 +- 请求 Schema:无 +- 身份与 Policy:Anonymous + - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 - 关联数据表:DB042、DB043 - 当前状态:待交叉评审 - 用途:游客和买家查看秒杀活动详情;返回公开字段、商品基础信息与抢购入口。 -- 方法与路径:`GET /api/seckill-activities/{activityId}/public` +- 方法与路径:`GET /api/seckill-activities/{activityId}` - operationId:`Seckill_GetActiveActivityDetail` #### 请求 @@ -4047,7 +4377,7 @@ SeckillOrderStatsResponse { #### 业务规则与并发 -- 仅返回 `status IN ('Scheduled','Ongoing')` 的活动;其他状态返回 410。 +- 仅返回 `status IN ('Published','Ongoing')` 的活动;其他状态返回 410。 - 已登录买家响应额外包含 `currentBuyerOrderCount`、`currentBuyerRemaining`(用于限购提示),按 `(activity_id, buyer_id)` 实时统计。 #### 缓存、事件或外部依赖 @@ -4062,10 +4392,13 @@ SeckillOrderStatsResponse { ### A228 秒杀下单 +- 请求 Schema:PlaceSeckillOrderRequest +- 身份与 Policy:BuyerOnly + - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 -- 关联数据表:DB043、DB044、DB045 +- 关联数据表:DB043;订单事实由 Ordering 的 DB061、DB062 持有 - 当前状态:待交叉评审 - 用途:买家抢购秒杀商品;服务端以数据库条件更新扣减秒杀库存、创建订单与秒杀订单项快照;事务保证不超卖、不少卖、不产生孤立记录。 - 方法与路径:`POST /api/seckill-orders` @@ -4087,7 +4420,7 @@ PlaceSeckillOrderRequest { #### 成功响应 - HTTP 状态:`201 Created` -- Response Header:`Location: /api/seckill-orders/{orderId}` +- Response Header:`Location: /api/orders/{orderId}`(A303) - 响应 Schema:`PlaceSeckillOrderResponse` ```text @@ -4126,18 +4459,20 @@ PlaceSeckillOrderResponse { #### 业务规则与并发 - 秒杀下单为高风险操作,必须使用 `Idempotency-Key`;缺失时返回 400 / `COMMON.VALIDATION_FAILED`。 +- 完成认证和固定请求字段校验后,先读取 PostgreSQL 幂等记录:相同 Key + 相同请求指纹直接重放首次结果,不再经过限流、时间、库存或限购校验;相同 Key + 不同指纹立即返回 `409 / IDEMPOTENCY.KEY_REUSED`。只有全新 Key 才进入后续可变业务校验。 - 同一数据库事务内顺序: 1. 按 `UPDATE seckill_inventory SET remaining = remaining - :qty, sold = sold + :qty, updated_at = now() WHERE activity_id = :aid AND status='Ongoing' AND start_at <= now() AND end_at > now() AND remaining >= :qty` 条件扣减秒杀库存;影响行数为 0 时整体事务回滚。 - 2. 校验 `(activity_id, buyer_id)` 维度已下单数量(含 `PendingPayment`、`Paid`、`Cancelled`)+ 本次 `quantity` 不超过 `perBuyerLimit`;超出时事务回滚。 - 3. 写入 `seckill_orders`(DB044,`orderId = order.id`)与 `seckill_order_items`(DB045,含 `seckillPrice` 快照与 `originalPrice`)。 - 4. 写入 Outbox `SeckillOrderCreated` 事件。 + 2. 在 DB044 `seckill_buyer_quotas` 对 `(activity_id, buyer_id)` 建唯一配额行,使用原子 UPSERT/条件更新保证 `purchased_quantity + :qty <= perBuyerLimit`;普通聚合查询或 Redis 不能作为限购正确性边界。取消成功按 `orderId` 幂等释放一次对应数量。 + 3. 通过 Ordering 公开应用契约创建 DB061 `orders` 与 DB062 `order_items` 的共享订单事实,保存 `seckillActivityId`、秒杀价、原价快照,并把活动的 `createdByMerchantUserId` 固定为该订单的 `assignedMerchantUserId`;Seckill 不建立第二套订单状态机。 + 4. Ordering 在同一受控事务中写入 `OrderCreatedIntegrationEvent` Outbox;Seckill 不再另造平行的订单创建事件。 - 不写入普通商品库存;`products.stock` 不受秒杀下单影响。 -- 失败优先级:限流 429 < 未开始 / 已结束 409 < 售罄 409 < 超过单用户限购 409 < 幂等键复用 409 < 业务异常 5xx。 +- 新请求的拒绝顺序为限流、时间窗口、库存、单用户限购及其他业务异常;已命中的幂等重放或 Key 冲突在这些可变校验之前处理。 #### 缓存、事件或外部依赖 -- Redis:`lock:seckill:order:{activityId}`(细粒度互斥,避免活动行成为热点)、`cache:seckill:activity:{activityId}`(事务成功后失效)、`cart:idempotency:{userId}:{key}`(幂等记录,TTL 24 小时)。 -- Outbox:`SeckillOrderCreated`,由 M09 站内消息与 C03 超时取消消费。 +- PostgreSQL:幂等请求指纹、首次结果、秒杀库存条件扣减和共享订单创建处于同一受控事务;相同 Key 重放首次结果,不依赖 Redis 保存唯一事实。 +- Redis:仅用于入口限流和活动元数据缓存;失败时按本接口的 429/503 降级规则处理,不参与库存与幂等正确性。 +- Outbox:Ordering 发布 `OrderCreatedIntegrationEvent` 供 M09 消费;C03 不消费订单创建事件,只按 Ordering 的 `expiresAt` 周期扫描待支付订单。 #### 验证场景 @@ -4149,163 +4484,14 @@ PlaceSeckillOrderResponse { - 商品已下架 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 - 地址不属于当前买家 → 409 / `RESOURCE.CONFLICT`,不泄露地址存在性。 -### A229 买家秒杀订单列表 +> 来源:[`interface-wqq.md`](interface/interface-wqq.md)。A301~A308 已统一归入 Ordering,A302/A303 已承接秒杀订单查询;DB061/DB062、跨模块应用契约和状态字段仍待评审。 -- 模块 / Tag:Seckill -- 需求编号:C01 -- 负责人:朱惠惠 -- 关联数据表:DB044、DB045 -- 当前状态:待交叉评审 -- 用途:买家分页查询本人秒杀订单,支持按状态、活动和时间筛选。 -- 方法与路径:`GET /api/seckill-orders` -- operationId:`Seckill_ListMyOrders` - -#### 请求 - -- Header:`Authorization: Bearer `(必填,角色 Buyer) -- Query 参数: - - `page`(默认 1) - - `pageSize`(默认 10,上限 50) - - `status`(可选,`PendingPayment` / `Paid` / `Shipped` / `Completed` / `Cancelled`) - - `activityId`(可选,按活动过滤) - - `createdFrom`、`createdTo`(可选,时间范围) - -#### 成功响应 - -- HTTP 状态:`200 OK` -- 响应 Schema:`SeckillOrderListResponse` - -```text -SeckillOrderListResponse { - items: SeckillOrderSummaryResponse[] - page: integer - pageSize: integer - total: integer - totalPages: integer -} - -SeckillOrderSummaryResponse { - orderId: uuid - activityId: uuid - activityName: string - productId: uuid - productName: string - productImageUrl: string - quantity: integer - seckillPrice: number - totalAmount: number - status: "PendingPayment" | "Paid" | "Shipped" | "Completed" | "Cancelled" - createdAt: string - expiresAt: string // PendingPayment 时返回 -} -``` - -#### 失败响应 - -| HTTP 状态 | 业务错误码 | 触发条件 | -|---:|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 分页或筛选参数非法 | -| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | - -#### 业务规则与并发 - -- 严格按 `buyer_id = current_user_id AND seckill_activity_id IS NOT NULL` 过滤。 -- 排序默认按 `createdAt desc`;相同 `createdAt` 时按 `orderId` 稳定排序。 -- 不返回完整地址或支付敏感信息;详细快照在 A230。 - -#### 缓存、事件或外部依赖 - -- 不缓存;订单状态实时读取 DB044。 - -#### 验证场景 - -- 买家查询本人秒杀订单 → 200,仅返回与当前买家关联的记录。 -- 按活动过滤 → 200,仅返回该活动的订单。 -- 跨用户查询 → 403,不泄露他人订单。 - -### A230 买家秒杀订单详情 - -- 模块 / Tag:Seckill -- 需求编号:C01 -- 负责人:朱惠惠 -- 关联数据表:DB044、DB045 -- 当前状态:待交叉评审 -- 用途:买家查看本人秒杀订单完整详情;包含活动快照、订单项快照、地址快照与状态时间线。 -- 方法与路径:`GET /api/seckill-orders/{orderId}` -- operationId:`Seckill_GetMyOrder` - -#### 请求 - -- Route 参数:`orderId: uuid` -- Header:`Authorization: Bearer `(必填,角色 Buyer) -- Body:无 - -#### 成功响应 - -- HTTP 状态:`200 OK` -- 响应 Schema:`SeckillOrderDetailResponse` - -```text -SeckillOrderDetailResponse { - orderId: uuid - activityId: uuid - activityName: string - productId: uuid - productName: string - productImageUrl: string - quantity: integer - seckillPrice: number - originalPrice: number // 商品原价快照 - totalAmount: number - status: "PendingPayment" | "Paid" | "Shipped" | "Completed" | "Cancelled" - addressSnapshot: AddressSnapshotResponse - timeline: OrderTimelineEntryResponse[] - paymentInfo: PaymentInfoResponse? - createdAt: string - expiresAt: string - paidAt: string? - cancelledAt: string? -} -``` - -#### 失败响应 - -| HTTP 状态 | 业务错误码 | 触发条件 | -|---:|---|---| -| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | -| 404 | `RESOURCE.NOT_FOUND` | 订单不存在、不属于当前用户或非秒杀订单 | - -#### 业务规则与并发 - -- 严格按 `order_id = :id AND buyer_id = current_user_id AND seckill_activity_id IS NOT NULL` 过滤;不满足任一条件返回 404,避免泄露订单存在性。 -- 地址快照来自下单时刻保存的 `orders.address_snapshot`,与 M04 共享字段。 -- 时间线包含创建、支付、发货、完成、取消等关键节点;时间均以 UTC 存储,前端按本地时区展示。 -- 支付信息(`paymentInfo`)仅在订单已支付后返回;支付卡号、Token 等敏感字段不出现。 - -#### 缓存、事件或外部依赖 - -- 不缓存;订单详情实时读取 DB044、DB045 与 M04 `orders`、`order_items`、`payments`。 - -#### 验证场景 - -- 买家查询本人秒杀订单 → 200,含活动快照、订单项快照、地址快照、时间线。 -- 跨用户访问 → 404,不泄露归属。 -- 已支付订单 → `paymentInfo` 返回;未支付订单不返回。 -- 已取消订单 → `cancelledAt` 与取消节点返回。 -- 普通订单(非秒杀)通过此接口访问 → 404,避免与 M04 详情接口混淆。 - ---- - -> 来源:[interface-wqq.md](interface/interface-wqq.md)。七个接口均为部分定义;DBxxx、幂等、状态字段和商家订单边界尚未确认。 - -### A301 提交订单 +### A301 提交订单 - **模块 / Tag**:Ordering - **需求编号**:F08 - **负责人**:韦乾强 -- **关联数据表**:DB001(orders)、DB003(order_items) +- **关联数据表**:DB061(orders)、DB062(order_items) - **当前状态**:部分定义 - **用途**:买家选择购物车商品和收货地址提交订单,系统原子扣减库存并返回订单号 - **方法与路径**:`POST /api/orders` @@ -4328,14 +4514,13 @@ SeckillOrderDetailResponse { ```json { "addressId": "uuid", - "cartItemIds": ["uuid"], - "idempotencyKey": "uuid" + "cartItemIds": ["uuid"] } ``` - **校验规则**: - `addressId`:必填,UUID格式,必须属于当前买家 - `cartItemIds`:必填,非空数组,每个元素为UUID格式 - - `idempotencyKey`:必填,UUID格式 + - `Idempotency-Key` 只从 Header 读取,Body 不重复传递 #### 成功响应 @@ -4351,7 +4536,8 @@ SeckillOrderDetailResponse { "orderNo": "ORD20260724001", "totalAmount": 299.00, "status": "PendingPayment", - "createdAt": "2026-07-24T10:00:00Z" + "createdAt": "2026-07-24T10:00:00Z", + "expiresAt": "2026-07-24T10:30:00Z" } } ``` @@ -4363,9 +4549,12 @@ SeckillOrderDetailResponse { | 400 | ORDER.INVALID_PARAM | 参数格式错误 | | 400 | ORDER.EMPTY_CART_ITEMS | 购物车商品列表为空 | | 400 | ORDER.INVALID_ADDRESS | 收货地址无效或不归属当前用户 | +| 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | +| 403 | AUTH.FORBIDDEN | 当前账号不是买家 | | 409 | ORDER.STOCK_INSUFFICIENT | 商品库存不足 | | 409 | ORDER.ITEM_NOT_AVAILABLE | 商品已下架或不可售 | -| 409 | ORDER.IDEMPOTENT_CONFLICT | 幂等键重复,返回原订单 | +| 409 | IDEMPOTENCY.KEY_REUSED | 同一幂等键被用于不同请求内容 | +| 503 | ORDER.DEFAULT_MERCHANT_UNAVAILABLE | Identity 未能解析唯一且启用的默认商家运营账号 | #### 业务规则与并发 @@ -4373,11 +4562,12 @@ SeckillOrderDetailResponse { 2. 库存扣减使用条件更新 `WHERE stock >= quantity`,避免超卖 3. 订单金额由服务端计算,不接受客户端传入 4. 订单项保存商品名称、图片、单价快照 +5. 本期是单店 B2C,不拆多商户子订单;普通订单创建时通过 Identity 公开应用契约解析唯一且启用的默认商家运营账号,并把 `assignedMerchantUserId` 保存为订单处理与通知归属。未配置、配置重复或账号不可用时整单失败,不创建无人处理的订单。 #### 缓存、事件或外部依赖 -- 发布 `OrderCreatedEvent` 到 Outbox -- 依赖 DB001(orders)、DB003(order_items)、DB004(products) +- 发布 `OrderCreatedIntegrationEvent` 到 Outbox +- 订单事实写入 DB061 `orders`、DB062 `order_items`;地址、购物车、商品、库存和默认商家运营账号通过对应模块公开应用契约协作,不直接访问其他模块内部表 #### 验证场景 @@ -4393,11 +4583,11 @@ SeckillOrderDetailResponse { - **模块 / Tag**:Ordering - **需求编号**:F09 - **负责人**:韦乾强 -- **关联数据表**:DB001(orders) +- **关联数据表**:DB061(orders)、DB062(order_items) - **当前状态**:部分定义 -- **用途**:买家分页查询自己的订单列表,支持按状态筛选 +- **用途**:买家分页查询自己的普通或秒杀订单,支持按状态、订单来源、秒杀活动和创建时间筛选 - **方法与路径**:`GET /api/orders` -- **operationId**:`Ordering_GetOrders` +- **operationId**:`Ordering_ListOrders` - **请求Schema**:无 - **响应Schema**:`OrderListResponse` - **身份与Policy**:BuyerOnly @@ -4411,6 +4601,9 @@ SeckillOrderDetailResponse { - `page`(可选,默认1):页码 - `pageSize`(可选,默认10,上限50):每页条数 - `status`(可选):筛选订单状态,`PendingPayment`/`Paid`/`Shipped`/`Completed`/`Cancelled` + - `orderType`(可选):`Normal` / `Seckill` + - `seckillActivityId`(可选,UUID):按秒杀活动筛选;传入时 `orderType` 固定按 `Seckill` 处理 + - `createdFrom`、`createdTo`(可选,UTC ISO 8601):创建时间范围 - **Header**:`Authorization: Bearer `(必需) - **Body**:无 @@ -4428,15 +4621,19 @@ SeckillOrderDetailResponse { { "orderId": "uuid", "orderNo": "ORD20260724001", + "orderType": "Seckill", + "seckillActivityId": "uuid", + "seckillActivityName": "暑期秒杀", "status": "PendingPayment", "totalAmount": 299.00, "createdAt": "2026-07-24T10:00:00Z", + "expiresAt": "2026-07-24T10:30:00Z", "itemSummary": "商品A x1,商品B x2" } ], "page": 1, "pageSize": 10, - "totalCount": 25, + "total": 25, "totalPages": 3 } } @@ -4447,11 +4644,14 @@ SeckillOrderDetailResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| | 400 | ORDER.INVALID_PAGE_PARAM | 分页参数非法 | +| 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | +| 403 | AUTH.FORBIDDEN | 当前账号不是买家 | #### 业务规则与并发 -1. 订单按创建时间倒序排列 -2. `itemSummary`最多展示3个商品名称,多的显示"+X件" +1. 订单按 `createdAt desc, orderId desc` 稳定排序。 +2. `itemSummary` 最多展示 3 个商品名称,多的显示“+X件”。 +3. A229 已取消;秒杀订单列表由本接口通过 `orderType=Seckill` 或 `seckillActivityId` 查询,不建立第二套订单查询事实。 #### 缓存、事件或外部依赖 @@ -4470,11 +4670,11 @@ SeckillOrderDetailResponse { - **模块 / Tag**:Ordering - **需求编号**:F09 - **负责人**:韦乾强 -- **关联数据表**:DB001(orders)、DB003(order_items) +- **关联数据表**:DB061(orders)、DB062(order_items) - **当前状态**:部分定义 - **用途**:买家查看单个订单的完整详情 - **方法与路径**:`GET /api/orders/{orderId}` -- **operationId**:`Ordering_GetOrderById` +- **operationId**:`Ordering_GetOrder` - **请求Schema**:无 - **响应Schema**:`OrderDetailResponse` - **身份与Policy**:BuyerOnly @@ -4500,9 +4700,17 @@ SeckillOrderDetailResponse { "data": { "orderId": "uuid", "orderNo": "ORD20260724001", + "orderType": "Seckill", + "seckill": { + "activityId": "uuid", + "activityName": "暑期秒杀", + "originalUnitPrice": 399.00, + "seckillUnitPrice": 199.00 + }, "status": "PendingPayment", "totalAmount": 299.00, "createdAt": "2026-07-24T10:00:00Z", + "expiresAt": "2026-07-24T10:30:00Z", "addressSnapshot": { "receiverName": "张三", "phone": "138****8888", @@ -4522,8 +4730,7 @@ SeckillOrderDetailResponse { } ], "statusHistory": [ - {"status": "PendingPayment", "time": "2026-07-24T10:00:00Z"}, - {"status": "Paid", "time": "2026-07-24T10:05:00Z"} + {"status": "PendingPayment", "time": "2026-07-24T10:00:00Z"} ], "availableActions": ["cancel"] } @@ -4534,14 +4741,16 @@ SeckillOrderDetailResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| +| 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 404 | ORDER.NOT_FOUND | 订单不存在 | | 403 | ORDER.ACCESS_DENIED | 订单不属于当前用户 | #### 业务规则与并发 -1. 订单项为快照,包含下单时的商品名称、图片、单价 -2. 地址为快照,包含下单时的收货信息 -3. `availableActions`根据当前状态展示可执行操作 +1. 订单项为快照,包含下单时的商品名称、图片和成交单价。 +2. 地址为快照,包含下单时的收货信息。 +3. 普通订单 `orderType=Normal` 且 `seckill=null`;秒杀订单返回活动 ID、活动名称、原价和秒杀价快照,承接已取消的 A230。 +4. `availableActions` 根据当前状态展示可执行操作。 #### 缓存、事件或外部依赖 @@ -4560,7 +4769,7 @@ SeckillOrderDetailResponse { - **模块 / Tag**:Ordering - **需求编号**:F09 - **负责人**:韦乾强 -- **关联数据表**:DB001(orders)、DB003(order_items)、DB004(products) +- **关联数据表**:DB061(orders)、DB062(order_items) - **当前状态**:部分定义 - **用途**:买家取消自己待支付的订单,触发库存回补 - **方法与路径**:`POST /api/orders/{orderId}/cancel` @@ -4600,21 +4809,22 @@ SeckillOrderDetailResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| +| 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 404 | ORDER.NOT_FOUND | 订单不存在 | | 403 | ORDER.ACCESS_DENIED | 订单不属于当前用户 | -| 409 | ORDER.INVALID_STATUS | 订单状态不允许取消(已支付/已发货/已完成/已取消) | +| 409 | ORDER.INVALID_STATUS | 订单状态不允许取消(已支付/已发货/已完成);已取消返回现有成功结果 | #### 业务规则与并发 -1. 只有 `PendingPayment` 状态可取消 -2. 取消与库存回补在同一事务内完成 -3. 使用条件更新 `WHERE status = 'PendingPayment'` 保证幂等 -4. `cancelReason` 记录为 `BUYER_CANCELLED` +1. `PendingPayment` 状态执行取消;订单已经是 `Cancelled` 时返回现有 `200` 结果,不重复回补库存;其他状态返回 409。 +2. 取消与库存回补通过公开应用契约处于同一受控事务:普通订单回补 Catalog,秒杀订单按 `seckillActivityId` 回补 Seckill 原活动库存并释放对应限购名额。 +3. 使用 `WHERE status = 'PendingPayment'` 条件更新和库存侧 `(orderId, orderItemId, reason)` 唯一幂等键,保证并发时最多取消和回补一次。 +4. `cancelReason` 记录为 `BUYER_CANCELLED`;C03 超时取消复用同一取消用例,仅将原因改为 `TIMEOUT`。 #### 缓存、事件或外部依赖 -- 发布 `OrderCancelledEvent` 到 Outbox -- 库存回补操作 DB004(products) +- 发布 `OrderCancelledIntegrationEvent` 到 Outbox +- 库存回补根据订单库存通道调用 Catalog 或 Seckill 公开应用契约,不直接修改其他模块内部表 #### 验证场景 @@ -4627,18 +4837,18 @@ SeckillOrderDetailResponse { ### A305 商家查询订单列表 -- **模块 / Tag**:Merchant +- **模块 / Tag**:Ordering - **需求编号**:F12 - **负责人**:韦乾强 -- **关联数据表**:DB001(orders)、DB003(order_items) +- **关联数据表**:DB061(orders)、DB062(order_items) - **当前状态**:部分定义 -- **用途**:商家分页查询本店订单,支持按状态筛选 +- **用途**:商家分页查询分配给当前运营账号的订单,支持按状态筛选 - **方法与路径**:`GET /api/merchant/orders` -- **operationId**:`Merchant_GetOrders` +- **operationId**:`Ordering_ListMerchantOrders` - **请求Schema**:无 - **响应Schema**:`MerchantOrderListResponse` - **身份与Policy**:MerchantOnly -- **资源归属**:只返回当前商家的订单 +- **资源归属**:只返回 `assignedMerchantUserId` 等于当前账号的订单 - **幂等要求**:GET请求天然幂等 #### 请求 @@ -4647,7 +4857,7 @@ SeckillOrderDetailResponse { - **Query参数**: - `page`(可选,默认1):页码 - `pageSize`(可选,默认10,上限50):每页条数 - - `status`(可选):筛选订单状态 + - `status`(可选):`PendingPayment` / `Paid` / `Shipped` / `Completed` / `Cancelled` - **Header**:`Authorization: Bearer `(必需) - **Body**:无 @@ -4674,7 +4884,7 @@ SeckillOrderDetailResponse { ], "page": 1, "pageSize": 10, - "totalCount": 15, + "total": 15, "totalPages": 2 } } @@ -4685,10 +4895,12 @@ SeckillOrderDetailResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| | 400 | ORDER.INVALID_PAGE_PARAM | 分页参数非法 | +| 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | +| 403 | AUTH.FORBIDDEN | 当前账号不是商家 | #### 业务规则与并发 -1. 只返回与当前商家商品相关的订单 +1. 只返回 `assignedMerchantUserId = currentUserId` 的订单;本项目不按商户租户拆分商品或结算。 2. 订单按创建时间倒序排列 #### 缓存、事件或外部依赖 @@ -4704,18 +4916,18 @@ SeckillOrderDetailResponse { ### A306 商家查询订单详情 -- **模块 / Tag**:Merchant +- **模块 / Tag**:Ordering - **需求编号**:F12 - **负责人**:韦乾强 -- **关联数据表**:DB001(orders)、DB003(order_items) +- **关联数据表**:DB061(orders)、DB062(order_items) - **当前状态**:部分定义 -- **用途**:商家查看本店订单的完整详情 +- **用途**:商家查看分配给当前运营账号的订单详情 - **方法与路径**:`GET /api/merchant/orders/{orderId}` -- **operationId**:`Merchant_GetOrderById` +- **operationId**:`Ordering_GetMerchantOrder` - **请求Schema**:无 - **响应Schema**:`MerchantOrderDetailResponse` - **身份与Policy**:MerchantOnly -- **资源归属**:订单必须属于当前商家的商品 +- **资源归属**:订单的 `assignedMerchantUserId` 必须等于当前账号 - **幂等要求**:GET请求天然幂等 #### 请求 @@ -4752,14 +4964,21 @@ SeckillOrderDetailResponse { }, "items": [ { + "orderItemId": "uuid", "productId": "uuid", "productName": "商品A", "imageUrl": "https://...", "unitPrice": 199.00, "quantity": 1, + "refundedQuantity": 0, + "fulfillableQuantity": 1, "subtotal": 199.00 } ], + "fulfillment": { + "state": "ReadyToShip", + "blockReason": null + }, "availableActions": ["ship"] } } @@ -4769,40 +4988,43 @@ SeckillOrderDetailResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| +| 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 404 | ORDER.NOT_FOUND | 订单不存在 | -| 403 | ORDER.ACCESS_DENIED | 订单不归属当前商家 | +| 403 | ORDER.ACCESS_DENIED | 订单未分配给当前商家运营账号 | #### 业务规则与并发 -1. 只返回与当前商家商品相关的订单项 -2. `availableActions`根据当前状态展示可执行操作 +1. 只返回分配给当前运营账号的整单及其订单项,不把一个订单拆成多商户子订单。 +2. `fulfillment.state` 是根据订单状态和 AfterSales 履约快照得到的展示字段,可取 `ReadyToShip`、`BlockedByAfterSales`、`PartiallyRefunded`、`FullyRefunded`、`Shipped`、`Completed`;它不是新的订单核心状态。 +3. `refundedQuantity` 与 `fulfillableQuantity` 由售后终态数量计算;存在处理中售后或无剩余可履约数量时,`availableActions` 不返回 `ship`。 #### 缓存、事件或外部依赖 -无 +- 通过 AfterSales 公开应用契约查询当前订单的履约阻断状态和各订单项累计已退款数量;不直接读取售后内部表。 #### 验证场景 1. 正常查询:返回完整订单详情 2. 订单不存在:返回404 3. 跨商家访问:返回403 +4. 存在处理中售后或部分退款:返回可理解的履约状态和准确剩余数量,不错误展示发货入口 --- ### A307 商家发货 -- **模块 / Tag**:Merchant +- **模块 / Tag**:Ordering - **需求编号**:F12 - **负责人**:韦乾强 -- **关联数据表**:DB001(orders) +- **关联数据表**:DB061(orders)、DB062(order_items) - **当前状态**:部分定义 - **用途**:商家对已支付订单执行发货操作 - **方法与路径**:`POST /api/merchant/orders/{orderId}/ship` -- **operationId**:`Merchant_ShipOrder` +- **operationId**:`Ordering_ShipOrder` - **请求Schema**:`ShipOrderRequest` - **响应Schema**:`ShipOrderResponse` - **身份与Policy**:MerchantOnly -- **资源归属**:订单必须属于当前商家的商品 +- **资源归属**:订单的 `assignedMerchantUserId` 必须等于当前账号 - **幂等要求**:以订单号为幂等键,重复发货返回成功 #### 请求 @@ -4835,7 +5057,13 @@ SeckillOrderDetailResponse { "status": "Shipped", "shippedAt": "2026-07-24T12:00:00Z", "expressCompany": "顺丰速运", - "trackingNo": "SF1234567890" + "trackingNo": "SF1234567890", + "shippedItems": [ + { + "orderItemId": "uuid", + "quantity": 1 + } + ] } } ``` @@ -4844,19 +5072,26 @@ SeckillOrderDetailResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| +| 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 404 | ORDER.NOT_FOUND | 订单不存在 | -| 403 | ORDER.ACCESS_DENIED | 订单不归属当前商家 | +| 403 | ORDER.ACCESS_DENIED | 订单未分配给当前商家运营账号 | | 409 | ORDER.INVALID_STATUS | 订单状态不允许发货(只有已支付可发货) | +| 409 | ORDER.AFTER_SALES_IN_PROGRESS | 订单存在会影响履约的处理中售后申请 | +| 409 | ORDER.NO_FULFILLABLE_ITEMS | 全部订单项均已退款,没有剩余可发货数量 | #### 业务规则与并发 -1. 只有 `Paid` 状态可发货 -2. 使用条件更新 `WHERE status = 'Paid'` 保证幂等 -3. 记录发货时间、物流公司和物流单号 +1. `Paid` 状态执行首次发货;已经是 `Shipped` 且物流公司、单号与首次请求一致时返回现有 `200` 结果。 +2. 已是 `Shipped` 但物流载荷不同,或处于其他不允许状态时返回 409;条件更新为 0 后必须读取现状再判定,不能把所有重复请求都当错误。 +3. 使用条件更新 `WHERE status = 'Paid'` 防止重复副作用,并记录发货时间、物流公司和物流单号。 +4. 发货前通过 AfterSales 公开应用契约取得履约快照。`PendingReview`、`PendingReturn`、`PendingReceipt`、`Refunding`、`RefundFailed` 等仍可能改变履约结果的申请阻断发货;`Rejected`、`Cancelled` 不阻断。 +5. `Refunded` 数量从原购买数量中扣除;仍有剩余数量时只发出剩余可履约数量,并在响应 `shippedItems` 中返回实际发货明细;全部数量均已退款时拒绝发货。 +6. A307 与 A412 提交售后必须先通过 Ordering 公开应用契约在同一 PostgreSQL 事务中锁定同一 `orders` 行并取得最新履约快照,锁保持到各自业务写入提交;禁止“先查询、后另开事务更新”。若发货先提交,售后按已发货规则重新判断;若售后申请先提交,发货必须看到占用结果并按上述规则处理。 #### 缓存、事件或外部依赖 -- 发布 `OrderShippedEvent` 到 Outbox +- 发布 `OrderShippedIntegrationEvent` 到 Outbox +- 在事务内锁定 Ordering 订单行后调用 AfterSales 的履约查询公开应用契约;Ordering 不直接读取或修改售后内部表。 #### 验证场景 @@ -4864,12 +5099,82 @@ SeckillOrderDetailResponse { 2. 重复发货:返回幂等成功 3. 订单未支付:返回409 4. 跨商家发货:返回403 +5. 存在处理中售后:返回409且订单仍为Paid +6. 部分退款完成:仅发出剩余数量;全部退款完成:返回无可履约商品 --- +### A308 买家确认收货 + +- **模块 / Tag**:Ordering +- **需求编号**:F09 +- **负责人**:韦乾强 +- **关联数据表**:DB061(orders) +- **当前状态**:部分定义 +- **用途**:买家确认已收到商品,将订单状态从 `Shipped` 变更为 `Completed` +- **方法与路径**:`POST /api/orders/{orderId}/confirm-receipt` +- **operationId**:`Ordering_ConfirmReceipt` +- **请求Schema**:无 +- **响应Schema**:`ConfirmReceiptResponse` +- **身份与Policy**:BuyerOnly +- **资源归属**:订单必须属于当前买家 +- **幂等要求**:以订单号为幂等键,重复确认返回成功 + +#### 请求 + +- **Route参数**:`orderId`(必需,UUID) +- **Query参数**:无 +- **Header**:`Authorization: Bearer `(必需) +- **Body**:无 + +#### 成功响应 + +- **HTTP状态**:`200 OK` +- **响应Schema**:`ConfirmReceiptResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "orderId": "uuid", + "status": "Completed", + "completedAt": "2026-07-24T14:00:00Z", + "completedBy": "BUYER_CONFIRMED" + } +} +``` + +#### 失败响应 + +| HTTP状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | +| 404 | ORDER.NOT_FOUND | 订单不存在 | +| 403 | ORDER.ACCESS_DENIED | 订单不属于当前用户 | +| 409 | ORDER.INVALID_STATUS | 订单状态不允许确认收货(只有已发货可确认) | + +#### 业务规则与并发 + +1. `Shipped` 状态执行首次确认;已是 `Completed` 且 `completedBy = BUYER_CONFIRMED` 时返回现有 `200` 结果。 +2. 已由 Worker 自动完成或处于其他不允许状态时返回 409;条件更新为 0 后读取现状再区分幂等重放与状态冲突。 +3. 使用条件更新 `WHERE status = 'Shipped'` 防止重复副作用,记录 `completed_at` 和 `completed_by = 'BUYER_CONFIRMED'`。 +4. 确认收货后触发评价入口开放(若 X01 已实现)。 + +#### 缓存、事件或外部依赖 + +- 发布统一的 `OrderCompletedIntegrationEvent` 到 Outbox;买家确认和 Worker 自动完成共用同一“订单已完成”事实 + +#### 验证场景 + +1. 正常确认收货:返回成功,状态变为 Completed +2. 重复确认:返回幂等成功 +3. 订单未发货:返回 409 +4. 跨用户确认:返回 403 + --- -> 来源:[interface-zhy.md](interface/interface-zhy.md)。已完成结构汇总;Payment、AfterSales 和对账接口存在模块边界及状态机冲突,当前不得直接冻结。 +> 来源:[`interface-zhy.md`](interface/interface-zhy.md)。A418 已取消并入 A414,A431 已取消且退款改用 Payment 应用契约,A434 已补齐退货信息;资金、售后状态和对账字段仍待数据库与 OpenAPI 评审。 ### A401 查询钱包余额 @@ -5024,7 +5329,7 @@ SeckillOrderDetailResponse { #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:发布 `WalletTopupSucceededIntegrationEvent`(待罗皓晨 M00 集成事件规范确认) +- 事件:本期没有已确认的跨模块消费者,不为模拟充值单独发布集成事件。 - 外部依赖:PostgreSQL #### 验证场景 @@ -5132,7 +5437,7 @@ SeckillOrderDetailResponse { - **模块 / Tag**:Payment - **需求编号**:M05-01-FR05 - **负责人**:张海洋 -- **关联数据表**:DB084(待评审)— `orders`(只读,用于查询订单金额/状态) +- **关联数据表**:Payment 不拥有订单表;订单金额、归属和状态通过 Ordering 公开应用契约读取 - **当前状态**:待交叉评审 - **用途**:进入支付前的订单金额、应付、钱包余额、可用渠道聚合查询 - **方法与路径**:`GET /api/payment/checkout/{orderId}` @@ -5152,7 +5457,7 @@ SeckillOrderDetailResponse { - **校验规则**: - `orderId` UUID 格式 - 订单归属当前 buyerId - - 订单状态为 `PendingPayment`(否则 409 + `PAYMENT.ORDER_NOT_PAYABLE`) + - `PendingPayment` 返回可支付收银台;已支付返回现有支付结果摘要;已取消或其他不可支付且无成功支付记录的状态返回 409 #### 成功响应 @@ -5167,6 +5472,8 @@ SeckillOrderDetailResponse { "orderId": "3f0ed9a9-...", "orderAmount": 199.00, "paidAmount": 0.00, + "orderStatus": "PendingPayment", + "canPay": true, "currency": "CNY", "walletBalance": 100.50, "insufficient": true, @@ -5183,14 +5490,14 @@ SeckillOrderDetailResponse { | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 订单不存在或非本人 | -| 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 订单状态非 `PendingPayment` | +| 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 订单已取消或处于其他不可支付且无成功支付记录的状态 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 - 不修改订单或钱包状态,纯查询 - 余额、订单金额、应付以服务端实时值(按 PAY-R01) -- 订单已支付 → 返回 `PAID` 状态但 `CheckoutResponse` 仍可读 +- 订单已支付 → 返回 `200`、`canPay=false`、`orderStatus=Paid` 和现有支付结果摘要,便于用户确认支付结果;已取消订单返回 409 #### 缓存、事件或外部依赖 @@ -5203,7 +5510,7 @@ SeckillOrderDetailResponse { - 正常:订单本人 + `PendingPayment` + 余额不足 → 返回 `insufficient=true` - 正常:订单本人 + `PendingPayment` + 余额充足 → 返回 `insufficient=false` - 异常:他人订单 → 404 + `RESOURCE.NOT_FOUND` -- 异常:订单已支付 → 409 + `PAYMENT.ORDER_NOT_PAYABLE` +- 正常:订单已支付 → 200,`canPay=false`,不再显示支付按钮 --- @@ -5212,7 +5519,7 @@ SeckillOrderDetailResponse { - **模块 / Tag**:Payment - **需求编号**:M05-01-FR05~FR09 - **负责人**:张海洋 -- **关联数据表**:DB081(待评审)— `wallets`、DB083(待评审)— `wallet_ledgers`、DB085(待评审)— `payments`、DB084(待评审)— `orders` +- **关联数据表**:DB081(待评审)— `wallets`、DB083(待评审)— `wallet_ledgers`、DB085(待评审)— `payments`;订单状态通过 Ordering 公开应用契约协作 - **当前状态**:待交叉评审 - **用途**:从买家钱包扣款并完成订单支付 - **方法与路径**:`POST /api/payment/orders/{orderId}/pay` @@ -5271,7 +5578,6 @@ SeckillOrderDetailResponse { | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 订单不存在或非本人 | | 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 订单状态非 `PendingPayment` 或已取消 | -| 409 | `PAYMENT.ALREADY_PAID` | 订单已支付成功(幂等命中首次结果) | | 409 | `PAYMENT.AMOUNT_MISMATCH` | `expectedAmount` 与订单金额不一致 | | 409 | `PAYMENT.INSUFFICIENT_BALANCE` | 钱包余额不足 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | @@ -5283,21 +5589,21 @@ SeckillOrderDetailResponse { - 钱包条件扣减 + 钱包流水 + 支付记录 + 订单状态 + Outbox **同一事务**(按 PAY-R06) - 与 C03 订单超时取消通过 `WHERE order.status = 'PendingPayment'` 条件竞争,唯一胜出(按 PAY-R05) - 余额不得为负(条件更新 + CHECK 约束)(按 PAY-R02) -- 成功提交后写入 `Idempotency-Key` 记录,相同 Key + 相同 amount + 相同 orderId → 返回首次结果 -- 重复支付请求返回原成功结果,不重复写入或重复发布事件(按 PAY-R11) +- 成功提交后写入 `Idempotency-Key` 记录,相同 Key + 相同 amount + 相同 orderId → 返回首次结果。 +- 同一订单已经支付成功时,无论请求使用原 Key 还是新的 Key,均返回既有 `200 PaymentResultResponse`,不重复扣款、写流水或发布事件;只有同一 Key 被用于不同请求内容时返回 `IDEMPOTENCY.KEY_REUSED`。 #### 缓存、事件或外部依赖 -- 缓存:写入幂等结果到 `Idempotency-Key` 存储(DB 或 Redis) +- 缓存:资金幂等结果持久化在 PostgreSQL,不以 Redis 作为唯一事实 - 事件:发布 `OrderPaidIntegrationEvent`(架构 §7.4 已确定第一条集成事件) -- 外部依赖:PostgreSQL + Ordering 模块 `orders` 表 +- 外部依赖:PostgreSQL + Ordering 公开应用契约;不得直接依赖 Ordering 内部 DbContext 或仓储 #### 验证场景 - 正常:订单 `PendingPayment` + 余额充足 + 金额一致 → 200 + `PaymentResultResponse` - 重复:相同 Idempotency-Key → 返回首次结果,不重复扣款 - 异常:余额不足 → 409 + `PAYMENT.INSUFFICIENT_BALANCE` -- 异常:订单已支付 → 409 + `PAYMENT.ALREADY_PAID` +- 重复:订单已支付 → 200,返回既有支付结果,不重复扣款 - 异常:订单已取消 → 409 + `PAYMENT.ORDER_NOT_PAYABLE` - 异常:金额不一致 → 409 + `PAYMENT.AMOUNT_MISMATCH` - 并发:与 C03 同时操作 → 唯一胜出,败方 409 + `PAYMENT.ORDER_NOT_PAYABLE` @@ -5529,7 +5835,7 @@ SeckillOrderDetailResponse { - **模块 / Tag**:AfterSales - **需求编号**:M10-FR01 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB084(待评审)— `orders` +- **关联数据表**:DB086(待评审)— `after_sales_requests`;订单项归属、状态与实付快照通过 Ordering 公开应用契约查询 - **当前状态**:待交叉评审 - **用途**:预检指定订单项是否可申请售后 - **方法与路径**:`GET /api/after-sales/eligibility` @@ -5568,7 +5874,7 @@ SeckillOrderDetailResponse { "reason": null, "maxRefundableAmount": 100.00, "maxRefundableQuantity": 1, - "availableTypes": ["RefundOnly", "ReturnAndRefund"], + "availableTypes": ["RefundOnly"], "deadlineAt": "2026-07-30T08:30:00Z" } } @@ -5581,25 +5887,26 @@ SeckillOrderDetailResponse { | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 订单或订单项不存在 | -| 409 | `AFTER_SALES.NOT_ELIGIBLE` | 订单未支付、已发货超期、不可售后状态 | #### 业务规则与并发 - 仅返回当前 buyerId 订单的可申请性 - 退款金额上限 = 实付单价 × 剩余可售后数量(M10 业务规则) -- 可申请类型根据订单状态决定:已支付/已发货 → RefundOnly;已发货 + 确认收货后 → ReturnAndRefund +- 不符合业务条件时仍返回 `200`、`eligible=false` 和稳定 `reason`,便于页面直接展示原因;订单项不存在或不属于当前买家仍统一返回 404。 +- 可申请类型根据订单状态、履约情况和剩余可售后数量计算,不由客户端推断。 +- 未发货的 `Paid` 订单只允许 `RefundOnly`;`Shipped` 或在售后期限内的 `Completed` 订单可以按资格返回 `RefundOnly`、`ReturnAndRefund`。 #### 缓存、事件或外部依赖 - 缓存:不缓存 - 事件:无 -- 外部依赖:PostgreSQL + Orders 模块 +- 外部依赖:PostgreSQL + Ordering 公开应用契约 #### 验证场景 - 正常:已支付订单 → 返回可申请 -- 异常:订单未支付 → 409 + `AFTER_SALES.NOT_ELIGIBLE` -- 异常:完成 > 7 天 → 409 + `AFTER_SALES.NOT_ELIGIBLE` +- 订单未支付 → 200,`eligible=false`,返回不可申请原因。 +- 完成超过 7 天 → 200,`eligible=false`,返回超期原因。 --- @@ -5631,18 +5938,16 @@ SeckillOrderDetailResponse { "orderItemId": "5a7c...", "type": "RefundOnly", "quantity": 1, - "reason": "DAMAGED", - "reasonNote": "外包装破损", - "evidence": ["https://...", "https://..."] + "reason": "Damaged", + "reasonNote": "外包装破损" } ``` - **校验规则**: - `orderId` / `orderItemId` 必填,UUID 格式 - `type` 枚举:`RefundOnly` / `ReturnAndRefund` - `quantity` 整数 ≥ 1 且 ≤ 剩余可售后数量 - - `reason` 枚举白名单(待 6.2 M10 业务规则定义) - - `reasonNote` 选填,≤ 500 字 - - `evidence` 选填,最多 9 张图 URL + - `reason` 枚举:`Damaged` / `QualityIssue` / `WrongItem` / `NotAsDescribed` / `Other` + - `reasonNote` ≤ 500 字;`reason=Other` 时必填,其他原因时选填 - 退款金额由 `quantity × 实付单价` 后端计算(按 M10 业务规则"不接受任意金额") - `Idempotency-Key` 必填 @@ -5663,9 +5968,8 @@ SeckillOrderDetailResponse { "quantity": 1, "calculatedAmount": 100.00, "currency": "CNY", - "reason": "DAMAGED", + "reason": "Damaged", "reasonNote": "外包装破损", - "evidence": ["https://..."], "status": "PendingReview", "createdAt": "2026-07-23T08:30:00Z" } @@ -5681,8 +5985,7 @@ SeckillOrderDetailResponse { | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 订单或订单项不存在 | | 409 | `AFTER_SALES.NOT_ELIGIBLE` | 订单不满足售后条件 | -| 409 | `AFTER_SALES.AMOUNT_EXCEEDS_PAID` | 申请数量超过剩余可售后数量 | -| 409 | `AFTER_SALES.DUPLICATE_APPLICATION` | 同一订单项已有"待审核"申请 | +| 409 | `AFTER_SALES.QUANTITY_EXCEEDS_AVAILABLE` | 申请数量超过该订单项剩余可售后数量,或并发申请已占用额度 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | @@ -5691,20 +5994,20 @@ SeckillOrderDetailResponse { - 退款金额由服务端计算(M10 规则:"不接受任意金额") - 申请数量不得超过剩余可售后数量(防重复申请) - 状态写入 `PendingReview`(M10 状态机) -- 同一 buyerId 同一订单项已有 `PendingReview` → 拒绝重复申请 +- 同一订单项可按剩余数量分次申请;仅处理中和已退款数量占用额度,不因存在另一笔 `PendingReview` 就整项禁止申请。 +- A412 必须与 A307 共用 Ordering 提供的订单级变更契约:在同一 PostgreSQL 事务中锁定目标 `orders` 行、读取最新履约状态并保持到售后申请写入提交,禁止查询后另开事务插入。申请先提交时占用数量进入履约快照;发货先提交时,本请求按已发货后的类型和库存规则重新校验。 #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:发布 `AfterSalesApplicationSubmittedIntegrationEvent`(待 M00 集成事件确认) -- 外部依赖:PostgreSQL +- 事件:发布已在 Messaging 契约登记的 `AfterSalesApplicationSubmittedIntegrationEvent`。 +- 外部依赖:PostgreSQL + Ordering 公开应用契约 #### 验证场景 - 正常:订单项可申请 → 201 + 详情 - 重复:相同 Idempotency-Key → 返回首次结果 -- 异常:申请数量 > 剩余可售后 → 409 + `AFTER_SALES.AMOUNT_EXCEEDS_PAID` -- 异常:订单项已有 PendingReview → 409 + `AFTER_SALES.DUPLICATE_APPLICATION` +- 异常:申请数量超过剩余可售后数量,或并发申请已先占用额度 → 409 + `AFTER_SALES.QUANTITY_EXCEEDS_AVAILABLE` --- @@ -5721,7 +6024,7 @@ SeckillOrderDetailResponse { - **请求 Schema**:`ListAfterSalesRequestsQuery` - **响应 Schema**:`AfterSalesRequestListResponse` - **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` -- **资源归属**:当前 buyerId 申请 / 当前 merchant 授权范围申请 +- **资源归属**:买家只看本人申请;商家只看关联订单 `assignedMerchantUserId` 等于当前账号的申请 - **幂等要求**:GET 天然幂等 #### 请求 @@ -5735,7 +6038,7 @@ SeckillOrderDetailResponse { - **Header**:`Authorization: Bearer ` - **Body**:(无) - **校验规则**: - - 买家仅看本人申请;商家仅看授权范围内申请 + - 买家仅看本人申请;商家仅看关联订单分配给当前运营账号的申请 - 标准分页 + 时间范围 + 枚举白名单 #### 成功响应 @@ -5780,7 +6083,7 @@ SeckillOrderDetailResponse { #### 业务规则与并发 -- 商家视图按 `merchantId` 过滤订单范围 +- 商家视图按关联订单的 `assignedMerchantUserId = currentUserId` 过滤 - 默认排序 `createdAt desc, requestId desc` #### 缓存、事件或外部依赖 @@ -5802,7 +6105,7 @@ SeckillOrderDetailResponse { - **模块 / Tag**:AfterSales - **需求编号**:M10-FR04 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_audit_logs` +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_status_histories` - **当前状态**:待交叉评审 - **用途**:查询单条售后申请的详细信息与状态时间线 - **方法与路径**:`GET /api/after-sales/requests/{requestId}` @@ -5810,7 +6113,7 @@ SeckillOrderDetailResponse { - **请求 Schema**:(无) - **响应 Schema**:`AfterSalesRequestDetailResponse` - **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` -- **资源归属**:当前 buyerId 应用 / 当前 merchant 授权范围内 +- **资源归属**:买家只能查看本人申请;商家只能查看关联订单分配给当前账号的申请 - **幂等要求**:GET 天然幂等 #### 请求 @@ -5840,9 +6143,8 @@ SeckillOrderDetailResponse { "quantity": 1, "calculatedAmount": 100.00, "currency": "CNY", - "reason": "DAMAGED", + "reason": "Damaged", "reasonNote": "外包装破损", - "evidence": ["https://..."], "status": "PendingReview", "createdAt": "2026-07-23T08:30:00Z", "timeline": [ @@ -5863,7 +6165,7 @@ SeckillOrderDetailResponse { #### 业务规则与并发 -- 时间线读 `after_sales_audit_logs` 表(按 DB087 推断) +- 时间线读取模块内的 `after_sales_status_histories`(DB087 待数据库设计确认),不读取通用操作审计 - 不返回内部审计字段(如 merchant 内部 ID) #### 缓存、事件或外部依赖 @@ -5932,12 +6234,12 @@ SeckillOrderDetailResponse { - 审核通过后不允许撤销(M10 业务规则) - 状态条件更新:`WHERE status = 'PendingReview' AND buyer_id = currentBuyerId` -- 撤销后保留 `audit_log` 记录 +- 撤销后保留领域状态历史 #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:发布 `AfterSalesApplicationCancelledIntegrationEvent` +- 事件:本期没有已确认的跨模块消费者,撤销事实保存在售后状态时间线,不单独发布集成事件。 - 外部依赖:PostgreSQL #### 验证场景 @@ -5953,7 +6255,7 @@ SeckillOrderDetailResponse { - **模块 / Tag**:AfterSales - **需求编号**:M10-FR05 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_audit_logs` +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_status_histories` - **当前状态**:待交叉评审 - **用途**:商家同意或拒绝售后申请 - **方法与路径**:`POST /api/after-sales/requests/{requestId}/audit` @@ -5961,7 +6263,7 @@ SeckillOrderDetailResponse { - **请求 Schema**:`AuditAfterSalesRequest` - **响应 Schema**:`AfterSalesRequestDetailResponse` - **身份与 Policy**:JWT Bearer + `MerchantOnly` -- **资源归属**:当前 merchant 授权范围内申请 +- **资源归属**:关联订单的 `assignedMerchantUserId` 必须等于当前商家账号 - **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) #### 请求 @@ -5973,22 +6275,20 @@ SeckillOrderDetailResponse { ```json { "decision": "Approve", - "auditNote": "同意申请", - "expectRefund": true + "auditNote": "同意申请" } ``` - **校验规则**: - - 申请归属当前 merchant + - 申请关联订单分配给当前商家账号 - 申请状态必须为 `PendingReview` - `decision` 枚举:`Approve` / `Reject` - - `expectRefund=true` 表示审核通过后系统将自动触发退款(A431) - `Idempotency-Key` 必填 #### 成功响应 - **HTTP 状态**:`200 OK` - **响应 Schema**:`AfterSalesRequestDetailResponse` -- **示例**:(同 A412 详情,status 变为 `PendingReturn` 或 `Refunding`) +- **示例**:(拒绝时 `status=Rejected`;退货退款审核通过时 `status=PendingReturn`;仅退款全部子操作成功时 `status=Refunded`) #### 失败响应 @@ -6000,25 +6300,31 @@ SeckillOrderDetailResponse { | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | | 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `PendingReview` | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 503 | `AFTER_SALES.REFUND_FAILED` | 审核已通过,但同步退款或必要库存回补失败;申请已记录为 `RefundFailed` | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 - 商家不能修改买家原始申请内容(业务规则) -- 状态条件更新:`WHERE status = 'PendingReview' AND merchant_id = currentMerchantId` -- 审核通过后若 `expectRefund=true` → 异步触发 A431 退款 -- `audit_log` 记录审核人与审核意见 +- 状态条件更新同时校验关联订单 `assigned_merchant_user_id = currentUserId` +- 审核结果由申请类型决定,客户端不能通过布尔字段选择是否退款: + - `ReturnAndRefund` 审核通过只进入 `PendingReturn`,等待 A434 和 A417,不在审核时退款或回补库存。 + - `RefundOnly` 审核通过后以 `Refunding` 作为事务内过渡并同步调用 Payment 退款应用契约;若 Ordering 快照表明订单仍为 `Paid` 且未发货,还必须按普通/秒杀原通道调用 Catalog 或 Seckill 库存回补契约。全部成功后本次 HTTP 返回 `Refunded`。 + - `RefundOnly` 对 `Shipped` 或 `Completed` 订单只退款、不回补库存。 +- 退款、必要的库存回补与售后终态通过公开应用契约加入同一受控数据库事务;任一步失败均不留下部分资金/库存结果,并在独立失败记录中把申请置为 `RefundFailed` 供 A419 重试。 +- 售后状态历史记录审核人、审核意见和状态变化;这是领域时间线,不是未选择的通用后台操作日志。 #### 缓存、事件或外部依赖 - 缓存:不缓存 - 事件:发布 `AfterSalesApplicationAuditedIntegrationEvent` -- 外部依赖:PostgreSQL + Payment 模块(通过应用能力调用 A431) +- 外部依赖:PostgreSQL + Ordering 售后快照 + Payment 退款应用契约 + Catalog/Seckill 库存回补应用契约 #### 验证场景 -- 正常:商家 Approve → 状态进入 `PendingReturn` 或 `Refunding` +- 正常:商家 Approve → 退货退款进入 `PendingReturn`;仅退款同步完成后返回 `Refunded` - 正常:商家 Reject → 状态进入 `Rejected` +- 异常:退款或必要库存回补失败 → 503 + `AFTER_SALES.REFUND_FAILED`,详情可查询到 `RefundFailed` - 异常:买家角色调用 → 403 + `AUTH.FORBIDDEN` - 异常:他人商家申请 → 404 + `RESOURCE.NOT_FOUND` - 异常:状态已非 `PendingReview` → 409 + `AFTER_SALES.INVALID_STATUS` @@ -6030,7 +6336,7 @@ SeckillOrderDetailResponse { - **模块 / Tag**:AfterSales - **需求编号**:M10-FR11 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_audit_logs` +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_status_histories` - **当前状态**:待交叉评审 - **用途**:商家确认收到退货,触发退款流程 - **方法与路径**:`POST /api/after-sales/requests/{requestId}/confirm-return` @@ -6038,7 +6344,7 @@ SeckillOrderDetailResponse { - **请求 Schema**:`ConfirmReturnRequest` - **响应 Schema**:`AfterSalesRequestDetailResponse` - **身份与 Policy**:JWT Bearer + `MerchantOnly` -- **资源归属**:当前 merchant 授权范围内申请 +- **资源归属**:关联订单的 `assignedMerchantUserId` 必须等于当前商家账号 - **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) #### 请求 @@ -6054,17 +6360,17 @@ SeckillOrderDetailResponse { } ``` - **校验规则**: - - 申请归属当前 merchant + - 申请关联订单分配给当前商家账号 - 申请类型必须为 `ReturnAndRefund` - 申请状态必须为 `PendingReceipt` - - `receivedQuantity` ∈ [1, 申请数量] + - 本期不支持部分收货,`receivedQuantity` 必须等于申请数量 - `Idempotency-Key` 必填 #### 成功响应 - **HTTP 状态**:`200 OK` - **响应 Schema**:`AfterSalesRequestDetailResponse` -- **示例**:(同 A412 详情,status 变为 `Refunding`) +- **示例**:(同 A412 详情,全部子操作成功后 `status=Refunded`) #### 失败响应 @@ -6076,107 +6382,30 @@ SeckillOrderDetailResponse { | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | | 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `PendingReceipt` | | 409 | `AFTER_SALES.WRONG_TYPE` | 申请类型非 `ReturnAndRefund` | +| 409 | `AFTER_SALES.RETURN_QUANTITY_MISMATCH` | 收货数量与申请数量不一致 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 503 | `AFTER_SALES.REFUND_FAILED` | 已确认收货,但同步退款或库存回补失败;申请已记录为 `RefundFailed` | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 状态条件更新:`WHERE status = 'PendingReceipt' AND merchant_id = currentMerchantId` -- 确认收到后异步触发 A431 退款 -- 库存按退货数量回补(按 M10 业务规则"已发货或已完成订单仅在退货且商家确认收货后按退货数量回补") +- 状态条件更新同时校验关联订单 `assigned_merchant_user_id = currentUserId` +- 确认收到后以 `Refunding` 作为事务内过渡;根据 Ordering 快照的 `orderType` 与 `seckillActivityId`,通过公开应用契约幂等回补 Catalog 普通库存或 Seckill 原活动库存,并同步执行 Payment 退款。HTTP 成功时已经进入 `Refunded`。 +- 库存回补数量等于整笔申请数量;本期不拆分部分收货或部分退款。仅退货退款在 A417 回补,已发货/已完成订单的仅退款不回补。 +- 回补、退款和售后终态加入同一受控数据库事务:全部成功后转为 `Refunded`;任一步失败不保留部分结果,并在独立失败记录中置为 `RefundFailed`,不得回滚成“从未确认收货”。 #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:发布 `AfterSalesReturnConfirmedIntegrationEvent` + 库存回补事件 -- 外部依赖:PostgreSQL + Payment(A431)+ Inventory +- 事件:Payment 退款应用契约只发布一次 `RefundCompletedIntegrationEvent`;收货确认与库存回补不另发没有消费者的事件。 +- 外部依赖:PostgreSQL + Payment 退款应用契约 + Catalog/Seckill 库存回补应用契约 #### 验证场景 -- 正常:商家确认退货 → 状态进入 `Refunding`,触发退款 +- 正常:商家确认退货 → 正确库存通道回补、退款成功,状态进入 `Refunded` - 异常:申请类型为 `RefundOnly` → 409 + `AFTER_SALES.WRONG_TYPE` - 异常:状态非 `PendingReceipt` → 409 + `AFTER_SALES.INVALID_STATUS` - ---- - -### A418 审核日志 - -- **模块 / Tag**:AfterSales -- **需求编号**:M10-FR04 -- **负责人**:张海洋 -- **关联数据表**:DB087(待评审)— `after_sales_audit_logs` -- **当前状态**:待交叉评审 -- **用途**:查询申请审核日志 -- **方法与路径**:`GET /api/after-sales/requests/{requestId}/audit-logs` -- **operationId**:`AfterSales_ListAuditLogs` -- **请求 Schema**:`ListAuditLogsQuery` -- **响应 Schema**:`AuditLogListResponse` -- **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` -- **资源归属**:当前 buyerId 申请 / 当前 merchant 授权范围内 -- **幂等要求**:GET 天然幂等 - -#### 请求 - -- **Route 参数**:`requestId`(UUID) -- **Query 参数**:`page` / `pageSize` / `sortBy` / `sortOrder` -- **Header**:`Authorization: Bearer ` -- **Body**:(无) -- **校验规则**: - - 申请归属当前 buyerId 或当前 merchant - -#### 成功响应 - -- **HTTP 状态**:`200 OK` -- **响应 Schema**:`AuditLogListResponse` -- **示例**: -```json -{ - "code": "success", - "message": "ok", - "data": { - "items": [ - { - "logId": "...", - "action": "Submitted", - "actor": "buyer", - "fromStatus": null, - "toStatus": "PendingReview", - "note": null, - "at": "2026-07-23T08:30:00Z" - } - ], - "page": 1, - "pageSize": 10, - "total": 1, - "totalPages": 1 - } -} -``` - -#### 失败响应 - -| HTTP 状态 | 业务错误码 | 触发条件 | -|---|---|---| -| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | -| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer / Merchant | -| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | -| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | - -#### 业务规则与并发 - -- 默认排序 `at asc, logId asc`(按时间顺序) -- 不返回内部审计字段(如 `merchant_internal_id`) - -#### 缓存、事件或外部依赖 - -- 缓存:不缓存 -- 事件:无 -- 外部依赖:PostgreSQL - -#### 验证场景 - -- 正常:本人申请 → 返回审核日志 -- 异常:他人申请 → 404 + `RESOURCE.NOT_FOUND` +- 异常:收货数量与申请数量不一致 → 409 + `AFTER_SALES.RETURN_QUANTITY_MISMATCH` --- @@ -6193,7 +6422,7 @@ SeckillOrderDetailResponse { - **请求 Schema**:`RetryRefundRequest` - **响应 Schema**:`AfterSalesRequestDetailResponse` - **身份与 Policy**:JWT Bearer + `MerchantOnly` -- **资源归属**:当前 merchant 授权范围内申请 +- **资源归属**:关联订单的 `assignedMerchantUserId` 必须等于当前商家账号 - **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) #### 请求 @@ -6208,7 +6437,7 @@ SeckillOrderDetailResponse { } ``` - **校验规则**: - - 申请归属当前 merchant + - 申请关联订单分配给当前商家账号 - 申请状态必须为 `RefundFailed` - `Idempotency-Key` 必填 @@ -6216,7 +6445,7 @@ SeckillOrderDetailResponse { - **HTTP 状态**:`200 OK` - **响应 Schema**:`AfterSalesRequestDetailResponse` -- **示例**:(同 A412 详情,status 变为 `Refunding`) +- **示例**:(同 A412 详情,重试成功后 `status=Refunded`) #### 失败响应 @@ -6227,22 +6456,25 @@ SeckillOrderDetailResponse { | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | | 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `RefundFailed` | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 503 | `AFTER_SALES.REFUND_FAILED` | 本次退款或必要库存回补再次失败;申请仍为 `RefundFailed` | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 状态条件更新:`WHERE status = 'RefundFailed' AND merchant_id = currentMerchantId` -- 重试时异步触发 A431 退款 +- 状态条件更新同时校验关联订单 `assigned_merchant_user_id = currentUserId` +- 重试时以 `Refunding` 作为事务内过渡,按申请中尚未完成的退款/库存结果复用同一组业务幂等键;普通库存、秒杀库存和资金入账均不得重复。HTTP 成功时返回 `Refunded`。 +- 全部子操作成功后转为 `Refunded`;任一步再次失败时仍为 `RefundFailed`,并保留安全错误码供排查。 #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:发布 `AfterSalesRefundRetriedIntegrationEvent` -- 外部依赖:PostgreSQL + Payment(A431) +- 事件:重试动作本身不发布集成事件;最终只按结果发布 `RefundCompletedIntegrationEvent` 或由 AfterSales 形成 `RefundFailedIntegrationEvent`。 +- 外部依赖:PostgreSQL + Ordering 售后快照 + Payment 退款应用契约 + Catalog/Seckill 库存回补应用契约 #### 验证场景 -- 正常:商家对 `RefundFailed` 重试 → 状态进入 `Refunding` +- 正常:商家对 `RefundFailed` 重试 → 同步完成并返回 `Refunded` +- 异常:再次失败 → 503 + `AFTER_SALES.REFUND_FAILED`,状态保持 `RefundFailed` - 异常:状态非 `RefundFailed` → 409 + `AFTER_SALES.INVALID_STATUS` --- @@ -6252,14 +6484,14 @@ SeckillOrderDetailResponse { - **模块 / Tag**:Payment - **需求编号**:C08-FR01 / FR02 / FR03 / FR04 / FR05 - **负责人**:张海洋 -- **关联数据表**:DB089(待评审)— `payment_callbacks`、DB085(待评审)— `payments`、DB084(待评审)— `orders` +- **关联数据表**:DB089(待评审)— `payment_callbacks`、DB085(待评审)— `payments`;订单状态通过 Ordering 公开应用契约协作 - **当前状态**:待交叉评审 - **用途**:接收模拟支付渠道的回调,更新支付与订单状态 - **方法与路径**:`POST /api/payment/callbacks` - **operationId**:`Payment_ReceiveCallback` - **请求 Schema**:`PaymentCallbackRequest` - **响应 Schema**:`PaymentCallbackResponse` -- **身份与 Policy**:内部服务级鉴权(Mock Channel Service,签名验证) +- **身份与 Policy**:模拟渠道 HMAC 签名鉴权(无用户 JWT) - **资源归属**:N/A(系统级) - **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 支付回调) @@ -6267,7 +6499,7 @@ SeckillOrderDetailResponse { - **Route 参数**:(无) - **Query 参数**:(无) -- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`X-Callback-Signature: `、`Content-Type: application/json` +- **Header**:`Idempotency-Key: `(必填)、`X-Callback-Signature: `(必填)、`Content-Type: application/json` - **Body**: ```json { @@ -6292,6 +6524,7 @@ SeckillOrderDetailResponse { - **HTTP 状态**:`200 OK` - **响应 Schema**:`PaymentCallbackResponse` +- **状态枚举**:`Processed`(正常处理或幂等重放)/ `RecordedForReconciliation`(迟到成功已登记对账差异) - **示例**: ```json { @@ -6311,10 +6544,8 @@ SeckillOrderDetailResponse { |---|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | | 401 | `PAYMENT.CALLBACK_INVALID_SIGNATURE` | 签名验证失败 | -| 401 | `AUTH.UNAUTHENTICATED` | 缺少服务 JWT | -| 409 | `PAYMENT.CALLBACK_DUPLICATE` | 同一 `callbackId` 重复到达 | -| 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 已取消订单收到迟到成功回调 → 进入对账差异 | -| 422 | `PAYMENT.CALLBACK_AMOUNT_MISMATCH` | 回调金额与订单金额不一致 | +| 409 | `PAYMENT.CALLBACK_AMOUNT_MISMATCH` | 回调金额与订单金额不一致 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一回调幂等键被用于不同请求内容 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 @@ -6323,12 +6554,12 @@ SeckillOrderDetailResponse { - 同事务:支付记录 + 订单状态 + Inbox/处理记录 + Outbox(按 C08-FR05) - 重复回调返回首次结果,不重复记账 - 乱序:按订单当前状态 + 事件时间决定接受/忽略/登记差异 -- 已取消订单收到迟到成功回调 → **进入对账差异**,不得直接改已支付(按 C08 业务规则) +- 已取消订单收到迟到成功回调时,原子登记对账差异并返回 `200`;`PaymentCallbackResponse.status=RecordedForReconciliation`,不得直接把订单改为 `Paid`。已成功受理的回调不以非 2xx 诱发渠道重复重试。 #### 缓存、事件或外部依赖 - 缓存:幂等记录存在 DB(不依赖 Redis) -- 事件:发布 `PaymentCallbackProcessedIntegrationEvent` / `OrderPaidIntegrationEvent`(按结果) +- 事件:仅实际完成支付时发布 `OrderPaidIntegrationEvent`;重复回调和登记对账差异不发布没有消费者的处理事件。 - 外部依赖:PostgreSQL + Ordering 模块 #### 验证场景 @@ -6336,21 +6567,21 @@ SeckillOrderDetailResponse { - 正常:未处理过的回调 → 处理成功 - 重复:相同 `callbackId` → 返回首次结果,不重复处理 - 异常:签名错误 → 401 + `PAYMENT.CALLBACK_INVALID_SIGNATURE` -- 异常:金额不一致 → 422 + `PAYMENT.CALLBACK_AMOUNT_MISMATCH` -- 异常:已取消订单收到 Success 回调 → 进入对账差异状态,订单不直接改 `Paid` +- 异常:金额不一致 → 409 + `PAYMENT.CALLBACK_AMOUNT_MISMATCH` +- 边界:已取消订单收到 Success 回调 → 200 + `RecordedForReconciliation`,订单不直接改 `Paid` --- ### A422 对账批次列表 -- **模块 / Tag**:Reconciliation +- **模块 / Tag**:Payment - **需求编号**:C08-FR06 - **负责人**:张海洋 - **关联数据表**:DB090(待评审)— `reconciliation_batches` - **当前状态**:待交叉评审 - **用途**:分页查询每日对账批次 - **方法与路径**:`GET /api/admin/reconciliation/batches` -- **operationId**:`Reconciliation_ListBatches` +- **operationId**:`Payment_ListReconciliationBatches` - **请求 Schema**:`ListBatchesQuery` - **响应 Schema**:`ReconciliationBatchListResponse` - **身份与 Policy**:JWT Bearer + `AdminOnly` @@ -6430,14 +6661,14 @@ SeckillOrderDetailResponse { ### A423 对账批次详情 -- **模块 / Tag**:Reconciliation +- **模块 / Tag**:Payment - **需求编号**:C08-FR06 - **负责人**:张海洋 - **关联数据表**:DB090(待评审)— `reconciliation_batches`、DB091(待评审)— `reconciliation_differences` - **当前状态**:待交叉评审 - **用途**:查询单批对账详情 - **方法与路径**:`GET /api/admin/reconciliation/batches/{batchId}` -- **operationId**:`Reconciliation_GetBatch` +- **operationId**:`Payment_GetReconciliationBatch` - **请求 Schema**:(无) - **响应 Schema**:`ReconciliationBatchDetailResponse` - **身份与 Policy**:JWT Bearer + `AdminOnly` @@ -6472,7 +6703,7 @@ SeckillOrderDetailResponse { "differenceCount": 2, "status": "HasDifferences", "summary": { - "byType": { "MissingPayment": 1, "AmountMismatch": 1 } + "byType": { "PaymentSucceededOrderNotUpdated": 1, "RefundAmountMismatch": 1 } }, "createdAt": "2026-07-23T01:00:00Z" } @@ -6485,7 +6716,7 @@ SeckillOrderDetailResponse { |---|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Admin | -| 404 | `RECONCILIATION.BATCH_NOT_FOUND` | 批次不存在 | +| 404 | `PAYMENT.RECONCILIATION_BATCH_NOT_FOUND` | 批次不存在 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 @@ -6501,20 +6732,20 @@ SeckillOrderDetailResponse { #### 验证场景 - 正常:管理员查询 → 返回详情 -- 异常:批次不存在 → 404 + `RECONCILIATION.BATCH_NOT_FOUND` +- 异常:批次不存在 → 404 + `PAYMENT.RECONCILIATION_BATCH_NOT_FOUND` --- ### A424 差异列表 -- **模块 / Tag**:Reconciliation +- **模块 / Tag**:Payment - **需求编号**:C08-FR07 / FR08 - **负责人**:张海洋 - **关联数据表**:DB091(待评审)— `reconciliation_differences` - **当前状态**:待交叉评审 - **用途**:分页查询某批次的所有差异 - **方法与路径**:`GET /api/admin/reconciliation/batches/{batchId}/differences` -- **operationId**:`Reconciliation_ListDifferences` +- **operationId**:`Payment_ListReconciliationDifferences` - **请求 Schema**:`ListDifferencesQuery` - **响应 Schema**:`ReconciliationDifferenceListResponse` - **身份与 Policy**:JWT Bearer + `AdminOnly` @@ -6525,7 +6756,7 @@ SeckillOrderDetailResponse { - **Route 参数**:`batchId`(UUID) - **Query 参数**: - - `type`(可选):`MissingPayment` / `AmountMismatch` / `DuplicateRefund` / `LateCallback` 等 + - `type`(可选):从下方已确认差异类型中选择 - `status`(可选):`Pending` / `InProgress` / `Resolved` - 标准分页 + 排序 - **Header**:`Authorization: Bearer ` @@ -6548,7 +6779,7 @@ SeckillOrderDetailResponse { { "differenceId": "...", "batchId": "...", - "type": "LateCallback", + "type": "LateSuccessCallback", "orderId": "3f0ed9a9-...", "paymentId": "8d2e9d11-...", "callbackId": "5a8e...", @@ -6571,16 +6802,18 @@ SeckillOrderDetailResponse { |---|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Admin | -| 404 | `RECONCILIATION.BATCH_NOT_FOUND` | 批次不存在 | +| 404 | `PAYMENT.RECONCILIATION_BATCH_NOT_FOUND` | 批次不存在 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 差异类型至少识别(按 C08-FR07): - - `LateCallback`:已取消订单收到迟到成功回调 - - `MissingPayment`:订单已支付但缺支付流水 - - `AmountMismatch`:支付/退款金额不一致 - - `DuplicateRefund`:退款重复 +- 差异类型至少识别: + - `PaymentSucceededOrderNotUpdated`:支付成功但订单未更新 + - `OrderPaidPaymentMissing`:订单已支付但缺支付记录或流水 + - `LateSuccessCallback`:已取消订单收到迟到成功回调 + - `RefundSucceededWalletCreditMissing`:售后退款成功但小金库未入账 + - `DuplicateWalletCredit`:同一退款发生重复入账 + - `RefundAmountMismatch`:退款记录、钱包流水或入账金额不一致 - 状态管理(按 C08-FR08):`Pending` / `InProgress` / `Resolved` #### 缓存、事件或外部依赖 @@ -6592,20 +6825,20 @@ SeckillOrderDetailResponse { #### 验证场景 - 正常:管理员查询 → 返回差异列表 -- 异常:批次不存在 → 404 + `RECONCILIATION.BATCH_NOT_FOUND` +- 异常:批次不存在 → 404 + `PAYMENT.RECONCILIATION_BATCH_NOT_FOUND` --- ### A425 差异处理 -- **模块 / Tag**:Reconciliation +- **模块 / Tag**:Payment - **需求编号**:C08-FR08 - **负责人**:张海洋 - **关联数据表**:DB091(待评审)— `reconciliation_differences` - **当前状态**:待交叉评审 - **用途**:管理员处理对账差异并标记状态 - **方法与路径**:`POST /api/admin/reconciliation/differences/{differenceId}/process` -- **operationId**:`Reconciliation_ProcessDifference` +- **operationId**:`Payment_ProcessReconciliationDifference` - **请求 Schema**:`ProcessDifferenceRequest` - **响应 Schema**:`ReconciliationDifferenceDetailResponse` - **身份与 Policy**:JWT Bearer + `AdminOnly` @@ -6626,7 +6859,7 @@ SeckillOrderDetailResponse { ``` - **校验规则**: - `differenceId` 必填 - - `action` 枚举:`MarkInProgress` / `MarkResolved` / `MarkIgnored` + - `action` 枚举:`MarkInProgress` / `MarkResolved` - 当前状态必须为 `Pending`(`MarkInProgress`)或 `InProgress`(`MarkResolved`) - `resolutionNote` 必填,≤ 1000 字 - `Idempotency-Key` 必填 @@ -6643,7 +6876,7 @@ SeckillOrderDetailResponse { "data": { "differenceId": "...", "batchId": "...", - "type": "LateCallback", + "type": "LateSuccessCallback", "status": "Resolved", "resolutionNote": "确认为模拟渠道测试回调,已通知商家", "resolvedAt": "2026-07-23T03:00:00Z", @@ -6659,8 +6892,8 @@ SeckillOrderDetailResponse { | 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Admin | -| 404 | `RECONCILIATION.DIFFERENCE_NOT_FOUND` | 差异不存在 | -| 409 | `RECONCILIATION.DIFFERENCE_ALREADY_PROCESSED` | 状态非法 | +| 404 | `PAYMENT.RECONCILIATION_DIFFERENCE_NOT_FOUND` | 差异不存在 | +| 409 | `PAYMENT.RECONCILIATION_DIFFERENCE_ALREADY_PROCESSED` | 状态非法 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | @@ -6673,111 +6906,17 @@ SeckillOrderDetailResponse { #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:发布 `ReconciliationDifferenceProcessedIntegrationEvent` +- 事件:处理结果保存在对账差异记录中;本期没有已确认的跨模块消费者,不单独发布集成事件。 - 外部依赖:PostgreSQL #### 验证场景 - 正常:管理员 MarkResolved → 状态进入 `Resolved` -- 异常:状态已为 `Resolved` → 409 + `RECONCILIATION.DIFFERENCE_ALREADY_PROCESSED` +- 异常:状态已为 `Resolved` → 409 + `PAYMENT.RECONCILIATION_DIFFERENCE_ALREADY_PROCESSED` - 异常:买家调用 → 403 + `AUTH.FORBIDDEN` --- -### A431 模拟退款 - -- **模块 / Tag**:Payment -- **需求编号**:M10-FR07 -- **负责人**:张海洋 -- **关联数据表**:DB088(待评审)— `refunds`、DB081(待评审)— `wallets`、DB083(待评审)— `wallet_ledgers` -- **当前状态**:待交叉评审 -- **用途**:将售后金额幂等退回买家小金库 -- **方法与路径**:`POST /api/after-sales/requests/{requestId}/refund` -- **operationId**:`Refund_Create` -- **请求 Schema**:`CreateRefundRequest` -- **响应 Schema**:`RefundDetailResponse` -- **身份与 Policy**:JWT Bearer + `MerchantOnly`(系统内部调用) -- **资源归属**:当前 merchant 授权范围内申请 -- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 退款入账) - -#### 请求 - -- **Route 参数**:`requestId`(UUID) -- **Query 参数**:(无) -- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` -- **Body**: -```json -{ - "expectedAmount": 100.00, - "currency": "CNY" -} -``` -- **校验规则**: - - 申请归属当前 merchant(或系统内部) - - 申请状态必须为 `Refunding`(已通过 A416 / A417 触发) - - `expectedAmount` 必须等于申请计算金额 - - `Idempotency-Key` 必填 - - 同一 Key + 相同 amount → 返回首次结果 - -#### 成功响应 - -- **HTTP 状态**:`200 OK` -- **响应 Schema**:`RefundDetailResponse` -- **示例**: -```json -{ - "code": "success", - "message": "ok", - "data": { - "refundId": "...", - "requestId": "b9c1...", - "buyerId": "...", - "amount": 100.00, - "currency": "CNY", - "status": "Succeeded", - "walletBalanceAfter": 200.50, - "createdAt": "2026-07-23T09:00:00Z", - "succeededAt": "2026-07-23T09:00:01Z" - } -} -``` - -#### 失败响应 - -| HTTP 状态 | 业务错误码 | 触发条件 | -|---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | -| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | -| 403 | `AUTH.FORBIDDEN` | 角色非 Merchant | -| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | -| 409 | `AFTER_SALES.INVALID_STATUS` | 申请状态非 `Refunding` | -| 409 | `PAYMENT.AMOUNT_MISMATCH` | `expectedAmount` 与申请计算金额不一致 | -| 409 | `PAYMENT.REFUND_FAILED` | 退款执行失败(写流水失败等) | -| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | -| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | - -#### 业务规则与并发 - -- 钱包入账 + 退款记录 + 钱包流水 + 申请状态更新 **同事务**(按架构 §7.2) -- 退款成功后订单**保持原核心状态**(按 M10 业务规则 + 架构 §7.6) -- 退款流水必须纳入 C08 每日对账(按 M10 业务规则) -- AfterSales 不直接改钱包数据;通过 Payment 公开应用能力**幂等**退回(按架构 §7.2) - -#### 缓存、事件或外部依赖 - -- 缓存:幂等记录存在 DB -- 事件:发布 `RefundCompletedIntegrationEvent` -- 外部依赖:PostgreSQL + AfterSales 模块 - -#### 验证场景 - -- 正常:审核通过触发 → 退款成功,余额增加 -- 重复:相同 Idempotency-Key → 返回首次结果,不重复入账 -- 异常:金额不一致 → 409 + `PAYMENT.AMOUNT_MISMATCH` -- 异常:写流水失败 → 409 + `PAYMENT.REFUND_FAILED`,申请状态回滚 - ---- - ### A432 退款详情 - **模块 / Tag**:Payment @@ -6787,11 +6926,11 @@ SeckillOrderDetailResponse { - **当前状态**:待交叉评审 - **用途**:查询单笔退款详情 - **方法与路径**:`GET /api/refunds/{refundId}` -- **operationId**:`Refund_Get` +- **operationId**:`Payment_GetRefund` - **请求 Schema**:(无) - **响应 Schema**:`RefundDetailResponse` - **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` -- **资源归属**:当前 buyerId 退款 / 当前 merchant 授权范围 +- **资源归属**:买家只看本人退款;商家只看关联售后订单分配给当前账号的退款 - **幂等要求**:GET 天然幂等 #### 请求 @@ -6802,12 +6941,26 @@ SeckillOrderDetailResponse { - **Body**:(无) - **校验规则**: - `refundId` UUID 格式 - - 资源归属当前 buyerId 或当前 merchant + - 资源归属当前买家,或关联售后订单分配给当前商家账号 #### 成功响应 - **HTTP 状态**:`200 OK` -- **响应 Schema**:`RefundDetailResponse`(同 A431) +- **响应 Schema**:`RefundDetailResponse`,独立于内部 `RefundResult`: + +```text +RefundDetailResponse { + refundId: uuid + requestId: uuid + paymentId: uuid + amount: decimal + currency: "CNY" + status: "Succeeded" | "Failed" + createdAt: string + completedAt: string? + failureMessage: string? +} +``` #### 失败响应 @@ -6844,18 +6997,18 @@ SeckillOrderDetailResponse { - **当前状态**:待交叉评审 - **用途**:分页查询退款记录 - **方法与路径**:`GET /api/refunds` -- **operationId**:`Refund_List` +- **operationId**:`Payment_ListRefunds` - **请求 Schema**:`ListRefundsQuery` - **响应 Schema**:`RefundListResponse` - **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` -- **资源归属**:当前 buyerId 退款 / 当前 merchant 授权范围 +- **资源归属**:买家只看本人退款;商家只看关联售后订单分配给当前账号的退款 - **幂等要求**:GET 天然幂等 #### 请求 - **Route 参数**:(无) - **Query 参数**: - - `status`(可选):`Pending` / `Succeeded` / `Failed` + - `status`(可选):`Succeeded` / `Failed` - `createdFrom` / `createdTo`(可选):时间范围 - 标准分页 + 排序 - **Header**:`Authorization: Bearer ` @@ -6903,7 +7056,7 @@ SeckillOrderDetailResponse { #### 业务规则与并发 -- 买家视图按 `buyerId` 过滤;商家视图按授权范围过滤 +- 买家视图按 `buyerId` 过滤;商家视图按关联订单 `assignedMerchantUserId` 过滤 - 默认排序 `createdAt desc, refundId desc` #### 缓存、事件或外部依赖 @@ -6919,9 +7072,118 @@ SeckillOrderDetailResponse { --- +### A434 买家提交退货/寄回信息 + +- **模块 / Tag**:AfterSales +- **需求编号**:M10-FR11 +- **负责人**:张海洋 +- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_status_histories`;退货信息作为申请从属数据由数据库设计确认是否单独建表 +- **当前状态**:待交叉评审 +- **用途**:买家提交退货的物流单号与快递公司(用于 `ReturnAndRefund` 类型的申请) +- **方法与路径**:`POST /api/after-sales/requests/{requestId}/return-info` +- **operationId**:`AfterSales_SubmitReturnInfo` +- **请求 Schema**:`SubmitReturnInfoRequest` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **身份与 Policy**:JWT Bearer + `BuyerOnly` +- **资源归属**:当前 buyerId 申请 +- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) + +#### 请求 + +- **Route 参数**:`requestId`(UUID) +- **Query 参数**:(无) +- **Header**:`Authorization: Bearer `、`Idempotency-Key: `(必填)、`Content-Type: application/json` +- **Body**: +```json +{ + "carrier": "SF", + "trackingNumber": "SF1234567890", + "shippedAt": "2026-07-23T08:30:00Z", + "note": "外包装完好" +} +``` +- **校验规则**: + - 申请归属当前 buyerId + - 申请类型必须为 `ReturnAndRefund` + - 申请状态必须为 `PendingReturn`(商家审核通过后、待退货) + - `carrier` 必填,1~50 字;本期不接入真实物流平台,允许填写“其他”及实际承运方名称 + - `trackingNumber` 必填,1~50 字符 + - `shippedAt` 选填,ISO 8601 UTC 且不得晚于当前时间;缺省时使用服务端提交时间 + - `note` 选填,≤ 500 字 + - `Idempotency-Key` 必填 + +#### 成功响应 + +- **HTTP 状态**:`200 OK` +- **响应 Schema**:`AfterSalesRequestDetailResponse` +- **示例**: +```json +{ + "code": "success", + "message": "ok", + "data": { + "requestId": "b9c1...", + "orderId": "3f0ed9a9-...", + "orderItemId": "5a7c...", + "type": "ReturnAndRefund", + "quantity": 1, + "calculatedAmount": 100.00, + "currency": "CNY", + "status": "PendingReceipt", + "returnInfo": { + "carrier": "SF", + "trackingNumber": "SF1234567890", + "shippedAt": "2026-07-23T08:30:00Z", + "note": "外包装完好" + }, + "createdAt": "2026-07-23T08:00:00Z", + "timeline": [ + { "status": "PendingReview", "at": "2026-07-23T08:00:00Z", "actor": "buyer" }, + { "status": "PendingReturn", "at": "2026-07-23T08:10:00Z", "actor": "merchant" }, + { "status": "PendingReceipt", "at": "2026-07-23T08:30:00Z", "actor": "buyer" } + ] + } +} +``` + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 字段错误(trackingNumber 格式、shippedAt 未来时间) | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | +| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | +| 404 | `RESOURCE.NOT_FOUND` | 申请不存在或非本人 | +| 409 | `AFTER_SALES.INVALID_STATUS` | 申请状态非 `PendingReturn` | +| 409 | `AFTER_SALES.WRONG_TYPE` | 申请类型非 `ReturnAndRefund` | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | + +#### 业务规则与并发 + +- 状态条件更新:`WHERE status = 'PendingReturn' AND buyer_id = currentBuyerId AND type = 'ReturnAndRefund'` +- 提交后状态变为 `PendingReceipt` +- 同一包裹可以承载同一订单的多笔退货申请,不对快递单号施加不符合现实的全局唯一约束;重复提交由申请状态和 `Idempotency-Key` 控制。 +- 领域状态历史记录提交人、必要退货摘要和状态变化 +- 商家在 A417 确认收货后 → 触发 Payment 退款应用契约 + +#### 缓存、事件或外部依赖 + +- 缓存:不缓存 +- 事件:发布 `AfterSalesReturnInfoSubmittedIntegrationEvent` +- 外部依赖:PostgreSQL + +#### 验证场景 + +- 正常:买家提交退货物流 → 状态进入 `PendingReceipt` +- 重复:相同 Idempotency-Key → 返回首次结果,不重复写入 +- 异常:他人申请 → 404 + `RESOURCE.NOT_FOUND` +- 异常:申请类型为 `RefundOnly` → 409 + `AFTER_SALES.WRONG_TYPE` +- 异常:状态非 `PendingReturn` → 409 + `AFTER_SALES.INVALID_STATUS` + --- -> 来源:[interface-lhc.md](interface/interface-lhc.md)。HTTP 主体能力已覆盖,仍须补正式 OpenAPI、DBxxx 和跨模块集成事件契约。 +> 来源:[`interface-lhc.md`](interface/interface-lhc.md)。HTTP 主体能力已覆盖;Messaging 集成事件和 SignalR 契约已登记,仍待 DB101~DB120、来源模块评审与真实 OpenAPI。 ### A501 查询本人消息列表 @@ -6929,13 +7191,13 @@ SeckillOrderDetailResponse { - 需求编号:X03-FR03、X03-FR10、X03-FR11 - 负责人:罗皓晨 - 关联数据表:待 `database-lhc.md` 确认 -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:按创建时间倒序分页查询当前用户自己的消息。 - 方法与路径:`GET /api/messages` - operationId:`Messaging_ListMessages` - 请求 Schema:Query 参数 - 响应 Schema:`MessageListResponse` -- 身份与 Policy:有效 JWT;仅买家或商家 +- 身份与 Policy:`BuyerOnly / MerchantOnly` - 资源归属:接收用户必须等于当前认证用户;服务端不接收 `userId` - 幂等要求:只读接口,天然幂等 @@ -7030,13 +7292,13 @@ SeckillOrderDetailResponse { - 需求编号:X03-FR04、X03-FR11 - 负责人:罗皓晨 - 关联数据表:待 `database-lhc.md` 确认 -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:查询当前用户拥有的一条完整站内消息。 - 方法与路径:`GET /api/messages/{messageId}` - operationId:`Messaging_GetMessage` - 请求 Schema:Route 参数 - 响应 Schema:`MessageDetailResponse` -- 身份与 Policy:有效 JWT;仅买家或商家 +- 身份与 Policy:`BuyerOnly / MerchantOnly` - 资源归属:消息接收用户必须等于当前认证用户 - 幂等要求:只读接口,天然幂等 @@ -7109,13 +7371,13 @@ SeckillOrderDetailResponse { - 需求编号:X03-FR05、C06-FR05 - 负责人:罗皓晨 - 关联数据表:待 `database-lhc.md` 确认 -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:为消息入口角标、首次连接和断线重连补偿提供当前未读总数。 - 方法与路径:`GET /api/messages/unread-count` - operationId:`Messaging_GetUnreadCount` - 请求 Schema:无 - 响应 Schema:`UnreadMessageCountResponse` -- 身份与 Policy:有效 JWT;仅买家或商家 +- 身份与 Policy:`BuyerOnly / MerchantOnly` - 资源归属:只统计当前认证用户 - 幂等要求:只读接口,天然幂等 @@ -7171,13 +7433,13 @@ SeckillOrderDetailResponse { - 需求编号:X03-FR06 - 负责人:罗皓晨 - 关联数据表:待 `database-lhc.md` 确认 -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:幂等地记录当前用户一条消息的首次已读时间。 - 方法与路径:`POST /api/messages/{messageId}/read` - operationId:`Messaging_MarkMessageRead` - 请求 Schema:Route 参数 - 响应 Schema:`MarkMessageReadResponse` -- 身份与 Policy:有效 JWT;仅买家或商家 +- 身份与 Policy:`BuyerOnly / MerchantOnly` - 资源归属:消息接收用户必须等于当前认证用户 - 幂等要求:同一用户对同一消息重复调用返回相同首次 `readAt` @@ -7238,13 +7500,13 @@ SeckillOrderDetailResponse { - 需求编号:X03-FR07 - 负责人:罗皓晨 - 关联数据表:待 `database-lhc.md` 确认 -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:将操作开始时当前用户已经存在的未读消息批量标记为已读。 - 方法与路径:`POST /api/messages/read-all` - operationId:`Messaging_MarkAllMessagesRead` - 请求 Schema:无 - 响应 Schema:`MarkAllMessagesReadResponse` -- 身份与 Policy:有效 JWT;仅买家或商家 +- 身份与 Policy:`BuyerOnly / MerchantOnly` - 资源归属:只更新当前认证用户 - 幂等要求:没有新的未读消息时重复调用返回 `markedCount = 0` @@ -7302,7 +7564,7 @@ SeckillOrderDetailResponse { - 需求编号:C10-FR06 - 负责人:罗皓晨 - 关联数据表:无 -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:供 Compose、Nginx 和运维检查 API 进程能否响应。 - 方法与路径:`GET /health/live` - operationId:`Infrastructure_GetLiveness` @@ -7360,7 +7622,7 @@ SeckillOrderDetailResponse { - 需求编号:C10-FR06、C10-FR12 - 负责人:罗皓晨 - 关联数据表:无 -- 当前状态:待评审 +- 当前状态:待交叉评审 - 用途:判断实例是否具备接收业务流量的必要依赖。 - 方法与路径:`GET /health/ready` - operationId:`Infrastructure_GetReadiness` @@ -7445,11 +7707,126 @@ SeckillOrderDetailResponse { - 未启用 RabbitMQ 或对象存储时不把它们报告为失败。 - 两个实例使用同一契约并返回不同 `instanceId`。 -## 四、非 HTTP 契约与附录 +## 四、非 HTTP 契约与模块协作 + +### 4.1 取消编号与历史保留 + +| 编号 | 当前类型 | 处理结论 | 替代契约 | +|---|---|---|---| +| A229 | 已取消 HTTP | 不再建立秒杀订单列表端点 | A302,使用 `orderType=Seckill` 或 `seckillActivityId` | +| A230 | 已取消 HTTP | 不再建立秒杀订单详情端点 | A303,返回可选秒杀活动与价格快照 | +| A418 | 已取消 HTTP | 不再单独查询“审核日志” | A414 统一返回售后领域状态时间线 | +| A431 | 已取消 HTTP | 不再建立公开退款命令端点 | Payment 退款应用契约,见 4.2.1 | + +以上编号均不得重新分配。OpenAPI 只生成 103 个有效 HTTP 契约,不生成这四个端点。 + +### 4.2 模块间公开应用契约 + +模块间不得直接引用对方内部 DbContext、仓储或数据表。当前接口链路至少需要以下公开应用边界,具体 C# 命名可在代码脚手架建立时按《命名规范》落地,但输入、输出与所有权不得改变: + +| 提供模块 | 使用方 | 公开能力 | 最小输入与输出 | 事实所有权 | +|---|---|---|---|---| +| Identity | Ordering | 校验本人地址并返回地址快照 | `buyerId + addressId -> AddressSnapshot` | Identity 拥有地址;Ordering 只保存下单快照 | +| Identity | Ordering | 解析并校验订单处理商家 | 普通订单必须解析唯一且启用的默认商家运营账号;秒杀订单校验活动创建人仍可用 | Identity 拥有账号与默认标记;Ordering 保存 `assignedMerchantUserId` 快照 | +| Identity | Review | 返回当前买家的安全展示名 | `buyerId -> maskedDisplayName`,无展示名时回退到自动用户名的脱敏值 | Identity 拥有用户资料;Review 只在评价创建时保存展示名快照 | +| Ordering、AfterSales、Seckill | Identity | 校验非默认商家能否禁用 | `merchantUserId -> hasBlockingWork + reason`,覆盖待履约/售后窗口与申请/未结束活动 | 各业务模块拥有工作状态;Identity 只在全部允许时改变账号状态 | +| Catalog | Cart、Ordering | 查询可售商品快照、条件扣减及幂等回补普通库存 | 商品/订单项/数量/原因/幂等键 -> 商品快照或库存结果 | Catalog 拥有商品与普通库存 | +| Catalog | Seckill | 发布活动时原子划转普通库存到秒杀配额 | 商品、活动、数量、幂等键 -> 划转结果 | Catalog 扣减普通库存;Seckill 拥有已划转配额 | +| Cart | Ordering | 读取本人已选条目并在下单成功后清理 | `buyerId + cartItemIds -> CheckoutItems` | Cart 拥有购物车 | +| Ordering | Payment | 查询支付快照并按状态条件标记已支付 | `buyerId + orderId -> amount + currency + assignedMerchantUserId + order status` | Ordering 拥有订单状态与处理商家归属 | +| Ordering | AfterSales | 在共享事务中锁定订单履约变更并返回售后校验快照 | `orderId + buyerId + orderItemId -> locked order status + 实付 + orderType + seckillActivityId + assignedMerchantUserId` | Ordering 拥有订单行和履约状态;行锁保持到调用方事务提交 | +| AfterSales | Ordering | 查询发货阻断与剩余可履约数量 | `orderId -> hasBlockingRequest + item[{orderItemId, refundedQuantity}]` | AfterSales 拥有申请状态和已退款数量;Ordering 计算并固化实际发货数量 | +| Ordering | Review | 校验评价资格并返回订单项快照 | `buyerId + orderItemId -> Completed + product/order snapshot` | Ordering 拥有订单完成与订单项归属事实 | +| Ordering | Catalog | 判断商品是否存在历史订单关联 | `productId -> hasHistoricalOrders` | Ordering 拥有历史订单关联;Catalog 据此保护删除 | +| Ordering | Seckill | 在秒杀库存条件扣减成功后创建共享订单事实 | 活动/商品/买家/地址/价格快照 -> 订单结果 | Ordering 是唯一订单事实来源 | +| Seckill | Ordering、AfterSales | 幂等回补原活动库存并释放买家限购额度 | 活动/订单项/买家/数量/原因/幂等键 -> 回补结果 | Seckill 拥有活动库存与买家配额 | +| Payment | AfterSales | 幂等退款入账 | `CreateRefundCommand -> RefundResult` | Payment 拥有钱包、退款和资金流水;AfterSales 拥有申请状态 | +| Ordering、Payment、AfterSales | Messaging | 发布带明确接收账号的已发生事实 | 标准事件 Envelope + `recipients[]` + 最小资源快照 | 来源模块决定业务事实和接收人;Messaging 只持久化与推送 | -### 4.1 Messaging 公共 Schema 与枚举 +本期采用单店 B2C,不建设商户租户、拆单、结算或商品归属模型。Identity 的默认商家标记最多一个,并由启动配置/种子数据保证存在一个启用账号;默认商家不能通过 A016 直接禁用,非默认商家存在阻断工作时也拒绝禁用,本期不自动重新分配。普通订单使用默认账号,秒杀订单使用活动的 `createdByMerchantUserId`;Ordering 持久化 `assignedMerchantUserId`。商家订单、售后和消息必须精确校验该账号,不得向全部 Merchant 角色广播。 -#### `MessageType` +发货与提交售后不得采用“先查后改”。A307 与 A412 都必须先通过 Ordering 应用契约在当前共享事务中锁定同一 `orders` 行并复核最新状态,锁保持到各自业务写入提交;A307 随后读取 AfterSales 履约快照。发货先提交时售后按已发货规则重算,售后先提交时处理中申请阻断发货、已退款数量从实际发货数量中扣除。该协作不新增 HTTP 接口或订单核心状态。 + +普通库存、秒杀配额、限购额度、订单、退款和售后均在同一 PostgreSQL 部署内,但各模块只通过上述公开应用契约写自己拥有的表;Redis 不参与唯一正确性。需要跨模块原子提交的用例由应用层编排受控共享事务,不允许调用方直接取得其他模块 DbContext 或仓储。 + +#### 4.2.1 A431 历史取消编号的替代退款应用契约 + + +> A431 是已取消的 HTTP 历史追踪编号,不再分配路径或 `operationId`。以下内容定义其替代方案 `IRefundService`,该应用契约不使用 Axxx;A432/A433 仍是外部查询接口。 + +- **模块 / Tag**:Payment(应用服务层) +- **需求编号**:M10-FR07 +- **负责人**:张海洋 +- **关联数据表**:DB088(待评审)— `refunds`、DB081(待评审)— `wallets`、DB083(待评审)— `wallet_ledgers` +- **当前状态**:已设计(内部契约) +- **用途**:由 AfterSales 模块审核通过后(来源 A416 / A417 / A419)调用,将售后金额幂等退回买家小金库 +- **调用方式**:进程内应用服务调用(**非 HTTP**) +- **应用服务签名**:`IRefundService.CreateRefundAsync(CreateRefundCommand command, CancellationToken cancellationToken) → RefundResult` +- **命令 Schema**:`CreateRefundCommand`(公开应用能力) +- **结果 Schema**:`RefundResult` +- **身份与 Policy**:内部模块信任(无 Policy) +- **资源归属**:AfterSales 先校验申请状态与归属;Payment 再按本模块持有的原支付事实校验买家、币种和可退款上限 +- **幂等要求**:**必须支持 `IdempotencyKey`**(同 1.12.1 退款入账) + +##### 命令输入 + +- **目标申请**:`requestId`(UUID) +- **原支付**:`paymentId`(UUID) +- **收款买家**:`buyerId`(UUID) +- **期待金额**:`expectedAmount`(decimal) +- **币种**:`currency`(默认 `CNY`) +- **幂等键**:`IdempotencyKey`(必填,UUID) + +##### 校验规则 + +- AfterSales 调用前必须已把申请推进到 `Refunding`;Payment 不读取或修改 AfterSales 内部表。 +- `paymentId`、`buyerId` 与 Payment 持有的原支付事实必须一致,`expectedAmount` 不得超过剩余可退款金额。 +- 同一 `IdempotencyKey` + 相同 `amount` → 返回首次结果 +- 同一 `IdempotencyKey` + 不同 `amount` → 抛 `IdempotencyKeyReusedException` + +##### 结果输出 + +- **RefundResult**: + - `refundId`:string + - `requestId`:string + - `buyerId`:string + - `amount`:decimal + - `currency`:string + - `status`:`Succeeded` / `Failed` + - `walletBalanceAfter`:decimal + +##### 业务规则与并发 + +- Payment 只操作本模块的钱包、退款、流水和 Outbox;由 AfterSales 编排时,这些写入加入同一受控 PostgreSQL 事务,但 Payment 不直接更新 `after_sales_requests`。 +- AfterSales 拥有申请状态并协调必要的库存回补:全部子操作成功后转为 `Refunded`;事务失败后用独立失败记录保留 `RefundFailed`,供 A419 安全重试。 +- 退款成功后订单**保持原核心状态**(按 M10 业务规则 + 架构 §7.6) +- 退款流水必须纳入 C08 每日对账(按 M10 业务规则) +- AfterSales 不直接改钱包数据;通过 Payment 公开应用能力**幂等**退回(按架构 §7.2) + +##### 缓存、事件或外部依赖 + +- 缓存:幂等记录存在 DB +- 事件:成功发布一次只包含申请买家接收人的 `RefundCompletedIntegrationEvent`;失败由 AfterSales 保存 `RefundFailed` 状态并形成包含买家与订单处理商家的通知事实 +- 外部依赖:PostgreSQL;AfterSales 仅通过本公开应用契约调用 + +##### 验证场景 + +- 正常:审核通过触发 → 退款成功,余额增加 +- 重复:相同 IdempotencyKey → 返回首次结果,不重复入账 +- 异常:金额不一致 → Payment 事务不入账,AfterSales 保存 `RefundFailed` +- 异常:写流水失败 → Payment 事务整体回滚,AfterSales 保存 `RefundFailed` + +##### 调用方 + +- A416 仅退款审核通过 → AfterSales 进程内调用 +- A417 商家确认收到退货 → AfterSales 进程内调用 +- A419 退款失败重试 → AfterSales 进程内调用 + +--- + +### 4.3 Messaging Schema、集成事件与 SignalR + +#### 4.3.1 `MessageType` 受控字符串枚举: @@ -7462,25 +7839,29 @@ SeckillOrderDetailResponse { | `OrderCompleted` | 订单完成 | | `AfterSalesSubmitted` | 售后申请已提交,提醒指定商家处理 | | `AfterSalesReviewed` | 售后审核完成,通知买家结果 | +| `AfterSalesPendingReturn` | 退货申请审核通过,提醒买家寄回商品 | +| `AfterSalesReturnSubmitted` | 买家已提交寄回信息,提醒指定商家处理 | +| `RefundSucceeded` | 售后退款成功并已退回小金库 | +| `RefundFailed` | 售后退款失败,可在售后详情查看或重试 | 后续新增消息类型属于兼容性扩展。客户端必须对未知值使用“业务通知”兜底展示,不能因此白屏。 -#### `RelatedResourceType` +#### 4.3.2 `RelatedResourceType` 受控字符串枚举:`Order`、`Payment`、`AfterSales`。 -#### `MessageAction` +#### 4.3.3 `MessageAction` 安全跳转描述,不包含前端内部路由字符串: | 字段 | 类型 | 必需 | 说明 | |---|---|---:|---| -| `target` | string | 是 | `OrderDetail` 或 `AfterSalesDetail` | +| `target` | string | 是 | `OrderDetail`、`PaymentDetail` 或 `AfterSalesDetail` | | `resourceId` | UUID | 是 | 目标业务资源 ID;进入目标页面时仍须重新鉴权 | 当关联资源不存在、已归档或当前用户已无权访问时,`action` 返回 `null`。 -#### `MessageSummaryResponse` +#### 4.3.4 `MessageSummaryResponse` | 字段 | 类型 | 可空 | 说明 | |---|---|---:|---| @@ -7495,7 +7876,7 @@ SeckillOrderDetailResponse { | `readAt` | UTC 时间 | 是 | 首次标记已读时间 | | `createdAt` | UTC 时间 | 否 | 消息创建时间 | -#### `MessageDetailResponse` +#### 4.3.5 `MessageDetailResponse` 包含 `MessageSummaryResponse` 的全部字段,并增加: @@ -7505,14 +7886,50 @@ SeckillOrderDetailResponse { 正文和摘要是消息创建时保存的历史快照,不随商品名称、订单展示文本或用户昵称变化。 -### 4.2 SignalR 实时契约(不占 Axxx) +#### 4.3.6 业务模块到 Messaging 的集成事件 -#### Hub 连接 +Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封向 Messaging 提交已经发生且已提交的业务事实: + +| 字段 | 类型 | 必需 | 说明 | +|---|---|---:|---| +| `messageId` | UUID | 是 | 全局唯一业务消息 ID,也是 Inbox 第一去重键 | +| `type` | string | 是 | 下表白名单值 | +| `schemaVersion` | string | 是 | 当前固定 `v1` | +| `occurredAt` | UTC 时间 | 是 | 业务事实发生时间 | +| `recipients` | array | 是 | 至少 1 项;每项包含 `userId` 和 `role`(`Buyer`/`Merchant`),明确列出接收账号,不允许按全角色广播 | +| `aggregateId` | UUID | 是 | 订单、支付或售后申请 ID | +| `correlationId` | string | 是 | 跨请求与消息链路追踪标识,与当前 Trace 关联但不暴露内部实现 | +| `data` | object | 是 | 仅包含生成标题、摘要、正文与安全跳转所需的最小业务快照 | + +事件登记与消息映射: + +| 来源模块 | `type` | Routing Key | 精确接收账号来源 | `MessageType` | `data` 最小字段 | 默认跳转 | +|---|---|---|---|---|---|---| +| Ordering | `OrderCreatedIntegrationEvent` | `ordering.order.created.v1` | 订单 `buyerId` | `OrderCreated` | `orderId`、`orderNo`、`totalAmount` | `OrderDetail` | +| Ordering | `OrderCancelledIntegrationEvent` | `ordering.order.cancelled.v1` | 订单 `buyerId` | `OrderCancelled` | `orderId`、`orderNo`、`cancelReason` | `OrderDetail` | +| Payment | `OrderPaidIntegrationEvent` | `payment.order.paid.v1` | 订单 `buyerId`、`assignedMerchantUserId` | `PaymentSucceeded` | `orderId`、`paymentId`、`amount` | 买家 `PaymentDetail`;商家 `OrderDetail` | +| Ordering | `OrderShippedIntegrationEvent` | `ordering.order.shipped.v1` | 订单 `buyerId` | `OrderShipped` | `orderId`、`orderNo`、`shippedAt` | `OrderDetail` | +| Ordering | `OrderCompletedIntegrationEvent` | `ordering.order.completed.v1` | 订单 `buyerId` | `OrderCompleted` | `orderId`、`orderNo`、`completedAt`、`completedBy` | `OrderDetail` | +| AfterSales | `AfterSalesApplicationSubmittedIntegrationEvent` | `after-sales.request.submitted.v1` | 申请 `buyerId`、订单 `assignedMerchantUserId` | `AfterSalesSubmitted` | `requestId`、`orderId`、`type` | `AfterSalesDetail` | +| AfterSales | `AfterSalesApplicationAuditedIntegrationEvent` | `after-sales.request.audited.v1` | 申请 `buyerId` | `AfterSalesReviewed` 或 `AfterSalesPendingReturn` | `requestId`、`decision`、`status` | `AfterSalesDetail` | +| AfterSales | `AfterSalesReturnInfoSubmittedIntegrationEvent` | `after-sales.return-info.submitted.v1` | 订单 `assignedMerchantUserId` | `AfterSalesReturnSubmitted` | `requestId`、`status` | `AfterSalesDetail` | +| Payment | `RefundCompletedIntegrationEvent` | `payment.refund.completed.v1` | 申请 `buyerId` | `RefundSucceeded` | `requestId`、`refundId`、`amount` | `AfterSalesDetail` | +| AfterSales | `RefundFailedIntegrationEvent` | `after-sales.refund.failed.v1` | 申请 `buyerId`、订单 `assignedMerchantUserId` | `RefundFailed` | `requestId`、`failureCode` | `AfterSalesDetail` | + +传输与幂等规则: + +- 事件发布到 `eshop.events` Exchange;Routing Key 使用上表固定值,新增事件仍遵守 `...v1`。 +- 来源模块在业务事务中写 Outbox;Worker 发布 RabbitMQ;Messaging 在保存消息的同一事务中写 Inbox。 +- Messaging 对 `recipients` 逐项生成消息,并以 `(messageId, recipientUserId, MessageType)` 建唯一约束;重复投递返回已处理结果,不重复生成消息或未读数。 +- `data` 不包含完整手机号、地址、支付凭证、JWT、密码或内部前端路由;消息文案由 Messaging 按每项接收人的 `role` 选择模板。 +- 消息保存成功后才触发 SignalR;RabbitMQ 或实时推送失败不回滚已经提交的来源业务事实。 + +#### 4.3.7 Hub 连接 | 项目 | 契约 | |---|---| | Hub 路径 | `/hubs/messaging` | -| 鉴权 | 有效买家或商家 JWT | +| 鉴权 | `BuyerOnly / MerchantOnly`(满足其中任一) | | 身份来源 | 服务端认证上下文中的用户 ID 和角色 | | 客户端订阅参数 | 无,不接受客户端传入任意 `userId`、角色或组名 | | 多实例 | 使用 Redis Backplane | @@ -7522,7 +7939,7 @@ SeckillOrderDetailResponse { 客户端主动退出后关闭连接。非主动断线使用有限退避自动重连;初次连接和每次重连成功后调用 A503,并按需调用 A501 补查断线期间消息。 -#### 服务端事件 `MessageCreated` +#### 4.3.8 服务端事件 `MessageCreated` 服务端向目标认证用户的全部在线连接推送 `MessageCreated`。载荷 Schema 为 `MessageCreatedPayload`: @@ -7562,206 +7979,139 @@ SeckillOrderDetailResponse { - 客户端按 `messageId` 去重轻提示;不得仅凭推送载荷修改订单、支付或售后最终状态。 - 本期不提供客户端调用的聊天、广播、已送达回执、任意加组或按用户订阅 Hub 方法。 -### 4.3 Messaging 错误码与跨模块待确认项 +### 4.4 Worker 内部契约 -| 错误码 | HTTP 状态 | 含义 | -|---|---:|---| -| `MESSAGE.NOT_FOUND` | 404 | 消息不存在或不属于当前用户 | +#### 4.4.1 C01 秒杀活动生命周期 -认证、验证、限流、依赖不可用和未知错误复用本文件第一章登记的通用错误码,不创建同义错误码。 +> 本任务由 `Mall.Worker` 执行,不提供 HTTP API,也不占用 Axxx 编号。 -#### 需要其他负责人评审的协作点 +##### 状态推进 -- Ordering、Payment 和 AfterSales 负责人需确认会触发通知的业务事实、事件 ID、业务 ID、接收用户和发生时间。 -- Identity 负责人需确认买家与商家认证身份、账号禁用和令牌失效规则可以同时约束 HTTP 与 SignalR。 -- 商家通知必须由来源模块明确指定接收账号或受控接收范围,不允许 Messaging 自行向全部商家广播私人订单或售后信息。 -- Catalog 负责人继续拥有 C07 商品缓存失效业务规则;罗皓晨只提供 Redis 与多实例缓存基础设施,不新增公开缓存控制接口。 +1. 周期扫描 `Published` 且 `startAt <= now()` 的活动,使用条件更新推进为 `Ongoing`。 +2. 周期扫描 `Ongoing` 且 `endAt <= now()` 的活动,使用条件更新推进为 `Ended`。 +3. 每次状态成功变化后触发对应活动列表和详情缓存失效;PostgreSQL 仍是活动状态与库存事实来源。 +4. `Draft` 和 `Cancelled` 不由 Worker 自动推进;A223 取消与 Worker 竞争时,只有一个条件更新成功。 -#### 实现前必须补齐 +##### 幂等与恢复 -- 创建并评审 `database-lhc.md`,登记消息表及必要的唯一约束、用户未读查询索引和关联 A501~A505。 -- 单独评审 Ordering、Payment、AfterSales 到 Messaging 的集成事件 Schema、Routing Key、Outbox/Inbox 幂等键和失败处理;这些不是 HTTP Axxx 接口。 -- 在后端脚手架建立后形成真实 OpenAPI,并保证 `operationId`、Schema、状态码和错误码与本文件一致。 -- 在测试计划中登记 X03、C06 和 C10 的分页、越权、重复已读、并发全部已读、断线重连、多标签页、多实例和健康检查场景。 +- 多实例 Worker 使用小批量扫描与 `FOR UPDATE SKIP LOCKED`(或等价条件更新)避免重复处理;重复扫描已经到达目标状态的活动无副作用。 +- 进程重启后继续以数据库时间字段扫描,不依赖内存定时器保存唯一任务事实。 +- 单条失败记录 `activityId`、目标状态与 `traceId` 后重试,不阻塞同批次其他活动。 -当前文档只能证明接口契约已形成待评审草案,不能证明接口已经实现、联调或通过验收。 +##### 验证场景 -### 4.4 C03 Worker 内部契约(不占 Axxx) +- 已发布活动到达开始时间后可通过 A228 抢购;结束时间后 A228 返回活动已结束。 +- 取消与自动开始并发时,最终只出现 `Cancelled` 或 `Ongoing` 中一个合法结果。 +- Worker 重启或多实例重复扫描不会重复推进、重复失效缓存或改写库存。 -本节描述后台任务与事件处理,不是可由客户端调用的 HTTP 接口。 - -订单超时自动取消(Worker接口) +#### 4.4.2 C03 订单超时自动取消 > **说明**:C03订单超时自动取消由Worker后台任务执行,不对外提供HTTP API。接口设计记录其与外部系统的交互关系。 -#### 业务规则 +##### 业务规则 1. **超时时间配置**:订单超时时间通过配置项 `OrderTimeoutMinutes` 管理,默认30分钟 2. **扫描策略**:Worker定时扫描 `PendingPayment` 状态且 `created_at + timeout < now()` 的订单 -3. **取消事务**:在同一事务内完成状态变更 `PendingPayment → Cancelled`、库存回补、写入 `cancelled_at` 和 `cancel_reason = 'TIMEOUT'` +3. **取消事务**:复用 A304 的内部取消用例,在同一受控事务内完成 `PendingPayment → Cancelled`、按普通/秒杀原通道回补库存并释放秒杀限购名额、写入 `cancelled_at` 和 `cancel_reason = 'TIMEOUT'` 4. **幂等保证**:使用条件更新 `WHERE status = 'PendingPayment'`,同一订单多次扫描只有一次成功 5. **支付竞争**:与M05支付并发时,条件更新确保只有一个成功 -#### 事件消费 +##### 任务触发 -- 消费 `OrderCreatedEvent`(由M04-01发布)触发后续超时跟踪 +- `Mall.Worker` 周期扫描 `expiresAt <= now` 且仍为 `PendingPayment` 的订单;不依赖进程内定时器或消费“订单创建”事件保存唯一任务事实 -#### 事件发布 +##### 事件发布 -- 发布 `OrderCancelledEvent`(`cancel_reason = 'TIMEOUT'`)到Outbox,供给M09站内消息 +- 发布 `OrderCancelledIntegrationEvent`(`cancelReason = 'TIMEOUT'`)到 Outbox,供 M09 站内消息消费 -#### 关键实现点 +##### 关键实现点 1. 扫描间隔建议 ≤ 超时时间/2 2. 每批次处理上限100条,避免长时间锁表 3. 失败重试3次后告警,订单保留待处理状态 4. 多实例Worker使用 `SELECT FOR UPDATE SKIP LOCKED` 避免重复处理 -#### 验证场景 +##### 验证场景 -1. 超时订单被自动取消,库存回补 +1. 普通与秒杀超时订单均被自动取消并回补原库存通道 2. 买家在超时前支付成功,取消被跳过 3. 并发取消与支付只有一个成功 4. Worker重启后继续扫描,不漏扫 -### 4.5 Catalog 与 Review 附录 +#### 4.4.3 M04-04 发货超时自动完成 -(本人模块新增) +> 本任务由 `Mall.Worker` 执行,不提供 HTTP API,也不占用 Axxx 编号;买家主动确认仍使用 A308。 -以下为本人模块在《接口设计.md》通用错误码之外新增的稳定业务错误码,待汇总时并入总表: +##### 业务规则 -| 错误码 | HTTP 状态 | 含义 | -|---|---:|---| -| `CATALOG.PRODUCT_NOT_FOUND` | 404 | 商品不存在或购物端不可见 | -| `CATALOG.CATEGORY_NOT_FOUND` | 404 | 分类不存在 | -| `CATALOG.CATEGORY_DISABLED` | 409 | 分类已停用,不能用于上架/新建 | -| `CATALOG.CATEGORY_NAME_CONFLICT` | 409 | 同父级下分类名称重复 | -| `CATALOG.INVALID_PRICE_RANGE` | 400 | `minPrice > maxPrice` | -| `CATALOG.INVALID_SORT_FIELD` | 400 | 排序字段不在白名单 | -| `CATALOG.PRODUCT_VERSION_CONFLICT` | 409 | 商品并发编辑版本冲突 | -| `CATALOG.PRODUCT_HAS_ORDERS` | 409 | 存在历史订单关联,禁止物理删除 | -| `CATALOG.PRODUCT_INCOMPLETE` | 409 | 上架时必填项/主图缺失 | -| `CATALOG.IMAGE_LIMIT_EXCEEDED` | 409 | 商品图片超过 8 张 | -| `CATALOG.INVALID_IMAGE` | 415 | 图片格式或尺寸不符合要求 | -| `CATALOG.PRIMARY_IMAGE_REQUIRED` | 409 | 已上架商品不得删至无主图 | -| `REVIEW.ORDER_ITEM_NOT_FOUND` | 404 | 订单项不存在或不属于当前买家 | -| `REVIEW.ORDER_NOT_COMPLETED` | 409 | 订单未完成,不能评价 | -| `REVIEW.ALREADY_REVIEWED` | 409 | 该订单项已评价 | -| `REVIEW.INVALID_IMAGE` | 415 | 评价图片格式或尺寸不符合要求 | - -#### 五、待确认事项 - -1. `DB021`~`DB025` 编号需与本人 `database-gxy.md` 交叉确认并固定;表字段、约束、索引以数据库设计为准。 -2. A124、A142、A143 依赖 Ordering(韦乾强)提供“订单项归属 + 订单完成状态 + 是否存在订单关联”的应用契约,需在联调前确认契约形态。 -3. 商品/评价缓存失效与搜索索引同步(C04、C07)由罗皓晨主责的公共能力协作,事件与缓存 Key 以《命名规范》与架构设计为准。 -4. 购物端商品详情(A103)是否内联轻量评分汇总,最终以评审结论为准;当前设计由 A140 统一提供汇总,保持模块边界清晰。 -5. 上传体积超限(A127、A141)当前引用 `COMMON.PAYLOAD_TOO_LARGE`(413),该码属 M00 公共错误码,需由罗皓晨在《接口设计.md》1.10 通用错误码表登记后统一引用;本文件不自建 `COMMON.*` 码。 -6. A140 公开评价的 `buyerDisplayName` 为脱敏昵称,其来源与脱敏规则需与 Identity(唐宇昊)确认,评价模块只做展示不落库敏感字段。 - -#### 六、枚举附录 - -对外接口一律使用 PascalCase 字符串枚举,数据库落库使用 `lower_snake_case`;客户端必须对未知枚举值做兜底展示。 - -| 枚举 | 使用接口 | 对外值(API) | 落库值(DB,参考) | 含义 | -|---|---|---|---|---| -| 分类状态 `status` | A110、A113、A114 | `Enabled` / `Disabled` | `enabled` / `disabled` | 分类是否作为购物端筛选入口 | -| 商品状态 `status` | A120~A126 | `Draft` / `Published` / `Unpublished` | `draft` / `published` / `unpublished` | 草稿 / 已上架 / 已下架 | -| 库存状态 `stockStatus` | A102、A103 | `InStock` / `SoldOut` | 由 `stock` 计算,不落库 | 有货 / 售罄 | -| 评价资格原因 `reason` | A143 | `Eligible` / `OrderNotCompleted` / `AlreadyReviewed` / `NotOwner` | 由订单与评价状态计算,不落库 | 可评价及不可评价原因 | -| 排序方向 `sortOrder` | A102、A120、A140 | `asc` / `desc`(小写,规范 1.11.3) | — | 升序 / 降序 | +1. 周期扫描 `Shipped` 且 `shippedAt + 7 days <= now()` 的订单。 +2. 复用 A308 的完成订单用例,使用 `WHERE status = 'Shipped'` 条件更新为 `Completed`,并记录 `completedAt`、`completedBy = 'Auto'`。 +3. 成功后只写一次 `OrderCompletedIntegrationEvent` Outbox;与买家主动确认并发时仅一个条件更新成功,失败方读取并返回当前终态,不重复发布事件。 +4. 订单事实和发货时间均来自 PostgreSQL;Worker 重启后继续扫描,不依赖进程内定时器保存唯一任务事实。 -> 落库值仅为跨文档参考,最终以 `database-gxy.md` 与实体映射为准;已删除商品在购物端按“不存在”处理,不作为对外枚举值暴露。 +##### 验证场景 -### 4.6 Payment 与 AfterSales 跨接口约束 - -#### 4.6.1 错误码统一 - -- `AUTH.*` / `RESOURCE.*` / `IDEMPOTENCY.*` / `COMMON.*` 按接口设计 1.10 节基础 -- 模块错误码:`PAYMENT.*` / `AFTER_SALES.*` / `RECONCILIATION.*` -- 同一错误场景使用同一错误码 + HTTP 状态 - -#### 4.6.2 幂等键一致性 - -涉及资金 / 状态 / 回调的接口统一: - -| 幂等范围 | 字段格式 | -|---|---| -| 客户端生成 | UUID v4 | -| 服务端 Key | `Idempotency-Key` Header | -| 储存 | DB082 / DB085 / DB088 / DB089 对应表唯一约束 | -| 保留期 | 与对应业务表相当(≥ 90 天) | +1. 发货满 7 天且仍为 `Shipped` 的订单被自动推进为 `Completed`。 +2. 买家在 Worker 扫描前主动确认后,Worker 跳过该订单。 +3. 多实例 Worker 与 A308 并发时只产生一次完成状态和一条完成事件。 +4. Worker 重启后继续扫描,不漏掉已到期订单。 -#### 4.6.3 响应包装 +#### 4.4.4 C08 每日对账 -所有成功响应统一为 `data` 包装(204 除外),按接口设计 1.7 节。 +> 本任务由 `Mall.Worker` 执行,不提供 HTTP API,也不占用 Axxx 编号;A422~A425 只负责查询和处理已生成的对账事实。 -#### 4.6.4 失败响应 +##### 业务规则 -全部使用 `application/problem+json`,按接口设计 1.8 节。 +1. 按 UTC 自然日生成前一日对账批次,日期范围采用左闭右开;同一范围建立唯一约束,重复执行复用同一批次。 +2. 对比 Payment 的支付、退款、钱包流水与 Ordering 的订单支付状态,生成匹配数量和稳定差异记录;不得通过直接改库隐藏差异。 +3. 批次与差异写入 PostgreSQL;只有批次完整核对成功后才标记 `Matched` 或 `HasDifferences`。 +4. 任务失败保持可重试状态并记录 `traceId`;重试不会生成第二个矛盾批次或重复差异。 -#### 4.6.5 操作审计 +##### 验证场景 -- 涉及状态变更的接口(A402 / A405 / A412 / A415 / A416 / A417 / A419 / A421 / A425 / A431)在 `audit_logs` 或对应表登记 actor、at、from_status、to_status -- 涉及资金的接口在 `wallet_ledgers` 写入流水 +- 同一日期任务重复执行只得到一个批次。 +- “支付成功但订单未更新”、迟到成功回调、退款与流水不一致均生成可由 A424/A425 处理的差异。 +- Worker 中途失败后重试可完成原批次,已登记差异不重复。 --- -### 4.7 Payment 与 AfterSales 协作范围 - -#### 4.7.1 不在个人原稿范围 - -- `database-zhy.md` 中 DB081~DB100 的字段、约束和索引由数据库设计任务单独维护,本文件只引用已确认 DBxxx。 -- 不在教师基线和需求规格之外自行扩展 C08;新增范围必须先完成需求确认。 -- 集成事件命名需要与 M00 可靠事件规范对齐,不能用外部 HTTP 接口替代内部事件。 -- `AdminOnly` Policy 名称和授权语义需要与 M00 公共鉴权约定对齐。 - -#### 4.7.2 评审清单 - -- [ ] 罗皓晨:对照接口设计 1.20 模板核对字段完整性 -- [ ] 韦乾强:核对 A401-A408 与 Ordering 的协作边界(订单状态、回调联动) -- [ ] 顾欣月:核对 A402 / A405 与 Catalog 库存联动(如有) -- [ ] 张海洋自审:核对 17 项 PAY 业务规则 + 12 项 M10 规则 + 11 项 C08 规则全部覆盖 - -#### 4.7.3 汇总与保留 - -- 六份原稿已完成首轮汇总;后续评审修正必须在同一任务中同步本文件第二、三、五章。 -- 个人文件 `interface/interface-zhy.md` 继续保留,不单独作为实现事实源。 -- 历史贡献通过 Git 记录保留 - ## 五、汇总审计与冻结条件 ### 5.1 当前成熟度 -本次只完成六份个人原稿的结构汇总和一致性审计,不代表 102 个接口已全部评审或可以直接编码。主文档当前整体成熟度为 **部分定义,未冻结**。 - -| 负责人 | 已登记 | 明确缺少或必须闭环 | 当前结论 | -|---|---:|---|---| -| 唐宇昊 | 23 | 缺浏览记录写入、浏览记录开关查询;A006~A014 身份范围与需求冲突 | X02 未闭环 | -| 顾欣月 | 21 | 单条评价读取需新增接口或删除现有跳转引用;图片暂存和商品创建顺序待统一 | 主体齐全,待评审 | -| 朱惠惠 | 19 | 无新增外部接口硬缺;A229/A230 应复用 Ordering,秒杀库存与订单边界待修正 | 主体齐全,边界冲突 | -| 韦乾强 | 7 | 缺买家确认收货;现有 DBxxx、幂等、状态字段和商家订单边界待修正 | F09 未闭环 | -| 张海洋 | 25 | 缺买家提交退货说明/寄回信息;内部退款与外部 HTTP 授权边界待重构 | X04 未闭环 | -| 罗皓晨 | 7 | 无新增业务 HTTP 硬缺;缺正式集成事件、DBxxx 和 OpenAPI 落地 | HTTP 主体齐全 | - -### 5.2 建议由原负责人补充的接口 - -| 建议编号 | 负责人 | 方法与路径 | operationId | 对应需求 | 必要性 | -|---|---|---|---|---|---| -| A024 | 唐宇昊 | `PUT /api/browsing-history/{productId}` | `Engagement_RecordBrowsingHistory` | M08-FR04 | 必需 | -| A025 | 唐宇昊 | `GET /api/browsing-history/settings` | `Engagement_GetBrowsingHistorySetting` | M08-FR07 | 必需 | -| A144 | 顾欣月 | `GET /api/reviews/{reviewId}` | `Review_GetReview` | A142 Location、A143 existingReviewId | 二选一:新增,或删除不可达引用 | -| A308 | 韦乾强 | `POST /api/orders/{orderId}/confirm-receipt` | `Ordering_ConfirmReceipt` | M04-04、F09 | 必需 | -| A434 | 张海洋 | `POST /api/after-sales/requests/{requestId}/return` | `AfterSales_SubmitReturn` | M10-FR11 | 必需 | - -建议编号只有对应负责人补齐完整详细定义并通过交叉评审后才正式占用;本表不能替代接口定义。 +本次已把六份最新个人原稿综合到唯一主接口文档,并解决已知的缺口、重复端点、模块命名和明显语义冲突。当前状态仍为 **部分定义,未冻结**:接口清单已经闭合,但 DBxxx、真实 OpenAPI、跨模块实现签名和交叉评审证据尚未完成。 + +| 负责人 | 追踪编号 | 有效 HTTP | 本次已闭合 | 仍需确认 | +|---|---:|---:|---|---| +| 唐宇昊 | 25 | 25 | A024/A025、F03 权限、令牌撤销、账号状态幂等 | DB001~DB006、刷新令牌 Schema、OpenAPI | +| 顾欣月 | 22 | 22 | A144、图片先后顺序、公开评价字段与展示名快照、C04 索引边界 | DB021~DB025、OpenAPI | +| 朱惠惠 | 19 | 17 | A229/A230 取消、商家活动路径、库存/限购边界、生命周期 Worker | DB041~DB044、公开应用契约签名、OpenAPI | +| 韦乾强 | 8 | 8 | A308、Ordering 命名、秒杀查询复用、取消与自动完成 Worker | DB061/DB062 完整字段、公开应用契约签名、OpenAPI | +| 张海洋 | 26 | 24 | A418/A431 取消、A434、退款契约、回调幂等与每日对账 Worker | DB081~DB091、退款事务编排、OpenAPI | +| 罗皓晨 | 7 | 7 | 买家/商家授权、事件映射、SignalR、健康检查 | DB101~DB120、来源模块评审、OpenAPI | +| **合计** | **107** | **103** | **4 个取消历史编号已隔离** | **尚不能宣称冻结或已实现** | + +### 5.2 已确认的综合决策 + +1. A024/A025、A144、A308、A434 已补齐,不再列为缺失接口。 +2. A229/A230 取消,秒杀订单列表和详情由 A302/A303 承接。 +3. A418 取消,售后状态时间线由 A414 一次返回,不建设重复的审核日志接口。 +4. A431 已取消并只保留历史追踪编号,退款通过不占 Axxx 的 Payment 公开应用契约完成。 +5. A305~A307 归属 Ordering;商家身份只影响路径和 Policy,不新增 Merchant 业务模块。 +6. F03 的 A006~A014 统一为 BuyerOnly;商家资料维护不在本期范围。 +7. 商品先创建草稿再上传图片;评价图片先进入当前买家暂存区,再由评价提交关联。 +8. C04 使用 PostgreSQL `pg_trgm`/GIN 同步索引,不通过 Outbox/Worker 复制搜索索引。 +9. 本期为单店 B2C;订单以 `assignedMerchantUserId` 指定处理账号,不建设多商户商品归属、拆单或结算模型。 +10. C01 活动推进、C03 超时取消、M04-04 自动完成和 C08 每日对账均由 `Mall.Worker` 扫描 PostgreSQL 事实并幂等执行。 ### 5.3 冻结前必须完成 -1. 由各负责人修正本人接口的需求冲突、状态机、资源归属、错误码和跨模块边界。 -2. 将 A229/A230 改为复用 A302/A303;秒杀筛选通过 Ordering 查询契约表达,不建立第二套订单事实。 -3. 将商家后台订单能力的 Tag 和 `operationId` 统一归 Ordering;后台不是独立业务模块。 -4. 补齐 A024、A025、A308、A434,并对 A144 采用“新增接口”或“删除不可达引用”中的一种明确方案。 -5. 完成六份数据库设计,替换推断或越界的 DBxxx;当前不得按错误 DBxxx 生成 Migration。 -6. 明确 Ordering、Payment、AfterSales、Messaging 之间的 Application/Contracts 与集成事件,禁止跨模块直接读写内部表。 -7. 生成并校验真实 OpenAPI,确保每个 `operationId`、Schema、状态码、Policy 与本文件一致。 -8. 至少一名其他成员完成交叉评审后,才把对应接口状态改为“已确认”;未确认接口不得宣称已冻结。 +1. 六名负责人完成个人数据库设计并汇总到《数据库设计》,逐项反查本文件中的 DBxxx、字段、约束、索引和事务边界。 +2. 将 103 个有效 HTTP 契约落成真实 OpenAPI,校验路径、方法、`operationId`、Schema 引用、状态码和安全方案均唯一有效。 +3. 把 4.2 的模块间应用边界落实为公开 Contracts/Application 接口,不允许跨模块直接读写内部表。 +4. Ordering、Payment、AfterSales 负责人确认 4.3 的事件字段、接收账号和触发时机;Messaging 完成 Inbox 去重与断线补偿设计。 +5. 为 4.4 的四类 Worker 固定扫描索引、批次大小、重试与多实例互斥策略,并登记对应 DBxxx。 +6. 对订单、秒杀、支付、回调、退款、售后和全部已读并发场景建立契约测试或验收用例。 +7. 每个负责人至少由一名其他成员完成交叉评审;确认后的接口才可把状态从“部分定义/待交叉评审”改为“已确认”。 + +在以上条件完成前,可以按已稳定的编号和路径继续数据库及 OpenAPI 设计,但不得宣称接口文档已经冻结、接口已经实现或通过验收。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" index 3b8f3d8..847c3d2 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" @@ -9,7 +9,7 @@ | 版本 | 日期 | 修改人 | 修改说明 | |---|---|---|---| -| v0.1 | 2026-07-22 | 罗皓晨 | 形成并完善系统架构初稿,明确技术选型、分层依赖、模块边界、角色权限、事件与分布式组件及六人纵向职责 | +| v0.1 | 2026-07-24 | 罗皓晨 | 形成并完善系统架构,明确技术选型、分层依赖、模块边界、角色权限、事件、Worker、库存与售后履约协作及六人纵向职责 | ## 一、架构目标与约束 @@ -69,7 +69,7 @@ | 领域协作 | 领域事件 | — | 在同一进程内表达并处理领域事实 | | 集成可靠性 | RabbitMQ 集成事件 + Outbox/Inbox | RabbitMQ 4.3.2 | Outbox 保证事务后可靠发布,Inbox/消费处理记录保证消费者防重 | | 缓存/共享通道 | Redis | 8.2.7 | 8.2 系列为当前具有明确长期支持周期的较新 GA 系列,官方标注支持至 2030-09-01 | -| 后台处理 | .NET Worker Service | .NET 10 | 执行 Outbox 投递、订单超时取消和对账任务 | +| 后台处理 | .NET Worker Service | .NET 10 | 执行 Outbox 投递、秒杀生命周期、订单超时取消、发货超时自动完成和每日对账任务 | | 本地编排 | Aspire | 13.4.4 | 声明 API、Worker 和基础设施依赖;Aspire 不设 LTS 分支,按官方策略使用当前唯一受支持版本 | | 生产部署 | Docker Compose + Nginx | Compose 规范 + 镜像摘要锁定 | 满足 C10 一键部署和至少 2 个 API 实例负载均衡验收 | | 对象存储 | S3 Compatible Object Storage(开发环境:SeaweedFS) | SeaweedFS 4.29 | 业务仅依赖 S3 兼容协议,避免绑定具体存储产品 | @@ -342,6 +342,9 @@ sequenceDiagram ### 7.3 订单履约与完成 - 商家只能把已支付订单推进为已发货,并记录发货时间;其他状态的发货请求必须拒绝。 +- 本期为单店 B2C,不建设多商户商品归属、拆单或结算;Ordering 为每张普通订单保存 Identity 解析的默认 `assignedMerchantUserId`,秒杀订单保存活动创建人,商家查询、发货、售后和消息接收均按该账号精确过滤。 +- Identity 对默认商家标记建立唯一约束,并由启动配置/种子数据保证存在一个启用账号;A016 不允许直接禁用默认商家,也不允许禁用仍有待履约订单、售后窗口/申请或未结束秒杀活动的其他商家。本期不做自动重新分配。 +- A307 发货与 A412 提交售后都先通过 Ordering 公开应用契约在当前 PostgreSQL 事务中锁定同一 `orders` 行并复核最新履约状态,锁保持到业务写入提交;随后 A307 通过 AfterSales 公开应用契约取得售后快照。处理中申请阻断发货,已退款数量从可履约数量中扣除,部分退款只发剩余数量,全部退款不再发货;模块之间不得直接读取对方内部表。 - 买家只能确认本人已发货订单,确认成功后以状态条件把订单推进为已完成,并记录完成时间和“买家确认”方式。 - `Mall.Worker` 扫描发货满 7 天且仍为已发货的订单,以同一状态条件推进为已完成,并记录“自动完成”方式;演示环境可以缩短配置,但不改变正式规则。 - 买家确认与自动完成并发时只能有一个状态更新成功,重复请求返回已有结果,不重复生成通知。 @@ -367,9 +370,9 @@ sequenceDiagram ### 7.6 四项选做功能 -- **评价晒图**:评价必须校验当前用户已完成订单项;评价记录与图片元数据分离,图片存 SeaweedFS。 +- **评价晒图**:评价必须校验当前用户已完成订单项;创建时通过 Identity 公开应用契约取得安全展示名并保存脱敏快照,公开列表不逐条跨模块查询用户资料;评价记录与图片元数据分离,图片存 SeaweedFS。 - **收藏/历史**:按用户隔离;收藏使用唯一约束防重,浏览历史对同一用户和商品更新最近时间。 -- **站内消息**:Messaging 消费订单、支付、发货和售后事件,按事件 ID、接收用户和消息类型去重;消息先落 PostgreSQL,再由 SignalR 推送;已读状态以数据库为准,WebSocket 只负责实时性。 +- **站内消息**:来源模块在事件中明确列出买家或订单处理商家接收账号,Messaging 不按角色全量广播;按消息 ID、接收用户和消息类型去重,消息先落 PostgreSQL,再由 SignalR 推送;已读状态以数据库为准,WebSocket 只负责实时性。 - **售后流程**:建立独立售后状态机,按订单项数量计算退款;已支付、已发货或完成后 7 天内允许申请,模拟退款通过 Payment 幂等退回小金库并纳入 C08 对账,订单核心状态不因部分退款被覆盖。 ### 7.7 C01 秒杀与防超卖 @@ -386,7 +389,10 @@ WHERE activity_id = @id ``` - 受影响行数为 1 才允许创建秒杀订单。 -- 库存扣减、订单创建和必要 Outbox 写入处于同一事务。 +- 发布活动时通过 Catalog 公开应用契约把普通库存原子划转为独立秒杀配额;取消活动时按本期规则保留已分配配额,不混回普通库存。 +- `Mall.Worker` 按数据库时间幂等推进 `Published → Ongoing → Ended`;秒杀下单同时校验状态和时间窗口。 +- 单用户限购由 `(activity_id, buyer_id)` 唯一配额事实和条件更新保证,取消成功按订单幂等释放,不以 Redis 或普通聚合查询承担并发正确性。 +- 秒杀库存扣减、限购占用、Ordering 共享订单创建和必要 Outbox 写入处于同一受控事务;Seckill 不建立第二套订单状态机。 - Redis 可用于活动热点读取和入口削峰,但不能成为唯一库存事实来源。 - 压测固定记录并发数、库存、成功/失败数、数据库最终库存和有效订单数,验证不超卖、不少卖。 @@ -395,7 +401,7 @@ WHERE activity_id = @id - 创建订单时写入 `expires_at = created_at + 30 分钟`。 - `Mall.Worker` 周期扫描已到期的待支付订单;演示环境只缩短配置值,不改变规则。 - 多 Worker 使用批量领取/跳过已锁定记录或等价机制,取消时执行带 `PendingPayment` 条件的状态更新。 -- 状态更新和库存回补同事务;支付也必须带待支付状态条件,因此支付与取消竞争只能一方成功。 +- 状态更新和库存回补同事务;普通订单回补 Catalog,秒杀订单回补原 Seckill 活动库存并释放限购额度。支付也必须带待支付状态条件,因此支付与取消竞争只能一方成功。 - Worker 重试安全,重复扫描不会重复回补库存。 ### 7.9 C04 商品搜索进阶 diff --git a/eshop-project-rules-upload/document-routing.reference.md b/eshop-project-rules-upload/document-routing.reference.md index b9c360c..8497399 100644 --- a/eshop-project-rules-upload/document-routing.reference.md +++ b/eshop-project-rules-upload/document-routing.reference.md @@ -177,10 +177,11 @@ 1. `二、接口清单` 中的编号分配、个人文件规则、目标负责人区间和汇总状态。 2. 以本文件中的统一清单和同编号详细定义作为实现事实源;需要核对负责人原始设计或交叉评审记录时,再读 `docs/02-设计文档/interface/interface-<姓名拼音首字母>.md`。 -3. `五、汇总审计与冻结条件` 中目标负责人的缺口、冲突和冻结阻塞项。 -4. 实际 OpenAPI、Endpoint、DTO、调用方和测试。 +3. `四、非 HTTP 契约与模块协作` 中目标模块的公开应用契约、事件、SignalR 与 Worker 边界。 +4. `五、汇总审计与冻结条件` 中目标负责人的缺口、冲突和冻结阻塞项。 +5. 实际 OpenAPI、Endpoint、DTO、调用方和测试。 -只有接口清单、没有详细 Axxx 时标记为 `部分定义`;字段、失败响应和业务规则未确认前不得直接编码。当前六份个人原稿已保存在 `docs/02-设计文档/interface/`,主接口文档已汇总 102 个不重复 Axxx 及其详细定义,但整体仍为“部分定义,未冻结”:A024、A025、A308、A434 尚缺,A144 需要在“新增单条评价读取”与“删除不可达引用”之间作出决策,A229/A230 与 Ordering 查询边界冲突。每次任务开始时重新检查,不永久假设此状态。 +只有接口清单、没有详细 Axxx 时标记为 `部分定义`;字段、失败响应和业务规则未确认前不得直接编码。当前六份个人原稿已保存在 `docs/02-设计文档/interface/`,主接口文档保留 107 个不重复 Axxx 追踪编号,其中 103 个为有效 HTTP 契约;A229、A230、A418、A431 均为已取消历史编号。整体仍为“部分定义,未冻结”:需继续完成数据库反查、真实 OpenAPI、跨模块公开契约和交叉评审。每次任务开始时重新检查,不永久假设此状态。 ### 6.5 `docs/02-设计文档/数据库设计.md` @@ -213,16 +214,16 @@ | 模块 | 需求定位 | 架构重点 | 常见关联 | |---|---|---|---| | M00 公共基建 | M00、需求 8/9 | 架构 4/5/10/11/14/15 | 全模块组合根、公共契约 | -| M01 用户鉴权 | M01-01~03、M06-03、M08 | 架构 5/8/9 | A001~A023 已登记,A024/A025 缺失,账号状态、Policy | -| M02 商品 | M02-01/02、M06-01、M07、C04 | 架构 6/7.5/7.6/7.9/7.11 | A101~A103、A110~A114、A120~A128、A140~A143 已登记,A144 待决策 | -| M03 购物车 | M03-01、C01 | 架构 7.1/7.7 | A201~A208、A220~A230 已登记,A229/A230 边界冲突,库存与订单协作 | -| M04 订单 | M04-01~04、M06-02、C03 | 架构 7.1/7.3/7.8 | A301~A307 已登记,A308 缺失,库存、支付、Worker | -| M05 支付 | M05-01、C08 | 架构 7.2/7.12 | A401~A408、A421~A425、A431~A433 已登记,订单、Outbox、对账 | +| M01 用户鉴权 | M01-01~03、M06-03、M08 | 架构 5/8/9 | A001~A025 已登记,账号状态、Policy、令牌撤销 | +| M02 商品 | M02-01/02、M06-01、M07、C04 | 架构 6/7.5/7.6/7.9/7.11 | A101~A103、A110~A114、A120~A128、A140~A144 已登记,图片与订单项协作 | +| M03 购物车 | M03-01、C01 | 架构 7.1/7.7 | A201~A208、A220~A228 已登记;秒杀绕过购物车;A229/A230 已取消并复用 A302/A303;活动状态由 Worker 推进 | +| M04 订单 | M04-01~04、M06-02、C03 | 架构 7.1/7.3/7.8 | A301~A308 已登记;按原通道回补库存;`assignedMerchantUserId` 控制商家范围;超时取消与自动完成由 Worker 执行 | +| M05 支付 | M05-01、C08 | 架构 7.2/7.12 | A401~A408、A421~A425、A432/A433;退款入账使用 Payment 应用契约,A431 已取消;每日对账由 Worker 生成批次 | | M06 后台 | M06-01~03 | 架构 5/6/8 | 商品、订单、账号各自主责 | | M07 评价 | M07 | 架构 7.5/7.6 | 订单项、对象存储 | | M08 收藏历史 | M08 | 架构 7.6 | 用户隔离、商品引用 | -| M09 消息 | M09、C06 | 架构 7.4/7.6/7.10 | A501~A505 已登记,PostgreSQL、SignalR、事件 | -| M10 售后 | M10、C08 | 架构 7.6/7.12 | A411~A419、A431~A433 已登记,A434 缺失,订单项、支付退款、对账 | +| M09 消息 | M09、C06 | 架构 7.4/7.6/7.10 | A501~A505 已登记,来源事件明确 `recipients[]`,PostgreSQL 持久化后再经 SignalR 推送 | +| M10 售后 | M10、C08 | 架构 7.6/7.12 | A411~A417、A419、A432~A434;A418/A431 已取消,退款使用 Payment 应用契约 | | C07 缓存 | C07 | 架构 7.11 | Catalog 规则与 M00 基础设施 | | C10 部署 | C10 | 架构 7.13/10/11/12/15 | A506/A507 已登记,Compose、Nginx、多实例 | -- Gitee From b9ca003ab2ffdaf72d4e3360b78728f1f67e10e7 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 13:27:42 +0800 Subject: [PATCH 054/118] =?UTF-8?q?docs(infra):=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E9=98=B6=E6=AE=B5=E6=88=90=E6=9E=9C=EF=BC=9B?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E6=8F=90=E4=BA=A4=E8=BE=B9=E7=95=8C=E4=B8=8E?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eshop-project-rules-upload/AGENTS.md | 9 ++++++++- eshop-project-rules-upload/eshop-manage-git.SKILL.md | 8 +++++--- .../eshop-project-workflow.SKILL.md | 10 +++++++--- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/eshop-project-rules-upload/AGENTS.md b/eshop-project-rules-upload/AGENTS.md index 85b9a34..861b2a8 100644 --- a/eshop-project-rules-upload/AGENTS.md +++ b/eshop-project-rules-upload/AGENTS.md @@ -167,7 +167,10 @@ - 分支格式为 `<类型>/<模块>-<任务>-<姓名拼音首字母>`; - 一个任务只使用一个分支和一个 PR/MR,不混入无关改动; - 合入 `dev` 前需要真实验证、CI 和至少一名其他成员交叉 Code Review; -- 未经用户明确要求,不提交、不推送、不创建 PR/MR; +- 一个范围明确、可以独立说明和回滚的任务阶段完成,且相关验证通过后,默认自动暂存并提交本阶段归属清楚的文件;该规则视为仓库级提交授权,不再逐次询问是否 Commit; +- 阶段未完成、验证失败、改动归属不清、存在无法隔离的他人改动,或当前位于 `master`/`dev` 时不得自动提交,必须先报告原因; +- 自动提交只使用精确文件路径,先检查工作区和暂存区;提交信息统一使用 `git commit -m "(): <任务概要>;<主要改动>"`,同时说明本阶段要完成什么以及实际做了什么; +- 自动 Commit 不代表自动 Push 或创建 PR/MR;未经用户明确要求,不推送、不创建 PR/MR; - 工作区不干净时先识别改动归属,不强制切换、不擅自 stash、不丢弃修改。 - 暂存时使用明确文件路径并先检查差异,禁止无检查执行 `git add .`; - `reset --hard`、`clean -fd`、强制删除、Rebase 和强推属于高风险操作,必须先确认精确目标与可恢复性;长期分支禁止强推,个人任务分支确需强推时只使用 `--force-with-lease`; @@ -290,6 +293,10 @@ 说明未验证功能、未覆盖边界、兼容性影响和环境限制;没有明显风险时写“暂无明显剩余风险。” +### 8. 阶段提交结果 + +说明是否已按阶段自动 Commit;成功时列出 Commit SHA 和完整 `-m` 内容,未提交时说明不满足哪一项安全条件。不得把未执行的提交描述为已完成。 + 不得只回复“完成了”或夸大完成度。 ## 十、模块级 AGENTS.md 编写要求 diff --git a/eshop-project-rules-upload/eshop-manage-git.SKILL.md b/eshop-project-rules-upload/eshop-manage-git.SKILL.md index 5eabd90..99b5043 100644 --- a/eshop-project-rules-upload/eshop-manage-git.SKILL.md +++ b/eshop-project-rules-upload/eshop-manage-git.SKILL.md @@ -28,9 +28,11 @@ description: 安全处理 E-Shop 多人协作中的分支、工作区、暂存 1. 暂存前检查 `git diff` 和未跟踪文件,只使用明确文件路径,不执行未经检查的 `git add .`。 2. 暂存后检查 `git diff --cached`,确认没有混入无关文件、敏感信息、生成文件或本地配置。 -3. 提交信息遵守仓库约定,准确表达类型、模块和单一目的,不伪造验证结论。 -4. 推送前重新确认分支、上游、提交范围和真实验证结果。 -5. 未经用户明确要求,不执行暂存、提交、推送或创建 PR/MR。 +3. 一个范围明确、可以独立说明和回滚的任务阶段完成,且相关验证通过后,默认自动暂存并提交该阶段归属清楚的文件;仓库规则已经提供 Commit 授权,不再逐次询问。 +4. 自动提交信息固定为 `git commit -m "(): <任务概要>;<主要改动>"`,遵守 Conventional Commits;任务概要写阶段目标,主要改动写真实完成内容,不伪造验证结论。 +5. 阶段未完成、验证失败、当前位于 `master`/`dev`、改动归属不清或无法隔离他人改动时,不得为了形成提交而强行暂存;应报告未提交原因。 +6. 推送前重新确认分支、上游、提交范围和真实验证结果。 +7. 自动 Commit 不授权 Push、PR/MR、合并、Rebase、Tag 或分支删除;这些操作仍须用户明确要求。 ## PR/MR、合并与发布 diff --git a/eshop-project-rules-upload/eshop-project-workflow.SKILL.md b/eshop-project-rules-upload/eshop-project-workflow.SKILL.md index fd858ed..fbc5411 100644 --- a/eshop-project-rules-upload/eshop-project-workflow.SKILL.md +++ b/eshop-project-rules-upload/eshop-project-workflow.SKILL.md @@ -65,9 +65,13 @@ description: 作为 E-Shop 仓库所有任务的项目级总入口,统一完 1. 运行专项 Skill 要求且仓库真实存在的验证命令。 2. 检查 `git diff --check`、实际差异、当前状态和无关改动是否保持原样。 -3. 只报告真实执行的命令与结果,不把计划、模板或未验证行为描述成完成。 -4. 说明实际读取的关键章节、使用的编号和文档成熟度;未读取的材料应能解释为与任务无关。 -5. 最终回复覆盖:修改内容、文件清单、README、模块级 AGENTS、验证命令、不过度设计说明和剩余风险。 +3. 把当前成果判断为一个可独立说明、验证和回滚的阶段;阶段未完成、验证失败或改动归属不清时不得自动提交。 +4. 阶段完成且安全条件满足时,使用 `$eshop-manage-git` 按精确路径自动暂存并提交本阶段文件,不再逐次请求 Commit 授权;不得混入工作区已有的其他任务改动。 +5. 自动提交信息使用 `git commit -m "(): <任务概要>;<主要改动>"`。任务概要说明本阶段目标,主要改动说明实际完成内容;两部分不得使用“更新”“改一下”等模糊词。 +6. 自动 Commit 不扩展为 Push、PR/MR、合并、Rebase 或分支删除;这些操作仍需用户明确要求。 +7. 只报告真实执行的命令与结果,不把计划、模板、未验证行为或未执行的提交描述成完成。 +8. 说明实际读取的关键章节、使用的编号和文档成熟度;未读取的材料应能解释为与任务无关。 +9. 最终回复覆盖:修改内容、文件清单、README、模块级 AGENTS、验证命令、不过度设计说明、剩余风险,以及阶段 Commit SHA 与完整提交信息;未自动提交时说明阻塞条件。 修改项目 Skill 后运行: -- Gitee From d6e4255c60a612a9e8c8a15d9cec97c7b26658ae Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 13:47:08 +0800 Subject: [PATCH 055/118] =?UTF-8?q?docs(design):=20=E5=BB=BA=E7=AB=8B?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E6=B5=81=E7=A8=8B=E8=AE=BE=E8=AE=A1=EF=BC=9B?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=E9=9C=80=E6=B1=82=E6=8E=A5=E5=8F=A3=E4=B8=8E?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E5=8D=8F=E4=BD=9C=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- ...74\350\257\264\346\230\216\344\271\246.md" | 6 + .../interface/interface-gxy.md" | 8 +- .../interface/interface-lhc.md" | 24 +- .../interface/interface-zhy.md" | 6 +- .../process/README.md" | 221 ++++++ ...57\344\273\230\346\265\201\347\250\213.md" | 224 ++++++ ...01\347\250\213\350\256\276\350\256\241.md" | 689 ++++++++++++++++++ ...75\345\220\215\350\247\204\350\214\203.md" | 4 +- ...45\345\217\243\350\256\276\350\256\241.md" | 10 +- ...56\345\272\223\350\256\276\350\256\241.md" | 146 ++-- 11 files changed, 1253 insertions(+), 87 deletions(-) create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/README.md" create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" diff --git a/README.md b/README.md index 83f544e..518ddaa 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ ├── docs/ # 项目文档 │ ├── 00-项目要求/ # 项目要求、验收标准、评分标准(教师发布,勿改) │ ├── 01-需求文档/ # 需求规格说明书 -│ ├── 02-设计文档/ # 架构、数据库、接口与跨技术栈命名规范 +│ ├── 02-设计文档/ # 业务流程、架构、数据库、接口与跨技术栈命名规范 │ ├── 03-测试文档/ # 测试计划、测试报告 │ ├── 04-会议记录/ # 小组会议纪要 │ └── 05-总结答辩/ # 项目总结报告、答辩材料 diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 7efe1b8..ed610fd 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -11,6 +11,10 @@ |---|---|---|---| | v0.1 | 2026-07-24 | 全体成员(罗皓晨统稿) | 形成并统一需求规格,明确四类角色、必做功能、4 项选做、7 项挑战、单店 B2C 边界、核心状态与六人模块职责 | +## 业务流程设计入口 + +本文件是正式需求的唯一事实源,不再按负责人拆分个人需求文件。跨角色、跨模块和包含状态变化的详细图统一维护在 [`../02-设计文档/process/业务流程设计.md`](../02-设计文档/process/业务流程设计.md);流程图只负责可视化本文件已经确认的业务语义,不替代功能需求、异常规则和验收标准。 + ## 一、引言 ### 1.1 编写目的 @@ -130,6 +134,8 @@ flowchart LR D --> E[重复取消不重复回补] ``` +F01~F13 的分模块详细流程、跨模块分支和异常回滚见 [`业务流程设计`](../02-设计文档/process/业务流程设计.md#三f01f13-核心业务流程)。本节只保留商城总体链路和需求级取消规则。 + ### 2.4 功能模块清单 | 模块编号 | 模块名称 | 优先级 | 对应验收项 | 负责人 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" index 9abcd11..f21332d 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" @@ -15,10 +15,10 @@ - 通用约定(前缀、鉴权、成功/失败响应包装、ProblemDetails、分页、幂等、状态码等)一律以总《接口设计》第一章为准,本文件不重复,只在接口内标注差异。 - 命名(模块词根、路径、`operationId`、Schema、错误码、字段大小写)以《[命名规范.md](../命名规范.md)》为准:Catalog 词根 `categories`/`products`/`product_images`,Review 词根 `reviews`/`review_images`。 - 状态枚举对外统一使用英文 `PascalCase`(规范 2.2),不暴露整数序号;数据库落库使用 `lower_snake_case`,二者映射见第六章枚举附录。本文件所有 `status`、`stockStatus`、`reason` 字段值均为对外 PascalCase。 -- 关联的 `DBxxx` 为本人 `DB021`~`DB040` 区间的临时登记,需与 `database-gxy.md` 交叉确认后固定;当前标记“待数据库确认”。 +- 关联的 `DBxxx` 为本人 `DB021`~`DB040` 区间的临时登记,需与 `docs/02-设计文档/database/database-gxy.md` 交叉确认后固定;当前标记“待数据库确认”。 - 覆盖需求:M02-01(F04、F05)、M02-02(F06)、M06-01(F11)、M07(X01)、C04;其中 C04 复用 M02-01 的同一列表接口,仅替换底层搜索实现,返回口径不变。 -### 关联数据表(临时登记,待 `database-gxy.md` 确认) +### 关联数据表(临时登记,待 `docs/02-设计文档/database/database-gxy.md` 确认) | DBxxx | 表(标准词根) | 所属模块 | 主要接口 | |---|---|---|---| @@ -1210,7 +1210,7 @@ ## 五、待确认事项 -1. `DB021`~`DB025` 编号需与本人 `database-gxy.md` 交叉确认并固定;表字段、约束、索引以数据库设计为准。 +1. `DB021`~`DB025` 编号需与本人 `docs/02-设计文档/database/database-gxy.md` 交叉确认并固定;表字段、约束、索引以数据库设计为准。 2. A124、A142、A143 依赖 Ordering(韦乾强)提供“订单项归属 + 订单完成状态 + 是否存在订单关联”的应用契约,需在联调前确认契约形态。 3. 罗皓晨只协作提供 C07 缓存基础设施与 Key 规范;Catalog 负责人维护缓存失效时机及 C04 的 PostgreSQL `pg_trgm`/GIN 查询与索引设计。 4. 上传体积超限(A127、A141)统一引用总《接口设计》1.10 已登记的 `COMMON.PAYLOAD_TOO_LARGE`(413);本文件不重复自建 `COMMON.*` 错误码。 @@ -1227,4 +1227,4 @@ | 评价资格原因 `reason` | A143 | `Eligible` / `OrderNotCompleted` / `AlreadyReviewed` | 由订单与评价状态计算,不落库 | 可评价及不可评价原因 | | 排序方向 `sortOrder` | A102、A120、A140 | `asc` / `desc`(小写,规范 1.11.3) | — | 升序 / 降序 | -> 落库值仅为跨文档参考,最终以 `database-gxy.md` 与实体映射为准;已删除商品在购物端按“不存在”处理,不作为对外枚举值暴露。 +> 落库值仅为跨文档参考,最终以 `docs/02-设计文档/database/database-gxy.md` 与实体映射为准;已删除商品在购物端按“不存在”处理,不作为对外枚举值暴露。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" index 4fbfaa5..6640b65 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" @@ -24,17 +24,17 @@ - C07 不新增缓存管理 HTTP 接口,继续复用 Catalog 的首页和商品详情接口;缓存命中与降级不得改变公开契约。 - C10 登记存活和就绪两个公共健康检查接口。它们不使用 `/api` 前缀,也不返回通用业务包装。 - 对象存储、Redis、RabbitMQ、Outbox/Inbox 和 Worker 属于内部基础设施或异步契约,不为了占用编号而创建无业务依据的公共 HTTP 接口。 -- 当前 `database-lhc.md` 尚未建立,Messaging 接口的关联数据表暂标记为“待数据库设计确认”;接口实现前必须补齐 DBxxx、字段、约束和索引追踪。 +- 当前 `docs/02-设计文档/database/database-lhc.md` 尚未建立,Messaging 接口的关联数据表暂标记为“待数据库设计确认”;接口实现前必须补齐 DBxxx、字段、约束和索引追踪。 ## 二、接口清单 | 编号 | 模块 | 需求编号 | 名称 | 方法 | 路径 | operationId | 请求 Schema | 响应 Schema | 鉴权 | 关联 DBxxx | 状态 | |---|---|---|---|---|---|---|---|---|---|---|---| -| A501 | Messaging | X03-FR03 | 查询本人消息列表 | GET | `/api/messages` | `Messaging_ListMessages` | Query 参数 | `MessageListResponse` | BuyerOnly / MerchantOnly | 待 `database-lhc.md` 确认 | 待评审 | -| A502 | Messaging | X03-FR04 | 查询本人消息详情 | GET | `/api/messages/{messageId}` | `Messaging_GetMessage` | Route 参数 | `MessageDetailResponse` | BuyerOnly / MerchantOnly | 待 `database-lhc.md` 确认 | 待评审 | -| A503 | Messaging | X03-FR05 | 查询本人未读消息数 | GET | `/api/messages/unread-count` | `Messaging_GetUnreadCount` | 无 | `UnreadMessageCountResponse` | BuyerOnly / MerchantOnly | 待 `database-lhc.md` 确认 | 待评审 | -| A504 | Messaging | X03-FR06 | 标记本人单条消息已读 | POST | `/api/messages/{messageId}/read` | `Messaging_MarkMessageRead` | Route 参数 | `MarkMessageReadResponse` | BuyerOnly / MerchantOnly | 待 `database-lhc.md` 确认 | 待评审 | -| A505 | Messaging | X03-FR07 | 标记本人当前消息全部已读 | POST | `/api/messages/read-all` | `Messaging_MarkAllMessagesRead` | 无 | `MarkAllMessagesReadResponse` | BuyerOnly / MerchantOnly | 待 `database-lhc.md` 确认 | 待评审 | +| A501 | Messaging | X03-FR03 | 查询本人消息列表 | GET | `/api/messages` | `Messaging_ListMessages` | Query 参数 | `MessageListResponse` | BuyerOnly / MerchantOnly | 待 `docs/02-设计文档/database/database-lhc.md` 确认 | 待评审 | +| A502 | Messaging | X03-FR04 | 查询本人消息详情 | GET | `/api/messages/{messageId}` | `Messaging_GetMessage` | Route 参数 | `MessageDetailResponse` | BuyerOnly / MerchantOnly | 待 `docs/02-设计文档/database/database-lhc.md` 确认 | 待评审 | +| A503 | Messaging | X03-FR05 | 查询本人未读消息数 | GET | `/api/messages/unread-count` | `Messaging_GetUnreadCount` | 无 | `UnreadMessageCountResponse` | BuyerOnly / MerchantOnly | 待 `docs/02-设计文档/database/database-lhc.md` 确认 | 待评审 | +| A504 | Messaging | X03-FR06 | 标记本人单条消息已读 | POST | `/api/messages/{messageId}/read` | `Messaging_MarkMessageRead` | Route 参数 | `MarkMessageReadResponse` | BuyerOnly / MerchantOnly | 待 `docs/02-设计文档/database/database-lhc.md` 确认 | 待评审 | +| A505 | Messaging | X03-FR07 | 标记本人当前消息全部已读 | POST | `/api/messages/read-all` | `Messaging_MarkAllMessagesRead` | 无 | `MarkAllMessagesReadResponse` | BuyerOnly / MerchantOnly | 待 `docs/02-设计文档/database/database-lhc.md` 确认 | 待评审 | | A506 | Infrastructure | C10-FR06 | API 存活检查 | GET | `/health/live` | `Infrastructure_GetLiveness` | 无 | `HealthStatusResponse` | Anonymous | 无 | 待评审 | | A507 | Infrastructure | C10-FR06 | API 就绪检查 | GET | `/health/ready` | `Infrastructure_GetReadiness` | 无 | `ReadinessStatusResponse` | Anonymous | 无 | 待评审 | @@ -109,7 +109,7 @@ - 模块 / Tag:Messaging - 需求编号:X03-FR03、X03-FR10、X03-FR11 - 负责人:罗皓晨 -- 关联数据表:待 `database-lhc.md` 确认 +- 关联数据表:待 `docs/02-设计文档/database/database-lhc.md` 确认 - 当前状态:待评审 - 用途:按创建时间倒序分页查询当前用户自己的消息。 - 方法与路径:`GET /api/messages` @@ -210,7 +210,7 @@ - 模块 / Tag:Messaging - 需求编号:X03-FR04、X03-FR11 - 负责人:罗皓晨 -- 关联数据表:待 `database-lhc.md` 确认 +- 关联数据表:待 `docs/02-设计文档/database/database-lhc.md` 确认 - 当前状态:待评审 - 用途:查询当前用户拥有的一条完整站内消息。 - 方法与路径:`GET /api/messages/{messageId}` @@ -289,7 +289,7 @@ - 模块 / Tag:Messaging - 需求编号:X03-FR05、C06-FR05 - 负责人:罗皓晨 -- 关联数据表:待 `database-lhc.md` 确认 +- 关联数据表:待 `docs/02-设计文档/database/database-lhc.md` 确认 - 当前状态:待评审 - 用途:为消息入口角标、首次连接和断线重连补偿提供当前未读总数。 - 方法与路径:`GET /api/messages/unread-count` @@ -351,7 +351,7 @@ - 模块 / Tag:Messaging - 需求编号:X03-FR06 - 负责人:罗皓晨 -- 关联数据表:待 `database-lhc.md` 确认 +- 关联数据表:待 `docs/02-设计文档/database/database-lhc.md` 确认 - 当前状态:待评审 - 用途:幂等地记录当前用户一条消息的首次已读时间。 - 方法与路径:`POST /api/messages/{messageId}/read` @@ -418,7 +418,7 @@ - 模块 / Tag:Messaging - 需求编号:X03-FR07 - 负责人:罗皓晨 -- 关联数据表:待 `database-lhc.md` 确认 +- 关联数据表:待 `docs/02-设计文档/database/database-lhc.md` 确认 - 当前状态:待评审 - 用途:将操作开始时当前用户已经存在的未读消息批量标记为已读。 - 方法与路径:`POST /api/messages/read-all` @@ -740,7 +740,7 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 ### 7.2 实现前必须补齐 -- 创建并评审 `database-lhc.md`,登记消息表及必要的唯一约束、用户未读查询索引和关联 A501~A505。 +- 创建并评审 `docs/02-设计文档/database/database-lhc.md`,登记消息表及必要的唯一约束、用户未读查询索引和关联 A501~A505。 - 由 Ordering、Payment、AfterSales 负责人确认 5.1 已登记的事件映射和接收账号;这些契约不占 HTTP Axxx 编号。 - 在后端脚手架建立后形成真实 OpenAPI,并保证 `operationId`、Schema、状态码和错误码与本文件一致。 - 在测试计划中登记 X03、C06 和 C10 的分页、越权、重复已读、并发全部已读、断线重连、多标签页、多实例和健康检查场景。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" index 715e701..ceeb82f 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" @@ -8,7 +8,7 @@ > **当前状态**:已汇总到主文档,个人文件继续保留用于贡献与评审追踪;实现、OpenAPI 和联调以 `../接口设计.md` 为准 > **关联规范**:`docs/02-设计文档/接口设计.md` v0.1 + `docs/02-设计文档/命名规范.md` + `docs/02-设计文档/Git团队协作流程.md` > **关联根命名空间**:`Mall.Modules.Payment`、`Mall.Modules.AfterSales` -> **关联数据库**:DB081~DB100(**待评审**:`database-zhy.md` 尚未创建,本文档字段暂时按命名规范推断) +> **关联数据库**:DB081~DB100(**待评审**:`docs/02-设计文档/database/database-zhy.md` 尚未创建,本文档字段暂时按命名规范推断) --- @@ -26,7 +26,7 @@ 4. `operationId` 统一 `_`,全小写 PascalCase 拼接。 5. 业务错误码格式:`PAYMENT.` / `AFTER_SALES.`;支付对账错误统一使用 `PAYMENT.RECONCILIATION_`,仍归属 Payment 模块,不建立独立 Reconciliation 业务模块。 6. 涉及资金、状态、回调的接口强制 `Idempotency-Key`(按 1.12.1)。 -7. 字段同时承担数据库来源的,在"关联数据表"标注推断;正式评审以 `database-zhy.md` 为准。 +7. 字段同时承担数据库来源的,在"关联数据表"标注推断;正式评审以 `docs/02-设计文档/database/database-zhy.md` 为准。 --- @@ -2235,7 +2235,7 @@ RefundDetailResponse { ### 4.1 不在本文件范围 -- `database-zhy.md`(DB081~DB100)—— 单独分支 `chore/database-zhy` 推进 +- `docs/02-设计文档/database/database-zhy.md`(DB081~DB100)由数据库设计任务单独推进。 - C08 FR11~FR15 扩展接口(dev 当前只有 FR01~FR10)—— 单独 PR 处理 - 集成事件命名最终确认(与罗皓晨对齐 M00 集成事件规范) - AdminOnly Policy 命名(与罗皓晨 M00 公共 HTTP 接口对齐) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/README.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/README.md" new file mode 100644 index 0000000..6cd91f8 --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/README.md" @@ -0,0 +1,221 @@ +# 业务流程目录与填写说明 + +> 适用目录:`docs/02-设计文档/process/` +> +> 目标:按负责人分目录、按业务模块分文档维护流程;需求规格仍保持一份,不按个人拆分。 + +## 一、目录怎么分 + +```text +process/ +├─ README.md # 目录、分工、模板和检查标准 +├─ 业务流程设计.md # 全局核心基线、状态基线、跨模块交接和追踪索引 +├─ tyh/ # 唐宇昊 +├─ gxy/ # 顾欣月 +├─ zhh/ # 朱惠惠 +├─ wqq/ # 韦乾强 +├─ zhy/ # 张海洋 +└─ lhc/ # 罗皓晨 +``` + +维护边界: + +- [`业务流程设计.md`](业务流程设计.md) 只保留全局核心链路、公共状态、跨模块直接交接、文档索引和成熟度,不长期重复保存个人模块的完整细节。 +- 每位负责人只在本人目录维护文档;一个业务模块或挑战项对应一份文档。 +- 统稿人只建立目录、文件名、空模板、核心基线和交接约束,不代替负责人填写模块主流程、状态转换或异常细节。 +- 个人模块文档完成并通过交叉评审后,根文档中的同类详细图改为链接;迁移期间不得同时修改两份相同流程。 +- 文件名使用“稳定模块编号 + 中文名称 + 流程”,例如 `M04-订单流程.md`、`C03-订单超时流程.md`。 + +## 二、每个人从哪里开始 + +每位负责人只需要按下面顺序操作: + +1. 在 [`../../01-需求文档/需求规格说明书.md`](../../01-需求文档/需求规格说明书.md) 找到本人模块的完整章节,确认前置条件、主流程、异常流程、状态和验收标准。 +2. 在 [`业务流程设计.md`](业务流程设计.md) 找到本人负责的 F01~F13 核心状态和跨模块入口、出口。 +3. 在本人目录找到对应模块文档;只有负责人本人填写业务细节。 +4. 画 X/C 前先确认其基础 F、核心接入状态和不可变结果,不能另起一套主链路。 +5. 跨模块部分在本人图中标出模块节点,并请直接协作人确认;统稿人再同步根文档交接图。 +6. 业务流程评审后,按流程中的业务动作、输入输出、状态和异常结果派生接口能力,并反查接口、数据库和架构是否完整承接。 +7. 完成后更新根追踪索引的文档链接和成熟度。 + +流程图只表达业务动作、判断、状态和异常结果。HTTP 路径、DTO、数据库字段、类名和部署命令分别回到接口、数据库和架构文档维护。 + +设计顺序固定为“需求确认 → 业务流程 → 接口/数据库/架构落地”。接口跟着流程走:Axxx 只能在业务流程完成后用于契约映射,不能把接口清单直接拼成流程,也不能用现有接口的缺失或冲突反向覆盖已确认的业务状态和分支。“接口文档先行”仅表示接口必须先于代码实现和调用方修改完成确认,不表示接口先于业务流程。 + +## 三、核心流程是唯一扩展基线 + +所有 X/C 流程都必须建立在 F01~F13 之上: + +- 图内必须标明基础 F、直接模块入口、接入时的核心状态、扩展出口以及回归的核心状态。 +- 扩展可以增加自己的数据和状态,但不能自行修改核心账号、商品或订单状态机。 +- 扩展失败不能破坏核心订单、支付、库存、快照和权限结果。 +- 需要改变核心流程时,先修改主需求和对应 F 流程并重新评审,不能让扩展图反向覆盖核心流程。 +- 基础 F 尚未确认时,扩展最多标记为“待交叉评审”,不能标记为“已确认”。 + +允许分层接入,但必须最终追溯到核心 F,例如: + +```text +C06 实时推送 → X03 持久化消息 → F08/F09/F10/F12 的已提交业务事实 +C08 退款对账 → X04 售后退款 → F09/F10 的订单项和支付事实 +``` + +## 四、负责人目录与模块文档 + +| 负责人 | 目录 | 核心模块文档 | 扩展/挑战文档 | 主要联调人 | +|---|---|---|---|---| +| 唐宇昊 | `tyh/` | `M01-用户与鉴权流程.md`、`M06-03-后台用户管理流程.md` | `M08-收藏与浏览历史流程.md` | 顾欣月、罗皓晨 | +| 顾欣月 | `gxy/` | `M02-分类与商品流程.md`、`M06-01-后台商品管理流程.md` | `M07-商品评价流程.md`、`C04-中文搜索流程.md` | 朱惠惠、韦乾强、罗皓晨 | +| 朱惠惠 | `zhh/` | `M03-购物车流程.md` | `C01-秒杀流程.md` | 顾欣月、韦乾强、张海洋 | +| 韦乾强 | `wqq/` | `M04-订单流程.md`、`M06-02-商家履约流程.md` | `C03-订单超时流程.md` | 朱惠惠、张海洋、罗皓晨 | +| 张海洋 | `zhy/` | [`M05-支付流程.md`](zhy/M05-支付流程.md)、`M10-售后流程.md` | `C08-支付回调与对账流程.md` | 韦乾强、罗皓晨 | +| 罗皓晨 | `lhc/` | `M09-站内消息流程.md` | `C06-实时推送流程.md`、`C07-缓存流程.md`、`C10-高可用流程.md` | 各相关业务负责人 | + +以上只规定目录、文件名、负责人和联调关系,不代表统稿人已经替负责人完成流程内容。跨模块流程不能由单方标记为“已确认”。 + +## 五、每张流程图必须包含什么 + +一张可评审的业务流程图至少应表达: + +1. 基础 F,以及扩展从核心流程哪个状态或确定结果开始。 +2. 谁触发流程,以及触发入口。 +3. 直接上游模块、传入的业务事实和状态。 +4. 必要的登录、角色、资源归属或状态前置条件。 +5. 主要业务动作和会改变结果的判断分支。 +6. 成功结果、状态变化和直接下游模块出口。 +7. 失败、拒绝、超时或并发冲突后的状态。 +8. 涉及取消、失败时的回滚或补偿责任。 +9. 扩展完成后回到哪个核心结果,或者作为哪个核心结果的旁路能力。 +10. 不得改变的核心状态、权限、金额、库存、快照和事实来源。 + +普通流程建议控制在 8~15 个节点。图过长时按业务阶段拆成两张,不要在一个节点中堆积整段需求文字。 + +业务图中的节点使用“查询本人钱包”“确认支付”“取消订单”等业务动作,不使用 Axxx、HTTP 路径、Controller 或 DTO 名称代替。图评审完成后,可在正文后追加“流程步骤 → Axxx”的映射表,用于派生接口契约和暴露契约缺口。 + +跨模块流程实行“双表达”:业务 Mermaid 图内部必须出现直接上游模块、输入事实、入口状态、直接下游模块和确定出口;同时在《业务流程设计》3.8 更新独立的模块交接图,集中说明成功、拒绝、状态竞争和事务失败分别回到哪里。独立交接图不能替代业务图中的模块节点。 + +## 六、可直接复制的模板 + +### 6.1 普通业务流程模板 + +````markdown +## 一、X01 商品评价与重复评价拦截 + +> - 覆盖:X01、M07 +> - 主责人:顾欣月 +> - 需求来源:《需求规格说明书》M07 +> - 基础核心流程:F09、F06 +> - 直接入口:M04 已完成且属于当前买家的订单项 +> - 扩展出口:M07 评价记录;M02 商品详情公开评价 +> - 回归核心结果:订单仍为 Completed +> - 不得改变:订单快照、商品销售状态、价格和库存 + +```mermaid +flowchart TD + A["买家从已完成订单进入评价"] --> B{"已登录且订单项属于本人?"} + B -- "否" --> X["拒绝操作"] + B -- "是" --> C{"订单项是否允许评价?"} + C -- "否" --> Y["提示不可评价原因"] + C -- "是" --> D["提交评分、内容和图片"] + D --> E{"是否重复评价?"} + E -- "是" --> Z["拒绝重复提交"] + E -- "否" --> F["保存评价"] + F --> G["商品详情展示评价结果"] +``` + +关键规则: + +- 写与图中判断直接相关的规则,不复制完整需求。 +- 无待确认项时写“无”。 + +待确认: + +- 与订单模块确认“已完成”的唯一判断口径。 +```` + +### 6.2 状态流转模板 + +````markdown +## 一、X04 售后状态流转 + +> - 覆盖:X04、M10 +> - 主责人:张海洋 +> - 需求来源:《需求规格说明书》M10 +> - 基础核心流程:F09、F10、F12 +> - 直接入口:M04 本人 Paid、Shipped 或完成后 7 天内的订单项 +> - 扩展出口:M10 售后结果;退款时由 M05 幂等退回钱包 +> - 回归核心结果:订单保持原核心履约状态 +> - 不得改变:订单项实付快照和核心订单状态 + +```mermaid +stateDiagram-v2 + [*] --> 待审核: 买家提交申请 + 待审核 --> 已撤销: 买家在审核前撤销 + 待审核 --> 已拒绝: 审核拒绝 + 待审核 --> 退款中: 仅退款审核通过 + 待审核 --> 待退货: 退货退款审核通过 + 待退货 --> 待收货: 买家提交退货 + 待收货 --> 退款中: 商家确认收货 + 退款中 --> 已退款: 钱包退款成功 + 退款中 --> 退款失败: 钱包退款失败 + 退款失败 --> 退款中: 幂等重试 + 已撤销 --> [*] + 已拒绝 --> [*] + 已退款 --> [*] +``` + +关键规则: + +- 每条箭头必须能在需求中找到触发条件。 +- 不确定的状态不要自行新增,写入“待确认”。 +```` + +## 七、成熟度怎么填写 + +| 状态 | 使用条件 | +|---|---| +| 待细化 | 只有登记项,还没有完整流程图 | +| 初稿 | 已覆盖主流程和主要异常,但负责人尚未完成自查 | +| 待交叉评审 | 负责人已对照需求自查,等待关联模块确认边界 | +| 已确认 | 主责人、直接协作人和所依赖的核心 F 负责人均已确认,需求文字、状态和出入口一致 | + +成熟度只表示“流程文档的确认程度”,不代表接口、代码或测试已经完成。 +基础 F 未确认时,依赖它的扩展流程不得标记为“已确认”。 + +## 八、提交前检查 + +- [ ] 图中的 F、X、C、M 编号与需求一致。 +- [ ] 已写清基础 F、核心接入状态和回归结果。 +- [ ] Mermaid 图内部包含直接上游模块输入和直接下游模块出口。 +- [ ] 跨模块流程已同步更新 3.8 独立交接图,且包含成功与失败出口。 +- [ ] 主流程、拒绝分支、失败分支和最终结果齐全。 +- [ ] 状态名称与需求规格说明书一致。 +- [ ] 没有增加或覆盖核心账号、商品、订单或支付状态。 +- [ ] 扩展失败不会改变核心权限、金额、库存、快照和事实来源。 +- [ ] 先完成业务流程和状态评审,再建立“流程步骤 → Axxx”映射。 +- [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径或 DTO 代替流程。 +- [ ] 没有自行发明接口编号、字段名或数据库状态码。 +- [ ] 跨模块流程已找直接协作人和基础 F 负责人评审。 +- [ ] 已更新追踪矩阵或登记表中的成熟度。 +- [ ] Mermaid 代码块能够正常渲染。 +- [ ] 相对链接有效,文件保持 UTF-8 编码。 +- [ ] 已运行 `git diff --check`。 + +## 九、不会画时怎么处理 + +先按下面格式把文字发给统稿人或直接协作人,不要凭猜测补图: + +```text +流程编号: +基础核心流程: +触发人: +直接上游模块与输入: +开始状态: +正常步骤: +失败情况: +直接下游模块与输出: +最终或回归状态: +不得改变的核心结果: +当前不确定点: +``` + +业务规则未确认时,保留“待确认”,不得为了让图看起来完整而自行补造业务语义。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" new file mode 100644 index 0000000..3f90118 --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" @@ -0,0 +1,224 @@ +# M05 支付流程 + +> 负责人:张海洋 +> 覆盖:M05-01、F10 +> 基础核心流程:F08、F09 +> 直接协作:韦乾强(M04 Ordering)、罗皓晨(M09 消息) +> 文档状态:初稿,待张海洋自审及 Ordering 交叉评审 +> 需求事实源:[需求规格说明书 M05-01](../../../01-需求文档/需求规格说明书.md) 的“M05-01 模拟支付(F10)”完整七节 + +## 一、范围与事实来源 + +本模块负责买家“小金库”的余额查询、模拟充值、充值记录、统一收银台、模拟支付和支付记录查询。它不接入真实支付渠道,不处理银行卡、第三方支付账户、真实资金结算、售后退款或 C08 异步回调。 + +本文先依据需求确定业务参与者、状态、判断分支、事务结果和模块出入口,再由这些流程步骤派生接口能力。A401~A408 只用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| M05-01/F10 需求 | 完整定义 | 作为业务语义事实源 | +| 本文业务流程 | 初稿 | 先确认角色、状态、分支、事务边界和模块出入口 | +| A401~A408 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| DB081~DB100 数据库设计 | 模板/占位 | 本文不发明表字段、枚举编码和索引 | +| X04/C08 | 独立扩展 | 只登记接入点,不混入 F10 核心状态机 | + +## 二、模块直接出入口 + +```mermaid +flowchart LR + ID["M01 Identity
已认证买家、角色和账号状态"] -->|"BuyerOnly 通过"| PAY["M05 Payment
钱包、充值、支付记录"] + ORD["M04 Ordering
订单号、买家归属、持久化应付金额、当前状态和支付截止时间"] -->|"本人订单事实"| PAY + PAY -->|"充值成功:确定余额和充值流水"| BUYER["买家钱包/充值记录页面"] + PAY -->|"支付成功:确定支付记录
原子推进 PendingPayment → Paid"| ORD + ORD -->|"Paid 订单"| MERCHANT["M06-02 商家待发货入口"] + PAY -. "事务提交后的支付成功事实" .-> MSG["M09 消息持久化/通知"] + TIMEOUT["C03 超时取消"] -->|"竞争 PendingPayment"| ORD + + ID -->|"游客、非买家、账号禁用或令牌失效"| X["拒绝访问,不返回钱包与订单数据"] + ORD -->|"非本人或不存在"| Y["按不存在/无权限处理,不泄露归属"] + PAY -->|"状态竞争或事务失败"| Z["返回当前最终状态
钱包不产生部分扣款"] +``` + +边界约束: + +- M05 只接受订单标识,不接受客户端指定最终扣款金额;实际金额来自 M04 已持久化订单事实。 +- M04、M05 位于同一模块化单体事务边界时,通过公开应用能力协调原子结果,不跨模块访问内部仓储。 +- 支付成功事实只能在支付事务整体成功后交给 M09;具体事件名、Outbox 和 Worker 重试方式由系统架构设计承接,不进入业务流程图。 + +## 三、小金库充值流程 + +```mermaid +flowchart TD + ID["M01:已认证且状态正常的买家"] --> A["查询本人钱包余额"] + A --> B["买家输入模拟充值金额并携带防重复标识"] + B --> C{"金额 > 0、≤ 10000 元
且最多两位小数?"} + C -- "否" --> X["拒绝充值,不增加余额"] + C -- "是" --> D{"该防重复标识已有处理结果?"} + D -- "同标识同请求" --> E["返回首次确定结果,不重复到账"] + D -- "同标识不同金额" --> Y["拒绝标识被不同请求复用"] + D -- "否" --> F["开启充值事务"] + F --> G["原子增加本人余额并记录充值流水和首次结果"] + G --> H{"事务提交成功?"} + H -- "否" --> Z["整体回滚,余额和流水均不改变"] + H -- "是" --> I["返回最新余额和确定充值结果"] + I --> J["买家可查询本人充值记录"] +``` + +充值结果要求: + +- 同一买家、同一充值动作和同一防重复标识只产生一次到账效果。 +- 首次确定结果必须可恢复,不能只存在于会丢失的临时介质中;具体持久化方式由接口和数据库设计承接。 +- 查询和写入全部按当前买家隔离,商家与管理员没有入口。 + +## 四、收银台与模拟支付主流程 + +```mermaid +flowchart TD + ID["M01 直接输入:已认证买家"] --> A["从下单成功页、订单列表或详情进入统一收银台"] + ORD["M04 直接输入:订单号、归属、持久化应付金额、当前状态和支付截止时间"] --> A + A --> B["服务端读取本人订单事实与本人钱包余额"] + B --> C{"订单当前状态?"} + C -- "Paid" --> P0["返回已有确定支付结果,不重复扣款"] + C -- "Cancelled/其他不可支付状态" --> X["拒绝支付并展示当前最终状态"] + C -- "PendingPayment" --> D{"余额充足?"} + D -- "否" --> E["说明差额并进入第三章充值流程"] + E --> B + D -- "是" --> F["买家确认支付并携带防重复标识"] + F --> G{"该防重复标识已有处理结果?"} + G -- "同标识同请求" --> P0 + G -- "同标识不同订单/请求" --> Y["拒绝标识被不同请求复用"] + G -- "否" --> H["开启支付事务并重新读取订单、金额和余额"] + H --> I["对本人钱包执行余额充足条件扣减"] + I --> J{"余额扣减条件命中?"} + J -- "否" --> R["整体回滚并返回余额不足或当前状态"] + J -- "是" --> K["条件推进本人订单 PendingPayment → Paid"] + K --> L{"订单状态条件更新成功?"} + L -- "否" --> R + L -- "是" --> M["记录钱包流水、确定支付记录、首次结果和待发布支付成功事实"] + M --> N{"支付事务提交成功?"} + N -- "否" --> R + N -- "是" --> O["M05 直接输出:支付记录已落库,订单为 Paid"] + O --> P["M04 展示最新详情;M06-02 可查询待发货订单"] +``` + +主流程不规定数据库锁的具体取得顺序,但必须满足以下原子结果: + +```text +钱包扣款成功 ++ 支付钱包流水已写入 ++ 支付记录已写入 ++ 首次处理结果已写入 ++ 订单 PendingPayment → Paid ++ 待发布的支付成功事实已写入 += 同一事务提交成功 +``` + +任一步失败时整体回滚,不允许出现“余额已扣但订单未支付”或“订单已支付但没有支付记录”的部分结果。 + +## 五、核心状态与并发竞争 + +F10 不增加“支付中”等订单状态。同步模拟支付提交前,订单仍是 `PendingPayment`;事务成功后直接成为 `Paid`。 + +```mermaid +stateDiagram-v2 + [*] --> PendingPayment: F08 下单事务成功 + PendingPayment --> Paid: F10 支付事务成功 + PendingPayment --> Cancelled: F09 主动取消或 C03 超时取消成功 + Paid --> Paid: 重复支付返回已有结果 + Cancelled --> Cancelled: 支付重试被拒绝 +``` + +支付与取消竞争: + +```mermaid +flowchart LR + A["订单 PendingPayment"] --> B["F10 支付事务
条件推进为 Paid"] + A --> C["F09/C03 取消事务
条件推进为 Cancelled"] + B --> D{"谁先成功更新状态?"} + C --> D + D -->|"支付胜出"| E["Paid;扣款、记录和支付成功事实同时提交"] + D -->|"取消胜出"| F["Cancelled;支付整体回滚且钱包不扣款"] + D -->|"本方未胜出"| G["重新查询并返回订单当前最终状态"] +``` + +## 六、结果查询与页面反馈 + +```mermaid +flowchart TD + A["已认证买家进入钱包、收银台或支付记录页"] --> B{"查询类型"} + B -- "钱包余额" --> C["返回本人实时余额"] + B -- "充值记录" --> D["分页返回本人充值记录"] + B -- "收银台" --> E["返回本人订单金额、状态、余额和可支付性"] + B -- "订单支付结果" --> F["返回已确定支付结果
无记录时的响应语义待评审"] + B -- "支付记录/详情" --> G["仅返回本人支付记录"] + C --> H["页面展示确定状态和下一步"] + D --> H + E --> H + F --> H + G --> H + A -->|"资源非本人"| X["404/403,不泄露他人记录"] +``` + +网络中断或结果未知时,客户端使用原防重复标识重试,或执行“查询订单支付结果”动作;当前该动作映射为 A406。不得生成新标识诱导重复付款。 + +## 七、异常、回滚与责任 + +| 场景 | M05 处理 | 最终状态/责任 | +|---|---|---| +| 游客、商家、管理员访问 | 拒绝 | 不返回钱包或支付数据 | +| 订单不存在或不属于当前买家 | 按不存在/无权限处理 | 不泄露归属 | +| 充值金额非法 | 拒绝充值 | 余额、流水不变 | +| 余额不足 | 不开启或回滚支付事务 | 订单保持 `PendingPayment` | +| 订单已支付 | 返回已有确定结果 | 订单保持 `Paid`,不重复扣款 | +| 订单已取消 | 拒绝支付 | 订单保持 `Cancelled` | +| 同一防重复标识、同一请求重试 | 返回首次确定结果 | 不重复产生副作用 | +| 同一防重复标识、不同请求 | 返回标识复用冲突 | 不执行新副作用 | +| 支付与取消并发 | 状态条件唯一胜出 | `Paid` 与 `Cancelled` 只能成立一个 | +| 钱包、订单、记录或支付成功事实任一步失败 | 整个支付事务回滚 | 不产生部分扣款或部分状态 | +| 事务后消息发布失败 | 保留已提交支付事实 | M09 按架构确定的可靠机制重试 | + +## 八、由流程派生的接口契约映射 + +本节是第二至七章业务流程的下游映射,不是流程输入。先确认“要完成什么业务动作、处于什么状态、成功或失败后得到什么结果”,再决定由哪个接口承载。若现有 Axxx 与流程冲突,先记录接口缺口并修改接口设计;只有业务需求本身变化时,才回到前文重新评审流程。 + +| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +|---|---|---|---| +| 查询本人钱包余额 | A401 | 仅返回当前买家的确定余额 | 待交叉评审 | +| 对本人钱包模拟充值 | A402 | 校验金额并幂等、原子地产生余额和充值流水结果 | 待交叉评审 | +| 查询本人充值记录 | A403 | 按当前买家隔离并分页返回充值记录 | 待交叉评审 | +| 打开本人订单收银台 | A404 | 基于 M04 持久化订单事实返回金额、状态、余额和可支付性 | 待交叉评审 | +| 确认模拟支付 | A405 | 幂等提交原子支付事务,并处理与取消的状态竞争 | 待交叉评审 | +| 查询订单支付结果 | A406 | 返回当前买家该订单的确定支付结果 | 待交叉评审 | +| 查询本人支付记录 | A407 | 按当前买家隔离并分页返回支付记录 | 待交叉评审 | +| 查询本人支付详情 | A408 | 仅返回当前买家可访问的单笔支付详情 | 待交叉评审 | + +接口详细定义与实现必须承接上述流程结果。当前接口设计拟使用 `Idempotency-Key` 承载“防重复标识”,并需满足接口设计 1.12 的幂等与并发规则以及 4.6 的资金类持久化幂等约束;HTTP 状态码、请求字段和错误码不得反向写入业务图。 + +## 九、扩展接入边界 + +- X04 售后退款通过 Payment 的公开应用能力幂等退回小金库,不直接修改钱包内部数据;不属于本文 F10 主流程。 +- C08 从“支付确认阶段”接入重复/乱序回调与每日对账;在明确替换 F10 的哪个同步步骤之前,不得让同步结果和异步回调同时成为最终支付事实。 +- X03/M09 只消费支付事务提交后的支付成功事实;消息失败不能反向修改支付或订单状态。具体事件名和可靠投递机制由系统架构设计派生。 + +## 十、由流程反查出的接口与数据待评审项 + +1. 已支付订单进入收银台时,流程要求返回已有确定结果;现有 A404 同时出现“非 `PendingPayment` 返回 409”和“已支付收银台仍可读”,接口语义需要按流程统一。 +2. 同 Key、同请求重放必须返回首次确定结果;现有 A405 对已支付订单定义为 `409 PAYMENT.ALREADY_PAID`,还需区分“原 Key 重放”和“新 Key 再次请求”并确认响应。 +3. 最终扣款金额必须来自 M04 持久化订单事实;A405 的 `expectedAmount` 只能承担客户端旧值冲突保护,不能成为扣款事实。 +4. 充值和支付幂等结果必须可恢复;A405 不得把 Redis 作为唯一幂等事实,接口 4.6.2 已要求资金类幂等记录使用数据库唯一约束。 +5. F10 同步核心流程不包含迟到回调;A406 的“取消订单收到迟到成功支付”属于 C08,`PAYMENT.NOT_FOUND` 也不能只根据订单是否为 `PendingPayment/Paid` 推断。 +6. 流程只要求每个买家拥有独立钱包且无钱包记录时余额语义确定;A401 尚未确认钱包是在注册时创建还是首次查询时按需初始化。 +7. 需求尚未定义充值记录和支付记录的 `Pending/Failed/Succeeded` 状态机;A403/A407 草案中的这些状态不能反向写进流程,需先完成业务确认。 +8. DB081~DB100 尚未形成可实施的完整表定义,数据库字段、约束和索引必须由数据库设计任务另行确认。 + +## 十一、验收证据清单 + +- [ ] 合法充值即时到账,重复充值不重复增加余额。 +- [ ] 非法金额、越权钱包和非买家身份被拒绝。 +- [ ] 本人 `PendingPayment` 订单支付后,余额、流水、支付记录、订单和待发布支付成功事实一致。 +- [ ] 余额不足不扣款,订单保持 `PendingPayment`。 +- [ ] 已支付订单重复提交不重复扣款,返回既有结果。 +- [ ] 已取消订单不能通过重试进入 `Paid`。 +- [ ] 支付与主动/超时取消并发时只有一个最终状态。 +- [ ] 模拟任一步失败时事务整体回滚。 +- [ ] 网络结果未知时使用原防重复标识或结果查询动作得到确定结果。 +- [ ] 保留页面、接口响应、数据库事务结果和可靠消息重试证据。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" new file mode 100644 index 0000000..0ba020f --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" @@ -0,0 +1,689 @@ +# 业务流程设计 + +> 组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 +> +> 编写日期:2026-07-24 版本:v0.1 +> +> 当前状态:部分定义;F01~F13 已对照总需求和教师验收完成基线校准,仍待各主责人交叉评审;X/C 只能基于核心流程扩展 + +## 修订记录 + +| 版本 | 日期 | 修改人 | 修改说明 | +|---|---|---|---| +| v0.1 | 2026-07-24 | 全体成员(罗皓晨统稿) | 建立集中式业务流程设计,覆盖核心主链路,并对照 F01~F13 需求与验收校准状态、模块交接、X/C 扩展点和核心结果保护规则 | + +## 一、文档定位与事实来源 + +本文档集中维护跨角色、跨模块、包含状态或异常分支的业务流程图,用于避免在主需求正文中堆叠复杂图示。 + +成员补充流程前先阅读 [`README.md`](README.md),按负责人、固定模板、成熟度和检查清单统一维护本文档。 + +文档职责: + +- [`../../01-需求文档/需求规格说明书.md`](../../01-需求文档/需求规格说明书.md) 定义正式范围、角色、业务规则、异常和验收,是需求事实源。 +- 本文档把已确认需求转换为可评审的参与者、主流程、状态、异常和模块出入口,不新增、删除或改变需求。 +- [`../系统架构设计.md`](../系统架构设计.md) 说明 API、Application、数据库、Worker、Redis、RabbitMQ 和事务等技术实现关系。 +- [`../接口设计.md`](../接口设计.md) 根据已确认流程中的业务动作和模块交接,定义 HTTP 路径、请求响应、鉴权、错误码和幂等契约。 +- [`../数据库设计.md`](../数据库设计.md) 定义表、字段、约束、索引和状态存储。 + +发生冲突时,以教师只读基线和主需求文档中已经确认的业务语义为准;本文档必须随主需求修正,不能反向用图覆盖文字规则。 + +设计顺序为“需求确认 → 业务流程 → 接口/数据库/架构落地”。流程先确定业务要发生什么,接口再承载流程中的动作与结果;现有 Axxx 只能用于流程完成后的映射和缺口检查,不能用接口清单反向拼接业务流程。“接口文档先行”只约束代码实现阶段,即接口契约必须先于实现与调用方变更确认。 + +## 二、绘图与维护约定 + +### 2.1 图的边界 + +本文档使用: + +- `flowchart TD` 表达角色操作、业务判断、成功和失败分支; +- `stateDiagram-v2` 表达订单等业务对象的合法状态流转; +- 简单跨模块关系直接写在节点标签中,不展开 Controller、DTO、SQL 或消息中间件细节。 + +技术调用时序仍放在系统架构设计,实体关系仍放在数据库设计。 + +### 2.2 图的阅读规则 + +- 每张图标明覆盖的 M/F/X/C 编号和主责人。 +- 判断节点的分支必须有明确含义。 +- 图只展示关键路径,完整字段规则、异常文本和验收证据回到主需求对应章节阅读。 +- PostgreSQL 始终是业务事实来源;缓存、消息和前端状态不改变业务结果。 +- 跨模块流程由主责人和直接协作人共同评审。 + +### 2.3 核心流程与扩展规则 + +F01~F13 是本项目业务流程基线。X01~X04 和 C01、C03、C04、C06、C07、C08、C10 必须从核心流程的明确业务结果接入,不得另画一套互相冲突的注册、商品、订单、支付或履约主链路。 + +每个扩展流程必须写明: + +1. 基于哪些 F01~F13; +2. 从核心流程哪个结果或状态接入; +3. 扩展结束后回到哪个核心结果,或作为哪个核心结果的旁路能力; +4. 不得改变的核心状态、权限、金额、库存、快照和事实来源。 + +扩展可以拥有独立数据和独立状态,但不得自行修改核心订单状态机、公开商品可见范围或角色权限。确需改变核心流程时,必须先修订主需求和本章对应 F 流程并重新评审,再调整扩展图。基础 F 尚未确认时,扩展只能保持“初稿”或“待交叉评审”,不能标记为“已确认”。 + +## 三、F01~F13 核心业务流程 + +### 3.0 商城核心闭环总览 + +> 覆盖:F01~F13 +> +> 作用:所有 X/C 流程选择接入点时的总基线 + +```mermaid +flowchart LR + A["游客浏览已上架商品
F04~F06"] --> B{"是否执行买家操作?"} + B -- "否" --> A + B -- "是" --> B1{"已有正常买家登录态?"} + B1 -- "否" --> C["注册或登录
F01~F02"] + B1 -- "是" --> E["维护本人购物车
F07"] + C --> E + C -. "可选维护" .-> D["维护本人资料与地址
F03"] + D -. "返回购物流程" .-> E + E --> E1["选择地址并由 M01 校验本人归属"] + E1 --> F["服务端校验并提交订单
F08"] + F --> G["订单进入 PendingPayment"] + G --> H{"买家支付还是主动取消?"} + H -- "支付" --> I["小金库原子支付
F10"] + I --> J["订单进入 Paid"] + J --> K["商家查询并发货
F12"] + K --> L["订单进入 Shipped"] + L --> M["买家确认或到期自动完成
F09"] + M --> N["订单进入 Completed"] + H -- "主动取消" --> O["订单进入 Cancelled
并原子回补库存 F09"] + + P["商家维护分类、商品和上下架
F11"] -->|"已上架结果"| A + Q["管理员禁用或启用买家/商家
F13"] --> Q1{"M01 账号治理结果"} + Q1 -- "正常" --> Q2["目标用户后续可主动登录"] + Q2 -. "用户主动发起" .-> C + Q1 -- "禁用" --> Q3["旧令牌失效;后续登录和受保护请求被拒绝"] +``` + +核心闭环的不可变结果: + +- 公开浏览只暴露已上架商品;商品详情的价格和库存不能替代下单时的服务端校验。 +- 下单成功必然得到唯一 `PendingPayment` 订单、订单项与地址快照,并完成一次库存扣减。 +- `PendingPayment` 只能由一次有效支付进入 `Paid`,或由一次有效取消进入 `Cancelled`;两个结果不能同时成立。 +- 取消成功必须按原扣减通道回补库存;支付成功后不得回补。 +- 只有 `Paid` 可以发货,只有 `Shipped` 可以完成;扩展功能不得用新增订单状态覆盖这一履约主链。 + +#### 3.0.1 核心状态基线 + +流程图根据已确认需求定义状态语义和合法转换;数据库与接口只负责映射存储编码和对外表示,不得反向新增、删除或改写业务状态。 + +| 业务对象 | 核心状态 | 状态性质 | +|---|---|---| +| 账号 | 正常、禁用 | 持久化状态;决定能否登录和继续使用旧令牌 | +| 商品 | 草稿/未上架、已上架、已下架 | 持久化销售状态;只有已上架进入公开列表和搜索;删除是满足约束后的终止结果,不是继续保留的销售状态 | +| 购物车条目 | 可结算、不可结算 | 根据商品上下架、实时库存、数量和归属实时派生,不新增独立业务状态机 | +| 订单 | `PendingPayment`、`Paid`、`Shipped`、`Completed`、`Cancelled` | 持久化状态;合法转换以 3.6 为唯一流程基线 | +| 钱包与支付 | 余额、不可变流水、确定支付记录 | F10 不定义“支付中”等额外业务状态;支付成功以支付记录落库且订单进入 `Paid` 为准 | + +账号状态: + +```mermaid +stateDiagram-v2 + [*] --> Normal: 创建正常账号 + Normal --> Disabled: 管理员禁用并撤销旧令牌 + Disabled --> Normal: 管理员启用 + Normal --> Normal: 重复启用幂等返回 + Disabled --> Disabled: 重复禁用幂等返回 +``` + +商品销售状态: + +```mermaid +stateDiagram-v2 + state "草稿/未上架" as Draft + state "已上架" as OnSale + state "已下架" as OffSale + state "物理删除完成(仅无历史关联的终止结果)" as Deleted + + [*] --> Draft: 创建并保存 + Draft --> OnSale: 完整性校验通过并主动上架 + OnSale --> OffSale: 商家主动下架 + OffSale --> OnSale: 重新校验通过并上架 + Draft --> Deleted: 无历史关联且确认删除 + OffSale --> Deleted: 无历史关联且确认删除 + Deleted --> [*] +``` + +#### 3.0.2 核心模块直接出入口 + +下图只表达业务模块之间允许直接传递的公开事实,不表示可以跨模块访问内部仓储、DbContext 或数据表。 + +```mermaid +flowchart LR + M01["M01 Identity
账号、角色、地址归属"] -->|"认证主体与角色"| M03["M03 Cart
本人购物车"] + M01 -->|"买家身份与地址快照契约"| M04["M04 Ordering
订单与履约状态"] + M01 -->|"认证主体与角色"| M05["M05 Payment
钱包与支付记录"] + + M06U["M06-03 管理入口"] -->|"禁用/启用命令"| M01 + M06P["M06-01 商家入口"] -->|"分类与商品维护命令"| M02["M02 Catalog
商品、价格、库存、销售状态"] + M02 -->|"已上架商品与实时价格库存"| M03 + M02 -->|"下单重读与库存条件更新"| M04 + M03 -->|"本人选中条目与数量
不传最终金额"| M04 + M04 -->|"订单号、归属、应付金额
状态 PendingPayment"| M05 + M05 -->|"确定支付记录
条件推进 PendingPayment → Paid"| M04 + M04 -->|"本期平台经营范围内 Paid 订单与履约快照"| M06O["M06-02 商家履约入口"] + M06O -->|"条件推进 Paid → Shipped"| M04 + + M04 -. "事务提交后的订单事实" .-> EXT["X03/C03 等扩展入口"] + M05 -. "事务提交后的支付事实" .-> EXT +``` + +| 来源模块 | 直接入口数据或命令 | 目标模块 | 直接出口结果 | 边界约束 | +|---|---|---|---|---| +| M01 Identity | 已认证用户 ID、服务端角色、账号状态 | M03、M04、M05、M06 | 允许或拒绝当前操作 | 目标模块不能自行修改账号、角色或令牌状态 | +| M01 Address | 当前买家选择的地址 ID | M04 Ordering | 归属校验结果和地址快照数据 | M04 只能读取并保存快照,不能修改地址 | +| M02 Catalog | 已上架状态、实时价格、实时库存 | M03 Cart | 可加购状态、实时小计和失效原因 | M03 不占用库存,也不能决定最终订单金额 | +| M02 Catalog | 销售状态、实时价格和实时库存 | M04 Ordering | 重读、校验、条件扣减或原通道回补结果 | 购买数量来自 M03 选中条目;M04 不能绕过 Catalog 规则直接改库存 | +| M03 Cart | 本人选中条目 ID 与数量 | M04 Ordering | 下单成功后清理结果;失败时原状保留 | 购物车金额仅供预览,M04 必须重读商品事实并重新计价 | +| M04 Ordering | 本人订单号、归属、持久化应付金额、`PendingPayment` | M05 Payment | 支付准入或当前最终订单状态 | M05 不接受客户端传入最终金额 | +| M05 Payment | 幂等支付命令和确定支付事实 | M04 Ordering | `PendingPayment → Paid` 的唯一条件更新结果 | 扣款、记录和状态必须形成一个原子结果 | +| M04 Ordering | 本期平台统一经营范围内的 `Paid` 订单和必要履约快照 | M06-02 | `Paid → Shipped` 结果 | 本期不按店铺/商家拆单;商家不能修改金额、支付事实、地址或订单项快照 | +| M06-01 | 分类、商品、上下架维护命令 | M02 Catalog | 最新商品销售状态 | 后台入口不拥有第二份商品事实 | +| M06-03 | 买家/商家账号禁用或启用命令 | M01 Identity | 最新账号状态和令牌失效结果 | 不允许修改角色或管理员账号 | + +跨模块流程只能使用上表中的公开输入和确定输出。出现失败、状态竞争或依赖不可用时,调用方读取目标模块返回的最终结果,不得自行补写另一模块的数据。 + +### 3.1 注册、登录、退出与账号治理 + +> 覆盖:M01-01、M01-02、M06-03;F01、F02、F13 +> 主责:唐宇昊;公共认证能力协作:罗皓晨 + +```mermaid +flowchart TD + A["游客选择注册或登录"] --> B{"选择注册?"} + B -- "是" --> C["填写手机号、密码和确认密码"] + C --> D{"格式、密码强度和两次输入一致?"} + D -- "否" --> E["保留非敏感输入并提示字段错误"] + D -- "是" --> F{"手机号是否已注册?"} + F -- "是" --> G["拒绝重复注册,不泄露其他账号资料"] + F -- "否" --> H["固定买家角色,生成唯一用户名并哈希密码"] + H --> I["创建正常账号和默认头像"] + I --> J["展示用户名并由用户明确进入登录"] + B -- "否" --> K["输入手机号和密码"] + J --> K + K --> L{"凭据正确?"} + L -- "否" --> M["统一提示账号或密码错误"] + L -- "是" --> N{"账号状态正常?"} + N -- "否" --> O["拒绝登录并提示账号停用"] + N -- "是" --> P["M01 签发登录凭证并返回服务端确认的角色"] + P --> V["M01 直接出口:认证主体、角色和账号状态"] + V --> Q["M03/M04/M05/M06 按各自资源规则继续校验"] + Q --> R{"刷新、访问受保护资源或退出?"} + R -- "刷新/访问" --> S{"令牌有效且账号仍正常?"} + S -- "是" --> Q + S -- "否" --> T["清理失效登录态并引导重新登录"] + R -- "退出" --> U["仅使当前令牌失效并返回登录页"] +``` + +关键说明: + +- 公开注册只能创建买家账号,不能由客户端指定商家或管理员角色。 +- 注册成功只展示账号摘要并引导登录,不默认建立登录态。 +- 账号不存在和密码错误使用统一提示;账号禁用在凭据正确后单独判断。 +- 用户主动退出只撤销当前令牌;管理员禁用账号的全部旧令牌失效流程见 3.7。 +- 前端路由只改善体验,服务端仍按 JWT、Policy 和资源归属校验权限。 +- 令牌失效状态无法确认时,受保护请求必须失败关闭,不能因依赖异常继续放行。 + +### 3.2 个人资料与收货地址 + +> 覆盖:M01-03;F03 +> 主责:唐宇昊;下单协作:韦乾强 + +资料维护: + +```mermaid +flowchart TD + A["M01 入口:访问个人中心"] --> B{"当前身份?"} + B -- "游客" --> X["引导登录并保留安全返回目标"] + B -- "商家或管理员" --> Y["403:拒绝访问买家私人资源"] + B -- "买家" --> C["查看用户名、默认头像和掩码手机号"] + C --> D{"选择资料操作?"} + D -- "重置用户名" --> E{"本项目期内是否仍有一次机会?"} + E -- "否" --> F["拒绝修改并说明次数已用完"] + E -- "是" --> G["服务端重新生成全局唯一用户名"] + G --> H["保存并返回最新资料"] + D -- "修改手机号" --> I["重新验证当前密码"] + I --> I1{"当前密码正确?"} + I1 -- "否" --> K["保持原资料和当前登录态并提示原因"] + I1 -- "是" --> J{"新手机号格式正确且全局唯一?"} + J -- "否" --> K["保持原资料和当前登录态并提示原因"] + J -- "是" --> L["保存新手机号并撤销修改前全部令牌"] + L --> M["清理登录态并要求重新登录"] +``` + +地址维护: + +```mermaid +flowchart TD + A["M01 Address:已登录买家进入地址管理"] --> B["按当前买家查询本人地址"] + B --> C{"新增、编辑、设默认或删除?"} + C -- "新增" --> D["校验收件人、联系电话、地区和详细地址"] + C -- "编辑/设默认/删除" --> E{"地址是否属于当前买家?"} + E -- "否" --> X["返回不存在或无权限,不泄露归属"] + E -- "是" --> F{"具体操作?"} + F -- "编辑" --> D + F -- "设默认" --> G["原子切换,最终最多一个默认地址"] + F -- "删除普通地址" --> H["删除本人地址"] + F -- "删除默认地址" --> I["删除后保持无默认地址"] + I --> J["提示买家后续重新选择"] + D --> K{"字段是否合法?"} + K -- "否" --> L["保留输入并提示字段错误"] + K -- "是" --> M["保存并返回最新地址"] + G --> N["返回唯一默认地址结果"] + H --> O["刷新本人地址列表"] + B -. "后续下单选择任一本人地址" .-> P["M04 直接入口:重新校验地址归属并生成地址快照"] +``` + +关键说明: + +- 用户名不能由客户端任意指定,只能在限次规则内由服务端重新生成。 +- 手机号修改属于敏感操作,必须验证当前密码;成功后修改前签发的全部令牌失效。 +- 地址必须按买家隔离,不能查看或修改他人地址。 +- 删除默认地址后不自动指定其他地址;“最多一个默认地址”允许当前没有默认地址。 +- 订单保存收货信息快照,后续修改地址不改变历史订单。 + +### 3.3 商品维护、上架与购物端浏览 + +> 覆盖:M02-01、M02-02、M06-01;F04、F05、F06、F11 +> 主责:顾欣月 + +后台分类与商品生命周期: + +```mermaid +flowchart TD + A["M06-01 入口:商家通过 Policy 进入管理"] --> B{"维护分类还是商品?"} + B -- "分类" --> C["查询、新增、编辑、启用、停用或申请删除分类"] + C --> D{"申请删除且存在商品或历史引用?"} + D -- "是" --> E["拒绝删除并引导停用"] + D -- "否" --> F["保存维护结果或完成无引用分类删除"] + B -- "商品" --> G["创建或编辑名称、分类、价格、库存、图片和描述"] + G --> H{"字段、图片、分类和并发状态有效?"} + H -- "否" --> I["拒绝保存并保留表单内容"] + H -- "是" --> J["保存草稿或保持当前销售状态"] + J --> K{"主动上架、下架、删除或暂不变更状态?"} + K -- "上架" --> L{"必填信息完整且分类已启用?"} + L -- "否" --> M["拒绝上架并指出缺失项"] + L -- "是" --> N["M02 状态变为已上架"] + K -- "下架" --> O["状态变为已下架"] + K -- "删除" --> P{"当前是否已上架?"} + P -- "是" --> P1["先执行下架,状态变为已下架"] + P -- "否" --> P2{"是否存在历史订单关联?"} + P1 --> P2 + P2 -- "是" --> Q["拒绝破坏性删除并保持已下架"] + P2 -- "否" --> R["完成物理删除,进入终止结果"] + K -- "暂不变更状态" --> S["返回管理列表,保持当前销售状态"] +``` + +购物端列表、搜索与详情: + +```mermaid +flowchart TD + A["游客、买家、商家或管理员进入购物端"] --> B["提交分页、分类、关键词、价格、库存和排序条件
关键词去除首尾空白,F05 至少按商品名称模糊匹配"] + B --> C{"参数是否合法?"} + C -- "否" --> D["返回字段级错误并保留查询条件"] + C -- "是" --> E["服务端强制过滤为已上架商品"] + E --> F{"是否有匹配结果?"} + F -- "否" --> G["展示空结果并提供清空筛选"] + F -- "是" --> H["展示主图、名称、当前价格和库存摘要"] + H --> I["进入详情并展示图片、描述、分类、价格和库存"] + I --> J{"商品仍为已上架?"} + J -- "否" --> K["旧链接显示不可售或不存在,不提供购买入口"] + J -- "是" --> L{"当前角色?"} + L -- "游客" --> M["触发买家操作时引导登录并保留意图"] + L -- "商家或管理员" --> N["仅浏览公开效果,不显示买家操作"] + L -- "买家" --> O{"当前库存是否充足?"} + O -- "否" --> P["保留详情展示,标记售罄并禁用购买"] + O -- "是" --> Q["M02 输出商品事实,进入 M03 加购流程
购买继续复用 F07 购物车结算与 F08 下单主链"] +``` + +关键说明: + +- 新建或编辑商品不会自动上架;只有商家主动上架且完整性校验通过后,状态才进入“已上架”。 +- 公开列表和搜索只返回已上架商品;下架商品的旧链接只能显示不可售状态。 +- 已上架但库存为 0 的商品仍可展示详情,但必须标记售罄并禁用购买。 +- 商品下架不删除历史订单快照、购物车、收藏或浏览历史中的关联记录,但购买入口必须失效。 +- 有历史订单关联的商品不得进行破坏性删除,应使用下架表达停售。 +- 本期不引入多商家商品归属模型;后台以商家 Policy 控制入口,不在流程图中自行增加店铺或租户边界。 + +### 3.4 购物车结算与提交订单 + +> 覆盖:M03-01、M04-01;F07、F08 +> 购物车主责:朱惠惠;订单主责:韦乾强;商品协作:顾欣月 +> 直接入口:M01 提供买家与地址归属;M02 提供商品销售状态、实时价格和库存 +> 直接出口:成功生成 M04 `PendingPayment` 订单并进入 M05;失败时购物车、库存和订单均不产生部分结果 + +```mermaid +flowchart TD + ID["M01 直接输入:已认证买家"] --> A["M03:买家维护本人购物车"] + CAT["M02 直接输入:销售状态、实时价格和库存"] --> B + A --> B{"商品已上架且数量合法并不超过实时库存?"} + B -- "否" --> BX["拒绝加入或调大,返回失效原因和最大可购量"] + B -- "是" --> C["按买家+商品唯一条目新增或累加"] + C --> D["幂等键防止重复累加,服务端保存选中状态"] + D --> E["买家选择可用条目并请求结算预览"] + E --> F["服务端重读归属、销售状态、实时价格和库存并计算总额"] + F --> G{"全部条目可结算?"} + G -- "否" --> GX["整次结算失败,仅标记问题条目并保留全部购物车数据"] + G -- "是" --> H["展示服务端金额,买家选择本人地址并提交下单幂等键"] + ADDR["M01 直接输入:本人地址记录与归属"] --> J + H --> I{"该买家+幂等键已有成功订单?"} + I -- "是" --> IX["返回原订单号,不重复扣库存或清理购物车"] + I -- "否" --> J["再次校验身份、地址、购物车、商品、库存和正数总额"] + J --> K{"最终校验通过?"} + K -- "否" --> KX["拒绝提交并保留购物车条目"] + K -- "是" --> L["开启事务,逐项条件扣减库存"] + L --> M{"全部库存扣减成功?"} + M -- "否" --> R["整体回滚:库存、订单、待发布订单创建事实和购物车均恢复原状"] + M -- "是" --> N["写唯一订单、地址和商品快照、服务端总额"] + N --> O["记录待发布的订单创建事实"] + O --> P["删除本次已结算购物车条目"] + P --> Q["提交事务,订单状态为 PendingPayment"] + Q --> S["M04 直接输出:订单号、应付金额和 PendingPayment"] + S --> T["进入 M05 收银台"] + N -. "写入失败" .-> R + O -. "订单创建事实记录失败" .-> R + P -. "清理失败" .-> R + Q -. "提交失败" .-> R +``` + +关键说明: + +- 前端显示的价格和库存不能作为下单事实,提交时必须由服务端重新校验。 +- 商品价格变化只刷新服务端计价,不自动把条目标为失效;商品未上架、资源归属错误或库存不足才阻止结算。分类停用是否影响既有已上架商品购买,须由商品主责确认后再进入核心规则。 +- 购物车条目的“可结算/不可结算”是实时派生结果,提交瞬间必须再次校验。 +- 库存扣减、订单和快照、待发布订单创建事实、已结算购物车清理属于一个原子业务结果。 +- 重复提交同一幂等请求只能返回首次结果,不能重复扣库存或生成订单。 +- 事务提交后的消息发布失败由架构确定的可靠机制重试,不回滚已经提交的订单。 + +### 3.5 小金库充值与模拟支付 + +> 主责:张海洋;订单协作:韦乾强 +> 模块文档:[`zhy/M05-支付流程.md`](zhy/M05-支付流程.md) +> 当前成熟度:初稿,待主责自审及 Ordering 交叉评审 + +根文档只保留 F10 核心基线: + +- 直接入口:M01 提供正常买家身份;M04 提供本人订单号、归属、持久化应付金额、当前状态和支付截止时间。 +- 原子结果:钱包扣款、钱包流水、支付记录、首次处理结果、`PendingPayment → Paid` 和待发布支付成功事实同一事务提交。 +- 直接出口:M04 获得唯一 `Paid` 结果,M06-02 可查询待发货订单,M09 消费事务后的支付事实。 +- F10 不新增“支付中”订单状态;支付与主动/超时取消只能有一个条件更新胜出。 +- 详细充值、支付、查询、异常、回滚、流程派生接口映射和待评审项统一在模块文档维护。 + +### 3.6 订单取消、发货与完成 + +> 覆盖:M04-02、M04-03、M04-04、M06-02;F09、F12 +> 主责:韦乾强;支付协作:张海洋;定时任务实现协作:罗皓晨 +> 直接入口:M05 返回 `Paid`;M06-02 提交发货;买家或系统定时任务提交取消/完成触发 +> 直接出口:M04 返回唯一最终状态、状态时间线和必要快照;事务提交后的事实可供 X03/C03 消费 + +```mermaid +stateDiagram-v2 + [*] --> PendingPayment: F08 下单事务提交成功 + PendingPayment --> Paid: F10 本人钱包支付事务成功 + PendingPayment --> Cancelled: F09 本人主动取消事务成功 + PendingPayment --> Cancelled: C03 创建满30分钟且系统自动取消成功 + Paid --> Shipped: F12 平台运营商家发货事务成功 + Shipped --> Completed: F09 订单所属买家确认收货 + Shipped --> Completed: F09 发货满7天且系统自动完成 + Cancelled --> [*] + Completed --> [*] +``` + +订单列表、详情与操作入口: + +```mermaid +flowchart TD + ID["M01 直接输入:已认证买家"] --> A["M04:买家进入订单列表"] + A --> B["按本人、状态和分页查询,创建时间倒序"] + B --> C["查看订单摘要或进入详情"] + C --> D{"订单是否属于当前买家?"} + D -- "否" --> X["返回不存在或无权限,不泄露订单内容"] + D -- "是" --> E["展示地址和商品快照、金额、状态时间线及支付信息"] + E --> F{"当前状态?"} + F -- "PendingPayment" --> G["显示去支付和主动取消入口"] + F -- "Shipped" --> H["显示确认收货入口"] + H -. "符合售后规则" .-> AS2["X04:从 Shipped 订单项接入独立售后流程"] + F -- "Completed" --> I["显示评价入口"] + I -. "X01" .-> RV["从 Completed 订单项接入评价流程"] + I -. "符合售后期限" .-> AS3["X04:从 Completed 订单项接入独立售后流程"] + F -- "Paid" --> J["显示等待商家发货"] + J -. "符合售后规则" .-> AS1["X04:从 Paid 订单项接入独立售后流程"] + F -- "Cancelled" --> K["不显示支付、发货或完成入口"] +``` + +支付与取消竞争: + +```mermaid +flowchart TD + A["订单状态为 PendingPayment"] --> B{"触发来源?"} + B -- "买家支付" --> P["M05 按 3.5 执行支付事务"] + B -- "买家主动取消" --> C{"已登录买家且订单属于本人?"} + B -- "C03 系统超时检查" --> D{"创建满30分钟且仍为 PendingPayment?"} + C -- "否" --> X["拒绝操作"] + C -- "是" --> E["开启取消事务并条件推进 PendingPayment → Cancelled"] + D -- "否" --> Y["跳过本次任务"] + D -- "是" --> E + P --> Q{"M05 返回的支付事务是否成功?"} + Q -- "是" --> PAID["提交后最终状态为 Paid"] + Q -- "否" --> Z["读取并返回订单最终状态"] + E --> F{"状态条件更新成功?"} + F -- "否" --> Z + F -- "是" --> G["按订单项库存来源回补普通或秒杀库存"] + G --> H["记录取消时间和待发布订单取消事实"] + H --> I{"取消事务全部成功?"} + I -- "否" --> R["整体回滚;人工请求或系统定时任务可按策略重试"] + I -- "是" --> CANCELLED["提交后最终状态为 Cancelled"] +``` + +商家发货与订单完成: + +```mermaid +flowchart TD + ID["M01 直接输入:已认证且状态正常的商家"] --> A["M06-02:分页查询本期平台统一经营范围内订单并查看详情"] + ORD["M04 直接输入:Paid 订单与必要履约快照"] --> A + A --> B{"角色、授权范围和订单状态均允许发货?"} + B -- "否" --> X["拒绝发货,不允许修改金额、支付事实或快照"] + B -- "是且状态为 Paid" --> C["事务内条件推进 Paid → Shipped"] + C --> C1{"状态条件更新成功?"} + C1 -- "否" --> Y["返回订单当前状态,不重复发货"] + C1 -- "是" --> D["记录发货时间和待发布订单发货事实"] + D --> E{"事务提交成功?"} + E -- "否" --> Y2["整体回滚,保持原状态并允许安全重试"] + E -- "是" --> F["M04 直接输出:订单状态为 Shipped,买家可查看结果"] + F --> G{"完成触发来源?"} + G -- "买家确认" --> H{"买家已登录、订单属于本人且仍为 Shipped?"} + G -- "系统自动完成" --> I{"发货满7天且仍为 Shipped?"} + H -- "否" --> Z["拒绝操作或返回已有完成结果"] + I -- "否" --> W["跳过本次任务"] + H -- "是" --> J["条件推进 Shipped → Completed,记录买家确认方式"] + I -- "是" --> K["条件推进 Shipped → Completed,记录自动完成方式"] + J --> M{"状态条件更新成功?"} + K --> M + M -- "否" --> Z2["返回已有完成结果"] + M -- "是" --> L["记录完成时间、完成方式和待发布订单完成事实"] + L --> N{"完成事务提交成功?"} + N -- "否" --> V["整体回滚,保持 Shipped 并允许安全重试"] + N -- "是" --> O["M04 直接输出:唯一 Completed 结果"] +``` + +关键说明: + +- 买家列表和详情只返回本人订单;本期平台运营商家共享统一经营订单的履约范围,不按店铺或商家账号拆单。 +- 买家支付、主动取消和 C03 超时取消竞争 `PendingPayment`,数据库状态条件决定唯一胜出结果。 +- 取消、原库存通道回补和待发布订单取消事实处于同一事务,重复取消不能重复回补。 +- 商家只能条件推进 `Paid → Shipped`;买家确认与系统自动完成只能竞争 `Shipped → Completed`。 +- 正式自动完成期限为发货满 7 天;C03 的正式超时取消期限为订单创建满 30 分钟。 +- X01 评价和 X04 售后只能从已确认的订单状态接入,不得反向覆盖核心订单状态。 + +### 3.7 后台角色与操作边界 + +> 覆盖:M06-01、M06-02、M06-03;F11、F12、F13 +> 主责:顾欣月、韦乾强、唐宇昊 +> 直接入口:M01 返回认证主体、角色和账号状态 +> 直接出口:商品命令交给 M02,履约命令交给 M04,账号治理命令交给 M01 + +```mermaid +flowchart TD + A["访问后台入口"] --> B{"是否已认证?"} + B -- "否" --> X["要求登录,不返回后台数据"] + B -- "是" --> C{"服务端确认角色和账号状态"} + C -- "账号禁用" --> X1["拒绝后台访问并清理失效登录态"] + C -- "买家" --> Y["403:拒绝进入后台业务"] + C -- "商家" --> D["进入商家端"] + D --> E{"选择商品管理还是订单履约?"} + E -- "商品" --> F["通过商家 Policy 向 M02 提交分类和商品命令"] + E -- "订单" --> G["向 M04 查询可处理订单并按合法状态发货"] + C -- "管理员" --> H["进入用户管理"] + H --> I["分页筛选买家和商家账号,手机号默认掩码"] + I --> J{"目标账号和操作是否合法?"} + J -- "否" --> Z["拒绝管理员账号、角色修改或其他越权操作"] + J -- "是" --> K{"禁用还是启用?"} + K -- "禁用" --> L["M01 将账号状态改为禁用并撤销旧令牌"] + K -- "启用" --> M["M01 将账号状态恢复正常"] + L --> N["目标无法登录,禁用前令牌不能访问受保护资源"] + M --> O["禁用前令牌不恢复,目标必须重新登录"] +``` + +关键说明: + +- 后台只是按角色组织入口,账号、商品和订单事实仍分别归 M01、M02、M04。 +- 商家不能通过商品或订单后台修改买家账号、支付事实或订单金额。 +- 管理员账号治理不提供角色修改、提权或普通用户私人资料编辑。 +- 后台页面隐藏入口不能替代服务端 Policy 和资源归属校验。 +- 重复禁用或启用按当前状态幂等返回;令牌失效结果无法确认时,禁用不得返回虚假成功。 +- 本期不定义管理员创建商家或修改角色流程,商家账号继续由已确认的受控初始化方式提供。 + +### 3.8 核心模块直接出入口详图 + +本节单独展示模块交接,便于接口、数据库和联调评审。每条实线都是同步业务入口或确定结果;虚线是事务提交后的扩展出口。任何失败出口都必须返回调用方,不允许调用方越过目标模块直接改表。 + +#### 3.8.1 Identity、Catalog 向 Cart、Ordering 提供输入 + +```mermaid +flowchart LR + A["M01 Identity
认证主体、角色、账号状态"] -->|"允许买家操作"| B["M03 Cart"] + A -->|"允许买家下单"| C["M04 Ordering"] + D["M01 Address
地址归属与快照契约"] -->|"本人地址校验成功"| C + E["M02 Catalog
销售状态、实时价格、实时库存"] -->|"加购/改数量校验"| B + B -->|"本人选中条目ID与数量
不含最终金额"| C + E -->|"下单重读与库存条件扣减"| C + + A -->|"未认证/角色错误/账号禁用"| X["401、403 或登录失效结果"] + D -->|"不存在或不属于买家"| Y["M04 拒绝整次下单"] + E -->|"商品未上架或库存不足"| Z["M03 标记问题条目
M04 拒绝整次提交"] +``` + +#### 3.8.2 Cart、Ordering、Payment 的交易交接 + +```mermaid +flowchart LR + A["M03 Cart
本人选中条目"] -->|"选中条目与数量"| B["M04 Ordering
服务端重读并计价"] + B -->|"事务成功"| C["订单 PendingPayment
库存已扣、快照已保存、购物车已清理"] + B -->|"事务失败"| X["库存与购物车保持原结果
不产生部分订单"] + C -->|"订单号、归属、应付金额"| D["M05 Payment"] + D -->|"支付事务成功"| E["支付记录已落库
订单 Paid"] + D -->|"余额不足或状态竞争失败"| F["订单保持当前最终状态
钱包不产生部分扣款"] + E -->|"Paid 订单"| G["M06-02 待发货入口"] + C -->|"买家主动取消"| H["M04 取消事务"] + H -->|"取消成功"| I["订单 Cancelled
原库存通道已回补"] + H -->|"状态竞争或事务失败"| J["返回当前订单状态
库存不产生部分回补"] +``` + +#### 3.8.3 Ordering 与商家履约、买家完成的交接 + +```mermaid +flowchart LR + A["M04 Ordering
Paid 订单与履约快照"] -->|"本期平台统一经营范围内查询"| B["M06-02 商家履约"] + B -->|"合法发货命令"| C["M04 条件推进 Paid → Shipped"] + B -->|"越权或状态非法"| X["拒绝发货并返回当前状态"] + C -->|"状态竞争或事务失败"| X + C -->|"Shipped 详情"| D["订单所属买家"] + D -->|"主动确认收货"| E["M04 条件推进 Shipped → Completed"] + W["系统定时任务
发货满7天"] -->|"自动完成触发"| E + E -->|"只提交一次"| F["Completed、完成时间和完成方式"] + E -->|"状态竞争或事务失败"| Y["返回当前状态,不重复发货或完成"] + C -. "事务提交后的发货事实" .-> MSG["X03 消息扩展入口"] + F -. "事务提交后的完成事实" .-> MSG +``` + +#### 3.8.4 后台入口与业务事实所属模块 + +```mermaid +flowchart LR + A["M06 后台统一入口"] --> A1{"已认证且账号正常?"} + A1 -- "否" --> X["拒绝访问并清理失效登录态"] + A1 -- "是" --> B{"服务端角色?"} + B -- "商家商品管理" --> C["M02 Catalog
分类与商品事实"] + B -- "商家订单履约" --> D["M04 Ordering
订单状态与快照"] + B -- "管理员账号治理" --> E["M01 Identity
账号状态与令牌失效"] + C -->|"最新销售状态"| F["F04~F06 购物端"] + D -->|"Shipped/Completed"| G["F09 买家订单"] + E -->|"正常/禁用"| H["F02 登录与全部受保护入口"] +``` + +直接交接约束: + +- 调用方只传业务标识和必要命令,目标模块重新校验当前身份、归属和状态。 +- 成功出口必须是已经提交的确定业务结果;处理中页面、前端缓存和推送消息不能作为模块交接事实。 +- 失败出口必须说明是认证、授权、资源归属、业务状态还是依赖异常,调用方不得自行猜测成功。 +- 跨模块事务按照系统架构已确认的模块化单体边界协调;不得用跨模块内部仓储依赖替代公开应用接口。 + +## 四、核心流程追踪矩阵 + +| 教师编号 | 核心状态或确定结果 | 直接入口 → 直接出口 | 本文流程 | 主责人 | 当前成熟度 | +|---|---|---|---|---|---| +| F01 | 创建“正常”买家账号 | 游客注册 → F02 登录 | 3.1 | 唐宇昊 | 基线已校准,待主责确认 | +| F02 | 有效令牌 + 服务端角色;退出后当前令牌失效 | M01 → M03/M04/M05/M06 | 3.1、3.8.1 | 唐宇昊 | 基线已校准,待主责确认 | +| F03 | 本人资料与地址;敏感修改后令牌状态明确 | M01 Address → M04 地址快照 | 3.2、3.8.1 | 唐宇昊 | 基线已校准,待主责确认 | +| F04 | 只返回已上架商品的分页列表 | M02 → 购物端列表 | 3.3 | 顾欣月 | 基线已校准,待主责确认 | +| F05 | 安全的关键词/组合查询结果 | 查询条件 → M02 → F04 列表 | 3.3 | 顾欣月 | 基线已校准,待主责确认 | +| F06 | 公开详情、最新价格库存和明确可售状态 | F04 列表 → M02 详情 → M03 | 3.3、3.8.1 | 顾欣月 | 基线已校准,待主责确认 | +| F07 | 本人购物车;条目可结算状态实时派生 | M01/M02 → M03 → M04 | 3.4、3.8.1 | 朱惠惠 | 基线已校准,待 Cart/Ordering 评审 | +| F08 | 唯一 `PendingPayment` 订单、快照、扣减库存和清理结果 | M01/M02/M03 → M04 → M05 | 3.4、3.8.2 | 韦乾强 | 基线已校准,待 Cart/Catalog 评审 | +| F09 | 本人订单可查;合法到达 `Cancelled` 或 `Completed` | M04/M05/系统定时任务 → M04 | 3.6、3.8.2~3.8.3 | 韦乾强 | 基线已校准,待 Payment 评审 | +| F10 | 确定支付记录且订单进入 `Paid` | M04 → M05 → M04/M06-02 | [M05 支付流程](zhy/M05-支付流程.md)、3.8.2 | 张海洋 | 初稿,待主责自审及 Ordering 交叉评审;C08 接入语义待决 | +| F11 | 商品处于草稿/未上架、已上架、已下架,或满足约束后完成删除 | M06-01 → M02 → F04~F06 | 3.3、3.7、3.8.4 | 顾欣月 | 基线已校准,待主责确认 | +| F12 | 合法 `Paid → Shipped` 并记录发货事实 | M04 → M06-02 → M04 | 3.6、3.7、3.8.3 | 韦乾强 | 基线已校准,待主责确认 | +| F13 | 买家/商家账号“正常 ↔ 禁用”,旧令牌结果明确 | M06-03 → M01 → 全部受保护入口 | 3.1、3.7、3.8.4 | 唐宇昊 | 基线已校准,待主责确认 | + +## 五、选做与挑战流程登记 + +以下流程仍以主需求中的文字规则为准。每项必须从表中核心状态或确定结果接入,扩展失败不得破坏“不可变核心结果”。 + +| 编号 | 基础核心流程 | 直接扩展入口 → 出口 | 不可变核心结果 | 主责人 | 当前状态 | +|---|---|---|---|---|---| +| X01 | F09、F06 | `Completed` 订单项 → 评价记录 → F06 公开评价 | 订单保持 `Completed`;快照、商品状态、价格和库存不变;同一订单项最多一条评价 | 顾欣月 | 待细化 | +| X02 | F02、F06,展示衔接 F04 | 已登录买家查看详情 → 本人收藏/浏览记录 | 不修改商品事实;游客不产生个人记录;严格按买家隔离 | 唐宇昊 | 待细化 | +| X03 | F08、F09、F10、F12;X04 可追加来源 | 核心事务提交事件 → 消息落库/查询/已读/离线补查 | 消息失败不回滚核心事务,也不能反向修改订单或支付状态 | 罗皓晨 | 待细化 | +| X04 | F09、F10、F12 | 本人 `Paid/Shipped/Completed` 订单项 → 独立售后状态 → 幂等退款 | 订单核心状态和快照不被“已退款”覆盖;退款不超实付且不重复入账 | 张海洋 | 待细化 | +| C01 | F11 + F04/F06 → F08 → F10/F09/F12 | F11 商家创建/发布活动;F06 买家进入秒杀入口 → 独立库存条件扣减 → 汇入 `PendingPayment` | 后续复用核心支付和履约;取消只回补原秒杀库存;支付成功不得回补 | 朱惠惠 | 待细化;库存划拨口径待确认 | +| C03 | F08、F10、F09 | `PendingPayment` 创建满 30 分钟 → 系统定时任务复用取消流程 → `Cancelled` | 与支付只能一个胜出;取消和原库存通道回补原子且幂等 | 韦乾强 | 部分定义 | +| C04 | F04、F05、F06 | 替换 F05 查询实现 → 返回同口径 F04 列表 → F06 详情 | 只公开已上架商品;权限、筛选口径和下单重校验不变 | 顾欣月 | 待细化 | +| C06 | 经 X03 接入 F08/F09/F10/F12 | X03 消息成功落库 → 实时推送/重连 → X03 补查 | 推送失败不改变消息事实和核心事务;客户端不得仅凭推送改状态 | 罗皓晨 | 待细化 | +| C07 | F04、F06、F11,约束 F08 | F04/F06 数据库读取前 Cache-Aside;F11 提交后失效 | PostgreSQL 仍是事实源;Redis 故障只影响性能;F08 始终重读数据库 | 罗皓晨、顾欣月 | 待细化;TTL 上限待确认 | +| C08 | F10、F09;退款对账关联 X04 | F10 支付确认阶段 → 回调幂等/乱序 → 稳定结果与每日对账 | 不重复扣款;`Cancelled` 收到迟到成功不得变为 `Paid`,只登记差异 | 张海洋 | 待细化;与同步 F10 的替换边界待确认 | +| C10 | F01~F13 全部横切 | 统一入口 → 双实例分发/故障切换 → 同一业务结果 | API、鉴权、权限、状态机和数据库结果不变;实例切换不得重复写或越权 | 罗皓晨 | 待细化 | + +当前不得直接冻结的三项: + +1. C01 必须先确认秒杀库存从普通库存划拨或冻结的口径,避免两个库存通道共同超用总库存。 +2. C08 必须确认其替换 F10 的哪个支付确认步骤,不能让同步支付和异步回调同时成为最终支付事实。 +3. C07 必须在设计与验收前确定 TTL、主动失效和失败重试的最长旧值窗口。 + +## 六、维护与评审规则 + +1. 负责人先在主需求对应模块章节中确认业务语义、状态和异常边界。 +2. 先完成业务流程、状态和直接模块出入口,再由流程步骤派生接口能力;不得按 Axxx 清单拼接流程。 +3. 先确认所依赖的 F01~F13 核心流程,再选择扩展入口、出口和不可变核心结果。 +4. 跨模块流程由扩展主责人和所依赖的核心 F 负责人共同评审直接出入口。 +5. 扩展图只能增加独立能力,不得复制或改写整条核心主链;需要改变核心时先修订需求和核心流程。 +6. 流程评审通过后,同一任务同步主需求文字、核心/扩展流程图和追踪矩阵。 +7. 根据已确认流程同步接口设计和数据库设计;接口契约确认后再修改代码及调用方。 +8. 涉及事务、Worker、消息、缓存或部署的技术实现,再同步系统架构设计。 +9. 图中不得使用未确认的 Axxx、DBxxx、字段、状态编码或实现完成度。 +10. 每轮修改检查 Mermaid 语法、相对链接、UTF-8、术语和 `git diff --check`。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" index 8a8ac04..051c625 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" @@ -101,8 +101,8 @@ - 图片和附件使用小写 `kebab-case`,例如 `order-checkout-flow.png`,不使用 `截图1.png`、`最终版2.png`。 - 文件名不得包含姓名、日期或版本,除非日报、周报、Migration、发布材料等规则明确要求。 - 接口并行设计阶段统一在 `docs/02-设计文档/interface/` 保存个人原稿,固定使用 `interface-<姓名拼音首字母>.md`,例如 `interface-tyh.md`;首字母必须全小写。 -- 数据库并行设计阶段仍在 `docs/02-设计文档/` 保存 `database-<姓名拼音首字母>.md`,例如 `database-lhc.md`,不另建个人文件夹。 -- 个人接口原稿用于贡献与交叉评审追踪,汇总后继续保留;`接口设计.md` 是实现、OpenAPI、联调和测试的唯一接口事实源,个人原稿不得覆盖总文档。个人数据库文件按《数据库设计》的汇总规则处理,最终数据库事实源仍为 `数据库设计.md`。 +- 数据库并行设计阶段统一在 `docs/02-设计文档/database/` 保存个人原稿,固定使用 `database-<姓名拼音首字母>.md`,例如 `database-lhc.md`;首字母必须全小写。 +- 个人接口和数据库原稿汇总后继续保留,但不得覆盖对应的 `接口设计.md` 和 `数据库设计.md` 主事实源。 ## 四、Vue 3、TypeScript、Vite、Pinia、Axios与UI diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" index 0aa4eee..3e52661 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" @@ -7190,7 +7190,7 @@ RefundDetailResponse { - 模块 / Tag:Messaging - 需求编号:X03-FR03、X03-FR10、X03-FR11 - 负责人:罗皓晨 -- 关联数据表:待 `database-lhc.md` 确认 +- 关联数据表:待 `docs/02-设计文档/database/database-lhc.md` 确认 - 当前状态:待交叉评审 - 用途:按创建时间倒序分页查询当前用户自己的消息。 - 方法与路径:`GET /api/messages` @@ -7291,7 +7291,7 @@ RefundDetailResponse { - 模块 / Tag:Messaging - 需求编号:X03-FR04、X03-FR11 - 负责人:罗皓晨 -- 关联数据表:待 `database-lhc.md` 确认 +- 关联数据表:待 `docs/02-设计文档/database/database-lhc.md` 确认 - 当前状态:待交叉评审 - 用途:查询当前用户拥有的一条完整站内消息。 - 方法与路径:`GET /api/messages/{messageId}` @@ -7370,7 +7370,7 @@ RefundDetailResponse { - 模块 / Tag:Messaging - 需求编号:X03-FR05、C06-FR05 - 负责人:罗皓晨 -- 关联数据表:待 `database-lhc.md` 确认 +- 关联数据表:待 `docs/02-设计文档/database/database-lhc.md` 确认 - 当前状态:待交叉评审 - 用途:为消息入口角标、首次连接和断线重连补偿提供当前未读总数。 - 方法与路径:`GET /api/messages/unread-count` @@ -7432,7 +7432,7 @@ RefundDetailResponse { - 模块 / Tag:Messaging - 需求编号:X03-FR06 - 负责人:罗皓晨 -- 关联数据表:待 `database-lhc.md` 确认 +- 关联数据表:待 `docs/02-设计文档/database/database-lhc.md` 确认 - 当前状态:待交叉评审 - 用途:幂等地记录当前用户一条消息的首次已读时间。 - 方法与路径:`POST /api/messages/{messageId}/read` @@ -7499,7 +7499,7 @@ RefundDetailResponse { - 模块 / Tag:Messaging - 需求编号:X03-FR07 - 负责人:罗皓晨 -- 关联数据表:待 `database-lhc.md` 确认 +- 关联数据表:待 `docs/02-设计文档/database/database-lhc.md` 确认 - 当前状态:待交叉评审 - 用途:将操作开始时当前用户已经存在的未读消息批量标记为已读。 - 方法与路径:`POST /api/messages/read-all` diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241.md" index 83909d5..f63d913 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241.md" @@ -6,66 +6,60 @@ ## 修订记录 | 版本 | 日期 | 修改人 | 修改说明 | -|------|------|--------|----------| -| v0.1 | 2026-07-24 | 罗皓晨 | 增加 DBxxx 表编号、六人区间、模块归属和统一表设计模板 | +|---|---|---|---| +| v0.1 | 2026-07-24 | 罗皓晨 | 明确 DBxxx 分工、个人原稿、单表模板、汇总和冻结规则 | ## 一、设计说明 -- 数据库:PostgreSQL,通过 EF Core 10 与 Npgsql 访问;精确版本以系统架构和依赖锁定结果为准。 -- 编码:统一使用 UTF-8。 -- Schema:默认使用 `public`,未经架构评审不按模块拆分数据库 Schema。 -- 命名:遵循《命名规范》第六章;表名使用复数 `snake_case`,主键使用 `id`,时间点使用 `_at`。 -- PostgreSQL 是业务事实来源;Redis、RabbitMQ、缓存和进程内存不得保存无法恢复的唯一业务事实。 -- 每个模块负责人只设计本人模块拥有的数据,不得通过直接修改其他模块表代替公开接口、Application 契约或集成事件。 +- 数据库使用 PostgreSQL,通过 EF Core 10 与 Npgsql 访问。 +- PostgreSQL 是业务事实来源;Redis、RabbitMQ、对象存储和进程内存不登记为业务表。 +- [《命名规范》](命名规范.md)第六章是表、字段、约束、索引和 Migration 命名的唯一事实源,本文件不重复命名规则。 +- [《接口设计》](接口设计.md)是 Axxx、字段和状态输入来源;接口未确认时,关联表必须标记“待接口确认”。 +- `数据库设计.md` 是 EF Core 实体、映射、Migration、初始化和测试的唯一数据库设计事实源。 +- `database/database-<姓名拼音首字母>.md` 是个人贡献原稿,汇总后继续保留,但不能覆盖主文档。 +- 当前尚未汇总完整表定义,文档成熟度为 **模板/占位,未冻结**。 -## 二、表编号、分工与编写要求 +## 二、DBxxx 分工与个人原稿 -### 2.1 DBxxx 编号与六人分配 +### 2.1 编号分配 -数据库表使用 `DB` 加三位数字作为文档追踪编号。编号不进入 PostgreSQL 真实表名、EF Core 实体名、DbSet、约束名或 Migration 名。 +DBxxx 只用于文档追踪和分工,不进入真实表名、实体、DbSet、约束、索引或 Migration 名。 -| 负责人 | 负责模块 | 数据表编号范围 | -|---|---|---| -| 唐宇昊 | Identity、Engagement | `DB001`~`DB020` | -| 顾欣月 | Catalog、Review | `DB021`~`DB040` | -| 朱惠惠 | Cart、Seckill | `DB041`~`DB060` | -| 韦乾强 | Ordering | `DB061`~`DB080` | -| 张海洋 | Payment、AfterSales | `DB081`~`DB100` | -| 罗皓晨 | Messaging、可靠事件公共表 | `DB101`~`DB120` | +| 负责人 | 负责模块 | DBxxx 范围 | 个人原稿 | +|---|---|---|---| +| 唐宇昊 | Identity、Engagement | `DB001`~`DB020` | `database/database-tyh.md` | +| 顾欣月 | Catalog、Review | `DB021`~`DB040` | `database/database-gxy.md` | +| 朱惠惠 | Cart、Seckill | `DB041`~`DB060` | `database/database-zhh.md` | +| 韦乾强 | Ordering | `DB061`~`DB080` | `database/database-wqq.md` | +| 张海洋 | Payment、AfterSales | `DB081`~`DB100` | `database/database-zhy.md` | +| 罗皓晨 | Messaging、可靠事件公共表 | `DB101`~`DB120` | `database/database-lhc.md` | -编号规则: +规则: -1. 一张 PostgreSQL 持久化表占用一个 DBxxx 编号;Redis Key、RabbitMQ 资源、对象存储 Bucket、视图展示项和 C10 部署资源不占用表编号。 -2. 每名成员只能在本人区间内新增表。表合入 `dev` 后编号保持稳定,表重命名但数据职责不变时保留编号。 -3. 表拆分时原表保留原编号,新表使用新编号;废弃表保留编号并标记“已废弃”,不得重新分配。 -4. 不得为了占满 20 个编号提前创建无业务依据的表;超出本人区间时由全组评审后重新分配。 -5. 跨模块业务事实由事实所属模块登记。调用方可以记录关联编号,但不得建立同义表或直接修改所属模块内部数据。 -6. 每个已登记表必须补齐字段、主键、外键、唯一约束、Check 约束、索引、状态含义和关联接口;只有表名没有完整定义时仍属于“部分定义”,不得直接生成 Migration。 +1. 一张 PostgreSQL 持久化表占一个 DBxxx;不得为了占满区间拆表。 +2. 每人只使用本人区间,只设计本人模块拥有的数据。 +3. 编号进入主文档后不得复用;拆表使用新编号,废弃表保留原编号并标记。 +4. 跨模块业务事实只由事实所有者登记,其他模块只能保存稳定引用或必要快照。 -### 2.2 个人数据库文件与汇总要求 +### 2.2 编写与汇总 -六名成员分别在 `docs/02-设计文档/` 下创建以下文件,不创建个人文件夹: +1. 每人只修改本人原稿,先登记表清单,再按第三章补齐完整定义。 +2. 个人原稿阶段不同时修改主文档,避免六人冲突。 +3. 六份原稿分别交叉评审并合入 `dev` 后,由罗皓晨在独立整合分支汇总本文件。 +4. 首次汇总后,个人原稿继续保留;后续变更必须在同一任务中同步个人原稿和主文档。 +5. 分支、提交、PR 和评审流程统一遵循[《Git 团队协作流程》](Git团队协作流程.md),本文件不重复规定。 -| 负责人 | 个人数据库文件 | 数据表编号范围 | -|---|---|---| -| 唐宇昊 | `database-tyh.md` | `DB001`~`DB020` | -| 顾欣月 | `database-gxy.md` | `DB021`~`DB040` | -| 朱惠惠 | `database-zhh.md` | `DB041`~`DB060` | -| 韦乾强 | `database-wqq.md` | `DB061`~`DB080` | -| 张海洋 | `database-zhy.md` | `DB081`~`DB100` | -| 罗皓晨 | `database-lhc.md` | `DB101`~`DB120` | +## 三、个人原稿内容 -每个个人数据库文件必须: +### 3.1 表登记清单 -1. 只使用本人 DBxxx 编号区间,只设计本人负责模块拥有的数据。 -2. 先给出表登记清单,再按 2.3 节模板逐张补齐字段、约束、索引、关系、删除行为和状态规则。 -3. 每张表列出关联 Axxx;接口尚未确认时标记“待接口确认”,不得自行编造接口编号。 -4. 个人文件是协作阶段材料,不是长期事实源。全部文件通过交叉评审后,由罗皓晨汇总到本文件并统一 ER 图、表清单和完整表定义。 -5. 汇总完成并确认无遗漏后,在同一文档任务中删除六份个人数据库文件;历史贡献通过 Git 记录保留,避免长期维护两套数据库事实。 +每份个人原稿先登记本人实际需要的表,不要求占满编号: -### 2.3 单张表详细定义模板 +| DBxxx | 真实表名 | 表用途 | 关联需求 | 关联接口 | 状态 | +|---|---|---|---|---|---| +| DBxxx | `table_name` | 说明唯一业务事实 | Mxx / Fxx / Xxx / Cxx | Axxx / 待接口确认 | 待评审 | -每名成员在本人 `database-<姓名拼音首字母>.md` 中按以下格式补齐每张表: +### 3.2 单表详细定义 ```markdown #### DBxxx 中文表名(real_table_name) @@ -73,10 +67,11 @@ - 负责人: - 所属模块: - 表用途: -- 关联接口:Axxx +- 关联需求: +- 关联接口: - 当前状态:待评审 -| 字段名 | PostgreSQL 类型 | 允许空 | 默认值 | 说明 | +| 字段名 | PostgreSQL 类型 | 允许空 | 默认值 | 说明与校验 | |---|---|---:|---|---| 约束: @@ -86,28 +81,59 @@ 索引: -| 索引名 | 字段 | 是否唯一 | 服务场景 | +| 索引名 | 字段与顺序 | 是否唯一 | 服务的接口或查询 | |---|---|---:|---| 关系与删除行为: -状态值与转换规则: +状态值与转换: + +并发、幂等与事务: + +敏感数据与脱敏: + +验证场景: ``` -表设计要求: +只有表名或字段列表不算完整定义。字段类型、空值、默认值、主外键、唯一/Check 约束、索引用途、删除行为和状态转换均必须明确。 + +## 四、主文档汇总 + +### 4.1 统一表清单 + +| DBxxx | 真实表名 | 所属模块 | 负责人 | 表用途 | 关联需求/接口 | 状态 | +|---|---|---|---|---|---|---| +| 待汇总 | 待汇总 | 待汇总 | 待汇总 | 六份个人原稿尚未完成 | 待汇总 | 模板/占位 | + +### 4.2 跨模块关系 + +跨模块关系由双方评审,不能由引用方单方面决定对方表结构: + +| 引用方 | 事实所有者 | 引用内容 | 引用方式 | 快照要求 | 状态 | +|---|---|---|---|---|---| +| Cart、Engagement、Review | Identity、Catalog | 用户 ID、商品 ID | 稳定 ID | 无 | 待确认 | +| Ordering | Identity、Catalog | 买家、商品、地址、成交信息 | 稳定 ID + 订单快照 | 必须 | 待确认 | +| Payment、AfterSales | Ordering | 订单、订单项、实付和状态 | 应用契约 + 稳定 ID | 按金额事实确认 | 待确认 | +| Messaging | Ordering、Payment、AfterSales | 接收人和业务目标 | 集成事件 + 稳定 ID | 消息展示摘要 | 待确认 | + +跨模块物理外键如需建立,必须由双方确认删除行为;禁止跨模块级联删除,也不得借外键直接读写其他模块内部表。 + +### 4.3 ER 图 + +六份原稿通过评审后,由罗皓晨生成全局 ER 图。ER 图覆盖已确认的主键、主要外键和关系基数,但不替代字段、约束、索引和状态定义。 -- 表名使用复数 `snake_case`;主键使用 `id`;外键列使用 `<单数实体>_id`。 -- 布尔字段使用 `is_` 或 `has_`;时间点使用 `_at`;金额使用 `_amount`;状态值使用稳定 `lower_snake_case`。 -- 字段必须写明 PostgreSQL 类型、空值、默认值和业务含义,不使用 `TINYINT`、`DATETIME`、`utf8mb4` 等 MySQL 专属定义。 -- 索引必须写明服务的真实查询,不为所有字段机械建索引。 -- 接口中的字段和状态必须能追踪到相应 DBxxx 设计,但公开 API 不得暴露内部表结构或允许跨模块直接改表。 +当前状态:**待汇总**。 -## 三、ER 图 +## 五、冻结与 Migration -各负责人完成 `database-*.md` 并通过交叉评审后,由罗皓晨在此汇总全局 ER 图。ER 图必须覆盖已确认表关系,但不得代替字段、约束和索引定义。 +主文档达到以下条件后才能冻结: -## 四、初始化脚本 +- 六份个人原稿均完成自审和交叉评审。 +- 统一表清单不再包含“待汇总”占位。 +- 每张表的字段、约束、索引、关系、状态和关联接口完整。 +- DBxxx、表名、约束名和索引名无重复。 +- 跨模块引用、订单快照和数据所有权已经双方确认。 +- 全局 ER 图和表创建依赖顺序完整。 +- [《接口设计》](接口设计.md)第五章中会影响表结构的阻塞项已解决,或相关表继续标记为“部分定义”。 -- Migration、初始化和种子数据的最终位置以实际后端结构为准,未建立后端工程前不预建空目录。 -- 演示数据不少于 30 个商品、3 个分类;不得使用真实个人敏感信息。 -- Migration 只能在对应 DBxxx 表结构标记为“已确认”且相关接口契约完成评审后创建。 +只有在主文档中标记为“已确认”的表才能创建 EF Core 实体和 Migration。初始化与 Seed 必须遵守已确认约束及教师演示数据要求,不得使用真实个人敏感信息。 -- Gitee From 6200df7c3be88921db21f6171b9025f4274ddf5e Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 13:47:50 +0800 Subject: [PATCH 056/118] =?UTF-8?q?docs(infra):=20=E5=AF=B9=E9=BD=90?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=B7=A5=E4=BD=9C=E6=B5=81=E8=A7=84=E5=88=99?= =?UTF-8?q?=EF=BC=9B=E5=A2=9E=E5=8A=A0=E6=B5=81=E7=A8=8B=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E4=B8=8E=E9=98=B6=E6=AE=B5=E8=87=AA=E5=8A=A8=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eshop-project-rules-upload/AGENTS.md | 11 +++- .../document-routing.reference.md | 52 +++++++++++++------ .../eshop-align-docs.SKILL.md | 24 ++++++--- .../eshop-deliver-feature.SKILL.md | 27 ++++++---- .../eshop-project-workflow.SKILL.md | 4 +- 5 files changed, 83 insertions(+), 35 deletions(-) diff --git a/eshop-project-rules-upload/AGENTS.md b/eshop-project-rules-upload/AGENTS.md index 861b2a8..eeb06bb 100644 --- a/eshop-project-rules-upload/AGENTS.md +++ b/eshop-project-rules-upload/AGENTS.md @@ -74,6 +74,8 @@ ## 四、按任务类型读取文件 +本节编号用于资料分类,不代表设计先后。新增功能或改变业务行为时,设计顺序固定为:教师要求与已确认需求 → 业务流程、状态、异常和模块出入口 → 接口、数据库与架构落地 → 代码与测试。现有接口、表或实现只能用于校验可实施性和发现偏离,不能反向拼接或覆盖已确认的业务流程。 + ### 1. 需求、范围与验收 涉及功能范围、角色、权限、业务规则、模块归属或验收结论时,读取: @@ -90,6 +92,7 @@ 涉及目录结构、依赖方向、公共组件、认证授权、事件、缓存、对象存储、可观测性或部署边界时,读取: - `docs/02-设计文档/系统架构设计.md` 中与任务有关的第 4、5 章和第 6~12、14、15 章对应小节; +- 涉及业务动作、状态或跨模块交接时,读取 `docs/02-设计文档/process/` 中的目标流程; - 相关项目文件、入口文件、依赖清单和配置文件; - 当前能力涉及的需求与验收章节。 @@ -100,6 +103,7 @@ 涉及表、字段、索引、关系、状态、事务、Migration 或演示数据时,读取: - `docs/02-设计文档/数据库设计.md`; +- `docs/02-设计文档/process/` 中已经确认的目标业务流程、状态和模块交接; - 对应需求与接口章节; - 现有实体、映射、DbContext、Migration、初始化或种子数据文件; - 相关测试。 @@ -108,16 +112,21 @@ `数据库设计.md` 当前仍可能包含字段不完整的模板内容和需要按 PostgreSQL/Npgsql 复核的示例。必须以目标表的完整设计及实际实体、映射和 Migration 交叉确认,不能把表标题当作可实施定义。 +六份个人数据库原稿保存在 `docs/02-设计文档/database/database-<姓名拼音首字母>.md`,仅用于贡献和交叉评审追踪。`docs/02-设计文档/数据库设计.md` 是实体、映射、Migration、初始化和测试的唯一数据库设计事实源;未在主文档中标记为“已确认”的表不得创建 Migration。 + ### 4. Web API、DTO 与模块间协作 涉及接口、请求响应、错误码、分页、鉴权或模块间调用时,读取: +- `docs/02-设计文档/process/` 中目标模块已经确认的业务动作、状态、异常和直接出入口; - `docs/02-设计文档/接口设计.md`; - 对应需求、数据库设计和验收标准; - 现有 Controller/Endpoint、DTO、应用服务、领域逻辑和测试; - OpenAPI/Swagger 契约文件(存在时)。 -必须遵守“接口文档先行”:模块间接口需要变化时,先确认并更新接口契约,再修改实现和调用方。不得通过跨模块 DbContext、内部仓储或直接改表代替公开接口。 +接口必须由已确认业务流程中的动作、输入输出、状态和异常结果派生。不得按 Axxx 清单拼接流程,也不得用现有接口缺口反向修改已确认业务语义;发生冲突时先记录接口设计缺口,只有需求变化时才重新评审流程。 + +必须遵守“接口文档先行于代码”:模块间接口需要变化时,在业务流程确认后先更新并确认接口契约,再修改实现和调用方。这里的“先行”只针对代码和调用方,不表示接口先于需求或业务流程。不得通过跨模块 DbContext、内部仓储或直接改表代替公开接口。 读取接口设计时,必须同时定位第一章相关通用约定、第二章目标 Axxx 清单和第三章同编号详细定义;缺少请求字段、响应、错误、鉴权或业务规则时先记录并补齐契约,不得凭清单名称猜测实现。 diff --git a/eshop-project-rules-upload/document-routing.reference.md b/eshop-project-rules-upload/document-routing.reference.md index 8497399..9dc41e2 100644 --- a/eshop-project-rules-upload/document-routing.reference.md +++ b/eshop-project-rules-upload/document-routing.reference.md @@ -76,11 +76,12 @@ | 任务类型 | 最小文档范围 | 需要扩展时 | |---|---|---| | 项目理解 | README、需求 2.4/8/9、架构 1/3/5/14/15 | 再读目标模块七节需求和架构 7.x | -| 新功能或行为变化 | 教师对应编号、需求目标模块七节、需求 5/9、相关设计、命名相关章节 | 跨模块时加入提供方契约和调用方 | -| API/DTO | 需求目标模块、接口 1.x 相关约定、接口清单与对应 Axxx、命名 2/7/17/19 | 鉴权读接口 1.6,列表读 1.11,幂等读 1.12 | +| 新功能或行为变化 | 教师对应编号、需求目标模块七节、需求 5/9、相关业务流程、相关设计、命名相关章节 | 跨模块时加入提供方契约和调用方 | +| 业务流程/流程图 | 需求目标模块七节、需求 5/8/9、业务流程设计目标章节 | 先确认业务动作、状态、异常和模块出入口;完成后再映射架构 7.x、接口 Axxx 和数据库 DBxxx | +| API/DTO | 需求目标模块、业务流程目标章节、接口 1.x 相关约定、接口清单与对应 Axxx、命名 2/7/17/19 | 先从流程提取接口能力;鉴权读接口 1.6,列表读 1.11,幂等读 1.12 | | 数据库/Migration | 需求目标模块、需求 5、数据库对应表、命名 2/6/17/19、实际实体/Migration | 并发事务再读架构 7.x 和接口 1.12 | -| 前端页面 | 需求目标模块、需求 7、架构 6、接口对应 Axxx、命名 2/4/7/17/19 | 多客户端再读架构 6.4 和接口 1.15 | -| 后端业务 | 需求目标模块、架构 4/5/7、接口对应 Axxx、数据库相关 DBxxx、命名 2/5/6/7/17/19 | 安全读架构 8,错误读架构 9 | +| 前端页面 | 需求目标模块、需求 7、业务流程目标章节、架构 6、接口对应 Axxx、命名 2/4/7/17/19 | 多客户端再读架构 6.4 和接口 1.15 | +| 后端业务 | 需求目标模块、业务流程目标章节、架构 4/5/7、接口对应 Axxx、数据库相关 DBxxx、命名 2/5/6/7/17/19 | 安全读架构 8,错误读架构 9 | | 缺陷诊断 | 预期行为来源、实际调用链、现有测试、日志/响应/只读数据 | 契约或设计冲突时再读对应设计章节 | | 文档维护 | 目标文档、直接上游事实源、直接下游消费者 | 完成度表述再读实现、测试与 Git 证据 | | 测试/验收 | 教师验收/评分、需求编号、测试计划/报告、实际实现与命令 | 挑战读需求 4、架构 7.x 和原始脚本 | @@ -130,7 +131,23 @@ | 十四、六人纵向架构职责 | 主责、联调对象和 M00 边界 | | 十五、分阶段实施 | 当前阶段是否允许引入某项能力 | -### 6.3 `docs/02-设计文档/命名规范.md` +### 6.3 `docs/02-设计文档/process/` + +本目录集中维护复杂业务流程图,避免在总需求正文中堆叠同一套详细图示: + +- 修改流程前先读 `docs/02-设计文档/process/README.md`,确认负责人、补充位置、模板、成熟度和检查清单。 +- `业务流程设计.md`:只维护全局核心基线、公共状态、跨模块交接、追踪矩阵和扩展登记。 +- `<负责人缩写>/<模块编号>-<模块名称>流程.md`:由负责人维护单个模块的主流程、状态、异常、模块出入口、由流程派生的接口映射和待评审项;根文档只链接,不重复保存细节。 +- `一、文档定位与事实来源`:需求、流程、架构、接口和数据库的职责边界。 +- `二、绘图与维护约定`:图类型、核心 F 扩展规则、状态和跨模块评审要求。 +- `三、F01~F13 核心业务流程`:商城核心闭环、核心状态、模块直接出入口,以及注册登录、资料地址、商品、购物车下单、支付、订单履约和后台角色流程。 +- `四、核心流程追踪矩阵`:F01~F13 的核心状态、直接入口出口、流程章节、主责人和成熟度。 +- `五、选做与挑战流程登记`:X01~X04 和已选 C 项的基础 F、扩展入口、不可变核心结果和状态。 +- `六、维护与评审规则`:主需求、流程图和下游设计的同步顺序。 + +业务语义仍以主需求为准;流程目录把需求转换为可评审的业务动作、状态、异常和模块交接。核心流程已经完成基线校准但仍待各主责人交叉评审;X/C 扩展必须从核心状态和直接模块出口接入。流程确认后,再派生 API、数据库和架构技术时序;Axxx、HTTP 状态、DTO、DBxxx、Worker、缓存和消息名不得用来拼接或反向覆盖业务流程。接口文档仍是实现阶段的唯一 API 契约,但它先行于代码,不先行于需求和业务流程。 + +### 6.4 `docs/02-设计文档/命名规范.md` 新增或重命名时,先读 `一、目标、优先级与 AI 执行规则`、`二、标准业务词汇` 和 `十九、提交前命名检查清单`,再按资产选择: @@ -155,7 +172,7 @@ 不要求每次全文读取 658 行。只读取公共三节与目标资产章节,并搜索现有同义名称。 -### 6.4 `docs/02-设计文档/接口设计.md` +### 6.5 `docs/02-设计文档/接口设计.md` 先按接口特征选择第一章: @@ -183,25 +200,26 @@ 只有接口清单、没有详细 Axxx 时标记为 `部分定义`;字段、失败响应和业务规则未确认前不得直接编码。当前六份个人原稿已保存在 `docs/02-设计文档/interface/`,主接口文档保留 107 个不重复 Axxx 追踪编号,其中 103 个为有效 HTTP 契约;A229、A230、A418、A431 均为已取消历史编号。整体仍为“部分定义,未冻结”:需继续完成数据库反查、真实 OpenAPI、跨模块公开契约和交叉评审。每次任务开始时重新检查,不永久假设此状态。 -### 6.5 `docs/02-设计文档/数据库设计.md` +### 6.6 `docs/02-设计文档/数据库设计.md` -读取设计说明、编号分配、个人文件规则、ER 图和初始化脚本。协作阶段读取目标负责人的 `docs/02-设计文档/database-<姓名拼音首字母>.md`;汇总完成后读取本文件中的统一表清单和完整定义,再核对实际实体、映射、DbContext、Migration、约束、索引与 Seed。 +读取设计说明、DBxxx 分工、个人原稿规则、单表模板、统一表清单、跨模块关系、ER 图和冻结条件。并行设计阶段读取目标负责人的 `docs/02-设计文档/database/database-<姓名拼音首字母>.md`;汇总完成后以主文档中的统一表清单和完整定义为事实源,再核对实际实体、映射、DbContext、Migration、约束、索引与 Seed。 -当前审计中六份 `database-*.md` 尚未创建,主数据库文档只有 PostgreSQL 公共规则、编号分配和表模板,尚无完整表定义。因此: +当前已建立 `docs/02-设计文档/database/` 原稿目录,但六份个人原稿尚未创建,主文档仍为“模板/占位,未冻结”。因此: - 不能把标题当作完整表定义。 - 不能从需求直接猜字段、类型或状态码。 - 表设计不完整时先补齐并评审,再实现 Migration。 +- 个人原稿汇总后继续保留,但不能覆盖主文档。 - 数据库文档与真实 Migration 冲突时明确记录偏离。 -### 6.6 `docs/03-测试文档/` +### 6.7 `docs/03-测试文档/` - `测试计划.md`:读取范围、类型、环境、用例编号、缺陷流程和进度。 - `测试报告.md`:读取实际轮次、统计、缺陷、典型分析和结论。 当前两份文件仍包含大量空白模板字段。模板只能规定记录格式,不能证明已执行、已通过或达到覆盖率。真实结论必须来自测试项目、命令、环境、原始输出、截图、响应、查询或平台记录。 -### 6.7 过程与交付文档 +### 6.8 过程与交付文档 - Git:`README.md` Git 章节和完整 `Git团队协作流程.md`。 - 日报:`reports/daily/README.md`、本人当天 Git 和验证证据。 @@ -235,11 +253,12 @@ 1. 教师对应验收编号。 2. 需求 2.4、目标模块完整七节、需求 5/8/9。 -3. 架构 4/5 和对应 7.x。 -4. 命名公共三节与目标资产章节。 -5. 接口通用相关小节、Axxx 清单与详细定义。 -6. 数据库目标表及实际实体/Migration。 -7. 目标前后端代码和现有测试。 +3. 业务流程设计中的目标流程;缺失时先登记并补充业务图。 +4. 架构 4/5 和对应 7.x。 +5. 命名公共三节与目标资产章节。 +6. 根据流程动作和模块出入口核对接口通用小节、Axxx 清单与详细定义。 +7. 根据流程中的业务事实和状态核对数据库目标表及实际实体/Migration。 +8. 目标前后端代码和现有测试。 ### 修复一个缺陷 @@ -282,6 +301,7 @@ ```powershell rg -n "^### M04-02 " docs/01-需求文档/需求规格说明书.md rg -n "F12|M06-02" docs/00-项目要求 docs/01-需求文档 +rg -n "F08|M04-01" docs/02-设计文档/process/业务流程设计.md rg -n "^### 7\\.8 " docs/02-设计文档/系统架构设计.md rg -n "A201~A300|Cart_AddCartItem" docs/02-设计文档/接口设计.md rg -n "cart_item|CartItem" docs backend frontend diff --git a/eshop-project-rules-upload/eshop-align-docs.SKILL.md b/eshop-project-rules-upload/eshop-align-docs.SKILL.md index e627e43..9d83f5b 100644 --- a/eshop-project-rules-upload/eshop-align-docs.SKILL.md +++ b/eshop-project-rules-upload/eshop-align-docs.SKILL.md @@ -29,10 +29,11 @@ description: 维护 E-Shop 教师要求、需求、架构、数据库、接口 | 目标文档 | 上游事实源 | 必查下游 | |---|---|---| | README/项目范围 | 教师项目要求、需求 2.2/2.4/8/9、架构 1/3/15 | 实际目录、入口、依赖、部署和可运行命令 | -| 需求规格 | 教师项目要求、验收/评分编号、用户确认 | 架构、API、数据库、实现与测试追踪 | -| 系统架构 | 需求目标模块、当前交付边界、真实项目/依赖/配置 | 各模块实现、部署和测试策略 | -| 数据库设计 | 需求业务规则/状态、接口字段、实际实体和 Migration | 查询调用方、Seed、测试与兼容性 | -| 接口设计 | 需求角色/流程、数据库、架构安全与错误约定 | OpenAPI、Endpoint、DTO、客户端和契约测试 | +| 需求规格 | 教师项目要求、验收/评分编号、用户确认 | 业务流程、架构、API、数据库、实现与测试追踪 | +| 业务流程设计 | 已确认需求的角色、主流程、状态、异常和模块边界 | 架构技术时序、接口、数据库、页面和流程测试 | +| 系统架构 | 需求目标模块、已确认业务流程、当前交付边界、真实项目/依赖/配置 | 各模块实现、部署和测试策略 | +| 数据库设计 | 已确认流程中的业务事实/状态、接口字段、实际实体和 Migration | 查询调用方、Seed、测试与兼容性 | +| 接口设计 | 已确认流程中的动作、输入输出、状态和异常,数据库与架构约束 | OpenAPI、Endpoint、DTO、客户端和契约测试 | | 测试计划/报告 | 验收标准、实际测试入口、环境、命令和原始结果 | 缺陷闭环、完成度和发布判断 | | 日报/周报 | 对应 README、成员真实 Git、工作区和验证证据 | 汇总、计划和风险,不冒用他人成果 | | 会议纪要 | 模板和真实参会、决策、负责人、截止时间 | 后续任务、需求确认和未决项 | @@ -40,19 +41,30 @@ description: 维护 E-Shop 教师要求、需求、架构、数据库、接口 接口清单没有对应 Axxx 详情、数据库章节只有表名、测试计划或报告仍为空白模板时,必须保留为“部分/模板/缺失”,不能为了文档看起来完整而虚构字段、统计或结论。 +复杂业务流程图集中维护在 `docs/02-设计文档/process/`。`业务流程设计.md` 只保留全局核心基线、公共状态、跨模块交接和追踪索引;各负责人按 `process/README.md` 在本人目录中以“一模块一文档”维护细节。主需求保留功能规则、文字步骤与流程链接,不重复嵌入同一张详细图;需求文字与流程图冲突时先修正业务语义,再同步图示。核心流程必须标明业务状态、直接模块入口和确定出口;X/C 扩展必须绑定基础 F、接入状态和不可变核心结果,不能另起一套主链路。 + +设计顺序固定为“已确认需求 → 业务流程 → 接口/数据库/架构落地 → 实现与测试”。业务图先使用业务动作确定状态和结果,再建立“流程步骤 → Axxx/DBxxx/技术时序”映射。不得把接口清单拼成流程,也不得把 HTTP 状态码、DTO、字段或事件名当作业务节点。“接口文档先行”只表示已派生的接口契约必须先于代码和调用方修改完成确认。 + ## 维护接口汇总 -- `docs/02-设计文档/接口设计.md` 是实现、OpenAPI、联调和测试的唯一接口事实源。 +- `docs/02-设计文档/接口设计.md` 是实现、OpenAPI、联调和测试的唯一接口契约,但其业务能力必须由已确认流程派生。 - 六份 `docs/02-设计文档/interface/interface-<姓名拼音首字母>.md` 作为个人贡献原稿长期保留,用于自审和交叉评审,但不能覆盖总文档。 - 负责人修改个人原稿时,同一任务必须同步总文档中的统一清单、同编号详细定义、需求追踪状态和未决项;不得只改个人文件。 - 汇总时检查 Axxx、`operationId` 和“HTTP 方法 + 路径”全局唯一,并把缺少字段、状态机、鉴权、DBxxx 或跨模块契约的接口标为“部分定义”或“待交叉评审”,不得为了凑齐数量改成“已确认”。 +## 维护数据库汇总 + +- `docs/02-设计文档/数据库设计.md` 是实体、映射、Migration、初始化和测试的唯一数据库设计事实源。 +- 六份 `docs/02-设计文档/database/database-<姓名拼音首字母>.md` 作为个人贡献原稿保留;并行编写阶段只改个人原稿,统一汇总后再同步主文档。 +- 汇总时检查 DBxxx、表名、约束名、索引名、数据所有权和跨模块关系;缺少字段、约束、索引、状态或评审的表不得标记为“已确认”。 + ## 建立一致性追踪 逐项核对: ```text -F/X/C/N/D 编号 → 负责人 → 需求与边界 → 数据设计 → API 契约 +F/X/C/N/D 编号 → 负责人 → 需求与边界 → 业务流程/状态/模块交接 +→ 数据设计与 API 契约 → 实际实现 → 权限与状态 → 测试场景 → 验证证据 → 当前状态 ``` diff --git a/eshop-project-rules-upload/eshop-deliver-feature.SKILL.md b/eshop-project-rules-upload/eshop-deliver-feature.SKILL.md index fdc9e01..e67d0d6 100644 --- a/eshop-project-rules-upload/eshop-deliver-feature.SKILL.md +++ b/eshop-project-rules-upload/eshop-deliver-feature.SKILL.md @@ -16,15 +16,17 @@ description: 按业务模块纵向交付 E-Shop 新功能或预期行为变更 1. 按 `$eshop-project-workflow` 的 `references/document-routing.md` 建立最小事实包,从教师基线和需求规格确认需求编号、负责人、角色、业务规则和验收条件。 2. 需求编号、功能名称、负责人或验收项相互冲突时停止实施并请求确认,不自行合并、重编号或替换负责人。 3. 写明本次包含项、明确排除项、依赖模块和受影响用户流程。 -4. 检查相关实现是否真实存在;不存在时先说明脚手架或契约缺口,不虚构代码结构。 -5. M00 或公共脚手架缺失时将其记录为前置依赖;除非当前任务明确属于 M00,不由单个业务功能任务顺手搭建全仓或代写公共负责人工作。 -6. 区分计划、已实现、已验证和缺失状态。 +4. 确认 `docs/02-设计文档/process/` 中目标流程已覆盖角色、状态、异常和模块出入口;缺失时先补流程,不从现有接口或代码反推预期业务。 +5. 检查相关实现是否真实存在;不存在时先说明脚手架或契约缺口,不虚构代码结构。 +6. M00 或公共脚手架缺失时将其记录为前置依赖;除非当前任务明确属于 M00,不由单个业务功能任务顺手搭建全仓或代写公共负责人工作。 +7. 区分计划、已实现、已验证和缺失状态。 ## 读取最小事实包 | 层面 | 必读章节与资产 | |---|---| | 范围 | 教师对应编号;需求 2.4、目标模块完整七节、需求 5/8/9;挑战任务追加需求 4 | +| 流程 | `process/README.md`、全局核心基线和目标负责人模块流程;确认状态、异常、直接输入输出和扩展接入点 | | 架构 | 架构 4/5,以及目标能力对应的 6、7.x、8、9、14、15 章 | | 命名 | 命名规范 1/2/19,再读取代码、API、数据库、事件、配置或测试对应资产章节 | | 契约 | 接口第一章相关约定、第二章目标 Axxx 清单、第三章同编号详情、实际 OpenAPI 和调用方 | @@ -32,7 +34,7 @@ description: 按业务模块纵向交付 E-Shop 新功能或预期行为变更 | 质量 | 对应验收编号、测试计划、真实测试入口、现有测试与原始结果 | - 只按稳定编号和标题定位,不依赖行号,也不默认加载其他成员的全部模块。 -- 接口只有清单没有 Axxx 详情时先补齐并确认契约;数据库只有表名没有字段、约束和索引时先补齐设计。 +- 先以需求和流程确定业务动作、状态、异常与模块交接,再派生接口能力和数据事实;接口只有清单没有 Axxx 详情时补齐契约,数据库只有表名没有字段、约束和索引时补齐设计。 - 文档与实现不一致时记录 `实现偏离` 和兼容影响,不静默采用更方便的一方。 - 纵向任务只覆盖用户确认的模块和链路;用户只要求其中一层时,明确其余层尚未交付。 @@ -41,6 +43,7 @@ description: 按业务模块纵向交付 E-Shop 新功能或预期行为变更 | 层面 | 必查内容 | |---|---| | 需求 | 对应 F/X/C/M 编号、角色、权限、主流程、异常和验收条件 | +| 流程 | 参与者、业务动作、判断分支、状态转换、模块直接出入口、失败和回归结果 | | 数据 | `数据库设计.md`、实体、映射、约束、索引、Migration、Seed 和历史兼容性 | | 契约 | `接口设计.md`、OpenAPI、DTO、错误码、分页、鉴权和调用方 | | 后端 | API/Endpoint、Application、Domain、Infrastructure、事务、事件和后台任务 | @@ -52,12 +55,14 @@ description: 按业务模块纵向交付 E-Shop 新功能或预期行为变更 ## 按安全顺序实施 1. 先固定需求与验收边界。 -2. 需要改变模块间接口时先更新接口设计或 OpenAPI 契约,再改实现与调用方。 -3. 需要持久化变更时同步实体、映射、Migration、约束、兼容性、Seed 和测试。 -4. 后端实现服务端参数校验、Policy、资源归属、事务、一致性、幂等和错误处理。 -5. 前端实现真实 API 调用、类型、角色入口和加载、空数据、成功、失败、无权限反馈。 -6. 通过公开 API、应用接口或集成事件协作,不跨模块直接使用内部 DbContext、仓储或表。 -7. 只为已经确认的共同需求建设公共能力;单模块逻辑留在本模块。 +2. 先确认业务流程、状态、异常和模块直接出入口;流程图使用业务动作,不使用 Axxx、HTTP 状态或 DTO 拼接流程。 +3. 从已确认流程派生所需接口能力、数据事实和技术时序;发现现有契约冲突时先修正设计缺口,不反向覆盖流程。 +4. 需要改变模块间接口时先更新接口设计或 OpenAPI 契约,再改实现与调用方。 +5. 需要持久化变更时同步实体、映射、Migration、约束、兼容性、Seed 和测试。 +6. 后端实现服务端参数校验、Policy、资源归属、事务、一致性、幂等和错误处理。 +7. 前端实现真实 API 调用、类型、角色入口和加载、空数据、成功、失败、无权限反馈。 +8. 通过公开 API、应用接口或集成事件协作,不跨模块直接使用内部 DbContext、仓储或表。 +9. 只为已经确认的共同需求建设公共能力;单模块逻辑留在本模块。 简单 CRUD 保持简单。只有架构文档已确认的复杂规则才使用 DDD、CQRS、Outbox、缓存或消息等机制。 @@ -71,7 +76,7 @@ description: 按业务模块纵向交付 E-Shop 新功能或预期行为变更 ## 交付结果 -按“需求 → 数据 → 契约 → 后端 → 前端 → 测试 → 文档”列出实际覆盖情况,并明确: +按“需求 → 流程 → 数据/契约 → 后端 → 前端 → 测试 → 文档”列出实际覆盖情况,并明确: - 已完成和已验证的纵向链路; - 未完成、未验证或由其他负责人承担的链路; diff --git a/eshop-project-rules-upload/eshop-project-workflow.SKILL.md b/eshop-project-rules-upload/eshop-project-workflow.SKILL.md index fbc5411..b3f98a1 100644 --- a/eshop-project-rules-upload/eshop-project-workflow.SKILL.md +++ b/eshop-project-rules-upload/eshop-project-workflow.SKILL.md @@ -29,7 +29,9 @@ description: 作为 E-Shop 仓库所有任务的项目级总入口,统一完 3. 将关键文档标记为 `完整定义`、`部分定义`、`模板/占位`、`实现偏离` 或 `缺失`。 4. 接口只有清单没有 Axxx 详情、数据库只有表名没有字段约束、测试文件只有模板时,先作为缺口报告,不把它们当作可实施契约或通过证据。 5. 同一事实冲突时,按教师基线、用户当前确认、真实实现与验证证据、已确认设计、模板与计划的顺序判断。 -6. 接口任务以 `docs/02-设计文档/接口设计.md` 为唯一实施契约;`docs/02-设计文档/interface/` 中的个人原稿只用于贡献追踪,修改后必须同步总文档。 +6. 新增功能或改变业务行为时,先由需求确认角色、规则和验收,再在 `docs/02-设计文档/process/` 确认业务流程、状态、异常和模块出入口;接口、数据库和架构从流程派生,不能按现有 Axxx 或 DBxxx 反向拼接流程。 +7. 业务流程确认后,接口任务以 `docs/02-设计文档/接口设计.md` 为唯一实施契约;“接口先行”只表示先于代码和调用方修改。`docs/02-设计文档/interface/` 中的个人原稿只用于贡献追踪,修改后必须同步总文档。 +8. 数据库任务以 `docs/02-设计文档/数据库设计.md` 为唯一实施契约;`docs/02-设计文档/database/` 中的个人原稿用于并行设计和评审,未确认表不得生成 Migration。 ## 选择专项 Skill -- Gitee From 71cc5fc59ecd61e63c082ee2360e8e9d6afd865f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=B5=B7=E6=B4=8B?= <2040454617@qq.com> Date: Fri, 24 Jul 2026 14:32:02 +0800 Subject: [PATCH 057/118] docs(process): add M10 after-sales + C08 payment callback flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M10-售后流程.md: - 按 M05 模板写 11 章流程文档(X04 完整模块) - 8 状态机:待审核、待退货、待收货、退款中、已退款、退款失败、已拒绝、已撤销 - 涵盖 11 项 FR(M10-FR01~FR11):可申请判断、提交申请、商家审核、状态流转、模拟退款、消息通知、撤销申请、退货处理、列表/详情 - 9 个 Mermaid 流程图:模块出入口、状态机、提交申请、商家审核、提交退货、确认收货、IRefundService 内部应用能力、撤销、消息反馈 - 跨模块协作:M04 订单项事实、M05 IRefundService 内部应用能力(不占 Axxx)、M09 通知、M02 库存回补 - 状态条件推进 + 失败回滚 + 异常场景反查 + 接口与数据待评审项 C08-支付回调与对账流程.md: - 按 M05 模板写 11 章流程文档(C08 挑战模块) - 3 个状态机:回调处理(Received/Processing/Processed/Ignored/Difference/Rejected/Failed)+ 对账批次(Pending/Matched/HasDifferences/Resolved)+ 差异(Pending/InProgress/Resolved) - 涵盖 10 项 FR(C08-FR01~FR10):回调接收、鉴别、幂等、乱序、事务一致性、对账批次、差异识别、差异闭环、稳定反馈、退款对账 - 10 个 Mermaid 流程图:模块出入口、3 个状态机、回调接收、幂等乱序、事务一致性、批次生成、差异闭环、退款对账 - 关键规则:已取消订单收到迟到成功不得改为 Paid,进入差异;退款对账覆盖 M10 退款成功 + 钱包流水 + 钱包入账三方比对 - v0.1 FR11~FR15 扩展待 PR 合入 dev;C08 与 F10 同步支付边界待定 Refs: 张海洋 2026-07-24 完成后听评审反馈 --- ...71\350\264\246\346\265\201\347\250\213.md" | 344 +++++++++++++++++ ...56\345\220\216\346\265\201\347\250\213.md" | 361 ++++++++++++++++++ 2 files changed, 705 insertions(+) create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" new file mode 100644 index 0000000..4c0de45 --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" @@ -0,0 +1,344 @@ +# C08 支付回调与对账流程 + +> 负责人:张海洋 +> 覆盖:C08、X04 对账 +> 基础核心流程:F10、F09 +> 直接协作:韦乾强(M04 Ordering)、罗皓晨(C10 多实例 + M00 后台基础设施 + M09 消息) +> 文档状态:初稿,待张海洋自审及 Ordering/集成事件 交叉评审 +> 需求事实源:[需求规格说明书 C08](../../../01-需求文档/需求规格说明书.md) 的"C08 支付回调幂等与对账"完整七节 +> 当前范围说明:dev 上的 C08 章节为 v0.1 FR01~FR10,本流程按此基线编写;优先级管理同步 F10 同步支付与 C08 异步回调的替换边界尚未冻结,本文档登记待确认项并以此为评审入口。 + +## 一、范围与事实来源 + +本模块负责模拟支付通道回调的接收、鉴别、幂等、乱序处理、事务一致性、每日对账批次生成、差异识别与闭环,以及售后退款对账。本挑战不接入真实支付机构、不自动执行未经确认的资金修复、不在为买家或商家展示的页面中暴露回调内部过程。 + +本文先依据需求确定业务参与者、状态、判断分支、事务结果和模块出入口,再由这些流程步骤派生接口能力。A421~A425 只用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| C08 需求 v0.1 FR01~FR10 | 完整定义 | 作为业务语义事实源 | +| C08 扩展 FR11~FR15 | 仅 zhy 本地草稿(commit 44a08d0,unreachable) | 待 PR 合入 dev 后再并入流程 | +| 本文业务流程 | 初稿 | 先确认角色、状态、分支、事务边界和模块出入口 | +| A421~A425 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| DB081~DB100 数据库设计 | 模板/占位 | 本文不发明表字段、枚举编码和索引 | +| 与 F10 同步支付的边界 | 待决 | 业务流程设计 §3.5 明确"暂不让同步和异步同时成为最终支付事实" | +| 与 X04 售后退款对账 | 全量接入 | 退款流水必须纳入 C08 每日对账 | + +## 二、模块直接出入口 + +```mermaid +flowchart LR + ORD["M04 Ordering
订单号、归属、当前状态、支付截止时间"] --> CB["C08 Payment Callback
回调接收、幂等、乱序处理"] + CH["模拟支付通道
唯一回调 ID + 支付流水 + 订单 + 结果 + 时间"] -->|"已签名回调载荷"| CB + CB -->|"唯一约束 + 签名校验"| CB + CB -->|"事务提交:支付记录 + 订单状态 + Inbox/处理记录 + Outbox"| ORD + CB -->|"支付成功事实供 X03 消费"| MSG["M09 消息持久化/通知"] + W1["Worker Service 调度"] -->|"每日定时"| BAT["对账批次生成"] + BAT -->|"匹配 / 差异"| DIFF["差异识别与闭环"] + DIFF -->|"管理员处理"| ADM["AdminOnly:差异处理"] + CB -. "回调历史" .-> BAT + AS["M10 售后退款成功"] -. "退款流水" .-> BAT + RECON["退款入账记录"] -. "钱包入账" .-> BAT + + CH -->|"签名错误、伪造或未知回调 ID"| X["拒绝,不写入数据库"] + ORD -->|"订单不存在或非本人"| Y["拒绝,不泄露归属"] + CB -->|"重复回调"| Z["返回首次结果,不重复记账或通知"] + BAT -->|"同一日期重复执行"| W["不重复生成矛盾批次"] +``` + +边界约束: + +- C08 回调通道身份使用服务端签名的模拟通道凭证;接入端不通过回调核实买家身份,回调只携带业务标识。 +- C08 回调处理与订单状态推进在同一事务内完成;事务失败整体回滚,事务中断可安全重试。 +- C08 对账只生成可追踪的差异记录,不直接修改业务数据;修复必须由管理员在受控流程内完成。 +- C08 暂不替代 F10 同步支付;触发路径上"同步支付走 M05,异步通道走 C08",任一成为最终支付事实需在订单提交时决定。 + +## 三、核心状态流转 + +### 3.1 回调处理状态 + +```mermaid +stateDiagram-v2 + [*] --> Received: 通道发送回调 + Received --> Processing: 签名校验通过 + Received --> Rejected: 签名错误或伪造 + Processing --> Processed: 事务提交成功 + Processing --> Ignored: 订单状态拒绝目标结果 + Processing --> Difference: 已取消订单收到迟到成功 + Processing --> Failed: 事务中断或字段错误 + Failed --> Processing: 安全重试 + Processed --> [*] + Rejected --> [*] + Ignored --> [*] + Difference --> [*] +``` + +合法转换回执: + +- `Processed` 表示回调已被纳入账务并完成订单状态推进。 +- `Ignored` 表示回调信号被业务规则主动忽略(如订单已 `Paid` 又收到重复成功)。 +- `Difference` 表示已登记差异,等待对账阶段暴露并由管理员处理。 +- 任一终态都保留 Inbox 记录,可重复分析但不可再修改业务状态。 + +### 3.2 对账批次状态 + +```mermaid +stateDiagram-v2 + [*] --> Pending: Worker 开始生成 + Pending --> Matched: 全部匹配无差异 + Pending --> HasDifferences: 存在差异 + HasDifferences --> Resolved: 管理员闭环所有差异 + Matched --> [*] + Resolved --> [*] +``` + +合法转换回执: + +- 同一日期同一范围不重复生成矛盾批次;Worker 重复执行以 `date + range` 唯一约束去重。 +- `HasDifferences` 必须保留所有差异条目和处理状态,差异未闭环时批次仍处于 `HasDifferences`。 + +### 3.3 差异处理状态 + +```mermaid +stateDiagram-v2 + [*] --> Pending: 批次生成时登记 + Pending --> InProgress: 管理员开始处理 + InProgress --> Resolved: 管理员闭环处理 + Pending --> [*] + InProgress --> [*] + Resolved --> [*] +``` + +合法转换回执: + +- 状态条件 `WHERE status = ?` 唯一推进,避免并发处理同一差异。 +- `Resolved` 必须保留处理说明和处理人,不得靠直接改库绕开记录。 + +## 四、回调接收与鉴别 + +```mermaid +flowchart TD + A["模拟支付通道发送回调"] --> B["接收 HTTP 回调
含 callbackId + paymentSerial + orderId + result + occurredAt + signature"] + B --> C{"签名校验通过?"} + C -- "否" --> X["拒绝:不写入数据库,不返回敏感信息"] + C -- "是" --> D{"callbackId 唯一?"} + D -- "否" --> E["命中 Inbox:返回首次处理结果"] + D -- "是" --> F{"字段合法?"} + F -- "否" --> Y["拒绝:字段错误或缺失"] + F -- "是" --> G["开启事务"] + G --> H["写入 Inbox 记录(状态 Processing)"] + H --> I["查询订单当前状态"] + I --> J["判定下一动作(见第五章 乱序处理)"] + J --> K{"事务提交成功?"} + K -- "否" --> KR["整体回滚,Inbox 回退到 Received"] + K -- "是" --> Z["进入事务一致性处理"] +``` + +签名与字段校验: + +- 签名按模拟支付通道约定计算 HMAC;签名不通过不得进入 Inbox。 +- 必填字段:`callbackId`、`paymentSerialNumber`、`orderId`、`result`、`occurredAt`、`amount`、`currency`。 +- `callbackId` 与 `paymentSerialNumber` 均建唯一约束;任一重复返回首次结果。 + +## 五、幂等处理与乱序 + +```mermaid +flowchart TD + A["已确认接收:Inbox Processing"] --> B{"该 callbackId 已存在结果?"} + B -- "是" --> BX["返回首次结果,不重复入库"] + B -- "否" --> C{"订单当前状态?"} + C -- "PendingPayment" --> D{"回调结果?"} + D -- "Success" --> E["条件推进 PendingPayment → Paid"] + D -- "Failed" --> E2["写入失败记录,订单保持 PendingPayment"] + C -- "Paid" --> F{"回调结果?"} + F -- "Success" --> FX["已支付成功回调,标记 Ignored,不重复写支付记录"] + F -- "Failed" --> FY["登记为差异:支付记录重复但订单已支付"] + C -- "Cancelled" --> G{"回调结果?"} + G -- "Success" --> GX["重要:已取消订单收到迟到成功 → 标记 Difference,不改为 Paid"] + G -- "Failed" --> GY["失败回调到达已取消订单,标记 Ignored"] + C -- "其他不可支付状态" --> H["标记 Ignored"] + E --> I["写入支付记录、订单状态、Outbox 支付成功事实"] + E2 --> I + FX --> I + FY --> I + GX --> I + GY --> I + H --> I + I --> J{"事务提交成功?"} + J -- "否" --> JR["整体回滚,Inbox 状态回退"] + J -- "是" --> K["更新 Inbox 状态为最终态(Processed/Ignored/Difference)"] +``` + +乱序关键规则: + +- `Cancelled` 订单收到迟到成功回调是核心规则,不得改为 `Paid`;必须标记 `Difference` 并由对账系统暴露。 +- 同一订单收到多次成功回调时,状态条件只允许一次从 `PendingPayment → Paid`;其余标记 `Ignored`。 +- 失败回调到达已支付订单,仍能作为支付记录的辅助记录,但不得重复创建支付记录或改变订单状态。 +- 回调事务必须在同一事务内完成:支付记录 + 订单状态 + Inbox 记录 + Outbox 事件。 + +## 六、事务一致性 + +```mermaid +flowchart TD + A["开启事务"] --> B["支付记录写入或命中"] + B --> C["订单状态条件更新"] + C --> D["Inbox/处理记录状态推进"] + D --> E["Outbox 支付成功事实写入"] + E --> F{"全部成功?"} + F -- "否" --> R["整体回滚,Inbox 保持 Processing"] + F -- "是" --> G["提交事务"] + G --> H["通知买家与商家"] +``` + +事务原子结果: + +```text +支付记录已写入(首次) ++ 订单状态条件更新 ++ Inbox/处理记录状态推进 ++ Outbox 支付成功事实写入 += 同一事务提交成功 +``` + +任一步失败时整体回滚,不允许出现"支付记录已写但订单未更新"或"订单已支付但无支付记录"的部分结果。 + +## 七、对账批次生成 + +```mermaid +flowchart TD + W["Worker 定时触发"] --> A["确定对账日期与范围"] + A --> B{"该日期+范围已存在批次?"} + B -- "是" --> BX["跳过:不重复生成"] + B -- "否" --> C["开启批次事务"] + C --> D["读取支付记录、订单状态、回调 Inbox"] + D --> E["读取退款记录、钱包入账、钱包流水"] + E --> F["比对支付记录 vs 订单状态"] + F --> G["比对退款成功 vs 退款流水 vs 钱包入账"] + G --> H["发现差异则逐条登记"] + H --> I["生成批次记录"] + I --> J{"事务提交成功?"} + J -- "否" --> JR["整体回滚,批次未生成"] + J -- "是" --> K["创建差异条目(若存在)"] + K --> L["通知管理员有批次生成"] +``` + +关键规则: + +- 同一日期同一范围不重复生成矛盾批次;以 `(date, range)` 唯一约束去重。 +- 批次范围只包含已提交事务的支付记录与退款记录,不包含处理中或失败中的回调。 +- 资金类比对必须用 PostgreSQL 条件查询与聚合;不在应用层先读后算。 + +## 八、差异识别与闭环 + +```mermaid +flowchart TD + A["管理员登录后台查看批次"] --> B["选择 HasDifferences 批次"] + B --> C["查看差异列表"] + C --> D{"选择单条差异"} + D -- "查看详情" --> E["展示订单号、支付/退款记录、状态时间线、Inbox 历史"] + D -- "开始处理" --> F["状态条件推进 Pending → InProgress"] + D -- "标记已解决" --> G["状态条件推进 *(InProgress) → Resolved"] + D -- "已解决" --> H["填写处理说明"] + H --> I["开启事务"] + I --> J["写入处理说明、处理人、处理时间"] + J --> K["推进差异状态"] + K --> L{"事务提交成功?"} + L -- "否" --> LR["整体回滚,差异状态保持"] + L -- "是" --> M["记录处理审计日志"] + G --> M + M --> N{"批次所有差异都已 Resolved?"} + N -- "是" --> O["批次状态推进 HasDifferences → Resolved"] + N -- "否" --> P["保持 HasDifferences"] +``` + +关键规则: + +- 差异修复必须可追踪,不能通过直接改库隐藏原因。 +- 状态条件 `WHERE status = 'Pending'` 或 `WHERE status = 'InProgress'` 唯一推进。 +- 售后显示退款成功但钱包未入账、钱包重复入账或退款金额不一致均必须进入对账差异。 + +## 九、退款对账 + +```mermaid +flowchart TD + A["对账批次生成时"] --> B["读取 M10 退款成功记录"] + B --> C["读取 wallets.transactions 中退款流水"] + C --> D["读取 refunds 表中对应记录"] + D --> E{"三方一致?"} + E -- "是" --> F["标记为匹配"] + E -- "否" --> G["登记差异条目"] + G --> H["管理员进入差异处理流程"] +``` + +退款对账匹配规则: + +- `refunds.success` 与 `wallet_ledgers` 中退款入账记录按 `(refund_id)` 一一对应。 +- 金额不一致、缺失或重复入账均登记为差异。 +- 售后退款成功但钱包未入账的情况必须被发现。 + +## 十、异常、回滚与责任 + +| 场景 | C08 处理 | 最终状态/责任 | +|---|---|---| +| 同一回调重复到达 | 返回首次结果 | 不重复记账或通知 | +| 成功与失败回调乱序 | 按状态机保留合法终态 | 冲突记录可追踪结果 | +| 已取消订单收到迟到成功 | 不改为 `Paid`,标记 Difference | 由对账系统暴露后管理员闭环 | +| 事务处理中断 | 整体回滚,安全重试 | 仍只处理一次 | +| Worker 重复执行对账 | 同一日期范围不重复生成 | 批次唯一约束 | +| 差异处理并发 | 状态条件唯一胜出 | 仅一次有效处理 | +| 签名错误或伪造 | 拒绝 | 不写入数据库 | +| 字段缺失或非法 | 拒绝 | 不写入数据库 | +| 通知暂时失败 | 回调事实保留 | M09 按可靠机制重试 | +| 管理员误操作 | 差异状态可恢复 | 不允许直接改库,必须通过差异处理流程 | + +## 十一、由流程派生的接口契约映射 + +本节是第二至十章业务流程的下游映射,不是流程输入。先确认"要完成什么业务动作、处于什么状态、成功或失败后得到什么结果",再决定由哪个接口承载。若现有 Axxx 与流程冲突,先记录接口缺口并修改接口设计;只有业务需求本身变化时,才回到前文重新评审流程。 + +| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +|---|---|---|---| +| 接收模拟支付通道回调 | A421 | 签名校验、字段校验、Inbox 唯一约束 + 乱序处理 | 待交叉评审 | +| 对账批次列表 | A422 | 按日期分页查询批次及状态 | 待交叉评审 | +| 对账批次详情 | A423 | 展示批次范围、总数、匹配/差异数和按类型汇总 | 待交叉评审 | +| 差异列表 | A424 | 按批次分页查询差异条目 | 待交叉评审 | +| 差异处理 | A425 | 管理员推进差异状态并记录处理说明 | 待交叉评审 | + +接口详细定义与实现必须承接上述流程结果。回调接口必须支持 `Idempotency-Key`(与 `callbackId` 同值);后台对账接口必须使用 `AdminOnly` 策略;签名校验和唯一约束必须在数据库侧强制。 + +## 十二、扩展接入边界 + +- F10 同步支付:当前 F10 走 M05 钱包同步支付;C08 走模拟异步通道回调。任一成为最终支付事实需在订单提交时确定;订单进入 `PendingPayment` 之后,只能由一方推进,避免双结果。 +- C10 多实例:两个 API 实例必须能同时处理回调并保持一致性;唯一约束 + 事务边界保证不重复;Nginx 负载均衡转发回调保持 `X-Forwarded-For` 与 traceId。 +- X04 售后退款:所有退款成功记录在批次生成阶段纳入三方对账;退款失败但订单已推进的状态由 M10 维护,不进入 C08 业务差异。 +- M09 消息:回调事务成功后 Outbox 支付成功事实;M09 通知失败按可靠机制重试,不反向修改回调事实。 +- C06 实时推送:差异闭环结果可推送给管理员;推送失败不影响差异处理事务。 + +## 十三、由流程反查出的接口与数据待评审项 + +1. C08 与 F10 同步支付的边界:流程要求二者任一成为最终支付事实;当前 M05 同步支付未明确"是否同时走 C08 通道",需在订单提交时确定走哪条路径。 +2. 回调签名密钥:流程要求按模拟支付通道约定验证字段和来源;A421 签名密钥管理与轮换由 M00 公共能力承接,待评审。 +3. 失败回调对账:流程要求按需识别"失败"语义;当前批次生成只覆盖成功支付的对账,失败回调的对账口径待评审。 +4. 回调幂等窗口:流程要求幂等记录可恢复;A421 Inbox 记录保留期与归档策略(数据库 vs 缓存)需在数据库设计中确认。 +5. 迟到回调时间窗口:流程未明确"多迟算迟到";迟到窗口与对账批次范围(如:批次范围只覆盖昨天完成的支付)需在 Worker 调度中定义。 +6. 差异状态字段:流程要求差异有受控状态;DBxxx 差异表字段(type、status、resolutionNote、resolvedBy、resolvedAt)需在数据库评审中确认。 +7. 退款对账范围:流程要求每日核对退款成功 + 退款流水 + 钱包入账;M10 退款是否纳入 C08 每日批次,还是按售后单独批次,待评审。 +8. 批次切分粒度:流程要求按日生成批次;当日订单量较大时是否按小时或范围切分,影响 Worker 性能与差异范围精度。 +9. 管理员差异化处理权限:流程要求 AdminOnly 推进差异;A425 是否允许区分"查看差异"与"处理差异"两个权限粒度,避免误操作。 +10. DB081~DB100 尚未形成可实施的完整表定义,数据库字段、约束和索引必须由数据库设计任务另行确认。 + +## 十四、验收证据清单 + +- [ ] 重复回调不重复记账、不重复通知,返回首次结果。 +- [ ] 成功与失败回调乱序到达时,订单状态按状态机保留合法终态。 +- [ ] 已取消订单收到迟到成功回调不改为 `Paid`,登记为差异。 +- [ ] 事务中断可安全重试,且不会重复处理。 +- [ ] Worker 重复执行对账不重复生成矛盾批次。 +- [ ] 差异处理并发仅一次有效推进。 +- [ ] 每日对账可列出匹配与差异,差异状态及处理记录可追踪。 +- [ ] 退款对账能够发现售后退款成功、退款流水和钱包入账之间的缺失、重复及金额不一致。 +- [ ] 买家和商家只看到稳定业务状态,不展示回调内部过程。 +- [ ] 管理员能够查看并闭环差异,差异修复可追踪。 +- [ ] 签名错误或伪造回调不被写入数据库。 +- [ ] 字段缺失或非法的回调不被写入数据库。 +- [ ] 保留回调重放脚本、乱序脚本、原始结果和对账清单。 +- [ ] 解释唯一约束、事务边界、Inbox/Outbox 和修复流程的工作原理。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" new file mode 100644 index 0000000..a8ce58b --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" @@ -0,0 +1,361 @@ +# M10 售后流程 + +> 负责人:张海洋 +> 覆盖:M10、X04 +> 基础核心流程:F08、F09、F10、F12 +> 直接协作:韦乾强(M04 Ordering)、罗皓晨(M09 消息 + M00 后台基础设施) +> 文档状态:初稿,待张海洋自审及 Ordering/C08 交叉评审 +> 需求事实源:[需求规格说明书 M10](../../../01-需求文档/需求规格说明书.md) 的"M10 售后流程(X04)"完整七节 + +## 一、范围与事实来源 + +本模块负责买家针对本人已支付、已发货或完成后 7 天内的订单项发起退款或退货申请,商家在后台审核并推进售后状态,退款采用模拟处理并退回买家小金库。它不接入真实退款渠道、不参与 C03 待支付订单超时扫描、不主动修改订单的核心履约状态。 + +本文先依据需求确定业务参与者、状态、判断分支、事务结果和模块出入口,再由这些流程步骤派生接口能力。A411~A419 与 A431~A434 只用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| M10/X04 需求 | 完整定义 | 作为业务语义事实源 | +| 本文业务流程 | 初稿 | 先确认角色、状态、分支、事务边界和模块出入口 | +| A411~A419、A432、A434 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| A431 内部应用能力 | 内部契约 | 作为退款入账的内部服务,不占 Axxx HTTP 编号 | +| DB081~DB100 数据库设计 | 模板/占位 | 本文不发明表字段、枚举编码和索引 | +| C08 退款对账 | 独立扩展 | 只登记接入点,不混入 F10 核心状态机 | +| C03 超时取消 | 独立流程 | 售后申请与退款处理不参与 C03 扫描 | + +## 二、模块直接出入口 + +```mermaid +flowchart LR + ID["M01 Identity
已认证买家、角色和账号状态"] -->|"BuyerOnly 通过"| AS["M10 AfterSales
申请、审核、状态时间线"] + ID -->|"MerchantOnly 通过"| AS + ORD["M04 Ordering
订单项、归属、实付快照、当前状态"] -->|"本人订单项事实"| AS + AS -->|"仅退款审核通过或退货确认收货后"| PAY["M05 Payment
IRefundService(内部应用能力)"] + PAY -->|"退款成功:钱包入账 + 退款流水"| AS + AS -->|"申请提交、审核结果、退款结果"| MSG["M09 消息持久化/通知"] + AS -->|"退货数量回补"| INV["M02 Catalog
库存按订单项来源通道回补"] + RECON["C08 对账 Worker"] -. "每日对账" .-> AS + + ID -->|"游客、非授权角色、账号禁用、令牌失效"| X["拒绝访问,不返回售后数据"] + ORD -->|"订单项不属于买家或不在售后期限/状态"| Y["拒绝申请,不泄露他人订单内容"] + AS -->|"状态竞争或事务失败"| Z["返回当前最终状态,不产生部分退款或部分状态"] +``` + +边界约束: + +- M10 只通过 M04 的订单项标识索取事实,不复制订单金额、地址或商品快照;金额由 M04 已持久化实付单价 × 申请数量计算,客户端不得指定最终金额。 +- M10 调用 M05 的 `IRefundService.CreateRefundAsync` 内部应用能力退款,不直接修改钱包数据;M05 不得重复入账或改变订单 `Paid` 状态。 +- M10 拒绝在订单行上重复审核;同一订单项的处理中申请阻断发货,已退款数量从可履约数量中扣除。 +- M10 通知失败不能反向修改售后事实;具体事件名、Outbox 和 Worker 重试方式由系统架构设计承接,不进入业务流程图。 + +## 三、核心状态流转 + +X04 售后有 8 个独立状态,不能与订单核心履约状态(`PendingPayment` / `Paid` / `Shipped` / `Completed` / `Cancelled`)合并或覆盖。 + +```mermaid +stateDiagram-v2 + [*] --> 待审核: 买家提交申请 + 待审核 --> 已撤销: 买家在审核前主动撤销 + 待审核 --> 已拒绝: 商家审核拒绝 + 待审核 --> 退款中: 仅退款审核通过 + 待审核 --> 待退货: 退货退款审核通过 + 待退货 --> 已撤销: 买家不允许撤销 + 待退货 --> 待收货: 买家提交退货物流 + 待收货 --> 已拒绝: 商家拒绝收货 + 待收货 --> 退款中: 商家确认收到退货 + 退款中 --> 已退款: 钱包退款成功 + 退款中 --> 退款失败: 钱包退款失败 + 退款失败 --> 退款中: 幂等重试 + 待审核 --> [*] + 已撤销 --> [*] + 已拒绝 --> [*] + 已退款 --> [*] + 退款失败 --> [*] +``` + +合法转换回执: + +- 申请只能由买家提交;只有 `待审核` 状态可被买家主动撤销。 +- 商家只能在 `待审核` 状态下审核;审核拒绝后不可再次审核。 +- 退货退款必须经历 `待退货 → 待收货 → 退款中`;商家确认收货是退款前置条件。 +- 重复退款请求返回首次结果,不重复写入钱包流水。 + +## 四、可申请判断与提交申请 + +```mermaid +flowchart TD + ID["M01:已认证且状态正常的买家"] --> A["从订单详情选择符合条件的订单项"] + ORD["M04:订单项、归属、实付快照、当前状态和已售后数量"] --> A + A --> B{"订单项归属本人?"} + B -- "否" --> X["拒绝访问,不泄露他人订单"] + B -- "是" --> C{"订单状态可申请?"} + C -- "PendingPayment" --> CY["拒绝:未支付订单不进入售后"] + C -- "Paid" --> CT1["仅退款:未发货订单只允许仅退款"] + C -- "Shipped" --> CT2["退款/退货均可"] + C -- "Completed 7 天内" --> CT2 + C -- "Cancelled 或超期" --> CY2["拒绝:已取消或超期不可申请"] + CT1 --> D + CT2 --> D + D{"申请数量 ≤ 剩余可售后数量?"} + D -- "否" --> DZ["提示可申请范围,不创建申请"] + D -- "是" --> E{"类型与状态匹配?"} + E -- "仅退款 + Paid" --> F["确定类型为 RefundOnly"] + E -- "退款/退货 + Shipped/Completed" --> G["确定类型为 ReturnAndRefund"] + F --> H["开启创建事务"] + G --> H + H --> I["计算金额 = 实付单价 × 申请数量"] + I --> J{"同一订单项已有 PendingReview?"} + J -- "是" --> JZ["拒绝重复申请"] + J -- "否" --> K["写入申请、申请单状态置 PendingReview、记录审计日志"] + K --> L{"事务提交成功?"} + L -- "否" --> LR["整体回滚,不创建申请"] + L -- "是" --> M["记录待发布申请提交事实"] + M --> N["通知商家审核"] + M --> O["买家可查看本人申请详情"] +``` + +关键规则: + +- 实付单价与可用数量由 M04 已持久化订单事实决定;客户端不得传入任意金额。 +- 同一买家、同一订单项已有 `PendingReview` 申请时拒绝重复申请,避免占用未售后数量。 +- `PendingPayment` 订单不进入售后;请走 F09 主动取消或等待 C03 自动取消。 +- 申请提交后写入 `audit_log`,明确记录申请人与提交时间。 + +## 五、商家审核与状态推进 + +```mermaid +flowchart TD + ID["M01:已认证且状态正常的商家"] --> A["分页查询待审核申请"] + AUTH["M04:订单项、归属、当前状态"] --> A + A --> B{"申请归属当前商家授权范围?"} + B -- "否" --> X["拒绝访问,不泄露申请内容"] + B -- "是" --> C{"申请状态为 PendingReview?"} + C -- "否" --> CY["拒绝:状态非法或不修改已审核申请"] + C -- "是" --> D{"决策?"} + D -- "Reject" --> E["记录审核意见"] + E --> F["状态条件推进 PendingReview → Rejected"] + F --> G["记录待发布审核拒绝事实"] + D -- "Approve" --> H["记录审核意见"] + H --> I{"申请类型?"} + I -- "RefundOnly" --> J["状态条件推进 PendingReview → Refunding"] + J --> K["异步调用 IRefundService 触发退款"] + I -- "ReturnAndRefund" --> L["状态条件推进 PendingReview → PendingReturn"] + L --> L1["保留审核意见,等待买家提交退货物流"] + K --> MR + L --> MR + G --> MR + MR{"事务提交成功?"} + MR -- "否" --> MRR["整体回滚,状态保持 PendingReview"] + MR -- "是" --> N["通知买家审核结果"] + N --> O["商家可查看审核结果与状态时间线"] +``` + +审核并发规则: + +- 两名商家对同一 `PendingReview` 申请同时审核时,状态条件只允许一次成功;败方收到 `409 + AFTER_SALES.INVALID_STATUS`。 +- 商家不能修改买家原始申请内容,只能在审核意见中表达观点。 +- 商家超时未处理不自动同意或拒绝;只持续显示待处理并提醒。 + +## 六、买家提交退货物流 + +```mermaid +flowchart TD + ID["M01:已认证且状态正常的买家"] --> A["按本人申请选择 ReturnAndRefund 待退货申请"] + A --> B{"申请归属本人且状态为 PendingReturn?"} + B -- "否" --> X["拒绝访问"] + B -- "是" --> C["输入快递公司、快递单号、寄出时间与备注"] + C --> D{"字段合法?"} + D -- "否" --> DZ["保留输入并提示字段错误"] + D -- "是" --> E{"快递单号已被使用?"} + E -- "是" --> EZ["拒绝重复使用同一单号"] + E -- "否" --> H["开启事务"] + H --> I["状态条件推进 PendingReturn → PendingReceipt"] + I --> J["记录退货物流信息、audit_log"] + J --> K{"事务提交成功?"} + K -- "否" --> KR["整体回滚,状态保持 PendingReturn"] + K -- "是" --> L["记录待发布退货物流提交事实"] + L --> M["通知商家待收货"] +``` + +退货物规则: + +- 状态条件 `WHERE status = 'PendingReturn' AND buyer_id = currentBuyerId` 唯一推进。 +- 快递单号全局唯一约束,避免多笔售后重复登记同一运单。 +- 仅退款(`RefundOnly`)申请不需要走本流程。 + +## 七、商家确认收货与退款入账 + +```mermaid +flowchart TD + ID["M01:已认证且状态正常的商家"] --> A["按授权范围查询 PendingReceipt 申请"] + A --> B{"申请归属当前商家且状态为 PendingReceipt?"} + B -- "否" --> X["拒绝访问"] + B -- "是" --> C["输入收到数量与备注"] + C --> D{"收到数量合法?"} + D -- "否" --> DZ["保留输入并提示错误"] + D -- "是" --> E["开启事务"] + E --> F["状态条件推进 PendingReceipt → Refunding"] + F --> G["按退货数量回补原库存通道(普通/C01 秒杀)"] + G --> H["记录回补事实与确认收货审计日志"] + H --> I["事务提交成功后异步调用 IRefundService 触发退款"] + I --> J["记录待发布退款事实"] + J --> K["通知买家退款处理中"] +``` + +退款入账流程(`IRefundService`,内部应用能力,必走): + +```mermaid +flowchart TD + CLI["调用方:A416 审核 + A417 确认收货 + A419 失败重试"] --> A["计算目标金额 = 实付单价 × 申请数量"] + A --> B{"金额与申请计算金额一致?"} + B -- "否" --> BZ["失败回滚"] + B -- "是" --> C{"幂等键已存在?"} + C -- "同 Key 同金额" --> D["返回首次结果"] + C -- "同 Key 不同金额" --> CZ["抛 IdempotencyKeyReusedException"] + C -- "否" --> E["开启退款事务"] + E --> F["原子写入:退款记录 + 钱包入账 + 钱包流水 + 申请状态 Refunding → Refunded"] + F --> G{"事务提交成功?"} + G -- "否" --> GR["整体回滚,状态保持 Refunding"] + G -- "是" --> H["记录待发布退款完成事实"] + H --> I["通知买家退款成功"] +``` + +退款入账原子结果: + +```text +钱包入账成功 ++ 退款记录已写入 ++ 钱包流水已写入 ++ 申请状态 Refunding → Refunded ++ 待发布的退款完成事实已写入 += 同一事务提交成功 +``` + +任一步失败时整体回滚,不允许出现"钱包已入账但退款记录缺失"或"申请已退款但订单被覆盖"的部分结果。 + +## 八、买家撤销申请 + +```mermaid +flowchart TD + ID["M01:已认证且状态正常的买家"] --> A["按本人申请选择 PendingReview 申请"] + A --> B{"申请归属本人?"} + B -- "否" --> X["拒绝访问"] + B -- "是" --> C{"状态为 PendingReview?"} + C -- "否" --> CY["拒绝:审核后不允许撤销"] + C -- "是" --> D["开启事务"] + D --> E["释放订单项已占用的未售后数量"] + E --> F["状态条件推进 PendingReview → Cancelled"] + F --> G["记录审计日志"] + G --> H{"事务提交成功?"} + H -- "否" --> HR["整体回滚,状态保持 PendingReview"] + H -- "是" --> I["记录待发布申请撤销事实"] + I --> J["通知商家申请已撤销"] +``` + +撤销售后规则: + +- 审核通过的申请不允许撤销或回退;买家只能等待自然终态。 +- 部分退款后订单仍可发起退货,但"已退款数量"从可履约数量中扣除。 + +## 九、消息通知与页面反馈 + +```mermaid +flowchart TD + A["已登录买家或商家进入售后页面"] --> B{"查询类型"} + B -- "申请列表" --> C["按本人或授权范围分页查询"] + B -- "申请详情" --> D["展示订单项快照、实付金额、申请内容、审核意见、状态时间线"] + B -- "审核日志" --> E["按 audit_log 时序展示所有状态变更"] + B -- "退款详情" --> F["展示退款金额、到账状态、流水编号"] + C --> G["页面展示确定状态和操作入口"] + D --> G + E --> G + F --> G + A -->|"资源非本人"| X["404/403,不泄露他人售后内容"] +``` + +通知触发点: + +- 申请提交 → 通知商家"待审核"。 +- 商家审核拒绝 → 通知买家"审核拒绝"。 +- 商家审核通过(仅退款)→ 通知买家"退款中"。 +- 商家审核通过(退货退款)→ 通知买家"待退货"。 +- 买家提交退货物流 → 通知商家"待收货"。 +- 商家确认收货 → 通知买家"退款中"。 +- 退款成功 → 通知买家"退款成功"。 +- 退款失败 → 通知买家"退款失败",商家可重试。 + +## 十、异常、回滚与责任 + +| 场景 | M10 处理 | 最终状态/责任 | +|---|---|---| +| 订单项不属于买家或不在售后期限/状态 | 拒绝申请 | 不创建申请,不泄露归属 | +| 申请数量超剩余可售后数量 | 拒绝申请 | 提示可申请范围 | +| 同一买家同一订单项重复申请 | 拒绝 | 不重复占用未售后数量 | +| 商家超时未审核 | 不自动同意/拒绝 | 持续显示待审核 | +| 商家越权审核他人申请 | 拒绝 | 403 | +| 两名商家并发审核 | 状态条件唯一胜出 | 败方收到 409 | +| 重复退款请求 | 返回首次结果 | 不重复入账 | +| 退款执行失败 | 状态保持 `退款失败` | 允许幂等重试,不伪装成功 | +| 通知暂时失败 | 售后事实保留 | M09 按可靠机制重试 | +| 账号禁用 | 不允许新建申请 | 已有申请继续由商家和系统处理 | +| 售后模块不直接修改钱包 | 通过 `IRefundService` 内部应用能力 | AfterSales 不持有钱包表权限 | +| 退款后订单核心履约状态 | 保持原状态 | 不新增"已退款"等破坏核心订单状态 | + +## 十一、由流程派生的接口契约映射 + +本节是第二至十章业务流程的下游映射,不是流程输入。先确认"要完成什么业务动作、处于什么状态、成功或失败后得到什么结果",再决定由哪个接口承载。若现有 Axxx 与流程冲突,先记录接口缺口并修改接口设计;只有业务需求本身变化时,才回到前文重新评审流程。 + +| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +|---|---|---|---| +| 售后资格预检 | A411 | 按订单项、状态、时限和剩余可售后数量判断资格 | 待交叉评审 | +| 提交售后申请 | A412 | 校验归属、类型、金额和防重复,并写入 PendingReview | 待交叉评审 | +| 申请列表 | A413 | 按本人或授权范围分页返回申请 | 待交叉评审 | +| 申请详情 | A414 | 展示订单项快照、实付金额、申请内容、审核意见、状态时间线 | 待交叉评审 | +| 撤销申请 | A415 | 仅 PendingReview 可撤销,状态条件推进 | 待交叉评审 | +| 商家审核 | A416 | 通过业务能力推进状态并发退款或转待退货 | 待交叉评审 | +| 提交退货物流 | A434 | 写入快递公司与运单,状态推进 PendingReturn → PendingReceipt | 待交叉评审 | +| 商家确认收货 | A417 | 状态条件推进 PendingReceipt → Refunding + 库存回补 | 待交叉评审 | +| 审核日志 | A418 | 按 audit_log 时序展示状态变更 | 待交叉评审 | +| 退款失败重试 | A419 | 状态条件推进 RefundFailed → Refunding | 待交叉评审 | +| 退款入账 | A431 内部应用能力 | 不占 Axxx HTTP 编号,进程内调用 IRefundService | 待交叉评审 | +| 退款详情 | A432 | 展示单笔退款金额、状态、流水编号 | 待交叉评审 | +| 退款列表 | A433 | 按本人或授权范围分页返回退款记录 | 待交叉评审 | + +接口详细定义与实现必须承接上述流程结果。当前接口设计拟使用 `Idempotency-Key` 承载"防重复标识",并需满足接口设计 1.12 的幂等与并发规则以及 4.6 的资金类持久化幂等约束;HTTP 状态码、请求字段和错误码不得反向写入业务图。 + +## 十二、扩展接入边界 + +- C08 收款对账:每日对账范围涵盖 M10 退款成功记录、退款流水和小金库入账,不一致项进入差异并由管理员闭环。 +- C03 超时取消:售后申请与退款处理不参与 C03 待支付订单超时扫描;只强制 `PendingPayment` 订单。 +- M09 消息:售后提交、审核、待退货、退款中、退款成功和退款失败均通过 M09 通知;通知失败按可靠机制重试,不能反向修改售后事实。 +- M04 订单:售后申请阻断发货;部分退款数量从可履约数量中扣除;全部退款后订单项不可发货。 +- M02 库存:未发货仅退款 → 回补库存;已发货仅退款 → 不回补库存;退货退款 → 商家确认收货后按退货数量回补。 + +## 十三、由流程反查出的接口与数据待评审项 + +1. 退款金额不一致:流程要求金额由服务端按实付单价 × 申请数量计算;A431 内部应用能力的 `expectedAmount` 只能承担客户端旧值冲突保护,不能成为扣款事实。 +2. 部分退款后订单状态:流程要求订单保持原核心履约状态;A407 支付记录列表与 A432 退款详情必须分别提供"未退款金额"和"已退款金额",不能合并为"已退款"状态。 +3. 卖家超时未处理:流程不自动同意或拒绝;A416 审核接口必须保留超时仍未处理的 PendingReview 状态,不能引入"超时自动拒绝"。 +4. 退货拦截发货:流程要求处理中申请阻断发货;M04 的发货接口必须能识别订单项未售后数量,禁止对未售后数量不足的订单项发货。 +5. 账号禁用售后:流程要求禁用账号不能新建或主动操作售后;A412 / A415 必须在账号禁用时拒绝;已有申请可被商家和系统继续处理。 +6. 库存回补口径:流程要求按订单项原库存来源通道回补;M02 库存接口必须区分普通库存与 C01 秒杀库存,不能把退款售后回补到错误通道。 +7. 状态机不可逆:流程要求"已退款 / 已拒绝 / 已撤销"为终止状态;A419 退款失败重试只能从 `退款失败` 推进,不能从"已退款"或"已拒绝"推进。 +8. 退货快递单号全局唯一:流程要求同一快递单号只能用于一笔售后;A434 提交退货物流要建唯一约束 DBxxx(待评审)。 +9. 商家并发审核:流程要求状态条件唯一胜出;A416 审核必须检查 `WHERE status = 'PendingReview'` 条件更新,失败方收到 `409 + AFTER_SALES.INVALID_STATUS`。 +10. 库存回补时机:流程要求未发货仅退款 + 退货退款确认收货都回补库存;A421 退款成功接口不负责库存回补,由 M04 / M02 在确认收货时完成。 +11. DB081~DB100 尚未形成可实施的完整表定义,数据库字段、约束和索引必须由数据库设计任务另行确认。 + +## 十四、验收证据清单 + +- [ ] 合法买家可对符合条件的订单项提交退款或退货申请。 +- [ ] 越权、超额、超时和重复申请被明确拦截,状态时间线一致。 +- [ ] 商家审核通过后仅退款申请立即进入退款;退货退款申请等待买家提交物流和商家确认收货。 +- [ ] 退款成功金额按订单项实付单价 × 申请数量正确返回本人小金库且立即可用。 +- [ ] 重复退款请求不重复增加钱包余额。 +- [ ] 退款失败状态保留并允许幂等重试,不伪装成功。 +- [ ] 通知失败时售后事实保留,由 M09 可靠重试。 +- [ ] 未发货订单仅退款 → 库存回补;已发货订单仅退款 → 不回补库存;退货退款确认收货后按退货数量回补。 +- [ ] 订单核心履约状态不被部分退款覆盖;可履约数量随已退款数量减少。 +- [ ] 状态机不被误用:审核后不允许撤销;已退款/已拒绝/已撤销为终止状态。 +- [ ] 卖家超时未处理不自动同意或拒绝。 +- [ ] 保存申请、审核、撤销、退款、重复处理、并发审核和越权场景证据。 -- Gitee From b14d9f7e85f5ed80641bf4afbf8ec3cc01be6ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A1=BE=E6=AC=A3=E6=9C=88?= Date: Fri, 24 Jul 2026 14:47:53 +0800 Subject: [PATCH 058/118] docs(process): add gxy catalog/order/review/search flow documents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - M02 分类与商品流程:F04/F05/F06 购物端浏览主流程与状态机 - M06-01 后台分类与商品管理:F11 商家写操作、并发保护与缓存失效责任 - M07 商品评价:X01 评价资格、唯一约束与公开汇总 - C04 中文搜索:F05 进阶搜索实现、降级与可重复压测口径 初稿,待 Cart/Ordering/Identity/C07 交叉评审。 --- ...34\347\264\242\346\265\201\347\250\213.md" | 263 ++++++++++++++++++ ...06\345\223\201\346\265\201\347\250\213.md" | 230 +++++++++++++++ ...41\347\220\206\346\265\201\347\250\213.md" | 258 +++++++++++++++++ ...04\344\273\267\346\265\201\347\250\213.md" | 239 ++++++++++++++++ 4 files changed, 990 insertions(+) create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" new file mode 100644 index 0000000..4bc1e57 --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" @@ -0,0 +1,263 @@ +# C04 中文搜索流程 + +> 负责人:顾欣月 +> 覆盖:C04 +> 基础核心流程:F04、F05、F06、M02-01;与根文档 [`业务流程设计.md`](../业务流程设计.md) 第 3.3 节中搜索接入点保持一致 +> 直接协作:顾欣月本人(M02-01 商品列表与搜索)、韦乾强(M04 订单)、罗皓晨(C07 缓存协作) +> 文档状态:初稿,待顾欣月自审及 Catalog/Ordering 交叉评审 +> 升级标记:在初稿基础上补强统一搜索契约、降级细节、性能压测口径、回归核心结果和验收对照 +> 需求事实源:[需求规格说明书 C04](../../../01-需求文档/需求规格说明书.md) 的完整七节 + +## 一、范围与事实来源 + +C04 在 M02-01 的基础搜索入口上增加中文分词模糊搜索、多条件筛选、排序和可重复的性能对比。用户仍通过同一商品列表完成搜索,不需要理解底层使用哪种实现;搜索失败、无结果或降级时也应获得清楚反馈。 + +本期使用字符 N-gram 等效分词和 PostgreSQL 倒排索引完成中文模糊搜索,不引入独立搜索引擎。本期不实现个性化排序、搜索广告、热词榜、搜索审核、同义词词典或后台全状态商品检索。 + +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。C04 不新增 Axxx 接口编号,而是替换 M02-01 列表查询的底层搜索实现。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| C04 需求 | 完整定义 | 作为业务语义事实源 | +| 本文业务流程 | 初稿 | 先确认角色、状态、分支和模块出入口 | +| 搜索适配器接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| DBxxx 索引维护 | 部分定义 | 本文不发明表字段、索引名和分词参数 | +| 性能对比原始结果 | 缺失 | 本文登记对比场景与口径,不预填压测结论 | + +## 二、模块直接出入口 + +```mermaid +flowchart LR + UI["购物端商品列表与搜索页"] -->|"统一搜索契约 IProductSearch"| AD["C04 搜索适配器"] + AD -->|"字符 N-gram 与倒排索引"| DB["PostgreSQL 商品事实与 pg_trgm/GIN 索引"] + AD -->|"返回口径一致的搜索结果"| UI + BASE["M02-01 基础模糊查询(LIKE/ILIKE)"] -. "降级回退" .-> AD + CACHE["C07 缓存(Cache-Aside)"] -. "读取前缓存" .-> AD + + M06["M06-01 商家商品事务"] -->|"商品事实与索引同步"| DB + AD -->|"调用方读取最新已上架商品"| LIST["M02-01 列表与 F04 公开浏览"] + AD -->|"商品 ID 与详情事实"| DET["F06 商品详情"] + + UI -->|"越权或参数非法"| X["字段级错误,保留查询条件"] + DB -->|"索引缺失或损坏"| Y["停止进阶查询并提示维护"] + AD -->|"进阶搜索执行失败"| Z["记录原因并回退基础模糊查询"] + CACHE -->|"缓存命中但索引缺失"| W["按降级处理,不复用旧进阶结果"] +``` + +边界约束: + +- C04 替换 F05 基础模糊查询的底层实现;F04 列表与 F06 详情的业务口径、参数白名单和已上架过滤不变。 +- 搜索关键词和筛选参数必须参数化处理,排序字段使用白名单,不得拼接不可信 SQL。 +- 公开搜索强制过滤草稿、下架和已删除商品;任何身份都不能通过请求参数绕过。 +- 商品数据与搜索索引由同一 PostgreSQL 实例维护,不存在独立搜索服务的异步数据副本。 +- 进阶搜索暂时不可用时,可在保证已上架过滤和参数安全的前提下回退基础模糊查询,并记录降级原因。 +- 缓存命中但底层索引缺失时按降级处理,不复用旧的进阶结果;缓存写入与失效由 C07 主责统一约定。 +- 扩展完成后回到的核心结果:F04 公开列表仍只返回已上架商品,F05 关键词查询仍按统一搜索契约返回同口径结果,F06 详情与下单重读条件不变;进阶搜索失败不改变这些核心结果。 + +## 三、中文搜索主流程 + +```mermaid +flowchart TD + A["用户输入中文关键词并按需选择分类、价格、库存和排序"] --> B["服务端校验输入并调用统一搜索契约 IProductSearch"] + B --> C["搜索适配器对关键词进行字符 N-gram 等效分词"] + C --> D["按分词结果召回候选商品"] + D --> E{"是否触发进阶查询?"} + E -- "是" --> F["基于 pg_trgm/GIN 倒排索引完成相关度排序"] + E -- "否" --> BAS["回退基础模糊查询(LIKE/ILIKE)"] + F --> G["应用多条件筛选:关键词、分类、价格区间、仅看有货、已上架"] + BAS --> G + G --> H{"结果是否需要再过滤已上架状态?"} + H -- "是" --> I["返回结果前再次保证商品为已上架"] + H -- "否" --> J["直接返回结果"] + I --> K["分页返回数据与当前筛选结果"] + J --> K + K --> L{"是否有结果?"} + L -- "否" --> M["展示空集合和当前条件,并允许清空或调整"] + L -- "是" --> N["展示商品摘要并保留查询状态"] +``` + +关键规则: + +- 中文分词对商品名称、分类名称和商品描述进行字符 N-gram 等效分词,支持中文多词查询、部分匹配和可解释的模糊召回。 +- 多条件筛选支持关键词与分类、价格区间、仅看有货和已上架条件组合使用。 +- 排序至少支持相关度及经过白名单约束的价格或时间排序;相同条件下顺序应稳定。 +- 结果一致性:搜索结果最终以商品当前状态为准,索引中的旧数据不得让下架商品重新公开。 + +## 四、索引更新与一致性 + +```mermaid +flowchart TD + A["M06-01 商家商品事务提交"] --> B["商品事实写入 PostgreSQL"] + B --> C["pg_trgm/GIN 数据库索引随商品数据同步更新"] + C --> D["下一次搜索查询使用最新商品状态和检索文本"] + D --> E{"下架商品是否仍出现在索引中?"} + E -- "是" --> F["结果过滤阶段强制按已上架状态过滤"] + E -- "否" --> G["直接返回最新结果"] + A -. "事务失败" .-> H["不更新索引,搜索结果保持原状"] +``` + +索引约束: + +- 商品创建、编辑、上下架或删除成功后,数据库索引随商品数据同步更新,不建设独立索引同步任务。 +- 索引缺失或损坏时停止使用进阶查询并提示维护,重建索引后恢复;任何情况下均过滤非公开商品。 +- 进阶搜索异常时记录错误并回退安全的基础模糊查询;无法保证正确性时明确提示稍后重试。 + +## 五、降级处理 + +```mermaid +flowchart TD + A["搜索请求进入 IProductSearch 适配器"] --> B{"进阶搜索是否可用?"} + B -- "是" --> C["执行进阶查询并返回结果"] + B -- "否" --> D["记录降级原因(索引缺失、查询失败等)"] + D --> E{"基础模糊查询能否保证已上架过滤和参数安全?"} + E -- "是" --> F["执行基础模糊查询(LIKE/ILIKE)"] + E -- "否" --> G["返回明确提示稍后重试,不返回错误数据"] + F --> H["返回结果前再次过滤已上架商品"] + H --> I["分页返回数据并标注降级原因"] + C --> I +``` + +降级约束: + +- 降级不是静默制造错误结果:系统需记录所用实现与原因,并保证核心过滤和权限不变。 +- 缓存命中但底层索引缺失时仍然按降级处理,不复用旧的进阶结果。 +- 降级日志需在可观测性中暴露(指标或事件),便于答辩与压测现场说明。 +- 进阶搜索长时间不可用且无法回退时,禁止返回陈旧的进阶结果或半成品数据,只能返回明确提示。 + +## 六、性能对比要求 + +```mermaid +flowchart TD + A["生成不少于 10000 条商品演示数据"] --> B["固定查询词、筛选条件和并发参数"] + B --> C["使用 50 并发持续 60 秒执行进阶搜索"] + C --> D["使用 50 并发持续 60 秒执行 LIKE/ILIKE 基线"] + D --> E["汇总成功率、P95、QPS、CPU 和 I/O"] + E --> F{"进阶搜索对比基线是否满足目标?"} + F -- "是" --> G["记录原始数据、环境参数和命令"] + F -- "否" --> H["调整索引或查询实现并复测"] + G --> I["保留可重复执行脚本与原始结果文件"] +``` + +性能压测口径(按需求固化): + +| 项目 | 进阶搜索目标 | LIKE/ILIKE 基线 | 备注 | +|---|---|---|---| +| 商品数据规模 | ≥ 10000 条 | ≥ 10000 条 | 数据集、查询词、筛选条件在两次对比中完全一致 | +| 并发 | 50 | 50 | 同一压测工具与脚本 | +| 持续时间 | 60 秒 | 60 秒 | 包含预热与正式采样两段 | +| 成功率 | ≥ 99% | 记录基线 | 含 5xx 视为失败 | +| P95 延迟 | ≤ 500 ms | 记录基线 | 与基线对比应降低至少 30% | +| 结果正确性 | 与基线核对 | 基线 | 关键查询词集合返回的 Top-N 完全一致 | + +结果回填位(执行后填入真实数据): + +- 数据生成方式与命令: +- 数据集规模与字段分布: +- 预热时长与正式采样时长: +- 进阶搜索 P95 / 成功率 / QPS: +- LIKE/ILIKE 基线 P95 / 成功率 / QPS: +- 进阶对比基线提升比例: +- 结果正确性核对样例: +- 原始日志与脚本路径: + +性能目标: + +- 进阶搜索成功率不低于 99%。 +- P95 不高于 500 ms。 +- 与 `LIKE/ILIKE` 基线相比,P95 应降低至少 30%。 +- 结果正确性需要核对,避免“更快但不准”的错误对比。 + +## 七、结果反馈与页面衔接 + +```mermaid +flowchart TD + A["用户提交搜索条件"] --> B{"结果类型"} + B -- "有结果" --> C["展示商品摘要并保留查询状态"] + B -- "无结果" --> D["展示空集合、当前条件和清空筛选入口"] + B -- "加载失败" --> E["保留查询条件并显示简短原因,允许重试"] + B -- "降级" --> F["展示结果并提示当前使用基础模糊查询"] + B -- "进阶搜索不可用且无法回退" --> G["提示稍后重试并保留查询条件"] + C --> H["用户继续翻页、调整条件或进入详情"] + D --> H + E --> H + F --> H + G --> H +``` + +用户反馈约束: + +- 用户反馈:保留用户查询条件;加载、无结果、降级和失败状态提供简洁说明、清空条件或重试入口。 +- 降级提示不应让用户误以为系统异常,只说明“搜索实现已切换”,不影响后续操作。 + +## 八、异常、回滚与责任 + +| 场景 | C04 处理 | 最终状态/责任 | +|---|---|---| +| 进阶搜索执行失败 | 记录错误并回退基础模糊查询 | 不返回数据库内部错误 | +| 索引缺失或损坏 | 停止进阶查询并提示维护 | 重建索引后恢复 | +| 特殊字符或超长关键词 | 参数校验与安全转义 | 不返回数据库内部错误 | +| 条件无匹配 | 返回正常空集合 | 展示调整关键词或清空筛选入口 | +| 高并发下响应变慢 | 通过压测记录瓶颈和资源参数 | 不以缓存掩盖错误结果 | +| 商家修改商品后下架 | 数据库索引同步更新 | 过滤阶段强制按已上架状态过滤 | +| 商家删除商品 | 数据库索引同步删除 | 不让下架商品重新出现在结果中 | +| 缓存命中但索引缺失 | 按降级处理 | 不复用旧的进阶结果 | + +## 九、由流程派生的接口契约映射 + +本节是第三至八章业务流程的下游映射,不是流程输入。先确认“要完成什么业务动作、处于什么状态、成功或失败后得到什么结果”,再决定由哪个接口承载。C04 不新增独立 Axxx 接口编号;进阶搜索替换 M02-01 列表查询底层实现,统一搜索契约由 M02 接口承载。 + +| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +|---|---|---|---| +| 中文分词模糊搜索 | 复用 M02-01 列表接口底层 | 按统一搜索契约返回与基础模糊查询同口径的结果 | 待交叉评审 | +| 多条件筛选与排序 | 复用 M02-01 列表接口参数 | 关键词、分类、价格、库存和排序组合生效 | 待交叉评审 | +| 进阶搜索降级 | 由 M02-01 接口返回结果 | 降级时返回结果与原因日志,不改变公开口径 | 待交叉评审 | +| 性能对比压测 | 不通过业务接口暴露 | 保留测试脚本与原始结果 | 待交叉评审 | + +接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 + +## 十、扩展接入边界 + +- F04/F06:保持 F04 列表与 F06 详情口径不变;C04 仅替换搜索实现,不修改公开浏览口径。 +- F05:基础模糊查询由 C04 进阶实现替换;返回字段、参数白名单和分页口径保持一致。 +- M02-01:搜索适配器由 M02-01 内部实现替换,前端和接口契约不变。 +- M06-01:商品事务提交后索引随 PostgreSQL 数据同步维护;不通过异步处理器复制搜索索引。 +- C07:缓存只能放在搜索查询路径之前;缓存命中但索引缺失时按降级处理,不复用旧的进阶结果。 +- M04:下单时由 M04 重读商品事实进行条件扣减,不信任搜索结果中的价格或库存。 + +## 十一、由流程反查出的接口与数据待评审项 + +1. 字符 N-gram 的最小长度和最大长度参数需要在数据库设计中明确,避免过短导致误命中或过长导致索引过大。 +2. pg_trgm/GIN 索引的具体列(商品名称、分类名称、描述)需要在数据库设计中确认是否全列建立或部分建立。 +3. 相关度排序的“相同条件下顺序应稳定”需要明确次级排序字段,建议在数据库设计中统一。 +4. 进阶搜索降级是否需要在响应中携带降级原因字段,需要与前端展示要求对齐。 +5. 性能对比环境的固定参数(CPU、内存、PostgreSQL 配置、连接池大小)需要在执行前统一记录,避免环境差异影响结论。 +6. 缓存命中与索引缺失同时发生时是否需要主动清除缓存,避免长时间返回错误结果。 +7. 进阶搜索失败时的告警和可观测性要求,需要与 M00 公共基建的可观测性约束一致。 +8. DBxxx 索引维护语句与分词参数需要在数据库设计任务中给出可执行定义,不在本流程中预填。 + +## 十二、验收证据清单 + +对照 `验收标准.md` 的 C04: + +- [ ] C04:中文多词查询能够通过分词或等效方案获得合理结果,并能解释为什么命中。 +- [ ] C04:关键词、分类、价格、库存和已上架条件可组合筛选,相关度、价格或时间排序结果正确且稳定。 +- [ ] C04:新建、修改、上下架商品后搜索结果立即遵守最新数据;重建索引前后均不会公开下架商品。 +- [ ] C04:进阶搜索异常时降级行为可观察、结果口径不越权,页面保留条件并提供明确反馈。 +- [ ] C04:在不少于 10000 条商品、50 并发、60 秒场景下完成与 `LIKE/ILIKE` 的可重复对比,达到既定成功率和 P95 目标。 +- [ ] C04:保留数据生成方式、环境参数、执行命令、原始结果和汇总报告,使结果能够复现。 +- [ ] 答辩能够说明 N-gram、倒排索引、同步更新、排序、降级和性能对比方法。 +- [ ] 性能压测结果按第六章“结果回填位”填入真实数据,不留空。 + +## 十三、提交前自检与升级路径 + +- [ ] 图中的 F、X、C、M 编号与需求一致,与根文档 3.3 节中搜索接入点一致。 +- [ ] 已写清基础 F、核心接入状态和回归结果;进阶搜索失败或降级不改变 F04~F06 核心结果。 +- [ ] Mermaid 图内部包含直接上游模块输入(M02-01、M06-01)和直接下游模块出口(F04、F06)。 +- [ ] 主流程、降级分支、索引更新与一致性、性能对比齐全。 +- [ ] 状态名称与需求规格说明书一致;没有新增商品状态或排序项。 +- [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径或 DTO 代替流程;没有自行发明接口编号、字段名或数据库状态码。 +- [ ] 已对照根文档 3.3 节中搜索接入点校准入口位置,C04 不另起一套主链路。 + +升级到“待交叉评审”的条件:自检完成、主流程与降级分支齐全、性能压测脚本可执行、第六章“结果回填位”保留、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 + +升级到“已确认”的条件:Catalog 主责确认统一搜索契约与参数白名单,Ordering 主责确认下单重读不受搜索实现影响,C07 主责确认缓存失效责任,根文档 3.3 节中搜索接入点对应追踪项成熟度同步更新;性能压测结果按口径填入第六章“结果回填位”。 \ No newline at end of file diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" new file mode 100644 index 0000000..2281805 --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" @@ -0,0 +1,230 @@ +# M02 分类与商品流程 + +> 负责人:顾欣月 +> 覆盖:M02-01、M02-02、F04、F05、F06 +> 基础核心流程:F01~F02、M01 Identity;与根文档 [`业务流程设计.md`](../业务流程设计.md) 第 3.3、3.8.1 节保持一致 +> 直接协作:朱惠惠(M03 购物车)、韦乾强(M04 订单)、唐宇昊(M01 身份)、罗皓晨(C07 缓存协作) +> 文档状态:初稿,待顾欣月自审及 Cart/Ordering/Identity 交叉评审 +> 升级标记:在初稿基础上补强 C07 缓存接入边界、C04 搜索契约衔接、回归核心结果表述和验收对照 +> 需求事实源:[需求规格说明书 M02-01](../../../01-需求文档/需求规格说明书.md) 与 [M02-02](../../../01-需求文档/需求规格说明书.md) 的完整七节 + +## 一、范围与事实来源 + +本模块负责购物端商品发现入口,覆盖分类筛选、关键词搜索、价格区间、库存条件、排序与组合查询,以及商品详情页的信息展示、可售状态判断、买家/游客操作衔接和评价入口衔接。它不承接商家后台的商品维护(属于 M06-01),不替代购物车的库存和归属校验,也不修改商品在历史订单中的快照。 + +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。A0xx 只用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| M02-01/F04、F05 需求 | 完整定义 | 作为业务语义事实源 | +| M02-02/F06 需求 | 完整定义 | 作为业务语义事实源 | +| 本文业务流程 | 初稿 | 先确认角色、状态、分支和模块出入口 | +| 分类与商品接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| DBxxx 分类/商品表 | 部分定义 | 本文不发明表字段、状态码和索引 | +| C07 缓存协作 | 待细化(罗皓晨主责、顾欣月协作失效规则) | 只登记接入点,不混入 F04~F06 核心浏览口径;具体 TTL 与失效策略由缓存主责确认 | +| C04 中文搜索进阶 | 待细化(顾欣月主责) | 在 M02-01 基础模糊查询入口上增强,业务口径与本文保持一致 | + +## 二、模块直接出入口 + +```mermaid +flowchart LR + ID["M01 Identity
已认证买家、游客角色识别"] -->|"BuyerOnly/游客均允许"| CAT["M02 Catalog
已上架商品、分类、实时价格与库存"] + ADM["M06-01 商家后台商品管理"] -->|"分类与商品维护命令(事务提交后)"| CAT + CACHE["C07 Cache-Aside
缓存命中则直返
事务提交后失效"] -. "读取前缓存" .-> CAT + SEARCH["C04 进阶搜索适配器
IProductSearch"] -. "替换 F05 底层实现" .-> CAT + + CAT -->|"已上架商品与实时价格库存"| CART["M03 Cart 加购与失效标记"] + CAT -->|"下单重读与库存条件更新"| ORD["M04 Ordering"] + + CAT -->|"分类与已上架商品第一页(强制已上架过滤)"| LIST["购物端列表/搜索页 F04/F05"] + CAT -->|"商品公开信息与可售状态"| DET["商品详情页 F06"] + LIST -->|"商品 ID"| DET + CART -. "收藏、加购或购买意图从详情页进入" .-> CAT + REV["M07 商品评价"] -->|"公开评价汇总与列表"| DET + + ID -->|"账号禁用、令牌失效或角色越权"| X["拒绝访问,不返回商品数据"] + ADM -->|"草稿、下架或已删除商品"| Y["购物端不得出现在公开浏览结果中"] + CAT -->|"商品不存在、已下架或库存归零"| Z["详情显示不可售,不提供购买入口"] + CACHE -->|"缓存不可用或数据陈旧"| W["回退 PostgreSQL 直读,不返回旧值"] + SEARCH -->|"进阶查询失败或索引损坏"| V["回退基础模糊查询并记录降级原因"] +``` + +边界约束: + +- 公开浏览只暴露已上架商品;草稿、下架或已删除商品不得通过搜索参数绕过。 +- 购物端的价格和库存只能作为浏览口径,下单与购物车写入必须由服务端在 M03、M04 中重新校验。 +- 商品模块不直接访问用户私有数据;买家专属操作(收藏、加购、购买)由 M08、M03、M04 提供,商品模块只提供事实输入和入口。 +- 评价(X01)汇总来自 M07,商品详情只做公开读取,不修改评价事实。 +- C07 缓存只能放在事实查询路径之前;缓存失效或不可用时,必须回退到 PostgreSQL 直读,不返回旧数据冒充成功;缓存写入与失效由缓存主责统一约定,本文不擅自规定 TTL。 +- C04 在 M02-01 列表入口上替换底层搜索实现,对外参数白名单、已上架过滤和返回口径与本文保持一致;接口与数据待评审项在第十章集中登记。 +- 扩展完成后回到的核心结果:F04 公开列表仍只返回已上架商品、F05 关键词查询仍按统一搜索契约返回、F06 详情仍按服务端最新状态展示。扩展失败不能改变上述核心结果。 + +## 三、购物端商品列表与组合筛选 + +```mermaid +flowchart TD + A["游客/买家/商家/管理员进入商品列表"] --> B["加载有效分类与已上架商品第一页
关键词去除首尾空白,参数校验通过"] + B --> C{"参数是否合法?"} + C -- "否" --> X["返回字段级错误并保留查询条件"] + C -- "是" --> D["服务端强制过滤为已上架商品
分类、价格区间、仅看有货、白名单排序组合生效"] + D --> E{"是否有匹配结果?"} + E -- "否" --> F["返回空集合与正确分页信息
展示当前条件并提供清空筛选入口"] + E -- "是" --> G["展示主图、名称、当前价格、库存摘要"] + G --> H["保留查询条件在页面地址或等效状态中"] + H --> I{"用户后续操作?"} + I -- "清空条件或翻页" --> B + I -- "进入详情" --> J["携带商品 ID 进入商品详情页"] + I -- "修改关键词或筛选" --> B +``` + +组合筛选规则: + +- 关键词、分类、价格区间和仅看有货之间按“同时满足”处理;价格下限不得大于上限。 +- 排序项和方向必须走白名单,禁止把客户端字段直接拼为查询语句。 +- 条件恢复:关键词、筛选、排序和页码应当反映在页面地址或等效可恢复状态中,刷新或返回时无需重新选择。 +- 失败反馈:首次加载显示骨架或加载状态;请求失败时保留原条件并允许重试,不以系统异常处理空结果。 + +## 四、商品详情与可售状态 + +```mermaid +flowchart TD + A["用户从列表/搜索/收藏/历史进入详情"] --> B["按商品 ID 加载公开信息"] + B --> C{"商品是否存在且为已上架?"} + C -- "否" --> X["展示不存在或暂不可售,禁用购买入口
提供返回列表入口"] + C -- "是" --> D["展示名称、主图/图片、描述、当前价格、库存和分类"] + D --> E{"当前角色?"} + E -- "游客" --> F["显示登录引导并保留目标商品与原操作意图"] + E -- "商家或管理员" --> G["仅展示公开效果,不显示买家专属操作"] + E -- "买家" --> H{"当前库存是否充足?"} + H -- "否" --> I["保留详情展示并标记售罄,禁用购买入口"] + H -- "是" --> J["可收藏、加购或购买;数量与最终价格仍由 M03/M04 服务端校验"] + D -. "已选 X01" .-> K["展示评分汇总和公开评价列表入口
评价提交资格由 M07 判断"] +``` + +关键规则: + +- 价格、库存和上下架状态以服务端最新数据为准,页面缓存不得作为下单依据。 +- 商品主图加载失败时使用占位图,不阻断其他信息浏览。 +- 后台改价、改库存或上下架后,详情重新获取时按服务端最新数据展示,不复用旧缓存。 +- 图片合规:单个商品最多 8 张,单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 400~4096 像素,第一张作为主图。 +- 已下架商品的旧链接只能显示不可售状态,不提供购买入口;历史订单中的商品快照仍可读,但不受当前上下架状态影响。 + +## 五、核心状态与并发边界 + +```mermaid +stateDiagram-v2 + [*] --> 草稿: M06-01 商家创建并保存 + 草稿 --> 已上架: 完整性校验通过并主动上架 + 草稿 --> 已删除: 无历史关联且确认删除 + 已上架 --> 已下架: 商家主动下架 + 已上架 --> 已删除: 无历史关联且确认删除 + 已下架 --> 已上架: 重新校验通过并上架 + 已下架 --> 已删除: 无历史关联且确认删除 + 已删除 --> [*] +``` + +购物端浏览口径只承认 `已上架` 状态,其他状态在公开列表、搜索和详情入口中均不出现。下架商品的历史订单快照、购物车、收藏与浏览记录由对应模块显示不可售,不被本模块删除。 + +并发与一致性: + +- 商品事务提交后由 M06-01 触发缓存失效;缓存不可用或失效失败时按 M02 直读 PostgreSQL 处理,不返回旧值冒充成功。 +- 购物端读取始终以 PostgreSQL 为事实来源;Redis 仅承担性能缓冲,不得覆盖浏览口径。 +- 商品在买家浏览瞬间被下架,详情页必须按服务端最新状态展示暂不可售,不复用缓存中的已上架结果。 + +## 六、结果反馈与页面衔接 + +```mermaid +flowchart TD + A["买家进入详情或列表"] --> B{"操作类型"} + B -- "搜索/筛选" --> C["展示加载、无结果或失败反馈
保留原条件与清空入口"] + B -- "查看详情" --> D["展示名称、价格、库存、分类、图片和描述"] + B -- "评价入口(X01)" --> E["展示评分汇总与公开评价列表"] + B -- "收藏/加购/购买" --> F{"当前身份"} + F -- "游客" --> G["引导登录并保留原商品与意图"] + F -- "买家" --> H["进入 M08 收藏 / M03 加购 / M04 下单主链"] + F -- "商家或管理员" --> I["不显示买家专属操作"] + C --> J["页面恢复或重试,不展示空白页"] + D --> J + E --> J + H --> J +``` + +角色化操作边界: + +- 游客进入购买相关操作时引导登录,登录后保留目标商品和原意图。 +- 买家专属操作必须同时受前端入口和服务端角色、资源归属校验保护。 +- 商家和管理员在购物端只以普通浏览身份查看公开商品,后台管理能力由 M06-01 提供,不在购物端扩展。 +- 公开评价入口只能读取,提交评价必须从已完成的订单入口走 M07。 + +## 七、异常、回滚与责任 + +| 场景 | M02 处理 | 最终状态/责任 | +|---|---|---| +| 游客、商家或管理员无越权入口 | 按角色提供或隐藏入口 | 服务端始终按 JWT 和 Policy 校验 | +| 商品不存在 | 返回“商品不存在”,提供返回列表入口 | 不暴露内部异常 | +| 商品已下架或被删除 | 显示“暂不可售”,禁用购买 | 历史订单快照仍可读 | +| 库存为零或数量超限 | 显示售罄或拒绝购买 | 由 M03/M04 决定是否调大或重新选择 | +| 关键词、分类、价格或排序非法 | 字段级错误,保留查询条件 | 不执行查询 | +| 图片加载失败 | 使用占位图 | 不阻断价格、库存和描述浏览 | +| 加载失败或网络中断 | 保留当前页面,允许重试 | 不把旧缓存价格当作最新价格 | +| 商品在浏览瞬间被下架 | 服务端按最新状态返回不可售 | 不复用缓存 | +| C07 缓存失效或不可用 | 回退 PostgreSQL 直读 | 不返回旧值冒充成功 | + +## 八、由流程派生的接口契约映射 + +本节是第三至七章业务流程的下游映射,不是流程输入。先确认“要完成什么业务动作、处于什么状态、成功或失败后得到什么结果”,再决定由哪个接口承载。若现有 Axxx 与流程冲突,先记录接口缺口并修改接口设计;只有业务需求本身变化时,才回到前文重新评审流程。 + +| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +|---|---|---|---| +| 查询商品列表与组合筛选 | A0xx Catalog 列表 | 强制已上架过滤、分页、白名单排序、组合筛选和空结果正常返回 | 待交叉评审 | +| 查询商品详情 | A0xx Catalog 详情 | 仅返回当前已上架商品的最新价格、库存、图片和描述 | 待交叉评审 | +| 查询有效分类 | A0xx Catalog 分类 | 返回购物端筛选入口使用的有效分类,停用分类不出现在筛选入口 | 待交叉评审 | +| 评价公开汇总与列表(X01 衔接) | 由 M07 派生 | 商品详情只读取 M07 公开结果,不在此模块内实现评价提交 | 待交叉评审 | + +接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 + +## 九、扩展接入边界 + +- C07 缓存:在商品详情和分类/列表查询路径前使用 Cache-Aside 读取;M06-01 提交商品事务后失效缓存,TTL 与主动失效策略由缓存主责人统一确认。PostgreSQL 仍是事实来源;缓存不可用时回退数据库直读,不掩盖错误。 +- C04 中文搜索:在 M02-01 列表查询入口上替换底层搜索实现,返回口径与基础模糊查询一致;公开浏览口径、参数白名单和已上架过滤不变。 +- M03 购物车:只接收本模块输出的已上架商品与实时价格库存;下架或库存归零由 M03 标记失效,不反向修改商品状态。 +- M04 订单:下单时由 M04 重读本模块的最新事实进行条件扣减,不信任购物端传入的金额和库存。 +- M07 评价:商品详情只读取 M07 公开评价与评分汇总;评价提交入口必须从已完成订单走 M07,商品详情不开放绕过入口。 + +## 十、由流程反查出的接口与数据待评审项 + +1. 列表与详情对已上架过滤必须服务端强制;接口需要确认是否在响应中显式携带“不可售原因”或仅按 HTTP 状态码区分,由 M02 与接口设计共同决定。 +2. 排序白名单字段集尚未在需求中枚举;接口设计前需要 M02 与评审人员确认价格、时间、相关度的默认与可选顺序。 +3. 图片合规校验在前端完成上传限制后仍需服务端再次校验;接口字段需要明确“主图”与“附加图”的上传顺序和替换规则。 +4. 商品详情是否暴露最新库存数或仅暴露“有货/无货”摘要,由需求决定展示口径;接口返回字段需要和前端展示要求对齐。 +5. 评价公开汇总字段(平均分、总条数的计算时机)与缓存策略相关,需要与 M07、C07 共同确认。 +6. C04 进阶搜索替换 F05 基础模糊查询时,需要保留旧接口的请求字段和返回口径,避免前端按字段名硬编码查询逻辑。 +7. DBxxx 分类/商品表字段尚未形成可实施的完整定义,数据库字段、约束、索引和 GIN/pg_trgm 维护策略必须由数据库设计任务另行确认。 +8. C07 的 TTL 与主动失效上限需要在本流程评审前完成,避免缓存值与商品最新事实长期不一致。 + +## 十一、验收证据清单 + +对照 `验收标准.md` 的 F04、F05、F06、N02、N04、N05: + +- [ ] F04:分页数据、总数和翻页结果正确,刷新或返回后查询条件仍可恢复;分类筛选有效,停用分类不出现在购物端筛选入口。 +- [ ] F05:关键词模糊搜索、组合筛选和白名单排序可独立及组合生效;F05 由 C04 替换底层实现后口径不变。 +- [ ] F06:商品名称、图片、描述、价格、库存和分类展示正确,并与后台最新有效修改一致;有货、售罄、下架、不存在和加载失败状态均能清楚区分。 +- [ ] N04:购物端任何身份均无法搜索到草稿、下架或已删除商品;游客、买家、商家和管理员看到符合权限的操作入口,服务端鉴权生效。 +- [ ] N02:图片失败或接口失败时页面仍可理解、可返回或可重试,不出现空白页。 +- [ ] N05:Chrome / Edge 最新版正常显示,无明显样式错乱。 +- [ ] 缓存:缓存失效或不可用时,公开浏览口径不返回旧值;缓存命中但底层数据已变更时按 M06-01 失效结果回退。 +- [ ] X01 衔接:已选 X01 的评分与评价入口展示正常,但未满足条件的用户不能从详情页绕过订单资格提交评价。 + +## 十二、提交前自检与升级路径 + +- [ ] 图中的 F、X、C、M 编号与需求一致,与根文档 3.3、3.8.1 一致。 +- [ ] 已写清基础 F、核心接入状态和回归结果;本图不改变 F04~F06 核心结果。 +- [ ] Mermaid 图内部包含直接上游模块输入(M01、M06-01)和直接下游模块出口(M03、M04、M07)。 +- [ ] 主流程、拒绝分支、失败分支和最终结果齐全;扩展不破坏核心权限、金额、库存、快照和事实来源。 +- [ ] 状态名称与需求规格说明书一致;没有新增核心商品状态。 +- [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径或 DTO 代替流程;没有自行发明接口编号、字段名或数据库状态码。 +- [ ] 已对照根文档 3.3 校准主流程、状态机和模块出入口,与 M06-01 边界一致。 + +升级到“待交叉评审”的条件:自检完成、主流程与异常分支齐全、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 + +升级到“已确认”的条件:直接协作人(Cart、Ordering、Identity)共同确认边界,C07 主责确认缓存接入边界,根文档 3.3 中对应追踪项成熟度同步更新。 \ No newline at end of file diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" new file mode 100644 index 0000000..dfdd101 --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -0,0 +1,258 @@ +# M06-01 后台分类与商品管理流程 + +> 负责人:顾欣月 +> 覆盖:M06-01、F11 +> 基础核心流程:F02、F04~F06、M02 Catalog;与根文档 [`业务流程设计.md`](../业务流程设计.md) 第 3.3、3.7 节保持一致 +> 直接协作:罗皓晨(M00 公共基建、C07 缓存主责)、唐宇昊(M01 Identity)、朱惠惠(M03 购物车)、韦乾强(M04 订单) +> 文档状态:初稿,待顾欣月自审及 Identity/Catalog/缓存协作交叉评审 +> 升级标记:在初稿基础上补强 MerchantOnly 鉴权链路、并发保护方式、缓存失效责任、回归核心结果和验收对照 +> 需求事实源:[需求规格说明书 M06-01](../../../01-需求文档/需求规格说明书.md) 的完整七节 + +## 一、范围与事实来源 + +本模块为商家提供分类与商品维护能力,覆盖分类查询与维护、商品分页查询、创建、查看、编辑、删除约束及上下架。维护结果需要正确反映到 M02 商品列表、详情和 C04 搜索能力,同时不得破坏历史订单中的商品快照。 + +本模块不包含多商家数据隔离、批量导入导出、定时上架、复杂审批流、商品操作审计功能或管理员代商家修改商品。 + +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。A2xx 只用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| M06-01/F11 需求 | 完整定义 | 作为业务语义事实源 | +| 本文业务流程 | 初稿 | 先确认角色、状态、分支和模块出入口 | +| 商家端写操作接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| DBxxx 分类/商品表 | 部分定义 | 本文不发明表字段、状态码和索引 | +| C07 缓存失效协作 | 待细化 | 只登记接入点,不混入商家写操作核心结果 | +| C04 搜索索引更新 | 待细化 | 仅约束 PostgreSQL 同步维护,不建设独立索引任务 | + +## 二、模块直接出入口 + +```mermaid +flowchart LR + ID["M01 Identity
MerchantOnly 认证与账号状态"] -->|"MerchantOnly + 账号正常"| ADM["M06-01 商家后台入口"] + ADM -->|"分类与商品维护命令(事务提交后)"| CAT["M02 Catalog
商品事实、分类、销售状态"] + CAT -->|"最新商品销售状态、分类与价格库存"| LIST["F04~F06 购物端浏览"] + CAT -->|"商品事实被 M06-01 修改"| CART["M03 Cart 失效条目重检"] + CAT -->|"下单重读与库存条件更新"| ORD["M04 Ordering"] + CAT -. "事务提交后事件" .-> CACHE["C07 缓存失效与重建
(缓存主责统一执行)"] + CAT -. "事务提交后数据库索引同步" .-> SEARCH["C04 中文搜索索引
(pg_trgm/GIN 由 PG 同步)"] + + ID -->|"游客、买家、管理员或账号禁用"| X["403 或 401,拒绝后台访问"] + ADM -->|"字段非法或并发冲突"| Y["拒绝保存并保留表单内容"] + ADM -->|"删除存在历史订单的商品"| Z["拒绝破坏性删除,引导改为下架"] + CACHE -->|"缓存失效失败"| W["不回滚商品事务,由缓存处理器重试"] + SEARCH -->|"索引异常"| V["暂停进阶搜索,回退基础查询"] +``` + +边界约束: + +- M06-01 仅拥有商家身份入口;游客、买家和管理员都不能调用任何写接口,前端隐藏入口不能替代服务端 Policy。 +- 商品模块只暴露分类与商品事实;商家不得修改买家账号、支付事实或订单金额。 +- 商品名称、有效分类、当前价格、库存和主图为必填;价格不得为负,库存必须为非负整数。 +- 商品事务提交后才允许触发缓存失效与搜索索引同步;事务失败时不发起任何外部动作。 +- 删除约束:存在历史订单关联时禁止破坏性删除,由系统建议改为下架。 +- 并发保护:编辑与上下架使用并发标记(version/etag)或 `WHERE updated_at` 条件更新,与 M04 订单并发口径由接口设计统一。 +- 缓存与索引责任划分:缓存失效与重建由 C07 主责统一执行,本模块只提交“商品事务已提交”信号;搜索索引由 PostgreSQL 同步维护,本模块不创建独立同步任务。 +- 扩展完成后回到的核心结果:F04~F06 公开浏览口径、M02 商品销售状态机、M03 购物车失效标记契约、M04 下单重读条件均不变;商家写操作不能绕过这些核心结果。 + +## 三、分类维护 + +```mermaid +flowchart TD + A["M06-01 入口:商家通过 MerchantOnly 进入分类管理"] --> B["查询当前分类列表(含名称、层级、排序、启停状态)"] + B --> C{"选择操作?"} + C -- "新增" --> D["填写名称、父级关系、排序和初始启停状态"] + C -- "编辑" --> E{"该分类是否被商品或历史引用?"} + C -- "启用或停用" --> F["切换启停状态并校验依赖"] + C -- "申请删除" --> E + E -- "是" --> X["拒绝破坏性删除,建议改为停用"] + E -- "否" --> G["完成删除或编辑保存"] + D --> H{"字段是否合法?"} + H -- "否" --> Y["字段级错误,保留已填内容"] + H -- "是" --> G + F --> I{"停用分类是否仍被商品引用?"} + I -- "是" --> J["允许停用但禁止新建或编辑该分类下的商品上架
购物端不再作为筛选入口"] + I -- "否" --> K["直接停用或启用,结果立即生效"] + G --> L["保存分类事实并返回最新分类列表"] + K --> L + J --> L +``` + +关键规则: + +- 停用分类不再作为购物端筛选入口;新建或编辑商品时不允许把停用分类作为上架分类。 +- 分类层级、名称和排序由商家维护;存在商品或历史引用时禁止破坏性物理删除。 +- 分类名称、父级关系和启停状态必须校验;非法输入返回字段级错误并保留已填内容。 + +## 四、商品创建与编辑 + +```mermaid +flowchart TD + A["商家进入商品管理"] --> B{"选择操作?"} + B -- "新建商品" --> C["填写名称、分类、价格、库存、主图/图片和描述"] + B -- "编辑商品" --> D["按商品 ID 加载当前内容并保留已填字段"] + C --> E{"必填项、价格、库存、分类和图片合规?"} + D --> E + E -- "否" --> X["字段级错误,保留表单内容并标记失败字段"] + E -- "是" --> F{"并发标记或版本条件是否一致?"} + F -- "否" --> Y["返回冲突提示,不静默覆盖已生效修改"] + F -- "是" --> G["开启商品事务并保存商品事实"] + G --> H{"事务提交成功?"} + H -- "否" --> Z["整体回滚,提示保存失败并允许安全重试"] + H -- "是" --> I["提交后触发缓存失效与搜索索引同步
返回最新商品事实"] +``` + +商品字段与图片校验: + +- 商品名称、有效分类、当前价格、库存和主图为必填;价格不得为负,库存必须为非负整数。 +- 图片上传到 S3 Compatible Object Storage;单商品最多 8 张,单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 400~4096 像素,第一张作为主图。 +- 图片上传失败时明确标记失败图片并允许重试,不清空其他表单字段。 +- 编辑商品时使用并发标记或条件更新防止静默覆盖;冲突时保留已填写内容并提示刷新确认。 + +## 五、商品上下架与删除约束 + +```mermaid +flowchart TD + A["商家选择目标商品"] --> B{"选择操作?"} + B -- "上架" --> C{"完整性校验通过且分类已启用?"} + C -- "否" --> X["拒绝上架并指出缺失字段"] + C -- "是" --> D["事务内将商品状态置为已上架"] + B -- "下架" --> E["事务内将商品状态置为已下架
购物端列表与搜索不再返回该商品"] + B -- "删除" --> F{"是否存在历史订单关联?"} + F -- "是" --> Y["拒绝破坏性删除,建议改为下架"] + F -- "否" --> G{"当前是否已上架?"} + G -- "是" --> H["先执行下架,状态变为已下架"] + G -- "否" --> I["完成物理删除,进入终止结果"] + H --> I + D --> J["提交后触发缓存失效与搜索索引同步"] + E --> J + I --> J +``` + +下架约束: + +- 下架不删除购物车记录、收藏记录、浏览记录或历史订单快照,由对应模块显示不可售状态。 +- 下架商品的旧链接只能显示不可售状态,不提供购买入口;历史订单快照不受影响。 +- 已上架但库存为 0 的商品仍可展示详情,但必须标记售罄并禁用购买;是否允许继续“已上架 + 售罄”展示由本期业务口径决定。 + +## 六、商品销售状态机与并发边界 + +```mermaid +stateDiagram-v2 + [*] --> 草稿: 商家创建并保存 + 草稿 --> 已上架: 完整性校验通过并主动上架 + 草稿 --> 已删除: 无历史关联且确认删除 + 已上架 --> 已下架: 商家主动下架 + 已上架 --> 已删除: 无历史关联且确认删除 + 已下架 --> 已上架: 重新校验通过并上架 + 已下架 --> 已删除: 无历史关联且确认删除 + 已删除 --> [*] +``` + +并发与一致性: + +- 商品写操作使用并发标记(version/etag)或 `WHERE` 条件更新防止静默覆盖;冲突时返回明确提示,不覆盖已生效数据。 +- 事务提交后才允许触发缓存失效与搜索索引同步;事务失败时不发起任何外部动作。 +- PostgreSQL 的 `pg_trgm`/GIN 数据库索引随商品数据同步维护,不通过异步处理器复制搜索索引。 +- 缓存失效失败不回滚已正确提交的商品事务;由缓存处理器重试并记录可追踪错误。 + +## 七、结果反馈与商家页面衔接 + +```mermaid +flowchart TD + A["商家在商品管理页完成操作"] --> B{"操作结果"} + B -- "保存成功" --> C["展示最新商品事实并刷新管理列表"] + B -- "上下架成功" --> D["展示新状态并提示是否需要继续编辑"] + B -- "删除受限" --> E["提示改为下架并保留当前商品"] + B -- "保存失败" --> F["显示失败原因并保留表单内容"] + B -- "并发冲突" --> G["提示刷新并保留已填写字段"] + B -- "图片上传失败" --> H["标记失败图片,允许重试或移除"] + C --> I["商家可继续维护其他商品或返回管理列表"] + D --> I + E --> I + F --> I + G --> I + H --> I +``` + +操作反馈约束: + +- 保存、上下架和删除均显示明确结果;危险操作需要确认,失败时保留可恢复的表单数据。 +- 商家端提交期间防止重复点击;网络中断或服务异常时给出可重试入口,不显示虚假成功。 + +## 八、异常、回滚与责任 + +| 场景 | M06-01 处理 | 最终状态/责任 | +|---|---|---| +| 游客、买家或管理员访问后台写接口 | 拒绝 | 401/403,不返回后台数据 | +| 字段、价格、库存或分类非法 | 拒绝保存 | 字段级错误,前端保留用户已填写内容 | +| 图片上传失败 | 标记失败图片并允许重试 | 不清空其他表单字段 | +| 两名操作人并发编辑 | 后提交者收到冲突提示 | 不静默覆盖已生效修改 | +| 删除存在历史订单的商品 | 拒绝物理删除 | 提示改为下架 | +| 上架条件不完整 | 拒绝上架 | 指出缺失字段 | +| 事务提交失败 | 数据回滚 | 页面显示保存失败,允许安全重试 | +| 缓存失效失败 | 不回滚商品事务 | 由缓存处理器重试并记录可追踪错误 | +| 搜索索引异常 | 暂停进阶搜索并回退基础查询 | 重建数据库索引后恢复 | +| 图片合规校验不通过 | 拒绝上传 | 不保存不合规图片 | + +## 九、由流程派生的接口契约映射 + +本节是第三至八章业务流程的下游映射,不是流程输入。先确认“要完成什么业务动作、处于什么状态、成功或失败后得到什么结果”,再决定由哪个接口承载。若现有 Axxx 与流程冲突,先记录接口缺口并修改接口设计;只有业务需求本身变化时,才回到前文重新评审流程。 + +| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +|---|---|---|---| +| 商家分页查询商品 | A2xx 商品后台列表 | 按商家身份过滤、分页、关键词、分类和上下架状态组合查询 | 待交叉评审 | +| 新建商品 | A2xx 商品创建 | 校验字段、分类、图片和并发状态,事务内保存商品事实 | 待交叉评审 | +| 编辑商品 | A2xx 商品编辑 | 并发保护、字段校验、事务保存并返回最新商品事实 | 待交叉评审 | +| 上下架切换 | A2xx 商品上下架 | 校验上架完整性;事务内条件更新销售状态 | 待交叉评审 | +| 商品后台删除 | A2xx 商品删除 | 仅允许无历史关联时物理删除;存在历史订单时返回拒绝并建议下架 | 待交叉评审 | +| 分类查询与维护 | A2xx 分类维护 | 提供分类列表、新增、编辑、启停和受限删除 | 待交叉评审 | +| 商品图片上传 | A2xx 商品图片 | 按合规要求上传到对象存储,返回主图与附加图标识 | 待交叉评审 | + +接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 + +## 十、扩展接入边界 + +- C07 缓存:商家端商品事务提交后由架构确定的可靠机制触发缓存失效;缓存不可用时不影响商品事务,由缓存处理器重试失效动作。 +- C04 搜索:商品事务提交后由 PostgreSQL 同步维护 `pg_trgm`/GIN 索引,不建设独立的索引同步任务;进阶搜索暂时不可用时回退基础模糊查询。 +- M03 购物车:商品下架、库存归零或被禁用后由购物车模块按 M03 规则标记失效,不反向写入商品状态。 +- M04 订单:商品事务不修改历史订单的地址或商品快照;价格或上下架变更不影响已有订单。 +- M01 Identity:本模块不修改账号、角色或令牌状态;账号禁用由 M06-03 独立流程处理。 + +## 十一、由流程反查出的接口与数据待评审项 + +1. 商品并发保护方式尚未在需求中枚举(version/etag、`WHERE updated_at` 等),需要在接口设计前与 Ordering 的并发口径统一。 +2. 删除判断“是否存在历史订单关联”的查询口径需要明确按订单状态筛选还是全量包含已取消订单,避免商家误判。 +3. 停用分类下已上架商品的可见性:是否允许继续展示直到商家主动下架或编辑,需要在接口层明确返回字段。 +4. 图片上传顺序和替换规则的接口字段(主图上传后是否自动替换旧主图)尚未定义,需在接口设计前与命名规范统一。 +5. 缓存失效失败的处理需要记录可追踪错误并由缓存处理器重试;接口响应不得返回缓存失效状态,避免商家误以为商品未上架。 +6. 搜索索引异常时的降级语义需要在接口和缓存层达成一致;商家端不感知底层使用哪种索引实现。 +7. DBxxx 分类/商品表字段尚未形成可实施的完整定义,数据库字段、约束、索引和 GIN/pg_trgm 维护策略必须由数据库设计任务另行确认。 +8. 商品图片上传接口与对象存储的兼容边界需要与系统架构设计同步,避免不同商家端入口使用不同的上传契约。 + +## 十二、验收证据清单 + +对照 `验收标准.md` 的 F11、N02、N04: + +- [ ] F11:商家可完成分类维护,以及商品新增、查询、编辑、受约束删除和上下架;商品必填项、价格、库存、分类和图片校验在前后端均生效。 +- [ ] F11:下架商品不会出现在购物端列表和搜索结果,旧链接不再允许购买;历史订单快照保持可读。 +- [ ] N04:游客、买家和管理员无法进入或调用商家写操作,后端返回正确的认证或授权结果。 +- [ ] N02:并发编辑、图片失败、保存失败和删除受限时均有明确反馈,已填写内容不会无故丢失。 +- [ ] 索引:商品变更后,PostgreSQL `pg_trgm`/GIN 索引随数据同步保持一致。 +- [ ] 缓存:缓存失效失败不回滚商品事务,由缓存处理器重试并记录可追踪错误。 +- [ ] 保存正常和异常操作的页面截图、并发冲突提示和图片失败标记证据。 +- [ ] 答辩能够说明商家事务与缓存失效的边界,以及搜索索引同步维护的责任划分。 + +## 十三、提交前自检与升级路径 + +- [ ] 图中的 F、X、C、M 编号与需求一致,与根文档 3.3、3.7、3.8.4 一致。 +- [ ] 已写清基础 F、核心接入状态和回归结果;商家写操作不改变 M02 核心销售状态机。 +- [ ] Mermaid 图内部包含直接上游模块输入(M01)和直接下游模块出口(M02、M03、M04、C07、C04)。 +- [ ] 主流程、拒绝分支、失败分支和最终结果齐全;并发保护与缓存失效责任划分清晰。 +- [ ] 状态名称与需求规格说明书一致;没有新增核心商品状态。 +- [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径或 DTO 代替流程;没有自行发明接口编号、字段名或数据库状态码。 +- [ ] 已对照根文档 3.7 校准后台角色与操作边界,与 M06-02/M06-03 边界一致。 + +升级到“待交叉评审”的条件:自检完成、主流程与异常分支齐全、并发与缓存责任划分清楚、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 + +升级到“已确认”的条件:Identity 主责确认 MerchantOnly 鉴权链路,C07 主责确认缓存失效责任,M02 与 M03 主责确认下游事实回退口径,根文档 3.7 中对应追踪项成熟度同步更新。 \ No newline at end of file diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" new file mode 100644 index 0000000..a7ba5fe --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" @@ -0,0 +1,239 @@ +# M07 商品评价流程 + +> 负责人:顾欣月 +> 覆盖:M07、X01 +> 基础核心流程:F06、F09;与根文档 [`业务流程设计.md`](../业务流程设计.md) 第 3.6 节中评价接入点保持一致 +> 直接协作:韦乾强(M04 订单)、唐宇昊(M01 身份)、顾欣月本人(M02 商品详情)、罗皓晨(C07 缓存协作) +> 文档状态:初稿,待顾欣月自审及 Ordering/Identity 交叉评审 +> 升级标记:在初稿基础上补强多订单项并发、图片上传状态机、脱敏快照生成时机、回归核心结果和验收对照 +> 需求事实源:[需求规格说明书 M07](../../../01-需求文档/需求规格说明书.md) 的完整七节 + +## 一、范围与事实来源 + +本模块允许买家对本人已完成订单中的商品提交一次评分、文字评价和可选图片,并在商品详情中公开展示评价与评分汇总。核心目标是让真实购买者方便表达体验,同时阻止未购买、未完成订单、越权和重复评价。 + +本模块不包含追评、评价点赞、买家自删、匿名评价、商家回复或隐藏、自动内容审核和评价运营后台。 + +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。A3xx 只用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| M07/X01 需求 | 完整定义 | 作为业务语义事实源 | +| 本文业务流程 | 初稿 | 先确认角色、状态、分支和模块出入口 | +| 评价接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| DBxxx 评价表 | 模板/占位 | 本文不发明表字段、状态码和索引 | +| F06 评价公开读取 | 完整定义 | 商品详情只读取,不在本模块内重复实现 | + +## 二、模块直接出入口 + +```mermaid +flowchart LR + ID["M01 Identity
BuyerOnly 认证与账号状态"] -->|"BuyerOnly + 账号正常"| RV["M07 Review
本人已完成订单项的评价"] + ORD["M04 Ordering
Completed 订单项与归属事实"] -->|"本人订单项归属与完成状态"| RV + DET["F06 商品详情"] -->|"读取公开评价与评分汇总"| RV + CACHE["C07 Cache-Aside
评价读取缓存"] -. "读取前缓存" .-> RV + + RV -->|"本人评价记录"| ORD + RV -->|"公开评价与评分汇总"| DET + RV -->|"买家脱敏展示名快照"| DET + + ID -->|"游客、商家或管理员"| X["403 或 401,拒绝提交评价"] + ORD -->|"订单未完成或不属于本人"| Y["拒绝提交,不泄露他人订单信息"] + RV -->|"同一订单项已有评价"| Z["返回已评价结果,不新增重复记录"] + CACHE -->|"缓存不可用或命中失效"| W["回退 PostgreSQL 直读"] +``` + +边界约束: + +- 评价入口必须从买家订单详情提供,商品详情页只展示公开评价,不能让用户绕过订单资格直接创建评价。 +- 同一订单项只能形成一条评价,数据库唯一约束或等效机制必须作为最终保障。 +- 评价公开展示时不得返回手机号、邮箱、内部用户标识等不必要的敏感信息。 +- 公开展示名在提交评价时形成脱敏快照;用户以后修改资料不改变历史评价展示,也不得为评价列表逐条查询用户资料。 +- 商品详情只读取公开评价,不在本模块内实现评价提交。 +- 缓存只能放在公开评价读取路径之前;评价事务提交后由缓存主责统一失效,缓存不可用时回退数据库直读,不返回旧数据。 +- 扩展完成后回到的核心结果:F06 商品详情仍只读取公开评价,F09 Completed 订单项状态保持不变,订单快照和支付事实不被评价结果覆盖。 + +## 三、评价提交主流程 + +```mermaid +flowchart TD + A["买家进入订单详情"] --> B{"订单项已完成且未评价?"} + B -- "否" --> X["不展示评价入口或拒绝提交"] + B -- "是" --> C["展示评价表单:评分、文字和最多 6 张可选晒图"] + C --> D["买家填写评分(1~5)、文字(1~500 字)并按需上传图片"] + D --> E{"评分、文字和图片均合规?"} + E -- "否" --> X1["字段级错误,保留已填内容"] + E -- "是" --> F["买家提交并附带防重复标识"] + F --> G{"同一订单项是否已有评价?"} + G -- "是" --> Y["返回已评价结果,不新增重复记录"] + G -- "否" --> H["开启事务并保存评价及图片关联"] + H --> I{"事务提交成功?"} + I -- "否" --> Z["整体回滚,提示稍后重试并保留已填内容"] + I -- "是" --> J["订单项变为已评价,提交后展示最新评价"] + J --> K["商品详情公开评价和评分汇总最终更新"] +``` + +关键规则: + +- 评价表单:1~5 分评分、1~500 字文字评价和最多 6 张可选晒图。 +- 图片合规:单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 200~4096 像素;逐张显示上传状态,失败图片可重试或移除。 +- 评分只能为 1~5 的整数;评价必须关联真实商品和订单项。 +- 只有订单项所属买家且订单状态为已完成时可以提交评价。 +- 提交期间防止重复点击;成功后明确标记“已评价”,失败时保留文字和已上传状态。 + +## 四、公开评价与评分汇总 + +```mermaid +flowchart TD + A["用户进入商品详情"] --> B["加载商品事实与公开评价"] + B --> C{"公开评价是否为空?"} + C -- "是" --> D["展示友好空状态,提供登录或购买引导"] + C -- "否" --> E["按时间倒序分页展示评分、文字、图片、评价时间和脱敏买家展示名"] + E --> F{"加载是否成功?"} + F -- "否" --> G["展示加载失败提示并允许重试"] + F -- "是" --> H["展示评分汇总(总数与平均分)和公开评价列表"] + H --> I{"当前身份?"} + I -- "游客" --> J["不显示提交入口,未登录请求被拒绝"] + I -- "买家" --> K["根据 M07 资格判断是否显示提交入口"] + I -- "商家或管理员" --> L["仅展示公开评价,不显示提交入口"] +``` + +公开展示约束: + +- 评价展示名在提交评价时形成脱敏快照,不在列表页按评价逐条查询用户资料。 +- 评分汇总根据有效评价计算总数和平均分;新增评价后结果最终更新且可追踪。 +- 商品详情页面不暴露评价人手机号、邮箱或内部用户标识。 +- 评价列表为空或加载失败时展示友好空状态或重试入口,不显示空白页。 + +## 五、评价状态与并发边界 + +```mermaid +stateDiagram-v2 + [*] --> 未评价: F09 订单项进入 Completed + 未评价 --> 已评价: M07 评价事务提交成功 + 未评价 --> 未评价: 重复提交被拒绝 + 已评价 --> 已评价: 重复提交或重复点击被拦截 +``` + +图片上传状态机: + +```mermaid +stateDiagram-v2 + [*] --> 待上传: 买家选择本地图片 + 待上传 --> 上传中: 服务端校验并上传至对象存储 + 上传中 --> 已上传: 上传成功并返回对象键 + 上传中 --> 失败: 类型/大小/尺寸不合规或网络错误 + 失败 --> 上传中: 买家点击重试 + 失败 --> 已移除: 买家移除失败图片 + 已上传 --> 已移除: 买家在提交前移除图片 + 已上传 --> 已绑定: 评价事务提交成功,图片关联到评价 + 已移除 --> [*] +``` + +并发与一致性: + +- 同一订单项重复评价或重复点击必须由数据库唯一约束或等效机制阻止,不能依赖前端去重。 +- 同一订单项在两个浏览器同时提交时,仅一个事务成功,另一个由唯一约束或 `WHERE NOT EXISTS` 条件返回已评价结果。 +- 同一订单的多订单项并发提交评价:每个订单项独立判断资格与唯一性,互不影响;任一订单项评价事务失败不影响其他订单项。 +- 评价事务与图片关联在同一受控事务内提交,任一写入失败时整体回滚,不留下“评价已存但图片缺失”的部分结果。 +- 评价公开读取最终以 PostgreSQL 为事实来源;缓存失效或读取失败时回退到数据库直读,不返回旧数据。 + +## 六、结果反馈与页面衔接 + +```mermaid +flowchart TD + A["买家在订单详情完成评价提交"] --> B{"提交结果"} + B -- "成功" --> C["订单项标记已评价;商品详情评价列表与评分汇总最终更新"] + B -- "已评价" --> D["提示已评价,不重复写入"] + B -- "字段或图片不合规" --> E["字段级错误并保留可恢复的表单内容"] + B -- "部分图片上传失败" --> F["标记失败项,允许重试或移除"] + B -- "网络失败" --> G["提示失败原因,允许重试"] + B -- "登录失效" --> H["引导重新登录并保留未提交内容"] + C --> I["买家可继续查看其他订单项或返回商品详情"] + D --> I + E --> I + F --> I + G --> I + H --> I +``` + +操作反馈约束: + +- 提交期间防止重复点击;成功后明确标记“已评价”,失败时保留文字和已上传状态。 +- 提交时登录失效需要引导重新登录并保留未提交内容;重新提交时仍执行完整资格校验。 +- 部分图片上传失败时只标记失败项,不要求重新填写整份评价。 + +## 七、异常、回滚与责任 + +| 场景 | M07 处理 | 最终状态/责任 | +|---|---|---| +| 游客、商家或管理员提交评价 | 拒绝 | 不返回提交入口 | +| 订单未完成、订单项不存在或不属于当前买家 | 拒绝 | 不泄露他人订单信息 | +| 同一订单项重复评价或重复点击 | 返回已评价结果 | 不新增重复记录 | +| 评分、文字或图片不合规 | 字段级错误 | 保留可恢复的表单内容 | +| 部分图片上传失败 | 标记失败项 | 允许重试或移除 | +| 提交时登录失效 | 引导重新登录 | 保留未提交内容,重新提交时执行完整资格校验 | +| 评价事务失败 | 整体回滚 | 提示稍后重试 | +| 评价列表为空或加载失败 | 友好空状态或重试入口 | 不显示空白页 | +| 越权修改或删除评价 | 拒绝 | 不修改评价事实 | +| 商品已被下架或删除 | 已提交评价不受影响 | 由 M02 控制购物端可见性 | + +## 八、由流程派生的接口契约映射 + +本节是第三至七章业务流程的下游映射,不是流程输入。先确认“要完成什么业务动作、处于什么状态、成功或失败后得到什么结果”,再决定由哪个接口承载。若现有 Axxx 与流程冲突,先记录接口缺口并修改接口设计;只有业务需求本身变化时,才回到前文重新评审流程。 + +| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +|---|---|---|---| +| 提交评价 | A3xx 评价提交 | 校验身份、订单项归属、完成状态、唯一性、字段与图片并原子写入 | 待交叉评审 | +| 查询本人可评价订单项 | A3xx 评价资格 | 按当前买家返回 Completed 且未评价的订单项 | 待交叉评审 | +| 查询商品公开评价 | A3xx 公开评价列表 | 分页返回评分、文字、图片、时间和脱敏展示名 | 待交叉评审 | +| 查询商品评分汇总 | A3xx 评分汇总 | 返回有效评价总数与平均分 | 待交叉评审 | + +接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 + +## 九、扩展接入边界 + +- F06 商品详情:只读取 M07 公开评价与评分汇总,不在商品详情页内提交评价;评价提交入口由订单详情提供。 +- F09 订单完成:评价入口只能从本人 Completed 订单项接入,不能由商品详情绕过。 +- M09 站内消息:评价成功落库属于已确认业务事实,通知发送由 M09 决定;本模块不直接发送通知。 +- C07 缓存:评价公开读取可使用 Cache-Aside 加速;评价事务提交后由架构确定的可靠机制失效缓存,缓存不可用时回退数据库直读。 +- 商家回复、隐藏或点赞:本期不实现;后续如需扩展,必须先修订主需求和本文档的边界约束。 + +## 十、由流程反查出的接口与数据待评审项 + +1. 评价唯一约束需要确认是数据库唯一索引还是等效应用层机制,并在数据库设计中明确。 +2. 评价图片上传顺序、是否允许后续追加图片以及失败重试上限,需要在接口层确定,避免买家多次提交不同图片集。 +3. 评分汇总字段(平均分、总条数)的计算时机与缓存策略相关,需要与 C07 缓存主责人共同确认。 +4. 评价公开列表的脱敏展示名规则需要在接口和前端达成一致;脱敏快照生成时机是提交时还是读取时需要确认。 +5. 商品详情读取评价是否要求登录状态、是否区分登录与游客可见范围,需要在接口设计中明确。 +6. 评价事务失败的回滚语义需要与图片上传失败处理保持一致,避免“评价已存但图片缺失”的部分结果。 +7. DBxxx 评价表字段尚未形成可实施的完整定义,数据库字段、约束和索引必须由数据库设计任务另行确认。 +8. 评价与图片上传的字段命名(`reviews`、`review_images`)已与命名规范统一,需在数据库设计中按词根实现。 + +## 十一、验收证据清单 + +对照 `验收标准.md` 的 X01、N02、N04: + +- [ ] X01:本人已完成订单项可以提交 1~5 分、文字和可选图片评价,并在对应商品中正确展示。 +- [ ] X01:未购买、订单未完成、他人订单项、游客、商家和管理员提交评价均被服务端拒绝。 +- [ ] X01:同一订单项重复提交不会产生第二条评价,连续点击也不会重复写入。 +- [ ] X01:公开列表与评分汇总正确,不泄露买家敏感信息;商品详情页不开放绕过订单资格的评价提交入口。 +- [ ] X01:图片不合规、网络失败和登录失效时反馈清楚,用户已输入内容可恢复。 +- [ ] N04:评价提交与图片关联在同一事务内提交,任一步失败整体回滚;评分与文字字段级错误不写入数据库。 +- [ ] N02:评价列表为空或加载失败时展示友好空状态或重试入口,不显示空白页。 +- [ ] 多订单项并发:同一订单的多个订单项独立评价互不影响;两个浏览器同时提交同一订单项时仅一个成功。 +- [ ] 答辩能够说明唯一约束或等效机制如何阻止重复评价、脱敏快照生成时机、评价事务与图片关联的原子性。 + +## 十二、提交前自检与升级路径 + +- [ ] 图中的 F、X、C、M 编号与需求一致,与根文档 3.6 节中评价接入点一致。 +- [ ] 已写清基础 F、核心接入状态和回归结果;评价结果不覆盖 Completed 订单状态或订单快照。 +- [ ] Mermaid 图内部包含直接上游模块输入(M01、M04)和直接下游模块出口(M04、F06)。 +- [ ] 主流程、拒绝分支、失败分支和最终结果齐全;并发场景下唯一约束生效。 +- [ ] 状态名称与需求规格说明书一致;没有新增订单状态。 +- [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径或 DTO 代替流程;没有自行发明接口编号、字段名或数据库状态码。 +- [ ] 已对照根文档 3.6 节中评价接入点校准入口位置,X01 不可绕过订单资格。 + +升级到“待交叉评审”的条件:自检完成、主流程与异常分支齐全、并发场景验证、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 + +升级到“已确认”的条件:Ordering 主责确认订单项归属与完成状态口径,Identity 主责确认买家脱敏快照生成时机,根文档 3.6 节中评价接入点对应追踪项成熟度同步更新。 \ No newline at end of file -- Gitee From aa2ecda7deaaca5358d92695dae8c81a692e90f2 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 14:48:06 +0800 Subject: [PATCH 059/118] =?UTF-8?q?docs(process):=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E7=BD=97=E7=9A=93=E6=99=A8=E6=B5=81=E7=A8=8B=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=EF=BC=9B=E8=A1=A5=E9=BD=90=E6=B6=88=E6=81=AF=E3=80=81=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E6=8E=A8=E9=80=81=E3=80=81=E7=BC=93=E5=AD=98=E4=B8=8E?= =?UTF-8?q?=E9=AB=98=E5=8F=AF=E7=94=A8=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../process/README.md" | 2 +- ...50\351\200\201\346\265\201\347\250\213.md" | 191 ++++++++++++++ ...23\345\255\230\346\265\201\347\250\213.md" | 218 ++++++++++++++++ ...57\347\224\250\346\265\201\347\250\213.md" | 244 ++++++++++++++++++ ...10\346\201\257\346\265\201\347\250\213.md" | 219 ++++++++++++++++ ...01\347\250\213\350\256\276\350\256\241.md" | 99 ++++++- 6 files changed, 965 insertions(+), 8 deletions(-) create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C06-\345\256\236\346\227\266\346\216\250\351\200\201\346\265\201\347\250\213.md" create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C10-\351\253\230\345\217\257\347\224\250\346\265\201\347\250\213.md" create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/M09-\347\253\231\345\206\205\346\266\210\346\201\257\346\265\201\347\250\213.md" diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/README.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/README.md" index 6cd91f8..49ccf5b 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/README.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/README.md" @@ -68,7 +68,7 @@ C08 退款对账 → X04 售后退款 → F09/F10 的订单项和支付事实 | 朱惠惠 | `zhh/` | `M03-购物车流程.md` | `C01-秒杀流程.md` | 顾欣月、韦乾强、张海洋 | | 韦乾强 | `wqq/` | `M04-订单流程.md`、`M06-02-商家履约流程.md` | `C03-订单超时流程.md` | 朱惠惠、张海洋、罗皓晨 | | 张海洋 | `zhy/` | [`M05-支付流程.md`](zhy/M05-支付流程.md)、`M10-售后流程.md` | `C08-支付回调与对账流程.md` | 韦乾强、罗皓晨 | -| 罗皓晨 | `lhc/` | `M09-站内消息流程.md` | `C06-实时推送流程.md`、`C07-缓存流程.md`、`C10-高可用流程.md` | 各相关业务负责人 | +| 罗皓晨 | `lhc/` | [`M09-站内消息流程.md`](lhc/M09-站内消息流程.md) | [`C06-实时推送流程.md`](lhc/C06-实时推送流程.md)、[`C07-缓存流程.md`](lhc/C07-缓存流程.md)、[`C10-高可用流程.md`](lhc/C10-高可用流程.md) | 各相关业务负责人 | 以上只规定目录、文件名、负责人和联调关系,不代表统稿人已经替负责人完成流程内容。跨模块流程不能由单方标记为“已确认”。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C06-\345\256\236\346\227\266\346\216\250\351\200\201\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C06-\345\256\236\346\227\266\346\216\250\351\200\201\346\265\201\347\250\213.md" new file mode 100644 index 0000000..dff6ef5 --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C06-\345\256\236\346\227\266\346\216\250\351\200\201\346\265\201\347\250\213.md" @@ -0,0 +1,191 @@ +# C06 实时推送流程 + +> 负责人:罗皓晨 +> 覆盖:C06;经 X03/M09 接入核心业务事实 +> 基础核心流程:F08、F09、F10、F12;鉴权依赖 F02、F13;X04 可追加售后来源 +> 直接协作:M09 Messaging、M01 Identity、C10 多实例运行环境 +> 文档状态:初稿,待罗皓晨自审及 Identity、Ordering、Payment、AfterSales、部署边界交叉评审 +> 需求事实源:[需求规格说明书 C06](../../../01-需求文档/需求规格说明书.md) 的“C06 实时消息推送”完整七节 + +## 一、范围与事实来源 + +C06 在 M09 消息已经成功持久化之后,为已登录买家和商家提供低延迟、按本人定向的实时到达能力。实时连接和轻提示只改善到达速度;消息内容、未读状态以及订单、支付、发货和售后状态仍分别以 PostgreSQL 中的业务事实为准。 + +本流程不实现在线客服、自由聊天、群聊、历史聊天同步、已送达回执、任意客户端加组或端到端加密。游客和管理员本期不建立 Messaging 实时连接。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| C06 需求与教师验收 | 完整定义,待需求冻结 | 作为断线重连、多标签页、多实例和持久化补查边界 | +| 本文业务流程 | 初稿 | 明确连接生命周期、定向推送、补查和失败隔离 | +| 接口设计 4.3.7、4.3.8 | 部分定义、待交叉评审 | 由流程派生 Hub 与服务端事件映射 | +| A501~A505 | 部分定义、待交叉评审 | 用于补查、详情和已读校正,不为 SignalR 新增 Axxx | +| Redis Backplane / C10 | 技术与部署能力待验证 | 只承接跨实例通道,不保存唯一消息事实 | + +## 二、直接出入口与不可变结果 + +```mermaid +flowchart LR + ID["M01 Identity
有效 JWT、用户 ID、角色和账号状态"] -->|"允许买家或商家建立本人连接"| RT["C06 实时推送"] + MSG["M09 Messaging
已提交的本人消息"] -->|"消息标识、最小展示信息和接收用户"| RT + C10["C10 多实例环境
Nginx 与共享实时通道"] -->|"连接转发与跨实例传播"| RT + RT -->|"本人全部在线连接"| TABS["PC Web 一个或多个标签页"] + TABS -->|"查看详情、校正未读或标记已读"| MSG + + ID -->|"令牌无效、账号禁用或身份不支持"| X["拒绝连接,不加入用户通道"] + RT -->|"断线、推送或共享通道失败"| Y["进入重连或降级提示
回到 M09 查询补偿"] +``` + +不可变结果: + +- C06 只能从“M09 消息事务已提交”接入,来源模块不得绕过 M09 直接广播未落库的成功事实。 +- 推送失败、重复或延迟都不能改变消息未读状态,也不能回滚订单、支付、发货或售后结果。 +- 客户端不得仅凭推送载荷修改订单、支付或售后最终状态;需要业务详情时重新调用所属模块接口。 +- Redis 只解决跨实例 Hub 消息传播,不保存永久消息、唯一未读数或唯一在线状态。 +- Redis Backplane 只传播跨实例 Hub 消息,不自动解决协商请求与 WebSocket 连接升级的实例落点;会话亲和或经验证的跳过协商策略由 C06/C10 联合评审后冻结。 + +## 三、连接鉴权与生命周期 + +下图中的状态是客户端运行时连接状态,不是持久业务状态。 + +```mermaid +stateDiagram-v2 + [*] --> Disconnected: 页面尚未连接 + Disconnected --> Connecting: 买家或商家具有有效登录态 + Connecting --> Connected: 服务端鉴权通过 + Connecting --> Disconnected: 鉴权失败或主动取消 + Connected --> Reconnecting: 非主动网络或实例中断 + Reconnecting --> Connected: 有限退避重连成功 + Reconnecting --> Degraded: 持续重连失败 + Degraded --> Reconnecting: 网络恢复或用户手动重试 + Connected --> Disconnected: 用户主动退出 + Degraded --> Disconnected: 用户主动退出 +``` + +连接规则: + +- 客户端携带有效 JWT 建立连接;服务端从认证上下文取得用户 ID 和角色,不接受客户端声明任意接收用户、角色或组。 +- 买家与商家可以复用技术通道,但消息接收范围、文案和安全操作入口仍按身份隔离。 +- 同一账号的每个有效标签页分别建立连接,服务端向该用户全部在线连接发送消息。 +- 用户主动退出后关闭当前连接;账号禁用、令牌撤销或版本失效时,不得继续建立有效连接。 +- 主动退出、被动断网、连接超时或 API 实例中断后,服务端必须清理该实例持有的断开连接状态;跨实例唯一在线状态不得只保存在单个 API 内存中。 +- 非主动断线采用有限退避重连。具体次数、间隔和“持续断线”提示阈值尚未冻结,进入第八章待评审项。 + +## 四、消息提交后的定向推送 + +```mermaid +flowchart TD + A["M09:本人消息事务提交成功"] --> B["取得接收用户和最小展示载荷"] + B --> C{"目标身份是否为本期支持的买家或商家?"} + C -- "否" --> X["不建立实时推送
消息事实仍保留"] + C -- "是" --> D["按服务端认证用户标识发送"] + D --> E["共享实时通道把消息传播到持有连接的 API 实例"] + E --> F{"目标用户是否有在线连接?"} + F -- "否" --> Y["结束实时尝试
等待 M09 补查"] + F -- "是" --> G["向该用户全部有效连接推送"] + G --> H{"标签页是否已展示同一消息标识?"} + H -- "是" --> I["忽略重复轻提示
不改变未读数"] + H -- "否" --> J["更新角标并显示非阻塞轻提示"] + J --> K["用户按需进入消息中心或业务详情"] + K --> L["通过 M09/目标模块重新查询确定事实"] + + D -. "发送失败" .-> Z["记录消息标识、实例和 traceId
不回滚 M09"] + E -. "共享通道失败" .-> Z + G -. "连接中断" .-> Z + Z --> Y +``` + +最小载荷边界: + +- 只包含消息标识、类型、标题/摘要、关联业务类型与标识、安全操作描述和服务端创建时间。 +- 不发送完整订单、支付信息、收货地址、密码、完整 Token、连接配置或内部前端路由。 +- 消息标识是客户端轻提示去重键;服务端创建时间是展示事实,不使用浏览器实际收到时间替代。 + +## 五、断线重连、多标签页与补查 + +```mermaid +flowchart TD + A["页面建立或恢复实时连接"] --> B{"连接鉴权成功?"} + B -- "否" --> X["按登录失效或无权限处理"] + B -- "是" --> C["查询 M09 当前未读数"] + C --> D["按需查询最近消息或消息列表"] + D --> E["校正本标签页角标和已展示消息集合"] + E --> F["保持实时连接"] + F --> G{"发生非主动断线?"} + G -- "否" --> F + G -- "是" --> H["进入有限退避重连,不阻塞页面其他功能"] + H --> I{"重连成功?"} + I -- "否,仍在阈值内" --> H + I -- "否,持续断线" --> J["显示简短连接状态
告知消息中心仍可查询"] + J --> W["暂停自动重连
等待网络恢复或用户手动重试"] + W -->|"网络恢复或用户重试"| H + I -- "是" --> C + + T1["标签页 1 标记消息已读"] --> DB["M09 提交共享已读事实"] + T2["标签页 2 保持打开"] --> REFRESH["下次补查或刷新未读数"] + DB --> REFRESH + REFRESH --> SAME["两个标签页得到一致已读结果"] +``` + +补查规则: + +- 初次连接和每次重连成功后至少校正未读数,并按需查询消息列表;不假设服务端会无限重放断线期间实时事件。 +- 多标签页各自接收实时提示,但已读状态统一写回 M09;任一标签页成功标记已读后,其他标签页通过补查得到一致结果。 +- 页面不可见或被浏览器节流时,不把客户端收到时间当作业务发生时间。 +- 持续断线只影响实时性,不应阻塞商品浏览、订单查询或消息中心的普通 HTTP 查询。 + +## 六、多实例、故障与责任 + +| 场景 | C06 处理 | 最终状态与责任 | +|---|---|---| +| 用户连接在实例 1、事件由实例 2 触发 | 通过共享实时通道传播到实例 1 | M09 消息保持唯一事实 | +| 同一账号打开多个标签页 | 向全部有效连接推送,客户端按消息标识去重 | 数据库未读数只增加一次 | +| 网络抖动造成重复连接或重复推送 | 允许连接恢复,轻提示按消息标识去重 | 不重复生成消息或改变业务状态 | +| Redis Backplane 暂时不可用 | 实时能力降级并记录指标 | M09 数据事实仍保留;只有 Identity 仍能安全完成鉴权时,受保护的 M09 HTTP 查询才能继续,否则按失败关闭策略处理 | +| 当前连接所在 API 停止 | 客户端进入重连并切换到存活实例 | 不承诺连接无中断,但消息不丢失 | +| 用户令牌过期、撤销或账号禁用 | 拒绝新连接;已有连接失效边界待 Identity 评审 | 不允许用客户端参数绕过认证 | +| 推送载荷处理失败 | 不显示或转为普通消息入口补查 | 不使用错误载荷改变订单状态 | +| 用户主动退出 | 关闭当前连接并清理本地实时状态 | 不删除 M09 历史消息 | +| 被动断网、连接超时或实例中断 | 服务端清理已断开的连接状态,客户端按有限退避重连 | 不把单实例内存连接表当作跨实例唯一在线事实 | + +## 七、由流程派生的契约映射 + +SignalR 连接和服务端事件不是 HTTP 接口,不占用 Axxx,也不得为本流程自行新增 A508。 + +| 流程能力 | 当前派生契约 | 事实来源 | 当前状态 | +|---|---|---|---| +| 买家或商家建立本人实时连接 | 接口设计 4.3.7“Hub 连接” | M01 认证上下文 | 待交叉评审 | +| M09 消息提交后向全部在线连接发送 | 接口设计 4.3.8“MessageCreated” | M09 已持久化消息 | 待交叉评审 | +| 初次连接或重连后校正未读数 | A503 | M09/PostgreSQL | 待交叉评审 | +| 补查断线期间消息 | A501;查看详情时使用 A502 | M09/PostgreSQL | 待交叉评审 | +| 任一标签页标记已读并校正 | A504、A505,随后复用 A503 | M09/PostgreSQL | 待交叉评审 | +| 两个 API 实例共享实时通道 | 接口设计 1.16、4.3.7 | Redis Backplane,不登记 DBxxx | 待 C10 部署验证 | + +架构承接章节: + +- 系统架构 7.10“C06 实时消息推送”:SignalR、断线补查和 Redis Backplane; +- 系统架构 7.13“C10 容器化部署与负载均衡”:Nginx WebSocket Upgrade、双实例与故障切换; +- 系统架构 8“安全设计”:JWT、账号状态、令牌撤销与资源隔离。 + +## 八、待交叉评审项 + +1. 与 Identity 确认令牌撤销、账号禁用和退出后,现有跨标签页连接何时关闭及失败时的安全策略。 +2. 确定有限退避的次数、间隔、抖动和“持续断线”提示阈值;这些参数只影响交互,不改变消息补查边界。 +3. 与 C10 确认 Nginx WebSocket 转发、事件触发实例和单实例停止的可重复证据方式,并冻结 SignalR 协商请求与连接升级的实例落点策略;可评审会话亲和或经验证的 WebSockets 跳过协商方案,不能假设 Redis Backplane 已解决该问题。 +4. 确认 Redis Backplane 故障和恢复的监控指标、日志及告警,不承诺恢复后重放全部实时事件。 +5. 由 Ordering、Payment、AfterSales 确认所有来源都先经过 M09 持久化,禁止业务模块直接向客户端广播。 +6. 真实 OpenAPI、Hub 集成测试和多标签页端到端测试尚未建立,本流程不得标记为已实现或已验证。 + +## 九、验收证据清单 + +- [ ] 买家在线时触发发货等已提交事实,无需刷新即可收到一条轻提示,消息中心存在同一消息。 +- [ ] 断网期间触发消息,恢复后自动重连,并通过未读数和消息列表补查。 +- [ ] 同一账号至少两个标签页均能收到通知;任一标签页标记已读后,其他标签页可校正为一致状态。 +- [ ] 用户 A、用户 B 同时在线时,只有明确接收人获得私人消息。 +- [ ] 买家、指定商家、无关商家和管理员同时在线时,推送范围符合身份和接收账号约束。 +- [ ] 连接落在实例 1、事件由实例 2 触发时能够通过共享实时通道送达,并保存实例与 Trace 证据。 +- [ ] 按已冻结的连接落点策略验证初次协商、WebSocket 升级和重连,证明请求跨两个 API 分发时不会因落点不一致失败。 +- [ ] 停止当前连接所在 API 后,客户端可重连到存活实例,消息和未读状态完整。 +- [ ] 主动退出、被动断网、连接超时和实例中断后,服务端均能清理断开连接状态,且无需依赖单实例内存保存唯一在线状态。 +- [ ] 暂停 Redis/实时推送后,来源业务和 M09 消息事实仍成功;Identity 可安全鉴权时用户可通过列表补查,否则受保护请求按失败关闭策略处理。 +- [ ] 连续消息和短暂断线不使用阻塞弹窗打断当前表单或支付操作。 +- [ ] 保存连接/重连时间、消息标识、实例标识、`traceId`、M09 查询结果和故障恢复日志。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" new file mode 100644 index 0000000..925ca27 --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" @@ -0,0 +1,218 @@ +# C07 缓存流程 + +> 负责人:罗皓晨 +> 覆盖:C07 +> 基础核心流程:F04、F06、F11;F08/F09 通过 Catalog 改变库存时触发失效,F08 最终仍重读 PostgreSQL +> 直接协作:顾欣月(M02 Catalog 与商品失效规则)、韦乾强(M04 Ordering 库存扣减/回补入口)、M00/C10 公共 Redis 与多实例环境 +> 文档状态:初稿,待罗皓晨自审及 Catalog、Ordering 交叉评审;TTL 和失效上限未冻结 +> 需求事实源:[需求规格说明书 C07](../../../01-需求文档/需求规格说明书.md) 的“C07 缓存与性能优化”完整七节 + +## 一、范围、职责与事实来源 + +C07 使用 Redis 优化本期固定首页商品摘要和购物端商品详情两个高频公开只读场景。游客和买家可以共享只含公开字段的缓存;个人字段、商家管理字段、管理员字段及购物车、订单、支付、售后写操作不进入本期缓存。 + +PostgreSQL 始终是价格、库存、上下架状态和商品内容的事实来源。缓存命中与未命中的 A102/A103 响应必须保持同一业务口径;Redis 故障可以使查询变慢,但不能改变公开范围、权限或结果正确性。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| C07 需求与教师验收 | 完整定义,待需求冻结 | 作为 Cache-Aside、写后失效、降级和压测边界 | +| 本文业务流程 | 初稿 | 明确读取、事务后失效、多实例和错误出口 | +| A102、A103 与 Catalog 写接口 | 部分定义、待交叉评审 | 由流程映射,不改变原接口响应 | +| DB022、DB023 | 仅为接口文档引用,数据库主文档未确认 | 不把接口引用写成已冻结表设计 | +| Redis Key、TTL 与热点保护 | 部分定义 | 只确认必须有界,具体参数进入待评审项 | + +职责边界: + +- 罗皓晨负责 Redis 接入、统一序列化、稳定 Key 约定、故障降级、多实例共享和压测环境。 +- 顾欣月负责确认商品创建、编辑、图片、上架、下架、删除及库存变化分别影响哪些详情和首页缓存。 +- Ordering 通过 Catalog 公开能力扣减或回补库存;Catalog 仍负责对外暴露最新商品事实和触发相应缓存失效。 +- C07 不反向修改商品、库存或订单数据,也不以 Redis 替代下单事务和库存条件更新。 + +## 二、模块直接出入口 + +```mermaid +flowchart LR + Q1["F04:公开商品列表/固定首页摘要查询"] --> CACHE["C07 Cache-Aside"] + Q2["F06:公开商品详情查询"] --> CACHE + CACHE -->|"命中且内容有效"| WEB["游客/买家公开响应"] + CACHE -->|"未命中、损坏或 Redis 降级"| CAT["M02 Catalog / PostgreSQL"] + CAT -->|"已上架商品的公开事实直接返回"| WEB + CAT -. "使用有限 TTL 尽力回填;写入失败不阻塞响应" .-> CACHE + + WRITE["F11:商品编辑、图片、上下架或删除
F08/F09:通过 Catalog 扣减或回补库存"] -->|"Catalog 事务提交成功"| INVALIDATE["C07 失效入口"] + INVALIDATE -->|"删除详情和受影响的固定首页缓存"| CACHE + INVALIDATE -->|"失败记录与受控重试"| RETRY["有限 TTL 约束最长旧值窗口"] + + ORDER["F08 提交订单"] -->|"重新读取销售状态、价格和库存"| CAT + CACHE -. "不得作为下单事实" .-> ORDER +``` + +不可变结果: + +- A102/A103 只返回已上架且允许公开的商品信息;任何身份都不能通过缓存命中看到草稿、下架、删除或管理字段。 +- F08 下单始终从 PostgreSQL 事实重新校验销售状态、价格和库存,不接受页面或 Redis 中的旧值作为交易依据。 +- Redis 写入、删除或重试失败只影响性能和约定的一致性窗口,不得让数据库事务回滚或返回无法判断新旧的副本。 +- 两个 API 实例共享同一 Redis 和 Key 规范,不能使用单实例内存保存跨实例唯一缓存事实。 + +## 三、Cache-Aside 读取流程 + +```mermaid +flowchart TD + A["页面显示结构一致的加载占位
发起固定首页摘要或商品详情查询"] --> B{"参数、公开范围和身份字段是否符合原接口?"} + B -- "否" --> X["按公开商品查询契约拒绝或返回不可用"] + B -- "是" --> C["构造包含环境、模块、资源、查询标识和版本的稳定 Key"] + C --> D{"Redis 是否可用?"} + D -- "否" --> DB["记录降级并查询 PostgreSQL"] + D -- "是" --> E{"Key 是否命中且可正常反序列化?"} + E -- "是" --> F["返回与原接口一致的公开响应并记录命中"] + E -- "否,未命中" --> DB + E -- "否,损坏或旧版本" --> G["删除异常 Key 并按未命中处理"] + G --> DB + DB --> H{"数据库查询结果?"} + H -- "已上架商品/摘要" --> I["生成原接口公开响应"] + H -- "不存在或不可公开" --> J["按原公开查询规则返回空结果或稳定不可用结果"] + H -- "查询失败" --> P["保留页面结构并显示统一错误反馈
提供就地重试入口"] + I --> K{"Redis 当前可写?"} + K -- "是" --> L["使用有限 TTL 回填"] + K -- "否" --> M["仅记录写入失败"] + L --> N["返回本次数据库结果"] + M --> N + J --> O["按待评审的短空值或受控直查策略处理"] + O --> Q["返回原查询结果并结束加载状态"] +``` + +读取规则: + +- 缓存 Key 必须区分环境、资源、稳定查询条件和结构版本;不得为任意查询参数无限生成 Key。 +- 固定首页摘要的具体查询条件、页大小和排序组合尚待 Catalog 确认,未确认前不冻结 Key 集合。 +- 正常值使用有限 TTL;不存在/不可公开结果是否使用短时空值,以及空值 TTL,须在第八章确认。 +- 缓存写入失败时,本次 PostgreSQL 查询结果仍正常返回;不得把缓存错误暴露为商品业务错误。 +- 商家管理查询和个人字段默认直接走原授权接口,不复用公共商品缓存。 +- 每次缓存读取记录命中/未命中、读取耗时和错误;回填、主动失效与降级分别记录写入、失效、错误和降级次数,并携带资源标识与 `traceId`,不得记录完整缓存值。 + +## 四、商品与库存变更后的失效 + +```mermaid +flowchart TD + A["Catalog 接收商品或库存变更"] --> B["在 PostgreSQL 事务内校验并写入最新事实"] + B --> C{"事务是否提交成功?"} + C -- "否" --> X["保持原数据库事实
不发布成功失效结果"] + C -- "是" --> D["确认受影响的商品详情和固定首页摘要范围"] + D --> E["删除商品详情 Key"] + D --> F["删除或版本化受影响的固定首页 Key"] + E --> G{"全部失效动作成功?"} + F --> G + G -- "是" --> H["本次失效动作完成"] + G -- "否" --> I["记录资源标识、失败范围和 traceId"] + I --> J["登记受控重试"] + J --> K{"重试是否在约定期限内成功?"} + K -- "是" --> H + K -- "否" --> L["由有限 TTL 约束旧值最长存在时间
并保留告警和证据"] + H --> M{"是否存在并发旧查询
在失效后回填旧值?"} + M -- "否" --> N["后续查询未命中并从 PostgreSQL 回填"] + M -- "是" --> O["按待确认的二次失效、版本或等价最小机制纠正"] + O --> P["选定机制生效前由有限 TTL 约束并记录证据"] + N --> Q["所有 API 实例共享同一 Redis 和失效结果"] + P --> Q +``` + +触发范围初稿: + +| 已提交变更 | 详情缓存 | 固定首页摘要 | 说明 | +|---|---:|---:|---| +| 新建草稿商品 | 通常无公开缓存 | 通常无公开缓存 | 未上架商品不得进入公开结果 | +| 名称、价格、库存、描述、分类变更 | 失效 | 若摘要字段或筛选结果受影响则失效 | 精确首页范围待 Catalog 确认 | +| 商品图片新增或删除 | 失效 | 若摘要缩略图受影响则失效 | A127/A128 尚未登记失效规则,待 Catalog 补齐 | +| 图片顺序或主图变化 | 待 Catalog 确认 | 待 Catalog 确认 | 当前未登记对应接口,不能写成已确认触发动作 | +| 上架 | 清理短空值/旧详情 | 失效 | 上架后下一次查询方可公开 | +| 下架 | 失效 | 失效 | 购物端旧链接不再允许购买 | +| 满足约束后删除 | 失效 | 失效 | A124 尚未登记失效规则,待 Catalog 补齐;不影响历史订单快照 | +| F08 库存扣减、F09 库存回补 | 失效 | 若摘要展示库存状态则失效 | 由 Catalog 公开库存能力触发 | + +上表是流程级触发提案,不是已经冻结的 Key 清单。C01 秒杀库存划拨是否影响普通商品公开库存及其缓存,需在 C01 库存口径确认后追加评审。 + +## 五、缓存运行状态与一致性窗口 + +缓存状态是查询派生状态,不改变商品的“草稿/已上架/已下架”业务状态。 + +```mermaid +stateDiagram-v2 + [*] --> Miss: Key 不存在或不可用 + Miss --> Cached: PostgreSQL 查询成功并回填 + Cached --> Cached: 有效读取 + Cached --> Invalidated: 商品事务提交后主动失效 + Cached --> Expired: 有限 TTL 到期 + Invalidated --> Miss + Expired --> Miss +``` + +一致性边界: + +- 主动失效用于缩短正常更新后的旧值窗口,有限 TTL 用于约束漏删、删除失败或重试失败时的最长旧值时间。 +- 若采用延迟二次失效,只用于减少“并发旧查询回填旧值”的窗口,不能替代事务提交后的首次失效和有限 TTL。 +- 理论最迟生效时间必须根据最终选定的首次失效、失败重试、剩余 TTL 和并发旧值保护机制分别确定计时起点后推导并验证,不能在参数未冻结时预设固定相加公式。 +- 在具体 TTL、重试次数和失效范围确认前,本流程不能标记为已确认或冻结。 + +## 六、故障、并发与身份隔离 + +| 场景 | C07 处理 | 最终状态与责任 | +|---|---|---| +| Redis 完全不可用 | 回退 PostgreSQL,记录降级与耗时 | 结果正确,性能可能下降 | +| Redis 回退后 PostgreSQL 也失败 | 保留页面结构,返回统一错误反馈和就地重试 | 不暴露 Redis、连接串或数据库技术细节 | +| 缓存值损坏或结构版本旧 | 视为未命中并删除异常 Key | 不向客户端返回错误结构 | +| 缓存写入失败 | 返回本次数据库结果 | 不改变接口业务结果 | +| 商品事务回滚 | 不产生成功失效动作 | 原缓存仍对应原数据库事实 | +| 事务提交后删除失败 | 记录范围并受控重试 | 有限 TTL 约束旧值窗口 | +| 事务前旧查询在失效后回填旧值 | 记录并按待确认的二次失效、版本或等价最小机制纠正 | 机制未冻结前由有限 TTL 约束,F08 仍重读数据库 | +| 热点 Key 同时过期 | 使用待确认的请求合并、短期互斥或等价最小方案 | 等待有超时与回退,不能无限阻塞 | +| 商品下架时并发旧读 | 失效并最终不再公开;F08 始终重读数据库 | 旧展示不能绕过下单校验 | +| 实例 1 完成变更、实例 2 查询 | 共享 Redis 与 Key 规范 | 在约定窗口内读取新值 | +| 游客与买家共享公开缓存 | 只保存双方共同可见字段 | 收藏、购物车等个人字段独立查询 | +| 商家或管理员查询 | 默认不使用本期公共缓存 | 不泄露管理字段或扩大权限 | + +## 七、由流程派生的契约映射 + +缓存是 A102/A103 的服务端实现能力,不新增业务 HTTP 接口,也不改变成功响应、失败响应、鉴权和公开范围。 + +| 流程能力 | 当前派生契约 | 数据设计 | 当前状态 | +|---|---|---|---| +| 固定首页/商品列表公开查询 | A102 | 接口文档引用 DB022/DB023,数据库主文档未确认 | 待 Catalog 交叉评审 | +| 购物端商品详情查询 | A103 | 接口文档引用 DB022/DB023,数据库主文档未确认 | 待 Catalog 交叉评审 | +| 商品内容、价格和库存编辑后失效 | A123 提交后的内部协作 | 目标表设计待数据库汇总 | 待 Catalog 交叉评审 | +| 上架、下架后失效 | A125、A126 提交后的内部协作 | 目标表设计待数据库汇总 | 接口已登记失效,待 Catalog 交叉评审 | +| 删除、商品图片新增/删除后失效 | A124、A127、A128 提交后的内部协作 | 目标表设计待数据库汇总 | 接口契约缺口:尚未登记缓存失效,待 Catalog 补齐 | +| F08/F09 库存扣减或回补后失效 | Catalog 公开库存应用能力,不新增 Axxx | 库存事实与索引待数据库评审 | 待 Catalog/Ordering 评审 | +| Redis 故障回退 PostgreSQL | 继续复用 A102/A103 响应口径 | Redis 不登记 DBxxx | 降级细节待架构和测试确认 | + +架构承接章节: + +- 接口设计 1.13“缓存与条件请求”:缓存不改变 API 契约与权限; +- 系统架构 7.11“C07 缓存与性能优化”:Cache-Aside、事务后失效和压测指标; +- 系统架构 7.1“下单事务”:F08 重新校验商品、价格和库存; +- 系统架构 7.13“C10 容器化部署与负载均衡”:两个 API 共享 Redis。 + +## 八、待交叉评审项 + +1. 与 Catalog 确认固定首页摘要的查询范围,以及编辑、图片、分类、上架、下架、删除和库存变化分别影响的 Key 集合。 +2. 确定详情、固定首页摘要和空值的 TTL;空值 TTL 必须短于正常值,且不能掩盖新上架商品。 +3. 确定首次失效失败后的重试机制、最大重试期限和理论最长旧值窗口,不能只写“最终一致”。 +4. 选择用于纠正并发旧查询回填的二次失效、版本或等价最小机制,并据此单独推导一致性窗口。 +5. 在请求合并、短期互斥或等价方案中选择满足当前规模的最小热点保护方式,并为等待设置超时和数据库回退。 +6. 与 Ordering 确认 F08 扣减、F09 回补的失效触发方式;C01 库存划拨待秒杀库存口径确认后再补。 +7. 确定压测的固定请求集合、并发参数、预热/冷缓存轮次和环境资源,确保开关缓存时可公平比较。 +8. DB022/DB023 仅为接口文档引用,须等待数据库主文档汇总确认;当前不得据此宣称表设计已完成。 + +## 九、压测与验收证据清单 + +- [ ] 缓存关闭与开启使用同一 Commit、数据库快照、机器资源、请求脚本和并发参数。 +- [ ] 数据不少于 30 个商品和 3 个分类,并固定首页与详情热点集合。 +- [ ] 每组包含预热、稳定采样和冷缓存轮次,预热数据不混入正式统计。 +- [ ] 记录请求数、成功率、吞吐量、平均耗时、P50、P95、P99、缓存命中/未命中次数、读取耗时、写入/失效次数、错误/降级次数、Redis 错误和 PostgreSQL 查询次数。 +- [ ] 缓存开关前后 A102/A103 业务字段、公开范围和错误结果一致。 +- [ ] 改价、库存、图片、上架、下架及多实例读取在约定一致性窗口内得到正确结果。 +- [ ] Redis 故障时可回退数据库;恢复后能够重新回填并产生正常命中。 +- [ ] Redis 与 PostgreSQL 同时失败时保留页面结构,展示统一错误反馈和就地重试,不暴露技术异常。 +- [ ] 构造事务前旧查询在首次失效后回填旧值的并发场景,按选定机制或有限 TTL 证明旧值窗口有界。 +- [ ] 热点 Key 过期时等待有界,不出现无限阻塞或无法解释的数据库冲击。 +- [ ] F08 在旧页面或旧缓存条件下仍按 PostgreSQL 最新状态、价格和库存决定下单结果。 +- [ ] 保存环境、Commit SHA、初始化方式、配置、原始压测输出、失效日志和数据库查询证据。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C10-\351\253\230\345\217\257\347\224\250\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C10-\351\253\230\345\217\257\347\224\250\346\265\201\347\250\213.md" new file mode 100644 index 0000000..6b67bbc --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C10-\351\253\230\345\217\257\347\224\250\346\265\201\347\250\213.md" @@ -0,0 +1,244 @@ +# C10 高可用流程 + +> 负责人:罗皓晨 +> 覆盖:C10 +> 基础核心流程:横切 F01~F13,并支撑 C06/C07 及 Worker 后台运行 +> 直接协作:M00 公共装配、M01 Identity、全部业务模块、C06 SignalR、C07 Redis +> 文档状态:初稿,待罗皓晨自审及全组交叉评审 +> 需求事实源:[需求规格说明书 C10](../../../01-需求文档/需求规格说明书.md) 的“C10 容器化部署与负载均衡”完整七节 + +## 一、验收范围与事实来源 + +C10 使用 Docker Compose 从同一版本启动 Nginx、PC Web、两个 API 实例、Worker、PostgreSQL、Redis、RabbitMQ 和 SeaweedFS,并通过 Nginx 统一入口证明请求分发、单个 API 实例停止后的服务可用、登录态连续和 SignalR 重连。 + +本文文件名沿用流程目录既定名称“高可用”,但当前承诺严格限定为教师 C10 要求的“单个 API 实例故障”验收能力。它不宣称 Nginx、PostgreSQL、Redis、RabbitMQ 或对象存储已经实现集群高可用、自动故障转移或跨机房容灾。上述依赖停止时,本文只定义正确的就绪、降级、失败和恢复边界。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| C10 需求与教师验收 | 完整定义,待需求冻结 | 作为 Compose、双 API、单实例故障和登录态验收边界 | +| 本文部署流程 | 初稿 | 明确启动、流量准入、故障切换、恢复和不可变业务结果 | +| A506、A507 | 部分定义、待交叉评审 | 承接存活和就绪检查,不单独证明业务连续性 | +| Compose/Nginx/镜像/Secret | 设计阶段,尚无真实资产证据 | 不写成已部署或已验证 | +| 各业务模块幂等与数据一致性 | 由各模块负责 | C10 不代替订单、支付、库存等业务规则 | + +## 二、统一入口与模块边界 + +```mermaid +flowchart LR + USER["游客、买家、商家、管理员
PC Web 浏览器"] -->|"唯一公开地址"| NGINX["Nginx 统一入口"] + NGINX -->|"API 请求与 WebSocket
只向可接收流量的实例转发"| API1["Mall.Api 实例 1
API 请求 + Messaging Hub"] + NGINX -->|"API 请求与 WebSocket
只向可接收流量的实例转发"| API2["Mall.Api 实例 2
API 请求 + Messaging Hub"] + NGINX -->|"静态资源"| WEB["Vue PC Web"] + + API1 --> PG["PostgreSQL
业务事实来源"] + API2 --> PG + API1 --> REDIS["Redis
共享令牌状态、缓存和实时通道"] + API2 --> REDIS + API1 --> MQ["RabbitMQ
集成事件传输"] + API2 --> MQ + API1 --> STORE["SeaweedFS
对象数据"] + API2 --> STORE + WORKER["Mall.Worker 独立容器"] --> PG + WORKER --> MQ + + API1 -->|"存活/就绪事实"| HEALTH["存活与就绪检查"] + API2 -->|"存活/就绪事实"| HEALTH +``` + +边界约束: + +- 浏览器只使用 Nginx 统一入口,不直接选择 API 实例,也不感知实例数量。 +- 后端容器端口默认只在内部网络可见,不直接暴露给公网;现场验收不得绕过 Nginx 访问业务接口。 +- Nginx 必须正确转发客户端 IP、协议、Host、请求 ID 和 WebSocket Upgrade 所需请求头;受控实例标识只用于脱敏验收证据。 +- Hub 握手路径中的 `access_token` Query 必须在 Nginx、ASP.NET Core、Serilog 和 Trace 中完整脱敏,集成、演示和发布环境只使用 HTTPS/WSS。 +- Redis Backplane 只传播跨实例 Hub 消息;SignalR 协商请求与 WebSocket 连接升级的实例落点必须另行冻结会话亲和或经验证的跳过协商策略。 +- 两个 API 使用同一 Commit SHA/版本 Tag 构建的同一镜像和等价业务配置;只允许实例标识等运行信息不同。 +- JWT Issuer、Audience、签名、Policy、账号状态和令牌失效语义在两个实例上必须一致。 +- PostgreSQL 保存业务事实;Redis、RabbitMQ、容器内存和前端状态不得成为无法恢复的唯一业务事实。 +- Worker 独立于任一 API 实例运行;后台任务的领取、幂等和状态规则仍由对应业务负责人定义。 + +## 三、Compose 启动与受控迁移 + +```mermaid +flowchart TD + A["部署人员确认 Docker/Compose、端口、持久卷和受控 Secret 可用"] --> B["选择同一 Commit SHA/版本 Tag 的镜像"] + B --> C["创建内部网络和持久卷"] + C --> D["启动 PostgreSQL、Redis、RabbitMQ 和 SeaweedFS"] + D --> E{"当前阶段必需依赖是否就绪?"} + E -- "否" --> X["保持应用未就绪
显示故障依赖并停止继续验收"] + E -- "是" --> F["执行一次受控数据库迁移"] + F --> G{"迁移是否唯一执行且成功?"} + G -- "否" --> Y["停止业务流量准入
保留日志并修复迁移问题"] + G -- "是" --> H["启动两个 API、Worker、PC Web 与 Nginx"] + H --> I["分别检查两个 API 的存活和就绪结果"] + I --> J{"两个 API 均可接收流量?"} + J -- "否" --> Z["仅允许符合就绪准入规则的实例用于诊断
C10 双实例启动与现场验收不通过"] + J -- "是" --> K["通过统一入口检查首页、健康与一条核心查询"] + K --> L["连续请求并用受控实例标识或日志证明双实例分发"] +``` + +启动规则: + +- Compose 提供明确的一条启动命令和一条日常停止命令;日常停止不得删除数据卷。 +- 数据库迁移必须是独立、受控且只执行一次的步骤,不能让两个 API 无约束并发迁移。 +- A506 只说明进程能够响应;A507 才表达 PostgreSQL 和当前必需依赖是否允许实例接收业务流量。 +- 未启用的可选依赖不得错误阻塞 API 就绪;当前环境哪些依赖属于“必需”仍需在第十章确认。 +- Aspire 只用于本地开发编排,C10 现场验收统一使用 Docker Compose 和 Nginx。 + +## 四、实例状态与流量准入 + +下图只描述 API 实例运行状态,不增加业务对象状态。 + +```mermaid +stateDiagram-v2 + [*] --> Stopped: 尚未启动 + Stopped --> Starting: 容器启动 + Starting --> NotReady: 进程存活但必需依赖未满足 + Starting --> Ready: 存活且就绪检查通过 + NotReady --> Ready: 依赖恢复并重新检查通过 + Ready --> NotReady: 必需依赖失败 + Ready --> Stopped: 实例停止 + NotReady --> Stopped: 实例停止 + Ready --> Recovering: 实例重启或版本恢复 + Recovering --> NotReady: 尚未满足就绪 + Recovering --> Ready: 就绪后重新接收流量 +``` + +流量规则: + +- 只有达到 `Ready` 的实例才允许接收新业务流量;`Stopped`、`Starting` 和 `NotReady` 实例不得持续接收新请求。 +- 恢复实例必须先通过 A507,再重新参与负载均衡,不能仅凭容器“running”状态加入。 +- 存活与就绪响应不得包含连接字符串、主机、端口、异常堆栈、凭据或其他敏感配置。 +- Nginx/Compose 如何使用探针、阈值和超时实现摘除及重新加入,当前仍是部署待评审项。 + +## 五、单 API 实例停止与恢复 + +```mermaid +flowchart TD + A["两个同版本 API 均就绪并接收流量"] --> B["用户通过 Nginx 完成登录和普通业务访问"] + B --> C["验收人员记录当前请求实例和确定业务结果"] + C --> D["停止承载部分请求或当前 SignalR 连接的 API 实例 1"] + D --> E["Nginx/健康判断识别实例 1 停止或未就绪"] + E --> F["摘除实例 1,新请求只转发到就绪实例 2"] + F --> R{"请求类型?"} + R -- "安全查询" --> G["客户端可在有限次数内重试"] + R -- "提交类请求" --> H["不得盲目自动重放"] + H --> I{"客户端是否已得到确定结果?"} + I -- "是" --> J["展示已确认结果"] + I -- "否" --> K["所属流程已定义幂等标识时复用原标识
否则使用已确认的业务查询动作确认结果"] + G --> L["实例 2 使用同一 JWT/Policy 校验并返回同一数据范围"] + K --> L + D --> M["C06 连接进入重连"] + M --> N["连接转移到实例 2 并通过 M09 补查"] + L --> O["用户保持登录,已提交业务事实不丢失、不重复"] + N --> O + O --> P["恢复实例 1"] + P --> Q["实例 1 通过存活、就绪与版本配置检查"] + Q --> S["重新加入流量并继续共享 PostgreSQL、Redis、RabbitMQ 和对象数据"] +``` + +故障切换的不变项: + +- 同一 Token、角色、资源和请求在两个实例上得到一致的认证、授权和数据范围结果。 +- 安全查询可以有限重试;会产生库存、金额或状态副作用的提交不得因实例切换被客户端盲目重放。 +- 结果未知时回到所属模块:业务流程已定义幂等标识时复用原标识,否则使用已确认的结果查询入口;C10 不自行假设所有提交都具备幂等标识。 +- 单实例停止不能删除 PostgreSQL、RabbitMQ、Redis 或对象存储的持久化数据,也不能停止独立 Worker 容器。 +- SignalR 连接允许短暂中断,但 M09 消息事实必须保留,重连后通过列表和未读数补偿。 +- 安全查询超过有限重试后必须进入统一维护/服务不可用页面,保留可理解提示和手动重试入口;不得停留在 Nginx 默认错误页、白屏或无限加载。 + +## 六、身份连续性与安全失败 + +```mermaid +flowchart TD + A["用户携带 JWT 通过统一入口访问"] --> B["请求落到任一就绪 API"] + B --> C["校验签名、Issuer、Audience、有效期、jti、账号状态和令牌版本"] + C --> D{"认证和当前账号状态是否可确定?"} + D -- "有效" --> E["继续执行 Policy、资源归属和业务状态校验"] + D -- "无效" --> X["拒绝访问并按登录失效处理"] + D -- "关键撤销状态无法安全确认" --> Y["拒绝受保护请求
具体安全失败策略待 Identity 评审"] + E --> F{"请求切换到另一实例?"} + F -- "否" --> G["返回当前业务结果"] + F -- "是" --> H["另一实例使用相同配置和共享状态重新校验"] + H --> G +``` + +安全边界: + +- Nginx 不保存业务 Session;登录连续性来自任一实例可验证的 JWT 和经 Identity 评审后的共享令牌失效状态。 +- 不得为了 Redis 故障时“保持可用”而静默绕过令牌撤销、账号禁用或资源归属校验。 +- 前端隐藏菜单、缓存身份或记录上一次成功实例都不能作为服务端授权依据。 +- 实例标识只用于 C10 请求分布证据,不进入业务判断,也不暴露主机名、IP 或内部网络信息。 + +## 七、依赖故障、降级与恢复责任 + +| 故障对象 | 允许继续的能力 | 必须停止或降级的能力 | 恢复责任与边界 | +|---|---|---|---| +| 单个 API | 存活实例继续处理查询和受控业务请求 | 当前连接短暂中断 | 恢复实例就绪后重新加入 | +| Worker | 普通同步 API 可继续 | Outbox 投递和后台任务暂缓 | 恢复后按各模块幂等规则继续,不重复业务结果 | +| Redis | C07 公开查询可回退 PostgreSQL;M09 消息数据事实仍保留 | C06 实时跨实例和缓存性能降级;若 Identity 无法确认令牌撤销状态,受保护的 M09 HTTP 请求必须拒绝或返回服务不可用 | 恢复后重新连接;令牌失效策略需 Identity 评审 | +| RabbitMQ | 已提交业务事务和同步查询可保留 | 集成事件实时传输暂缓 | Outbox 保留待发布事实,恢复后重投并由消费者防重 | +| PostgreSQL | 存活端点仍可反映进程 | 数据库业务请求不得伪装成功;实例应未就绪 | 恢复后重新检查,不能用缓存冒充完整事实 | +| SeaweedFS | 与对象无关的业务可按契约继续 | 新上传和依赖对象内容的操作按所属模块失败/占位规则处理 | 对象恢复后继续,不写入无效对象引用 | +| Nginx | 内部容器可用于诊断 | 用户统一入口不可用 | 恢复入口不应要求重建业务数据 | + +本表定义正确失败边界,不表示这些共享依赖已经具备冗余高可用。演示时不得把“能看到容器状态”写成依赖故障已经自动切换。 + +## 八、数据卷、版本与运行责任 + +- PostgreSQL、需要持久化的 Redis 数据、RabbitMQ 和 SeaweedFS 使用明确持久卷;应用容器重建不删除业务数据和对象文件。 +- 日常停止、单实例重启和应用升级不得隐式删除数据卷;清空演示数据必须使用独立、明确且经确认的破坏性步骤。 +- 前端、API、Worker 和 Nginx 镜像必须能追溯到 Commit SHA 或版本 Tag,不依赖 `latest` 作为唯一标识。 +- 仓库只保存安全示例配置;真实密码、Token、私钥和生产连接信息通过环境变量或受控 Secret 注入。 +- 容器日志包含服务名、实例标识和 `traceId`,但不得输出完整 Token、连接密码、Secret 或敏感请求体。 +- C10 负责运行装配,不替代每个业务模块对并发、幂等、事务、Migration 兼容性和故障恢复的验证责任。 + +## 九、由流程派生的契约映射 + +| 流程能力 | 当前派生契约 | 数据设计 | 当前状态 | +|---|---|---|---| +| 判断 API 进程能否响应 | A506 `/health/live` | 无 DBxxx | 待交叉评审 | +| 判断 PostgreSQL 和当前必需依赖是否可用 | A507 `/health/ready` | 无 DBxxx | 必需依赖矩阵待确认 | +| 证明两个实例分别响应 | A506/A507 的受控 `instanceId` 或结构化日志 | 无 DBxxx | 证据方案待部署评审 | +| Nginx 转发 SignalR WebSocket | 接口设计 4.3.7“Hub 连接” | Redis Backplane 不登记 DBxxx | 待 C06/C10 联合验证 | +| 实例切换后保持认证授权 | 接口设计 1.6;系统架构 8 | 令牌失效数据设计待 Identity/数据库确认 | 待交叉评审 | +| Worker、Outbox 与依赖恢复 | 系统架构 7.4 及对应业务 Worker 契约 | 相关 DBxxx 尚未冻结 | 各模块分别负责 | + +架构承接章节: + +- 系统架构 7.13“C10 容器化部署与负载均衡”:双实例、统一入口、WebSocket 和版本化镜像; +- 系统架构 10.2“健康端点”:存活与就绪检查; +- 系统架构 11“环境与部署”:Aspire、集成环境和演示环境边界; +- 系统架构 12“测试策略”:请求分布、WebSocket、单实例故障和登录态演示; +- 系统架构 15“分阶段实施”:C10 属于第四阶段联合验收,不阻塞前期核心闭环。 + +## 十、待交叉评审项 + +1. 确定数据库 Migration 唯一执行机制、失败回滚和版本不兼容时的停止条件。 +2. 冻结演示环境 A507 的必需依赖矩阵;可选依赖未启用时不得错误阻塞就绪。 +3. 确定 Nginx/Compose 使用存活或就绪事实的方式、失败阈值、超时、摘除和恢复实例重新加入机制。 +4. 与 C06 冻结 SignalR 协商请求与 WebSocket 连接升级的实例落点策略;可评审会话亲和或经验证的 WebSockets 跳过协商方案,Redis Backplane 本身不能替代该决策。 +5. 与 Identity 确认 Redis 中令牌失效数据的持久化范围,以及 Redis 不可用时受保护请求的安全失败策略。 +6. 与各业务负责人确认提交类请求的幂等/结果查询入口,实例故障时不得由前端统一盲目重试。 +7. 确定 PC Web 安全查询的重试上限,以及统一维护/服务不可用页面与 Nginx 默认错误页的具体替换方式;友好失败出口本身是必达结果。 +8. 确定实例标识、请求分布、连接落点和恢复实例重新入池的脱敏证据方式。 +9. 当前尚无真实 Compose、Nginx、镜像、环境配置或运行结果,本流程不得标记为已部署或已验证。 + +## 十一、现场验收证据清单 + +- [ ] 从停止状态执行一条 Compose 启动命令,必需容器、内部网络和持久卷状态清晰。 +- [ ] 两个 API 使用同一版本镜像和等价业务配置,迁移仅由受控步骤执行一次。 +- [ ] 浏览器只通过 Nginx 完成登录和业务访问,后端容器端口默认不直接暴露公网。 +- [ ] Nginx 正确转发客户端 IP、协议、Host、请求 ID 和 WebSocket Upgrade 请求头,并保留脱敏追踪证据。 +- [ ] Hub 连接按已冻结的落点策略完成协商、WebSocket 升级和重连;Nginx、ASP.NET Core、Serilog 与 Trace 证据均不出现完整 `access_token` Query。 +- [ ] 连续请求通过实例标识或日志证明至少到达两个就绪 API。 +- [ ] 任一 API 未能就绪时明确判定 C10 双实例启动与现场验收不通过,不以单实例运行冒充达标。 +- [ ] 停止 API 实例 1 后,商品查询、本人消息查询和已登录访问由实例 2 继续处理。 +- [ ] 实例切换前后 JWT、Policy、账号状态、资源归属和数据范围一致。 +- [ ] 触发一条实时消息,证明 Nginx WebSocket 转发和跨实例实时通道;停止连接实例后可重连补查。 +- [ ] 恢复实例 1 后,只有通过就绪检查才重新接收请求。 +- [ ] 重启应用容器但保留数据卷后,用户、商品、消息和对象文件仍存在。 +- [ ] 分别演示 Worker、Redis、RabbitMQ、PostgreSQL 或 Nginx 故障时的正确停止、降级或恢复边界,不夸大为共享依赖高可用。 +- [ ] 短暂故障仅对安全查询有限重试;持续故障展示统一维护/服务不可用页面、可理解提示和手动重试,不出现 Nginx 默认错误页、白屏或无限加载。 +- [ ] 提交类请求在结果未知时,仅在所属流程已定义时复用原幂等标识,否则使用已确认的业务查询确认,不因自动重放产生重复写。 +- [ ] 游客、会员、商家和管理员分别在两个实例上验证菜单入口、接口授权和数据范围一致;跨身份请求均被拒绝,合法登录态不因实例切换丢失。 +- [ ] 保存 Compose 配置、示例环境、Commit SHA/镜像 Tag、容器清单、网络/卷说明、健康结果、请求分布、故障恢复和脱敏日志。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/M09-\347\253\231\345\206\205\346\266\210\346\201\257\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/M09-\347\253\231\345\206\205\346\266\210\346\201\257\346\265\201\347\250\213.md" new file mode 100644 index 0000000..67ca38a --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/M09-\347\253\231\345\206\205\346\266\210\346\201\257\346\265\201\347\250\213.md" @@ -0,0 +1,219 @@ +# M09 站内消息流程 + +> 负责人:罗皓晨 +> 覆盖:M09、X03 +> 基础核心流程:F08、F09、F10、F12;鉴权依赖 F02、F13;X04 可追加售后来源 +> 直接协作:韦乾强(M04 Ordering)、张海洋(M05 Payment、M10 AfterSales)、唐宇昊(M01 Identity) +> 文档状态:初稿,待罗皓晨自审及 Ordering、Payment、AfterSales、Identity 交叉评审 +> 需求事实源:[需求规格说明书 M09](../../../01-需求文档/需求规格说明书.md) 的“M09 站内消息通知(X03)”完整七节 + +## 一、范围与事实来源 + +M09 负责把订单、支付和售后模块已经提交的业务事实转换为可持久化、可查询、可标记已读的本人站内消息。消息中心覆盖买家和被明确指定的商家运营账号;游客没有稳定接收身份,管理员本期没有消息中心。 + +本流程不实现自由聊天、群聊、在线客服、短信、邮件、营销群发、已送达回执或消息模板后台。M09 只消费已经发生的事实,不发起订单、支付、发货或售后状态变化。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| M09/X03 需求 | 完整定义,待需求冻结 | 作为角色、规则、异常和验收事实源 | +| 本文业务流程 | 初稿 | 明确事件入口、消息状态、异常、模块出口和不可变结果 | +| A501~A505、接口设计 4.3 | 部分定义、待交叉评审 | 由流程派生并做契约映射,不作为流程输入 | +| DB101~DB120 | 模板/占位,未冻结 | 不发明消息、Inbox 或 Outbox 的具体 DBxxx、字段、约束和索引 | +| C06 实时推送 | 独立挑战流程 | 只在消息提交成功后接入,不承担消息持久化 | + +## 二、模块直接出入口 + +```mermaid +flowchart LR + ID["M01 Identity
已认证用户、角色、账号状态"] -->|"允许买家或商家访问本人消息"| MSG["M09 Messaging
消息生成、查询与已读"] + ORD["M04 Ordering
已提交的创建、取消、发货、完成事实"] -->|"事件标识、业务标识、明确接收人"| MSG + PAY["M05 Payment
已提交且幂等确定的支付结果"] -->|"支付事实与明确接收人"| MSG + AFTER["M10 AfterSales
已提交的申请、审核、寄回或退款事实"] -->|"售后事实与明确接收人"| MSG + + MSG -->|"本人消息列表、详情、未读数与已读结果"| BUYER["买家消息中心"] + MSG -->|"本人经营消息与安全操作入口"| MERCHANT["指定商家消息中心"] + MSG -. "消息提交成功后的旁路出口" .-> REALTIME["C06 实时推送"] + BUYER -->|"用户按需打开安全操作入口"| TARGET["M04/M05/M10 业务详情"] + MERCHANT -->|"用户按需打开安全操作入口"| TARGET + + ID -->|"游客、管理员、账号禁用或令牌失效"| X["拒绝访问,不返回私人消息"] + ORD -->|"事务回滚或事实未确定"| Y["不生成成功消息"] + PAY -->|"事务回滚或结果未确定"| Y + AFTER -->|"事务回滚或结果未确定"| Y + MSG -->|"事件非法"| Z["拒绝并记录安全原因
不自动重试无效事件"] + MSG -->|"临时处理或事务失败"| RETRY["记录失败并等待可靠重试
不改变来源业务结果"] +``` + +边界约束: + +- 直接入口必须是来源模块已经提交的业务事实,并包含稳定事件标识、发生时间、业务标识和明确接收账号。 +- 接收人归属以用户 ID 为准;角色只决定文案、入口和允许的操作,不允许按“全部买家”或“全部商家”广播私人业务事实。 +- 确定出口是 PostgreSQL 中可查询的消息、未读数和首次已读结果;实时提示、前端角标和 Redis 均不是消息事实来源。 +- 消息中的安全操作入口只描述目标业务对象。进入目标页面时仍由 M04、M05 或 M10 重新校验身份、归属和当前状态。 + +## 三、业务事实生成消息 + +```mermaid +flowchart TD + UP["M04/M05/M10:业务事务提交成功"] --> A["提交事件标识、事实类型、发生时间、业务标识和明确接收人"] + A --> B{"事件字段、类型和接收人是否完整合法?"} + B -- "否" --> X["记录失败和 traceId
不生成半完整消息"] + B -- "是" --> C["按接收账号逐项处理"] + C --> D{"该事件、接收人和消息类型是否已有确定结果?"} + D -- "是" --> E["返回既有处理结果
不重复新增或增加未读数"] + D -- "否" --> F{"接收身份和数据范围是否匹配?"} + F -- "否" --> Y["停止该接收项并记录原因
整事件/部分成功边界待评审"] + F -- "是" --> G["按接收身份生成标题、摘要、正文和安全操作入口"] + G --> H["保存历史文案快照并设为未读"] + H --> I["同时保存本次消费的幂等结果"] + I --> J{"消息事务是否提交成功?"} + J -- "否" --> Z["本次整体不生效
等待来源可靠事实重试"] + J -- "是" --> K["M09 确定出口:消息可查询且未读数增加一次"] + K -. "提交后旁路" .-> L["交给 C06 尝试实时推送"] + L --> M{"实时推送是否成功?"} + M -- "是" --> N["在线用户收到轻提示"] + M -- "否" --> O["保留消息事实
用户稍后通过消息中心补查"] +``` + +关键规则: + +- 被回滚、仍在处理或结果不确定的业务操作不得生成“成功”消息。 +- 同一业务事实可按不同接收身份生成不同文案,但同一事件、接收账号和消息类型只能得到一份对应消息。 +- 消息正文、摘要和创建时间是生成时的历史快照,不因商品名称、订单展示文本或用户昵称后来变化而重写。 +- 事件重复投递只能返回既有处理结果,不能重复生成消息、重复增加未读数或重复触发相同业务通知。 +- 消息事务失败时不得留下只有消费记录或只有消息正文的部分结果。 + +## 四、消息查询、详情与已读 + +```mermaid +flowchart TD + ID["M01:已认证且状态正常的买家或商家"] --> A["进入本人消息中心"] + A --> B{"选择查询动作"} + B -- "列表" --> C["按本人、已读状态和消息类型分页查询"] + B -- "未读数" --> D["统计本人当前未读消息"] + B -- "详情" --> E["按消息标识和本人归属查询详情"] + C --> F["按创建时间和消息标识稳定倒序返回"] + D --> G["返回当前数据库未读数"] + E --> H{"消息属于本人且存在?"} + H -- "否" --> X["按不存在处理,不泄露接收人和正文"] + H -- "是" --> I["返回历史正文与当前可用的安全操作入口"] + I --> J{"关联资源仍存在且当前身份仍有权访问?"} + J -- "否" --> K["正文仍可查看,操作入口为空"] + J -- "是" --> L["允许进入目标模块并再次校验"] + I --> M{"用户是否选择标记该消息已读?"} + A --> N["用户选择全部已读"] + M -- "否" --> V["保持当前已读状态"] + M -- "是" --> O{"当前消息仍为未读?"} + O -- "是" --> P["记录首次已读时间"] + O -- "否" --> Q["返回原首次已读结果"] + N --> R["服务端记录本次操作开始时间"] + R --> S["只更新本人且创建时间不晚于该时间的未读消息"] + S --> T["并发到达的新消息保持未读"] + P --> U["返回最新已读结果并校正角标"] + Q --> U + T --> U +``` + +查询与权限规则: + +- 列表、详情、未读数和写操作都必须包含当前认证用户范围,不能先读取任意消息再由客户端过滤。 +- 买家和商家可以复用消息能力,但不能跨身份或跨账号查看、标记、跳转到他人资源。 +- 管理员身份不自动获得查看任意用户私人消息的权限。 +- 列表按页码分页;新消息导致后续页位移属于当前已知边界,本期不提前引入游标分页。 +- 全站提供容易发现但不过度突出的消息入口和未读角标;列表加载时显示与页面结构一致的占位,空数据、请求失败、失败重试和分页加载都给出明确反馈。 +- 标记已读成功后立即更新当前列表项与角标;请求失败时恢复操作前状态并提供就地重试,不能把前端乐观状态当作数据库已读事实。 + +## 五、消息状态与批量边界 + +消息只维护“未读/已读”状态,不复制订单、支付或售后状态机。 + +```mermaid +stateDiagram-v2 + [*] --> Unread: 消息事务提交成功 + Unread --> Read: 本人首次标记已读 + Read --> Read: 本人重复标记,返回首次已读时间 +``` + +状态约束: + +- 首次已读时间由服务端生成并持久化;重复或并发标记不得覆盖首次时间。 +- “全部已读”只覆盖操作开始时已经存在的当前用户未读集合,不影响操作期间新到达的消息。 +- 本期不提供物理删除消息流程。后续确需清理时,必须先定义保留期、归档和验收追踪规则。 +- Redis 或前端角标可以加速展示,但未读状态始终以 PostgreSQL 查询结果为准。 + +## 六、来源事实、接收人和业务出口 + +下表只登记业务语义。具体集成事件名称、字段和 Routing Key 由接口设计 4.3 承接,并仍需来源模块交叉评审。 + +| 已提交业务事实 | 直接来源 | 目标接收人 | 消息业务出口 | 当前边界 | +|---|---|---|---|---| +| 订单创建成功 | M04 Ordering | 订单买家 | 买家订单详情 | 不通知无关商家 | +| 订单取消成功 | M04 Ordering | 订单买家 | 买家订单详情 | 只在取消事务提交后生成 | +| 支付成功 | M05 Payment | 订单买家、订单指定处理商家 | 买家支付/订单详情、商家待发货订单 | 接收商家必须由订单事实明确给出 | +| 订单发货 | M04 Ordering | 订单买家 | 买家订单详情 | 只接受唯一 `Shipped` 结果 | +| 订单完成 | M04 Ordering | 订单买家 | 买家订单详情 | 买家确认与自动完成只通知唯一胜出结果 | +| 售后申请提交 | M10 AfterSales | 申请买家、订单指定处理商家 | 买家售后详情、商家审核入口 | X04 未确认前保持待交叉评审 | +| 售后审核或待寄回 | M10 AfterSales | 申请买家 | 买家售后详情 | 文案必须反映已提交审核结果 | +| 买家提交寄回信息 | M10 AfterSales | 订单指定处理商家 | 商家售后详情 | 不按全部商家广播 | +| 退款成功或失败 | M05 Payment / M10 AfterSales | 申请买家;接口设计 4.3.6 草案对退款失败另列订单指定商家 | 售后详情 | 主需求只明确买家,当前存在契约冲突,待 Payment/AfterSales/Identity 评审;不用消息反向修改退款状态 | + +## 七、异常、补偿与责任 + +| 场景 | M09 处理 | 最终状态与责任 | +|---|---|---| +| 来源事务回滚或事实未确定 | 不生成成功消息 | 来源模块继续拥有业务状态 | +| 事件字段、类型或接收人非法 | 记录失败、`traceId` 和安全原因 | 不生成半完整消息,不自行猜接收人 | +| 同一事件重复到达 | 返回既有处理结果 | 不新增消息、不增加未读数 | +| 可靠消息通道暂时不可用 | 由来源模块保留待发布事实并重试 | 已提交业务结果不回滚 | +| 消息事务失败 | 整体不生成,允许可靠重试 | 不留下消息/消费记录的部分结果 | +| SignalR 或 Redis 不可用 | M09 数据事实仍保留并记录实时推送失败;Identity 仍能安全鉴权时可继续查询 | 若令牌撤销状态无法确认,受保护请求按 Identity 失败关闭策略处理 | +| 查询他人消息 | 与不存在统一处理 | 不泄露消息是否存在、接收人或正文 | +| 关联资源被归档或失去权限 | 返回历史消息正文,移除操作入口 | 目标模块状态不被消息覆盖 | +| 全部已读期间新消息到达 | 新消息保持未读 | 批量结果只覆盖操作开始时集合 | +| 列表首次加载、空数据或分页请求失败 | 保留消息中心页面结构,分别显示加载占位、空状态或就地重试 | 不把加载失败显示成“没有消息” | +| 单条或全部已读请求失败 | 恢复操作前的列表项和角标,提示用户重试 | 数据库未提交时不得保留虚假已读状态 | + +## 八、由流程派生的契约映射 + +本节是第三至七章的下游映射。现有接口与数据设计若不能承载流程,应先登记契约缺口,不得用 Axxx、HTTP 字段或未确认表结构反向修改前述业务语义。 + +| 流程能力 | 当前派生契约 | 数据设计 | 当前状态 | +|---|---|---|---| +| 接收已提交事实并按接收人幂等生成消息 | 接口设计 4.3.6“业务模块到 Messaging 的集成事件” | DB101~DB120 尚未分配具体表 | 来源模块待交叉评审;`RefundFailedIntegrationEvent` 的商家接收人范围与主需求存在契约冲突 | +| 查询本人消息列表 | A501 | 待 `database-lhc.md` 和数据库主文档确认 | 待交叉评审 | +| 查询本人消息详情与安全操作入口 | A502 | 待确认 | 待交叉评审 | +| 查询本人未读数 | A503 | 待确认 | 待交叉评审 | +| 首次标记单条消息已读 | A504 | 待确认 | 待交叉评审 | +| 将操作开始前的本人当前消息全部已读 | A505 | 待确认 | 待交叉评审 | +| 消息提交后实时推送 | 接口设计 4.3.7“Hub 连接”、4.3.8“MessageCreated” | 复用已持久化消息事实 | 转入 C06,待部署与测试评审 | + +架构承接章节: + +- 系统架构 7.4“领域事件与集成事件”:可靠发布、可靠消费和防重边界; +- 系统架构 7.6“四项选做功能”:消息接收账号、持久化后推送和数据库已读事实; +- 系统架构 7.10“C06 实时消息推送”:消息提交后的实时到达旁路。 + +## 九、待交叉评审项 + +1. Ordering、Payment、AfterSales 与 Identity 共同确认商家运营账号的精确接收范围,禁止由 Messaging 自行按角色扩散。 +2. 各来源模块确认事件触发时机、明确接收人和最小业务快照;X04 未确认前,其售后来源只作为待评审接入点。 +3. 数据库设计需在罗皓晨的 DB101~DB120 区间明确消息、Inbox 及必要可靠事件表,并定义唯一约束、索引和删除行为。 +4. A501~A505 仍需生成真实 OpenAPI,并完成 HTTP 契约交叉评审。 +5. 接口设计 4.3 的来源事件、接收人、SignalR Hub 与载荷仍需来源模块评审和非 HTTP 契约测试,不属于 OpenAPI 接口。 +6. 同一来源事件包含多个接收项时,单个接收项无效应使整事件失败还是允许已明确的其他接收项成功,尚需来源模块共同确认。 +7. 主需求只明确退款成功和失败通知申请买家,但接口设计 4.3.6 的 `RefundFailedIntegrationEvent` 草案另列订单 `assignedMerchantUserId`;Payment、AfterSales 与 Identity 必须先解决该契约冲突并确认精确接收账号。 +8. 消息安全操作入口需由目标模块确认当前身份与资源归属的重新校验方式。 +9. “全部已读”的服务端截止时间与并发新消息边界需要进入数据库约束和契约测试。 + +## 十、验收证据清单 + +- [ ] 订单创建、取消、支付、发货、完成和售后事实只在来源事务提交后生成消息。 +- [ ] 买家与指定商家收到符合身份的内容,游客、管理员和无关账号不收到私人消息。 +- [ ] 同一事件重复投递至少两次,只形成一份对应接收人的消息。 +- [ ] 本人列表、详情、筛选、分页和未读数正确,越权请求不泄露消息内容。 +- [ ] 单条已读、重复已读、并发已读和全部已读满足首次时间及集合边界。 +- [ ] 消息入口和列表分别验证加载占位、空状态、请求失败重试与分页加载反馈。 +- [ ] 标记已读成功时列表与角标立即更新;模拟失败时恢复原状态并提供重试。 +- [ ] 可靠消息或实时推送故障时,来源业务结果不回滚,消息能够按既定责任恢复或补查。 +- [ ] 关联资源不可访问时,历史正文仍可查看,但不返回无效或越权操作入口。 +- [ ] 保留事件标识、消息标识、接收用户、`traceId`、数据库结果和重试结果的脱敏证据。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" index 0ba020f..8f96055 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" @@ -2,21 +2,22 @@ > 组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 > -> 编写日期:2026-07-24 版本:v0.1 +> 编写日期:2026-07-24 版本:v0.2 > -> 当前状态:部分定义;F01~F13 已对照总需求和教师验收完成基线校准,仍待各主责人交叉评审;X/C 只能基于核心流程扩展 +> 当前状态:部分定义;F01~F13 已对照总需求和教师验收完成基线校准,仍待各主责人交叉评审;X03、C06、C07、C10 已形成个人流程初稿,仍待主责自审与直接协作人交叉评审 ## 修订记录 | 版本 | 日期 | 修改人 | 修改说明 | |---|---|---|---| | v0.1 | 2026-07-24 | 全体成员(罗皓晨统稿) | 建立集中式业务流程设计,覆盖核心主链路,并对照 F01~F13 需求与验收校准状态、模块交接、X/C 扩展点和核心结果保护规则 | +| v0.2 | 2026-07-24 | 罗皓晨 | 补充 M09、C06、C07、C10 个人流程入口,新增消息、缓存和单 API 实例故障的直接交接图,并更新扩展流程成熟度 | ## 一、文档定位与事实来源 本文档集中维护跨角色、跨模块、包含状态或异常分支的业务流程图,用于避免在主需求正文中堆叠复杂图示。 -成员补充流程前先阅读 [`README.md`](README.md),按负责人、固定模板、成熟度和检查清单统一维护本文档。 +成员在本人目录补充流程细节前先阅读 [`README.md`](README.md),按负责人、固定模板、成熟度和检查清单维护个人模块文档;本文只同步全局核心基线、直接交接和追踪索引。 文档职责: @@ -626,6 +627,90 @@ flowchart LR E -->|"正常/禁用"| H["F02 登录与全部受保护入口"] ``` +#### 3.8.5 核心业务事实、M09 与 C06 的交接 + +```mermaid +flowchart LR + ID["M01 Identity
买家/商家身份和账号状态"] -->|"本人消息查询与已读操作鉴权"| MSG["M09 Messaging"] + ID -->|"实时连接鉴权"| RT["C06 实时推送"] + ORD["M04 Ordering
F08/F09/F12 已提交事实"] -->|"事件标识、业务标识和明确接收人"| MSG + PAY["M05 Payment
F10 已提交支付事实"] -->|"买家与指定商家接收人"| MSG + AFTER["M10 AfterSales
X04 已提交事实(可选来源)"] -. "待 X04 交叉评审" .-> MSG + + MSG -->|"首次处理"| STORED["本人未读消息已持久化"] + MSG -->|"重复事件"| EXISTING["返回既有结果
不新增消息或未读数"] + MSG -->|"事件非法"| REJECT["拒绝并记录/告警
不自动重试无效事件"] + MSG -->|"临时处理或事务失败"| RETRY["记录失败并等待可靠重试
不改变来源业务结果"] + STORED -. "提交后旁路" .-> RT + RT -->|"在线连接可用"| ONLINE["本人全部在线连接收到轻提示"] + RT -->|"断线或推送失败"| QUERY["通过 M09 列表与未读数补查"] + ONLINE --> QUERY + STORED -->|"用户按需打开安全操作入口"| TARGET["M04/M05/M10 重新校验资源权限"] +``` + +交接约束: + +- Ordering、Payment 和 AfterSales 只能提交已经完成事务的确定事实,并明确买家或订单指定处理商家;不得按角色全量广播私人消息。 +- M09 完成校验、幂等判断和消息持久化后才能进入 C06;来源模块不得直接向客户端广播未落库的成功事实。 +- C06 失败只影响实时到达,M09 消息、未读状态和来源核心事务均不改变。 +- 消息中的业务入口不继承永久权限,进入目标模块时必须重新校验当前身份、归属和状态。 + +#### 3.8.6 Catalog、C07 与购物端读取的交接 + +```mermaid +flowchart LR + READ["F04/F06 公开商品查询"] --> CACHE["C07 Cache-Aside"] + CACHE -->|"有效命中"| RESPONSE["返回原公开商品响应"] + CACHE -->|"未命中、损坏或 Redis 降级"| CAT["M02 Catalog / PostgreSQL"] + CAT -->|"已上架商品事实"| RESPONSE + CAT -. "有限 TTL 回填;写入失败只影响性能" .-> CACHE + + WRITE["F11 商品变更"] -->|"Catalog 事务提交成功"| INVALIDATE["失效详情和受影响的固定首页缓存"] + STOCK["F08 扣减 / F09 回补"] -->|"通过 Catalog 改变库存事实"| INVALIDATE + WRITE -->|"事务回滚"| KEEP["不产生成功失效结果"] + INVALIDATE --> RESULT{"本次失效是否成功?"} + RESULT -->|"删除失败"| RETRY["受控重试并由有限 TTL 约束旧值窗口"] + RESULT -->|"成功"| WINDOW{"是否存在并发旧查询
在失效后回填?"} + WINDOW -->|"否"| MISS["后续读取未命中并从 PostgreSQL 回填"] + WINDOW -->|"是"| PROTECT["按待确认的二次失效、版本或等价最小机制纠正"] + + ORDER["F08 提交订单"] -->|"重读销售状态、价格和库存"| CAT + CACHE -. "不得作为交易事实" .-> ORDER +``` + +交接约束: + +- C07 只缓存游客与买家共同可见的公开字段;个人、商家管理和管理员字段不复用公共 Key。 +- 商品事务提交后才触发失效;事务回滚不得生成新的成功失效结果。 +- F08 扣减、F09 回补及后续 C01 库存划拨的具体失效范围由 Catalog、Ordering 和相关负责人交叉评审。 +- Redis 故障回退 PostgreSQL;缓存只能影响性能和约定的一致性窗口,不能改变权限、价格、库存或上下架事实。 + +#### 3.8.7 C10 统一入口与单 API 实例故障交接 + +```mermaid +flowchart LR + WEB["PC Web 浏览器"] -->|"唯一公开地址"| NGINX["Nginx 统一入口"] + NGINX -->|"只向可接收流量的实例转发"| API1["同版本 API 实例 1"] + NGINX -->|"只向可接收流量的实例转发"| API2["同版本 API 实例 2"] + API1 --> SHARED["共享 PostgreSQL、Redis、RabbitMQ 和对象存储"] + API2 --> SHARED + WORKER["独立 Worker"] --> SHARED + + API1 -->|"实例停止或未就绪"| HEALTH["Nginx/健康判断识别不可接收流量"] + HEALTH -->|"摘除实例 1"| SURVIVE["新请求转到就绪实例 2"] + SURVIVE -->|"安全查询"| RETRY["有限重试"] + SURVIVE -->|"提交结果未知"| VERIFY["已定义时复用原幂等标识
否则使用已确认的业务查询确认结果"] + API1 -->|"C06 连接中断"| RECONNECT["重连到存活实例并由 M09 补查"] + RECOVER["实例 1 恢复"] -->|"就绪检查通过后"| NGINX +``` + +交接约束: + +- 两个 API 使用一致的 JWT、Policy、账号状态和令牌失效规则;无法安全确认撤销状态时不得静默放行。 +- 查询请求可以有限重试,提交类请求不得盲目重放;结果未知时回到所属业务流程确认。 +- C06 经共享实时通道跨实例送达并允许重连;C07 在 Redis 故障时回退 PostgreSQL。 +- 本节只承诺 C10 验收中的单 API 实例故障能力,不代表 Nginx、PostgreSQL、Redis 或 RabbitMQ 已实现全链路高可用。 + 直接交接约束: - 调用方只传业务标识和必要命令,目标模块重新校验当前身份、归属和状态。 @@ -659,15 +744,15 @@ flowchart LR |---|---|---|---|---|---| | X01 | F09、F06 | `Completed` 订单项 → 评价记录 → F06 公开评价 | 订单保持 `Completed`;快照、商品状态、价格和库存不变;同一订单项最多一条评价 | 顾欣月 | 待细化 | | X02 | F02、F06,展示衔接 F04 | 已登录买家查看详情 → 本人收藏/浏览记录 | 不修改商品事实;游客不产生个人记录;严格按买家隔离 | 唐宇昊 | 待细化 | -| X03 | F08、F09、F10、F12;X04 可追加来源 | 核心事务提交事件 → 消息落库/查询/已读/离线补查 | 消息失败不回滚核心事务,也不能反向修改订单或支付状态 | 罗皓晨 | 待细化 | +| X03 | F02、F13;F08、F09、F10、F12;X04 可追加来源 | 核心事务提交事件 → 消息落库/查询/已读/离线补查 | 消息失败不回滚核心事务,也不能反向修改订单、支付或售后状态 | 罗皓晨 | [M09 站内消息流程](lhc/M09-站内消息流程.md):初稿,待主责自审;来源事件、精确商家接收账号及核心流程边界待 Ordering、Payment、AfterSales、Identity 交叉评审 | | X04 | F09、F10、F12 | 本人 `Paid/Shipped/Completed` 订单项 → 独立售后状态 → 幂等退款 | 订单核心状态和快照不被“已退款”覆盖;退款不超实付且不重复入账 | 张海洋 | 待细化 | | C01 | F11 + F04/F06 → F08 → F10/F09/F12 | F11 商家创建/发布活动;F06 买家进入秒杀入口 → 独立库存条件扣减 → 汇入 `PendingPayment` | 后续复用核心支付和履约;取消只回补原秒杀库存;支付成功不得回补 | 朱惠惠 | 待细化;库存划拨口径待确认 | | C03 | F08、F10、F09 | `PendingPayment` 创建满 30 分钟 → 系统定时任务复用取消流程 → `Cancelled` | 与支付只能一个胜出;取消和原库存通道回补原子且幂等 | 韦乾强 | 部分定义 | | C04 | F04、F05、F06 | 替换 F05 查询实现 → 返回同口径 F04 列表 → F06 详情 | 只公开已上架商品;权限、筛选口径和下单重校验不变 | 顾欣月 | 待细化 | -| C06 | 经 X03 接入 F08/F09/F10/F12 | X03 消息成功落库 → 实时推送/重连 → X03 补查 | 推送失败不改变消息事实和核心事务;客户端不得仅凭推送改状态 | 罗皓晨 | 待细化 | -| C07 | F04、F06、F11,约束 F08 | F04/F06 数据库读取前 Cache-Aside;F11 提交后失效 | PostgreSQL 仍是事实源;Redis 故障只影响性能;F08 始终重读数据库 | 罗皓晨、顾欣月 | 待细化;TTL 上限待确认 | +| C06 | F02、F13;经 X03 接入 F08/F09/F10/F12,X04 可追加来源 | X03 消息成功落库 → 实时推送/重连 → X03 补查 | 推送失败不改变消息事实和核心事务;客户端不得仅凭推送改状态 | 罗皓晨 | [C06 实时推送流程](lhc/C06-实时推送流程.md):初稿,待主责自审;连接失效规则及 C10 多实例/WebSocket 边界待评审 | +| C07 | F04、F06、F11;F08/F09 通过 Catalog 改变库存时触发失效,F08 始终重读 PostgreSQL | F04/F06 数据库读取前 Cache-Aside;商品或库存事务提交后失效 | PostgreSQL 仍是事实源;Redis 故障只影响性能;F08 不接受缓存作为交易事实 | 罗皓晨、顾欣月 | [C07 缓存流程](lhc/C07-缓存流程.md):初稿,待主责自审;TTL、主动失效、删除失败重试和库存变更触发范围待 Catalog/Ordering 评审 | | C08 | F10、F09;退款对账关联 X04 | F10 支付确认阶段 → 回调幂等/乱序 → 稳定结果与每日对账 | 不重复扣款;`Cancelled` 收到迟到成功不得变为 `Paid`,只登记差异 | 张海洋 | 待细化;与同步 F10 的替换边界待确认 | -| C10 | F01~F13 全部横切 | 统一入口 → 双实例分发/故障切换 → 同一业务结果 | API、鉴权、权限、状态机和数据库结果不变;实例切换不得重复写或越权 | 罗皓晨 | 待细化 | +| C10 | F01~F13 全部横切;支撑 C06/C07 与 Worker 后台运行 | 统一入口 → 双实例分发/单 API 实例故障切换 → 同一业务结果 | API、鉴权、权限、状态机和数据库结果不变;实例切换不得重复写或越权 | 罗皓晨 | [C10 高可用流程](lhc/C10-高可用流程.md):初稿,待主责自审;当前只定义单 API 实例故障边界,鉴权一致性、各模块幂等和依赖就绪规则待全组评审 | 当前不得直接冻结的三项: -- Gitee From 05c8a8fa655de7de7df2030ac08cafd97629867d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=B5=B7=E6=B4=8B?= <2040454617@qq.com> Date: Fri, 24 Jul 2026 14:52:13 +0800 Subject: [PATCH 060/118] docs(process): fix M10 state machine + tighten C08 references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M10 售后流程.md: - 修正状态机:删除 3 处非法转换 - 待退货 --> 已撤销(M10 业务规则不允许) - 待收货 --> 已拒绝(需求仅允许商家确认收货) - 退款失败 --> [*](退款失败非终态,可重试) - 状态机统一改为英文 PascalCase(与 M05 模板一致) - 新增 X04 状态名对照表(中文显示 + 英文代码 + 状态描述) - 第十三节第 10 条:A421 改为 A431 内部应用能力 - 第七章 + 第十三节去掉 C01 秒杀具体描述(属朱惠惠范围) C08 支付回调与对账流程.md: - 第四章引用修正:第五章 乱序处理 → 幂等与乱序处理 - 第七节批次对账范围:明确只读取 Processed 状态的回调 Inbox - 第八节 mermaid 语法:*(InProgress)* 星号去除 Refs: zhy 评审反馈 1-7 项优化 --- ...71\350\264\246\346\265\201\347\250\213.md" | 6 +- ...56\345\220\216\346\265\201\347\250\213.md" | 59 +++++++++++-------- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" index 4c0de45..413f4a8 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" @@ -128,7 +128,7 @@ flowchart TD F -- "是" --> G["开启事务"] G --> H["写入 Inbox 记录(状态 Processing)"] H --> I["查询订单当前状态"] - I --> J["判定下一动作(见第五章 乱序处理)"] + I --> J["判定下一动作(见第五章 幂等与乱序处理)"] J --> K{"事务提交成功?"} K -- "否" --> KR["整体回滚,Inbox 回退到 Received"] K -- "是" --> Z["进入事务一致性处理"] @@ -210,7 +210,7 @@ flowchart TD A --> B{"该日期+范围已存在批次?"} B -- "是" --> BX["跳过:不重复生成"] B -- "否" --> C["开启批次事务"] - C --> D["读取支付记录、订单状态、回调 Inbox"] + C --> D["读取支付记录、订单状态、状态为 Processed 的回调 Inbox"] D --> E["读取退款记录、钱包入账、钱包流水"] E --> F["比对支付记录 vs 订单状态"] F --> G["比对退款成功 vs 退款流水 vs 钱包入账"] @@ -237,7 +237,7 @@ flowchart TD C --> D{"选择单条差异"} D -- "查看详情" --> E["展示订单号、支付/退款记录、状态时间线、Inbox 历史"] D -- "开始处理" --> F["状态条件推进 Pending → InProgress"] - D -- "标记已解决" --> G["状态条件推进 *(InProgress) → Resolved"] + D -- "标记已解决" --> G["状态条件推进 InProgress → Resolved"] D -- "已解决" --> H["填写处理说明"] H --> I["开启事务"] I --> J["写入处理说明、处理人、处理时间"] diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" index a8ce58b..4627399 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" @@ -33,7 +33,7 @@ flowchart LR AS -->|"仅退款审核通过或退货确认收货后"| PAY["M05 Payment
IRefundService(内部应用能力)"] PAY -->|"退款成功:钱包入账 + 退款流水"| AS AS -->|"申请提交、审核结果、退款结果"| MSG["M09 消息持久化/通知"] - AS -->|"退货数量回补"| INV["M02 Catalog
库存按订单项来源通道回补"] + AS -->|"退货数量回补"| INV["M02 Catalog
库存按订单项归属通道回补"] RECON["C08 对账 Worker"] -. "每日对账" .-> AS ID -->|"游客、非授权角色、账号禁用、令牌失效"| X["拒绝访问,不返回售后数据"] @@ -54,32 +54,43 @@ X04 售后有 8 个独立状态,不能与订单核心履约状态(`PendingPa ```mermaid stateDiagram-v2 - [*] --> 待审核: 买家提交申请 - 待审核 --> 已撤销: 买家在审核前主动撤销 - 待审核 --> 已拒绝: 商家审核拒绝 - 待审核 --> 退款中: 仅退款审核通过 - 待审核 --> 待退货: 退货退款审核通过 - 待退货 --> 已撤销: 买家不允许撤销 - 待退货 --> 待收货: 买家提交退货物流 - 待收货 --> 已拒绝: 商家拒绝收货 - 待收货 --> 退款中: 商家确认收到退货 - 退款中 --> 已退款: 钱包退款成功 - 退款中 --> 退款失败: 钱包退款失败 - 退款失败 --> 退款中: 幂等重试 - 待审核 --> [*] - 已撤销 --> [*] - 已拒绝 --> [*] - 已退款 --> [*] - 退款失败 --> [*] + [*] --> PendingReview: 买家提交申请 + PendingReview --> Cancelled: 买家在审核前主动撤销 + PendingReview --> Rejected: 商家审核拒绝 + PendingReview --> Refunding: 仅退款审核通过 + PendingReview --> PendingReturn: 退货退款审核通过 + PendingReturn --> PendingReceipt: 买家提交退货物流 + PendingReceipt --> Refunding: 商家确认收到退货 + Refunding --> Refunded: 钱包退款成功 + Refunding --> RefundFailed: 钱包退款失败 + RefundFailed --> Refunding: 幂等重试 + PendingReview --> [*] + Cancelled --> [*] + Rejected --> [*] + Refunded --> [*] ``` 合法转换回执: -- 申请只能由买家提交;只有 `待审核` 状态可被买家主动撤销。 -- 商家只能在 `待审核` 状态下审核;审核拒绝后不可再次审核。 -- 退货退款必须经历 `待退货 → 待收货 → 退款中`;商家确认收货是退款前置条件。 +- 申请只能由买家提交;只有 `PendingReview` 状态可被买家主动撤销。 +- 商家只能在 `PendingReview` 状态下审核;审核拒绝后不可再次审核。 +- 退货退款必须经历 `PendingReturn → PendingReceipt → Refunding`;商家确认收货是退款前置条件。 +- 退款失败**不是**终止状态,可通过幂等重试回到 `Refunding`。 - 重复退款请求返回首次结果,不重复写入钱包流水。 +### 3.1 X04 状态名对照 + +| 中文显示 | 英文代码(PascalCase) | 状态描述 | +|---|---|---| +| 待审核 | `PendingReview` | 买家提交申请后等待商家审核 | +| 待退货 | `PendingReturn` | 退货退款审核通过后等待买家提交退货物流 | +| 待收货 | `PendingReceipt` | 买家提交退货物流后等待商家确认收货 | +| 退款中 | `Refunding` | 退款执行中(仅退款审核通过 / 退货确认收货后) | +| 已退款 | `Refunded` | 钱包退款成功,终止状态 | +| 退款失败 | `RefundFailed` | 钱包退款失败,可重试 | +| 已拒绝 | `Rejected` | 商家审核拒绝,终止状态 | +| 已撤销 | `Cancelled` | 买家主动撤销(仅 `PendingReview` 时),终止状态 | + ## 四、可申请判断与提交申请 ```mermaid @@ -195,7 +206,7 @@ flowchart TD D -- "否" --> DZ["保留输入并提示错误"] D -- "是" --> E["开启事务"] E --> F["状态条件推进 PendingReceipt → Refunding"] - F --> G["按退货数量回补原库存通道(普通/C01 秒杀)"] + F --> G["按退货数量回补订单项原库存通道"] G --> H["记录回补事实与确认收货审计日志"] H --> I["事务提交成功后异步调用 IRefundService 触发退款"] I --> J["记录待发布退款事实"] @@ -338,11 +349,11 @@ flowchart TD 3. 卖家超时未处理:流程不自动同意或拒绝;A416 审核接口必须保留超时仍未处理的 PendingReview 状态,不能引入"超时自动拒绝"。 4. 退货拦截发货:流程要求处理中申请阻断发货;M04 的发货接口必须能识别订单项未售后数量,禁止对未售后数量不足的订单项发货。 5. 账号禁用售后:流程要求禁用账号不能新建或主动操作售后;A412 / A415 必须在账号禁用时拒绝;已有申请可被商家和系统继续处理。 -6. 库存回补口径:流程要求按订单项原库存来源通道回补;M02 库存接口必须区分普通库存与 C01 秒杀库存,不能把退款售后回补到错误通道。 +6. 库存回补口径:流程要求按订单项原库存来源通道回补;具体库存通道归属由 M02 协作时确认;M10 不替代 M02 决定库存通道。 7. 状态机不可逆:流程要求"已退款 / 已拒绝 / 已撤销"为终止状态;A419 退款失败重试只能从 `退款失败` 推进,不能从"已退款"或"已拒绝"推进。 8. 退货快递单号全局唯一:流程要求同一快递单号只能用于一笔售后;A434 提交退货物流要建唯一约束 DBxxx(待评审)。 9. 商家并发审核:流程要求状态条件唯一胜出;A416 审核必须检查 `WHERE status = 'PendingReview'` 条件更新,失败方收到 `409 + AFTER_SALES.INVALID_STATUS`。 -10. 库存回补时机:流程要求未发货仅退款 + 退货退款确认收货都回补库存;A421 退款成功接口不负责库存回补,由 M04 / M02 在确认收货时完成。 +10. 库存回补时机:流程要求未发货仅退款 + 退货退款确认收货都回补库存;A431 内部应用能力(退款入账)不负责库存回补,由 M04 / M02 在确认收货时完成。 11. DB081~DB100 尚未形成可实施的完整表定义,数据库字段、约束和索引必须由数据库设计任务另行确认。 ## 十四、验收证据清单 -- Gitee From 90c86864ed643253e3f25ef95617bc21e8c01721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=B5=B7=E6=B4=8B?= <2040454617@qq.com> Date: Fri, 24 Jul 2026 15:01:35 +0800 Subject: [PATCH 061/118] docs(process): tighten C08 batch + refund reconciliation terminology MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 第七节 E 节点:'退款记录/钱包入账/钱包流水' 改为 'M10 退款记录、wallet_ledgers 中退款入账记录' - 第九节 C 节点:'wallets.transactions' 错误命名改为 'wallet_ledgers' - 第七节关键规则:明确批次范围条件(payment.status = 'Succeeded' + Processed 状态 Inbox) --- ...\216\345\257\271\350\264\246\346\265\201\347\250\213.md" | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" index 413f4a8..bb04c88 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" @@ -211,7 +211,7 @@ flowchart TD B -- "是" --> BX["跳过:不重复生成"] B -- "否" --> C["开启批次事务"] C --> D["读取支付记录、订单状态、状态为 Processed 的回调 Inbox"] - D --> E["读取退款记录、钱包入账、钱包流水"] + D --> E["读取 M10 退款记录、wallet_ledgers 中退款入账记录"] E --> F["比对支付记录 vs 订单状态"] F --> G["比对退款成功 vs 退款流水 vs 钱包入账"] G --> H["发现差异则逐条登记"] @@ -225,7 +225,7 @@ flowchart TD 关键规则: - 同一日期同一范围不重复生成矛盾批次;以 `(date, range)` 唯一约束去重。 -- 批次范围只包含已提交事务的支付记录与退款记录,不包含处理中或失败中的回调。 +- 批次范围只包含已提交事务的支付记录(对应 `payment.status = 'Succeeded'`)、M10 已退款的记录和 `Processed` 状态的回调 Inbox,不包含处理中或失败中的回调。 - 资金类比对必须用 PostgreSQL 条件查询与聚合;不在应用层先读后算。 ## 八、差异识别与闭环 @@ -262,7 +262,7 @@ flowchart TD ```mermaid flowchart TD A["对账批次生成时"] --> B["读取 M10 退款成功记录"] - B --> C["读取 wallets.transactions 中退款流水"] + B --> C["读取 wallet_ledgers 中退款入账记录"] C --> D["读取 refunds 表中对应记录"] D --> E{"三方一致?"} E -- "是" --> F["标记为匹配"] -- Gitee From 5bbdb274ca650c047d3d8c4a496597d687487324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=83=A0=E6=83=A0?= <2205590672@qq.com> Date: Fri, 24 Jul 2026 15:27:30 +0800 Subject: [PATCH 062/118] =?UTF-8?q?docs(process):=20=E6=96=B0=E5=A2=9E=20M?= =?UTF-8?q?03=20=E8=B4=AD=E7=89=A9=E8=BD=A6=E4=B8=8E=20C01=20=E7=A7=92?= =?UTF-8?q?=E6=9D=80=E6=B5=81=E7=A8=8B=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...22\346\235\200\346\265\201\347\250\213.md" | 254 +++++++++++++++++ ...51\350\275\246\346\265\201\347\250\213.md" | 256 ++++++++++++++++++ 2 files changed, 510 insertions(+) create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" new file mode 100644 index 0000000..ce13ea9 --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" @@ -0,0 +1,254 @@ +# C01 秒杀与防超卖流程 + +> 负责人:朱惠惠 +> 覆盖:C01-01、M03-01 与 M04-01 的秒杀衔接、X04 不参与秒杀取消回补 +> 基础核心流程:F11(商品上下架)、F04/F06(活动浏览)、F08(下单)、F10(支付)、F09/F12(取消 / 发货) +> 直接协作:顾欣月(M02 Catalog 与 M06-01 活动维护)、韦乾强(M04 Ordering 与 C03 超时取消)、张海洋(M05 Payment 与 C08 回调) +> 文档状态:初稿,待朱惠惠自审及 Catalog/Ordering 交叉评审;库存划拨口径与 C08 异步替换边界按根文档要求保持“待决” +> 需求事实源:[需求规格说明书 C01](../../../01-需求文档/需求规格说明书.md) 的“C01 秒杀与防超卖”完整七节 + +## 一、范围与事实来源 + +本扩展在面向买家的高并发秒杀场景下保证库存“只减不超、不少不丢”:每一份秒杀库存只能被一名买家以一份订单成功购买,重复请求不能产生重复扣减或重复订单,所有失败请求不得留下半扣减、未提交订单或孤立记录。本期秒杀以“限时一口价活动”为模型,关联一个已上架的普通商品和一份独立维护的秒杀库存。活动期内买家点击“立即抢购”直接提交秒杀订单,跳过普通加车流程;活动结束或库存耗尽后入口立刻失效,进入商品详情时只能看到普通购买。 + +数据库事务是秒杀正确性的唯一事实来源。Redis、消息队列、Nginx 限流和前端防抖只承担性能与体验,不得用作并发正确性边界。本文按“先确认业务参与者和原子结果,再确定流程步骤和不可变核心事实”的顺序编写;A8xx(A801 浏览活动、A802 抢购提交、A803 抢购结果查询、A804 商家维护活动)只用于流程完成后的契约映射与缺口检查,不能反向拼接流程。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| C01 需求 | 完整定义 | 作为业务语义事实源 | +| F04/F06/F08/F10/F09/F12 核心流程 | 已校准基线 | 作为秒杀接入点与回归目标 | +| C03 超时取消 | 部分定义(韦乾强负责) | 仅引用其回补通道 | +| C08 异步回调 | 待细化(张海洋负责) | 仅引用其对秒杀订单的幂等规则 | +| C10 高可用 | 待细化(罗皓晨负责) | 仅引用其实例分发的最终一致性 | +| C07 缓存 | 待细化(罗皓晨、顾欣月负责) | 仅引用其对活动列表的失效策略 | +| A8xx 接口 | 部分定义、未冻结 | 由流程派生并做映射 | +| DB201~DB210(C01 表) | 模板/占位 | 本文不发明字段、约束和索引 | +| X04 售后退款 | 独立扩展 | 仅登记边界,不卷入秒杀取消回补 | + +## 二、扩展直接出入口 + +```mermaid +flowchart LR + ID["M01 Identity
已认证买家、角色、账号状态"] -->|"BuyerOnly 通过"| SEC["C01 Seckill
活动、秒杀库存、个人限购"] + MERCHANT["M06-01 商家运营"] -->|"创建 / 发布 / 取消 / 状态推进"| SEC + PUBLIC["买家秒杀入口 / 活动列表 / 商品详情"] -->|"活动浏览、倒计时、立即抢购"| SEC + SEC -->|"活动基础信息、主图、秒杀价、原价、倒计时、剩余库存和已售数量"| PUBLIC + + SEC -->|"通过 Ordering 公开应用契约
写入共享 orders / order_items
带 orderType=Seckill、activityId、秒杀价快照、限购配额占用"| ORD["M04 Ordering"] + ORD -->|"PendingPayment 订单"| PAY["M05 Payment"] + ORD -->|"买家主动或 C03 超时取消 → 回补秒杀库存并释放限购名额"| SEC + SEC -. "事务提交后" .-> MSG["M09 消息持久化 / 通知"] + + ID -->|"游客、商家或账号禁用"| X["拒绝抢购,不创建独立秒杀订单"] + ORD -->|"支付回调回写、售后退款"| Y["按 M04/M05/M10 公开契约处理,不允许直接 UPDATE 秒杀库存"] + C08["C08 异步回调"] -. "作用于秒杀订单的幂等回写" .-> ORD + + RATE["Nginx / API / 应用 / 连接池限流"] -->|"429 / 409 快速失败"| SEC + CA["C07 缓存"] -->|"活动列表、商品基础信息读取"| PUBLIC + SEC -. "库存事实" .-> CA +``` + +边界约束: + +- C01 不与 M03 购物车模块直连;秒杀订单通过 M04 Ordering 公开应用契约写入共享 `orders / order_items`,不建立独立的秒杀订单状态机。 +- 秒杀库存只走数据库条件更新;Redis、队列、客户端状态都不能单独承担正确性。 +- 秒杀活动取消或结束后,已存在订单按 M04/M05/M09 流程继续走完;取消与回补必须落到秒杀原通道,不污染普通库存。 +- M03 购物车不参与秒杀:秒杀成功不写购物车,秒杀回补也不联动购物车;普通加购不读秒杀库存。 + +## 三、活动维护、状态推进与倒计时 + +```mermaid +flowchart TD + A["商家在 M06-01 进入秒杀活动维护"] --> B{"新建还是编辑?"} + B -- "新建" --> C["绑定已上架商品、设定开始时间、结束时间、秒杀价、库存总量和单用户限购"] + C --> D{"开始 ≥ 当前 UTC、结束 > 开始、库存 ≤ 商品当前可售库存且为正整数?"} + D -- "否" --> X["拒绝保存并指出缺失项"] + D -- "是" --> E["保存为草稿,状态 Draft"] + B -- "编辑" --> EE{"活动状态?"} + EE -- "Draft / Published / Running" --> FF["允许修改开始前 / 限购字段;运行中开始时间不得回拨"] + EE -- "Ended / Cancelled" --> X["拒绝编辑"] + E --> F{"手动发布?"} + F -- "否" --> EE1["保持 Draft"] + F -- "是" --> G["状态变为 Published,并写入活动开始 UTC 时间"] + G --> H["基于数据库 UTC now() 自动判定:start ≤ now < end → Running;now ≥ end → Ended"] + H --> I["活动详情可在卖家与买家入口查询"] + I --> J["剩余库存售罄时立即标记 Sold Out 并禁用抢购入口"] +``` + +状态机: + +```mermaid +stateDiagram-v2 + [*] --> Draft: 商家保存 + Draft --> Published: 商家主动发布 + Draft --> Cancelled: 商家取消 + Published --> Running: 数据库 UTC now() ≥ 开始时间且 < 结束时间 + Published --> Cancelled: 商家取消 + Running --> Ended: 数据库 UTC now() ≥ 结束时间 + Running --> Cancelled: 商家取消 + Ended --> [*] + Cancelled --> [*] + Published --> Published: 重复发布幂等返回 +``` + +关键约束: + +- 活动状态字段由数据库维护并参与所有业务校验;商家只能在 Draft / Published / Running 时执行取消,Ended / Cancelled 拒绝重复状态变更。 +- 状态推进在数据库侧以 UTC `now()` 为权威,避免应用实例时钟漂移造成提早或延后成功。 +- 活动取消后已存在订单继续走完;未提交请求直接拒绝;本期不回收已分配秒杀库存,避免被普通订单夹带走量。 + +## 四、活动浏览与库存倒计时展示 + +```mermaid +flowchart TD + A["买家进入秒杀入口 / 商品详情秒杀 Banner"] --> B["服务端读取活动当前状态、剩余库存和已售数量"] + B --> C{"活动状态?"} + C -- "Draft" --> X1["入口隐藏,普通商品详情展示"] + C -- "Published" --> X2["展示开始倒计时,按钮置灰"] + C -- "Cancelled" --> X3["入口隐藏,普通商品详情展示"] + C -- "Ended" --> X4["入口隐藏,普通商品详情展示"] + C -- "Running" --> D{"剩余可售库存?"} + D -- "= 0" --> X5["入口标记已售罄,按钮置灰"] + D -- "> 0" --> E["倒计时至结束时间,按钮可点击"] + E --> F["前端轮询 / 缓存刷新需要与数据库 remaining 一致"] + F --> G["不允许“前端还有库存但下单失败”或“前端售罄但实际还能抢”"] +``` + +关键约束: + +- 倒计时统一以数据库 UTC 时间计算;同 / 跨实例用户看到一致的剩余库存和倒计时。 +- 前端轮询或服务端推送给出的剩余库存必须与数据库实际一致,禁止相反情景。 +- 活动列表与商品基础信息读取可经 C07 缓存,但秒杀库存本身不进入缓存。 + +## 五、立即抢购主流程 + +```mermaid +flowchart TD + A["买家点击立即抢购,携带商品 ID、活动 ID、数量、幂等键"] --> B["服务端解析登录身份(JWT + role=buyer)"] + B --> C{"身份合法?"} + C -- "否" --> X["401,引导登录"] + C -- "是" --> D{"活动存在且状态为 Running?"} + D -- "否" --> Y["按不存在 / 已结束 / 未开始返回明确原因"] + D -- "是" --> E{"当前 UTC 时间落在开始和结束之间?"} + E -- "否" --> Y + E -- "是" --> F{"数量为正整数?"} + F -- "否" --> Y["数量非法"] + F -- "是" --> G{"同 (买家+活动+幂等键) 已有处理结果?"} + G -- "同键同请求" --> P0["返回首次确定结果,不重复扣减库存"] + G -- "同键不同请求" --> Z["拒绝标识被不同请求复用"] + G -- "否" --> H{"活动剩余可售库存 ≥ 请求数量?"} + H -- "否" --> Z1["已售罄"] + H -- "是" --> I{"单用户当前限购内?"} + I -- "否" --> Z2["超过单用户限购"] + I -- "是" --> J["开启秒杀事务"] + J --> K["条件更新秒杀库存:remaining -= qty AND 状态/窗口/活动 ID 条件命中,期望受影响行数 = 1"] + K --> K1{"受影响行数符合预期?"} + K1 -- "否" --> ZR["并发竞争失败:按 409 / 已售罄返回"] + K1 -- "是" --> L["原子占用 (activity_id, buyer_id) 限购配额行"] + L --> M["通过 Ordering 公开应用契约写入共享 orders / order_items,带 orderType=Seckill、activityId、秒杀价快照"] + M --> N["写入待发布订单创建事实(OrderCreatedIntegrationEvent)"] + N --> O{"秒杀事务整体提交?"} + O -- "否" --> ZR["整体回滚:库存未扣减、限购未占用、订单未生成、待发布事实未写入"] + O -- "是" --> P["返回订单号、活动 ID、剩余库存与购买结果"] + P --> Q["跳转 M04 后续支付;M05/C03/M06-02/M09/M10 按既有流程继续"] +``` + +不可变核心事实: + +- 条件更新 `remaining = remaining - :qty`、`sold = sold + :qty` 必须使用 `WHERE remaining >= :qty AND status = 'Running' AND now() BETWEEN start_at AND end_at AND activity_id = :id` 的更新语句;命中受影响行数 = 1 才算扣减成功。 +- 事务短小:秒杀事务只覆盖秒杀库存行、限购配额占用、订单写入和必要快照;事务内禁止调用外部 HTTP、等待用户输入或长计算。 +- 锁粒度按活动 ID 单行:抢购事务只对单个活动的库存和订单写入加锁,不全表扫描。 + +## 六、取消、回补与限购名额释放 + +```mermaid +flowchart TD + A["买家主动取消或 C03 系统定时任务触发取消"] --> B{"订单 orderType = Seckill 且存在 activityId?"} + B -- "否" --> X["走 M04 普通取消通道,不进入秒杀回补"] + B -- "是" --> C["开启取消事务"] + C --> D["条件推进 PendingPayment → Cancelled,影响订单状态"] + D --> E{"订单状态条件更新成功?"} + E -- "否" --> Y["返回失败,已支付或状态竞争"] + E -- "是" --> F["按订单项 (activityId, qty) 条件回补秒杀库存:remaining += qty、sold -= qty,影响行数 = 1"] + F --> G["释放 (activity_id, buyer_id) 限购配额:已用数量 -= qty,影响行数 ≥ 1"] + G --> H["记录取消时间和待发布订单取消事实"] + H --> I{"取消事务整体提交?"} + I -- "否" --> Z["整体回滚,允许定时任务或买家请求安全重试"] + I -- "是" --> J["C01 直接输出:秒杀库存回补、限购释放、订单 Cancelled"] + J --> K["M04 通知买家与商家;消息消费由 M09 按既有流程完成"] +``` + +关键约束: + +- 秒杀取消回补必须落到原活动的秒杀可售库存;不得回补到普通商品库存。 +- 同一笔订单的重复取消请求只能回补一次:以 `(order_id, 'CANCELLED')` 或订单状态条件更新作为幂等保障。 +- 买家主动取消、C03 超时取消的库存回补在同一事务内完成;任何一步失败整体回滚,不产生“库存已回补但订单仍为 PendingPayment”的部分结果。 +- 已 `Paid` 订单不走取消回补;后续退款 / 退货按 M10 售后流程处理。 +- 秒杀库存回补以条件更新回写到 `remaining` 并扣减 `sold`;活动结束后回补仍允许,只是不再允许新抢购。 + +## 七、与其他挑战模块的衔接 + +```mermaid +flowchart LR + A["C01 秒杀事务提交"] -->|"OrderCreatedIntegrationEvent"| B["M09 Outbox / 站内消息"] + A -->|"PendingPayment 订单"| C["C03 30 分钟超时检查"] + C -->|"超时取消"| D["M04 取消事务 → C01 回补通道"] + A -->|"支付成功后回调"| E["C08 异步回调幂等"] + A -->|"任一 API 实例受理"| F["C10 双实例分发"] + A -. "活动列表 / 商品基础信息" .-> G["C07 Cache-Aside"] +``` + +- C08:支付回调在 M05 上做幂等,作用覆盖秒杀订单;C01 不暴露独立的支付通道。 +- C10:秒杀入口由任一实例受理,最终一致性必须由数据库条件更新承担;不能由实例本地状态决定成败。 +- C07:活动列表、商品基础信息可缓存,必须按已定义的失效策略更新;秒杀库存与状态字段不进入缓存。 + +## 八、由流程派生的接口契约映射 + +本节是第三至第七章业务流程的下游映射,不是流程输入。先确认“业务动作、当前状态、成功或失败后得到什么结果”,再决定由哪个接口承载。若现有 Axxx 与流程冲突,先记录接口缺口并修改接口设计;只有业务需求本身变化时,才回到前文重新评审流程。 + +| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +|---|---|---|---| +| 浏览秒杀活动 | A801 | 仅返回当前请求可见的活动状态、剩余库存、已售数量和倒计时 | 待交叉评审 | +| 秒杀立即抢购 | A802 | 校验身份、活动状态、窗口、数量、限购、幂等;以条件更新原子扣减并生成订单 | 待交叉评审 | +| 查询抢购结果 | A803 | 返回本人订单号、活动 ID 和购买结果;非本人返回“不存在 / 无权限” | 待交叉评审 | +| 商家维护秒杀活动 | A804 | 在 Draft / Published / Running 允许创建、发布、取消;Ended / Cancelled 拒绝重复变更 | 待交叉评审 | +| 秒杀库存回补 | A805 | 按订单项 `(activityId, qty)` 条件回补;同订单多次取消只回补一次 | 待交叉评审 | + +接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码与 OpenAPI、ProblemDetails 不得反向写入业务图;接口设计 1.12 通用幂等规则与 4.6 资金类幂等约束同样适用于秒杀订单。 + +## 九、扩展接入边界 + +- 不修改 F01~F13 核心订单状态机;PendingPayment → Paid / Cancelled 的两路竞争通过数据库条件更新自然处理,秒杀不引入额外状态。 +- 不预留额外支付通道:秒杀订单沿用 M05 模拟支付;不引入邀请码 / 概率中奖 / 限购用户分组 / 多 SKU 组合 / 跨活动互斥等本期不实现的扩展。 +- 不替代 C07 缓存策略:秒杀列表和商品基础信息走 C07 缓存;秒杀库存与状态字段不进入缓存。 +- 不改变 M03 购物车:M03 仅承载普通加购;C01 立即抢购不写购物车,取消也不联动购物车。 +- 与 X04 售后退款的衔接:已支付秒杀订单的售后按 M10 处理;本文仅记录 C01 不参与退款库存通道,防止污染普通库存。 +- 双实例 / 负载均衡:C10 接管的秒杀入口由任一实例受理;条件更新和限购配额占用的数据库事实保证最终一致性。 + +## 十、由流程反查出的接口与数据待评审项 + +1. 抢购接口 A802 的最终扣减库存与价格必须走服务端重读:客户端不得指定秒杀价或库存;当前 A802 草案若允许 `expectedAmount / expectedQty` 参与业务判定,需要明确“仅作为客户端旧值冲突保护”,不得成为扣减事实。 +2. 单用户限购以 `(activity_id, buyer_id)` 唯一配额事实为准;A802 必须先占用配额再进入主流程;同 Key 同请求重放首配结果,不得再次增加限额。 +3. 库存语义:`remaining + sold + frozen = 初始总量`;本期不启用 `frozen`,所有提交要么直接成功,要么立即失败;若后续启用 `frozen`,A802 需要回看本文第五节并保留 6.6 异常分支。 +4. C08 异步回调:若 C08 替换 F10 的部分支付确认步骤,必须先在根文档决定其接入 F10 的哪个同步步骤;不能让同步支付和异步回调同时成为最终支付事实。 +5. C03 超时取消:必须在 M04 完成条件推进 `PendingPayment → Cancelled` 后进入秒杀回补通道;不能绕过 M04 直接 UPDATE 秒杀库存。 +6. 活动取消运营动作:A804 的“取消”动作只对 Draft / Published / Running 生效;取消后已存在订单继续按既有流程走完,本期不回收已分配库存,避免与普通订单混淆。 +7. 限流分级:A802 与 A801 / A8xx(普通购物车接口)应分桶限流,秒杀高并发不得拖垮普通商品查询;Nginx / API / 应用 / 连接池任意一层都能给出 429。 +8. 时间口径:服务端使用 UTC 写入;状态推进与活动判断以数据库 `now()` 为权威;应用节点间的时钟轻微漂移不影响业务结果。 +9. 数据隔离:A801~A805 全部按 `(buyer_id, activity_id)` 或 `(订单 ID, 活动 ID)` 双重过滤;越权返回“不存在 / 无权限”统一错误,不暴露记录是否存在。 +10. DB201~DB210 尚未形成可实施的完整表定义;秒杀活动表、库存表、限购配额表、幂等记录表的字段、约束和索引必须由数据库设计任务另行确认。 + +## 十一、验收证据清单 + +- [ ] 100 并发请求抢 10 件库存:成功订单数 = 10,剩余可售 = 0,已售 = 10;其余 90 个请求以 409 / 已售罄或 429 / 限流明确失败;无 5xx 长期堆积。 +- [ ] 校验数据库一致性:`remaining + sold + frozen = 初始总量`;成功订单一一对应一次库存扣减;失败请求无扣减记录、无订单。 +- [ ] 单用户限购:同一买家连续两次抢购仅一笔成功;第二次返回“超过单用户限购”或“已售罄”;数据库同一 `(activity_id, buyer_id)` 仅一笔秒杀订单。 +- [ ] 时间窗口:把开始时间改为未来 1 分钟后立刻抢购 → 全部返回“活动未开始”,库存不变;到达开始时间后可正常抢购;把结束时间改为过去 1 分钟后立刻抢购 → 全部返回“活动已结束”,库存不变。 +- [ ] 幂等:在约定窗口内用同一标识连续提交两次 → 仅生成一笔订单;剩余库存只扣减一次;两次响应携带同一订单号。 +- [ ] 取消与回补:抢购成功后主动取消或触发 C03 超时取消 → 秒杀可售库存回补 +1,已售 -1;同笔订单重复取消请求只回补一次。 +- [ ] 流量隔离:秒杀压测同时反复访问普通商品列表与详情 → 普通接口响应未因秒杀压测显著恶化;秒杀入口 P50 / P95 / P99 记录在压测报告中。 +- [ ] 双实例:在两实例 API 环境下重复硬指标验收 → 成功订单数 = 10 不变;Nginx / API 实例标识日志显示请求被分发到至少两个实例。 +- [ ] 活动取消:草稿 / 已发布 / 进行中活动可取消;已结束或已取消活动拒绝重复状态变更;取消后已分配库存不回收。 +- [ ] 缓存一致性:秒杀库存不进入缓存;活动列表与商品基础信息走缓存时,按失效策略更新;售罄状态变化必须以数据库为准重新加载活动详情。 +- [ ] 越权访问:用买家 B 身份请求买家 A 的秒杀订单或抢购资格 → 拒绝并返回“不存在 / 无权限”,不暴露记录是否存在。 +- [ ] 接口可观察:所有秒杀请求日志包含买家 ID、活动 ID、商品 ID、请求数量、抢购结果、受影响行数、订单号(成功时)和 traceId;不记录 Token、密码或支付卡号。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" new file mode 100644 index 0000000..e9241b3 --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" @@ -0,0 +1,256 @@ +# M03 购物车流程 + +> 负责人:朱惠惠 +> 覆盖:M03-01、F07 +> 基础核心流程:F01、F02、M02 公开浏览、F08 下单、M09 消息 +> 直接协作:韦乾强(M04 Ordering)、顾欣月(M02 Catalog)、张海洋(秒杀边界 C01) +> 文档状态:初稿,待朱惠惠自审及 Catalog/Ordering 交叉评审 +> 需求事实源:[需求规格说明书 M03-01](../../../01-需求文档/需求规格说明书.md) 的“M03-01 购物车管理(F07)”完整七节 + +## 一、范围与事实来源 + +本模块负责买家在登录态下维护本人购物车,覆盖查看列表、加入商品、修改数量、删除条目、单选 / 全选、选择失效处理、服务端计价和下单前 / 下单事务内的购物车清理。购物车只承担“下单前的暂存区”,不承载营销、优惠、推荐、凑单,也不维护独立状态机;选中状态、价格、库存上限由服务端实时派生,客户端不得越权决定订单金额。 + +本文先依据需求确定业务参与者、上游输入、状态派生口径、原子结果和模块出入口,再由这些动作派生接口契约。A1xx(A101 加购、A102 查看、A103 改数量、A104 删除、A105 选中、A106 服务端计价、A107 清空、A108 幂等记录)只用于流程完成后的契约映射与缺口检查,不能反向拼接流程,也不能用现有 Axxx 反向覆盖业务语义。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| M03-01/F07 需求 | 完整定义 | 作为购物车业务语义事实源 | +| 本文业务流程 | 初稿 | 先确认参与者、上游输入、状态派生、原子结果和模块出入口 | +| A1xx 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| DB0xx(cart_items、cart_idem 等) | 模板/占位 | 本文不发明字段、约束和索引 | +| X02 收藏与浏览历史 | 独立扩展 | 仅登记边界,不混入 F07 主流程 | +| C01 秒杀 | 独立扩展 | 立即抢购绕过购物车,C01 仅与本文确定“不读写购物车”的边界 | + +## 二、模块直接出入口 + +```mermaid +flowchart LR + ID["M01 Identity
已认证买家、角色、账号状态"] -->|"BuyerOnly 通过"| CART["M03 Cart
条目、选中状态、服务端金额"] + CAT["M02 Catalog
销售状态、实时价格、实时可售库存"] -->|"加购 / 改数量 / 结算校验输入"| CART + BUYER["买家购物车页、加购入口、收银台"] -->|"维护 / 选择 / 去结算动作"| CART + CART -->|"本人选中条目、数量、选中状态、服务端金额"| ORD["M04 Ordering
服务端重读、计价、原子扣减"] + CART -->|"本人全部条目(含失效)"| BUYER + CART -->|"本人可结算与服务端金额"| CHECKOUT["结算预览 → M04 提交入口"] + + ORD -->|"提交事务成功:清理已下单条目"| CART + ORD -->|"事务回滚:购物车条目原状保留"| CART + CAT -->|"商品下架 / 库存归零 / 启用状态变更"| CART + + ID -->|"游客、商家、管理员或账号禁用"| X["拒绝访问,不创建、不读写购物车"] + CAT -->|"商品不存在或非已上架"| Y["加购 / 调大请求拒绝"] + CART -->|"资源不存在或非归属本人"| Z["按不存在 / 无权限处理,不泄露归属"] +``` + +边界约束: + +- M03 不直接接受前端传入的最终金额或处理商家,所有计价与归属以 M02 / M04 服务端重读为准。 +- M03 不预留库存;库存扣减由 M04 在下单事务内条件更新完成。 +- 下单事务成功后由 M04 在同一事务内删除已下单条目;事务回滚时购物车条目原状保留。 +- C01 秒杀绕过 M03,不读取、不写入购物车条目;普通购物车条目不受秒杀扣减 / 回补影响。 +- X02 收藏 / 浏览历史只引用商品 ID,不依赖购物车条目;不反向写入购物车。 + +## 三、加购与数量累加 + +```mermaid +flowchart TD + ID["M01:已认证且状态正常的买家"] --> A["买家在商品列表 / 详情提交加购:商品 ID + 数量 + 可选幂等键"] + CAT["M02 直接输入:商品销售状态、实时价格、实时可售库存"] --> B + A --> B{"商品已上架且数量合法并不超过实时可售库存?"} + B -- "否" --> X["拒绝加购并返回当前最大可购值与失效原因"] + B -- "是" --> C{"同一买家+商品已存在条目?"} + C -- "否" --> D["新增条目,写入当前数量、小计和最新修改时间"] + C -- "是" --> E{"新累加数量是否仍不超过实时可售库存?"} + E -- "否" --> X["拒绝累加,返回当前最大可设值"] + E -- "是" --> F["条目数量=旧数量+新数量,重算小计"] + D --> G{"携带幂等键?"} + F --> G + G -- "否" --> H["直接落库"] + G -- "是" --> G1{"该 (买家+幂等键) 已有处理结果?"} + G1 -- "同键同请求" --> H2["返回首次确定结果,不重复累加"] + G1 -- "同键不同请求" --> Y["拒绝标识被不同请求复用"] + G1 -- "否" --> H + H --> I["提交并返回最新条目、最大可购值、当前小计和生效时间"] +``` + +关键约束: + +- 主键为 `(buyer_id, product_id)`,同一组合在同一购物车中只允许一条;重复加购按数量累加,禁止多行并存。 +- 数量上下限 `1 ≤ 数量 ≤ 商品当前实时可售库存`,调小 / 删除不受上限约束,但不允许设为 0 或负数。 +- 加购、改数量、累加均按实时库存拒绝越界请求,并返回当前最大可设值;前端据此截断,不依赖前端控制。 +- 携带稳定幂等键时,同一买家、同一幂等键在窗口内重复提交视为同一请求,不重复累加。 +- 所有动作必须按 `(buyer_id, product_id)` 归属过滤;条目 ID 不允许跨用户访问。 + +## 四、查看 / 修改数量 / 删除 / 清空 + +### 4.1 查看购物车 + +```mermaid +flowchart TD + A["M01:买家进入购物车页"] --> B["按当前买家 ID 拉取本人全部条目"] + B --> C["服务端读取每个商品的实时销售状态与可售库存"] + C --> D{"商品仍可售且库存 > 0?"} + D -- "是" --> E["标记为可结算,返回实时单价、当前数量、小计、是否选中"] + D -- "否" --> F["标记失效并写明失效原因(下架 / 售罄 / 禁用)"] + E --> G["购物车页统一渲染:选中、未选中、失效三类状态"] + F --> G + G --> H["返回最大可设库存和失效原因给前端,按需本地分页"] +``` + +### 4.2 修改数量 + +```mermaid +flowchart TD + A["买家修改条目数量"] --> B{"条目归属当前买家?"} + B -- "否" --> X["按不存在 / 无权限处理,不泄露归属"] + B -- "是" --> C{"新数量为正整数?"} + C -- "否" --> Y["拒绝修改并提示;不允许设为 0 或负数"] + C -- "是" --> D{"可售库存 ≥ 新数量?"} + D -- "否" --> Y["拒绝调大;调小始终允许"] + D -- "是" --> E["落库并返回最新数量、小计与最大可设值"] +``` + +### 4.3 删除与清空 + +```mermaid +flowchart TD + A["买家提交删除 / 清空"] --> B{"按 (买家+条目ID) 或 (买家) 过滤?"} + B -- "否" --> X["拒绝访问,不暴露归属"] + B -- "是" --> C{"是否幂等删除?"} + C -- "否" --> Y["删除条目并返回最新列表"] + C -- "是" --> Z["重复请求按首次结果幂等返回"] + Z -. "条目不存在" .-> Z1["按不存在 / 无权限处理"] + Y --> W["含失效条目一并清除"] + W --> V["返回最新购物车或空状态"] +``` + +关键说明: + +- 删除 / 清空全部幂等执行,重复删除同一 ID 结果一致;条目不存在或归属错误返回统一响应。 +- 查看、修改、删除、清空都不返回他人条目;前端不缓存购物车金额或选中状态作为最终结果。 + +## 五、选中、失效与服务端计价 + +```mermaid +flowchart TD + A["买家切换单选 / 全选、反选"] --> B{"条目归属当前买家?"} + B -- "否" --> X["拒绝切换"] + B -- "是" --> C{"条目当前可结算?"} + C -- "否" --> Y["拒绝选中并返回失效原因"] + C -- "是" --> D["服务端写入选中状态,按需触发全选 / 反选批量更新"] + D --> E["购物车页即时刷新选中结果"] + + P["买家请求结算预览"] --> Q["服务端按当前买家+选中条目读取商品实时单价"] + Q --> R{"全部条目仍可结算?"} + R -- "否" --> S["整体拒绝整次结算,仅标记问题条目并保留全部购物车"] + R -- "是" --> T["服务端计算选中总额 = Σ 实时单价 × 当前数量"] + T --> U["返回选中条目与总额给前端,前端只用来展示;提交订单以服务端再次重读为准"] +``` + +关键约束: + +- 选中状态保存在服务端;刷新和重新登录后仍按服务端记录渲染;失效条目禁止被选中。 +- 选中总额一律由服务端按实时单价计算,前端展示金额仅供参考;客户端不得指定最终金额。 +- 失效原因必须来自 `下架 / 库存归零 / 禁用` 三类客观状态,不允许写入主观提示。 + +## 六、与订单模块的协作:结算下单与清理 + +```mermaid +flowchart TD + SEL["买家在购物车提交选中条目 + 地址 ID + 幂等键"] --> A["M03:从本人购物车读取选中条目"] + CAT["M02 直接输入:销售状态、实时价格、实时库存"] --> B + ID["M01 直接输入:身份与地址归属"] --> C + A --> B["服务端重新校验上下架、库存和归属"] + B --> C["校验地址归属当前买家且状态正常"] + C --> D{"校验全部通过?"} + D -- "否" --> X["整次下单拒绝,仅把问题条目标记失效并保留全部购物车"] + D -- "是" --> E["M04 开启下单事务"] + E --> F["条件扣减普通库存(M04 与 M02 内部完成)"] + F --> G["M04 创建订单与订单项快照、保存服务端总额、订单状态 PendingPayment"] + G --> H["M04 在同一事务内删除本次已结算的购物车条目"] + H --> I["待发布订单创建事实(OrderCreatedIntegrationEvent)已写入"] + I --> J{"订单事务整体提交?"} + J -- "否" --> R["整体回滚:库存、订单、待发布事实、购物车均恢复原状"] + J -- "是" --> K["M03 直接输出:已下单条目被清理;其余购物车条目继续保留"] + K --> L["M04 返回订单号、应付金额与 PendingPayment → 进入 M05 收银台"] +``` + +不变量: + +- 库存扣减、订单与快照写入、待发布订单创建事实、购物车清理属于同一下单事务,任一失败整体回滚。 +- 订单回滚时购物车条目原状保留,禁止“订单失败但条目丢失”。 +- 买家主动取消订单或 C03 超时取消后,本期不自动恢复购物车条目;用户希望重新购买需手动再次加车。 +- C01 秒杀绕过购物车:秒杀成功订单与取消后库存回补均不读写本文购物车条目。 + +## 七、异常、回滚与责任 + +| 场景 | M03 处理 | 最终状态 / 责任 | +|---|---|---| +| 游客、商家、管理员访问 | 拒绝 | 不创建、不返回购物车数据 | +| 账号禁用、令牌失效 | 拒绝 | 不读写购物车 | +| 商品下架或库存归零 | 拒绝加购 / 调大 / 累加;现有条目标记失效 | 保留可见、可删、可下调,前端展示明确原因 | +| 数量非法(0、负数、非整数) | 拒绝修改 | 维持旧数量 | +| 修改超过实时库存上限 | 拒绝调大;返回最大可设值 | 维持旧数量 | +| 越权:他人条目 ID | 按不存在 / 无权限处理 | 不泄露归属与存在性 | +| 携带幂等键重复提交 | 返回首次确定结果 | 不重复累加 | +| 携带幂等键但请求体不同 | 拒绝标识复用 | 不执行副作用 | +| 下单事务回滚 | 购物车条目原状保留 | 不允许“订单失败但条目丢失” | +| 订单主动取消 / C03 超时 | 本期不恢复购物车条目 | 避免与重新加入状态混淆 | +| 失效条目清理 | 仅随用户主动删除 / 清空 | 后台不主动清理,便于排查 | +| C01 秒杀并发抢购 | 不涉及购物车 | 秒杀走独立库存与限购通道 | +| 网络失败、500、401 | 给出明确错误码:401 引导登录、400 字段问题、5xx 提供重试入口 | 不依赖前端缓存重建购物车 | + +## 八、由流程派生的接口契约映射 + +本节是第三至第七章业务流程的下游映射,不是流程输入。先确认“业务动作、当前状态、成功或失败后得到什么结果”,再决定由哪个接口承载。若现有 Axxx 与流程冲突,先记录接口缺口并修改接口设计;只有业务需求本身变化时,才回到前文重新评审流程。 + +| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +|---|---|---|---| +| 加入购物车(可选幂等) | A101 | 校验数量 + 库存,同 `(买家+商品)` 累加,幂等不重复累加 | 待交叉评审 | +| 查看本人购物车 | A102 | 仅返回本人条目,含可结算 / 失效标记与失效原因 | 待交叉评审 | +| 修改本人条目数量 | A103 | 实时校验库存上限;调小始终允许;返回最新数量与最大可设值 | 待交叉评审 | +| 删除本人条目 / 清空 | A104 | 按 `(买家+条目ID)` 过滤;幂等;多次删除同一 ID 结果一致 | 待交叉评审 | +| 切换单选 / 全选 / 反选 | A105 | 服务端持久化选中;失效条目不允许被选中 | 待交叉评审 | +| 服务端计价(结算预览) | A106 | 实时重读单价并计算总额;前端不能指定金额 | 待交叉评审 | +| 一键清空购物车 | A107 | 幂等;只影响本人;失效条目一并清理 | 待交叉评审 | +| 加购幂等记录 | A108 | 按 `(买家+幂等键)` 持久化记录;同键同请求重放首次结果 | 待交叉评审 | + +接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段、错误码与幂等键传递方式不得反向写入业务图,接口设计 1.12 通用幂等规则统一承载。 + +## 九、扩展接入边界 + +- C01 秒杀:立即抢购绕过购物车,Seckill 与 Ordering 自行完成资格、限购、活动库存和订单校验;本文购物车不参与秒杀扣减 / 回补,也不被秒杀回补触发的库存变化影响。M04 必须按 `orderType` 与 `seckillActivityId` 区分库存回补通道,确保秒杀回补不误增普通库存。 +- X02 收藏 / 浏览历史:仅引用商品 ID 维度的公开数据,不读写购物车条目;用户在收藏页点击“加入购物车”时调用本文 A101,遵循同一所有权与库存上限校验。 +- M09 站内消息:仅消费 M04 下单事务提交后发布的 `OrderCreatedIntegrationEvent`;M03 不主动发布消息,也不依赖消息反馈修改条目。 +- M06-01 后台商品上下架:通过商品销售状态变更触发购物车失效标记,不直接修改他人购物车条目。 + +## 十、由流程反查出的接口与数据待评审项 + +1. 本文要求购物车主键为 `(buyer_id, product_id)`,单一组合唯一;若现有 A1xx 在多次加购时按 “条目 ID 自增” 创建多条记录,接口语义必须改为“按 `(买家, 商品)` 唯一累加”。 +2. 数量上下限必须在服务端实时校验并返回最大可设值;A103 必须区分“调大拒绝”和“调小允许”,不能统一返回字段错误导致前端无法截断。 +3. 加购幂等键的窗口期需要与库存 / 上限校验配合:同一幂等键只能重放首次成功结果,不同请求体携带同键视为标识复用并被拒绝。 +4. 下单成功后,订单模块在事务内清理购物车条目;若现有 A1xx 与 A8xx(下单)跨事务异步清理,必须先改为同事务清理,保证事务回滚不丢条目。 +5. 结算预览返回的服务端金额是“可选预览”,下单时必须再重读一次商品与库存;A8xx 不能复用 A106 的金额作为最终扣款事实。 +6. 失效条目清理:本流程要求“仅随用户主动删除 / 清空”,A104 必须不复用物理删除批量逻辑;后台清理需另起保留期规则,不在本文范围。 +7. 购物车数据归属全部按 `(买家 ID, 商品 ID)` 或 `(买家 ID, 条目 ID)` 双重过滤;A1xx 不能仅按条目 ID 给出可访问性。 +8. 价格变动:商品改价后购物车再次展示用实时单价;现有接口若缓存条目的小计或反推金额,需在列表时重算并返回最新单价。 +9. DB0xx 尚未形成可实施的完整表定义,购物车表、幂等记录表的字段、约束和索引必须由数据库设计任务另行确认。 + +## 十一、验收证据清单 + +- [ ] 加购、查看、改数量、删除、单选 / 全选、清空、结算预览 8 类接口全部覆盖;前端刷新与重新登录后数据不丢失。 +- [ ] 同一买家同一商品多次加入只生成一条记录,数量按调用顺序正确累加,最终数量不超过实时库存上限。 +- [ ] 修改数量超过商品实时可售库存时拒绝并返回最大可设值;改为 0、负数或非整数被拒绝并提示原因。 +- [ ] 选中条目总额由服务端按实时单价计算,前端篡改金额或数量再提交被服务端拒绝,订单总额与数据库一致。 +- [ ] 商品下架后,已加入条目在购物车页标记“不可结算”,不可调大、不可累加、不能勾选进入结算;库存为 0 或被禁用同样标记。 +- [ ] 失效条目可下调数量、可删除;下调到合法值后恢复可结算状态。 +- [ ] 越权:用买家 B 身份请求买家 A 的条目被拒绝,响应不暴露该条目是否存在及归属信息。 +- [ ] 下单成功后,对应购物车条目在同一事务内被清除;订单事务回滚时购物车条目原状保留,未出现“订单失败但条目丢失”。 +- [ ] 买家主动取消订单或 C03 超时取消后,对应购物车条目本期不自动恢复。 +- [ ] 并发:同一条目同时被改数量和删除,最终只出现删除结果或最新数量,两者不会同时生效导致数据错乱。 +- [ ] 幂等:相同请求幂等标识在约定窗口内重复提交不重复累加数量,返回结果一致。 +- [ ] C01 衔接:秒杀路径独立执行活动库存与个人限购校验,超卖拒绝且不影响普通购物车条目。 +- [ ] 库存与下单协作:购物车不预留库存,订单提交事务内完成扣减;C03 超时取消正确回补库存,购物车无需联动处理。 +- [ ] 性能与可用性:购物车页 30~100 条目在常规环境下加载时间低于 2 秒;大量条目启用分页,禁止无上限返回。 +- [ ] 日志:加购失败、并发回滚、下单清理与订单回滚均留下 traceId、买家 ID、商品 ID 与原因码;日志不包含完整 Token、密码或卡号。 +- [ ] 界面反馈:列表的加载、空数据、错误、删除成功、修改成功、失效原因和“去结算”被禁用给出明确提示;操作失败可一键重试,不要求整页刷新或重新登录。 -- Gitee From e9a095ea3136c08e58dfc52b180b8d661dda550a Mon Sep 17 00:00:00 2001 From: FISH SOUP Date: Fri, 24 Jul 2026 15:58:11 +0800 Subject: [PATCH 063/118] =?UTF-8?q?docs(process):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=94=90=E5=AE=87=E6=98=8A=20F01/F02/F03/F13/X02=20=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../process/README.md" | 2 +- ...50\345\206\214\346\265\201\347\250\213.md" | 181 ++++++++++++++++ ...00\345\207\272\346\265\201\347\250\213.md" | 196 +++++++++++++++++ ...60\345\235\200\346\265\201\347\250\213.md" | 198 ++++++++++++++++++ ...41\347\220\206\346\265\201\347\250\213.md" | 188 +++++++++++++++++ ...06\345\217\262\346\265\201\347\250\213.md" | 182 ++++++++++++++++ ...01\347\250\213\350\256\276\350\256\241.md" | 12 +- 7 files changed, 953 insertions(+), 6 deletions(-) create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/README.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/README.md" index 49ccf5b..e2d10e1 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/README.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/README.md" @@ -63,7 +63,7 @@ C08 退款对账 → X04 售后退款 → F09/F10 的订单项和支付事实 | 负责人 | 目录 | 核心模块文档 | 扩展/挑战文档 | 主要联调人 | |---|---|---|---|---| -| 唐宇昊 | `tyh/` | `M01-用户与鉴权流程.md`、`M06-03-后台用户管理流程.md` | `M08-收藏与浏览历史流程.md` | 顾欣月、罗皓晨 | +| 唐宇昊 | `tyh/` | [`M01-01-用户注册流程.md`](tyh/M01-01-用户注册流程.md)、[`M01-02-用户登录与退出流程.md`](tyh/M01-02-用户登录与退出流程.md)、[`M01-03-个人信息与收货地址流程.md`](tyh/M01-03-个人信息与收货地址流程.md)、`M06-03-后台用户管理流程.md` | [`M08-商品收藏与浏览历史流程.md`](tyh/M08-商品收藏与浏览历史流程.md) | 顾欣月、罗皓晨 | | 顾欣月 | `gxy/` | `M02-分类与商品流程.md`、`M06-01-后台商品管理流程.md` | `M07-商品评价流程.md`、`C04-中文搜索流程.md` | 朱惠惠、韦乾强、罗皓晨 | | 朱惠惠 | `zhh/` | `M03-购物车流程.md` | `C01-秒杀流程.md` | 顾欣月、韦乾强、张海洋 | | 韦乾强 | `wqq/` | `M04-订单流程.md`、`M06-02-商家履约流程.md` | `C03-订单超时流程.md` | 朱惠惠、张海洋、罗皓晨 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" new file mode 100644 index 0000000..2cc1c50 --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" @@ -0,0 +1,181 @@ +# M01-01 用户注册流程 + +> - 覆盖:M01-01、F01 +> - 主责人:唐宇昊 +> - 需求来源:[《需求规格说明书》M01-01](../../../01-需求文档/需求规格说明书.md) 的“M01-01 用户注册(F01)— 唐宇昊”完整七节 +> - 基础核心流程:F02(账号体系入口) +> - 直接入口:游客在公共注册入口提交手机号、密码和确认密码 +> - 直接出口:M01 创建“正常”买家账号,引导进入 F02 登录流程;账号事实后续供 M03、M04、M05、M06、M07、M08、M09 复用 +> - 回归核心结果:账号状态为“正常”,角色固定为买家,全站不出现管理员或商家账号 +> - 不得改变:F02 已签发登录态、未注册的购物车或浏览历史归属 + +## 一、范围与事实来源 + +本流程负责公开注册页面、服务端校验、用户名生成和默认资料创建。它不提供商家或管理员在线注册、不发送短信验证码、不支持自定义头像、不提供找回密码或账号合并。 + +A001 由本流程派生,仅在流程评审通过后用于契约映射;现有清单不能反向决定或拼接业务流程。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| M01-01/F01 需求 | 完整定义 | 作为业务语义事实源 | +| 本文业务流程 | 初稿 | 先确认角色、判断、状态、事务边界和模块出入口 | +| A001 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| DBxxx 用户表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | +| M00 公共认证能力 | 内部 P0 | 只登记接入点,规则由 M00 维护 | + +## 二、模块直接出入口 + +```mermaid +flowchart LR + VIS["游客
未认证、无任何身份"] -->|"公开注册入口"| REG["M01-01 Identity
注册事务"] + REG -->|"正常买家账号 + 默认资料"| F02["M01-02 登录流程"] + REG -->|"账号摘要(不含敏感字段)"| UI["注册成功页"] + REG -. "注册事件事实" .-> LATER["M09 待登记:注册成功通知(非本期)"] + VIS -. "尝试指定商家或管理员" .-> REJ["忽略或拒绝角色字段"] + VIS -. "重复手机号或非法输入" .-> FAIL["返回字段级错误,不创建账号"] +``` + +边界约束: + +- 注册只产出买家账号,不接受客户端指定角色。 +- 公开注册不与任何受保护业务共享事务边界;账号创建成功后由 F02 独立负责登录态。 +- 默认资料由系统生成,不允许用户上传头像或指定用户名。 + +## 三、注册主流程 + +```mermaid +flowchart TD + A["游客进入公开注册页"] --> B["填写手机号、密码和确认密码"] + B --> C{"字段格式、密码强度和两次输入一致?"} + C -- "否" --> X["保留非敏感输入并提示字段错误"] + C -- "是" --> D{"手机号是否已注册?"} + D -- "是" --> Y["拒绝重复注册,不泄露其他账号资料"] + D -- "否" --> E{"客户端是否尝试传入角色字段?"} + E -- "是" --> E1["忽略或拒绝该字段,始终创建买家"] + E -- "否" --> F["服务端按 F01 规则生成唯一用户名"] + F --> G{"用户名生成成功?"} + G -- "否" --> G1["提示系统繁忙,不创建账号"] + G -- "是" --> H["对密码执行可靠哈希"] + H --> I["开启注册事务"] + I --> J["写入正常买家账号、默认头像和唯一用户名"] + J --> K{"事务提交成功?"} + K -- "否" --> Z["整体回滚,不创建账号"] + K -- "是" --> L["返回不含敏感信息的账号摘要"] + L --> M["页面提示成功并展示用户名"] + M --> N["由用户明确进入 F02 登录流程"] +``` + +主流程要求: + +- 手机号作为账号标识,用户名仅用于展示;前端不得展示完整手机号或明文密码。 +- 密码哈希必须使用可靠算法;明文密码、确认密码和哈希前的中间结果不得写入日志、响应或数据库。 +- 用户名生成冲突必须在受控次数内安全重试;多次冲突时不创建账号,避免重复账号。 +- 注册成功不默认建立登录态,必须由用户进入 F02 重新提交凭据。 + +## 四、注册输入校验 + +```mermaid +flowchart TD + A["接收注册请求"] --> B["读取手机号、密码、确认密码"] + B --> C{"手机号匹配 ^1[3-9]\d{9}$ 且无前后空白?"} + C -- "否" --> X1["拒绝:手机号格式错误"] + C -- "是" --> D{"密码长度 8~16 且同时包含字母和数字?"} + D -- "否" --> X2["拒绝:密码强度不足"] + D -- "是" --> E{"密码不等于手机号、手机号倒序或生成后用户名?"} + E -- "是" --> X3["拒绝:密码过于简单"] + E -- "否" --> F{"确认密码与密码一致?"} + F -- "否" --> X4["拒绝:两次密码输入不一致"] + F -- "是" --> G["字段校验通过,进入手机号唯一性检查"] +``` + +校验要求: + +- 拒绝 `+86`、`0086`、空格、连字符或固话号码;只接受中国大陆 11 位手机号。 +- 不得强制特殊字符、大小写混合或验证码;本期不存储历史密码。 +- 校验失败时,前端保留已填写的手机号和提示,密码字段不回显。 +- 字段级错误不得泄露其他账号是否存在或格式细节。 + +## 五、用户名生成与默认资料 + +```mermaid +flowchart TD + A["字段校验通过且手机号唯一"] --> B["按 F01-FR05 生成用户名:u_ + 8 位不易混淆字符"] + B --> C{"全局唯一?"} + C -- "否,受控次数内重试" --> B + C -- "仍冲突" --> X["提示系统繁忙,请稍后重试"] + C -- "是" --> D["对密码执行可靠哈希"] + D --> E["创建账号:状态=正常、角色=买家、默认头像、唯一用户名"] + E --> F["登记创建时间并返回账号摘要"] +``` + +默认资料约束: + +- 用户名生成使用 `u_` 前缀加 8 位不易混淆字符;字符集由接口设计在评审前确认。 +- 默认头像由系统提供统一资源,不接受用户上传。 +- 创建成功后账号状态只能为“正常”,本期注册流程不提供“待激活”或“待验证”中间状态。 + +## 六、并发、幂等与异常 + +```mermaid +flowchart TD + A["两个请求同时注册同一手机号"] --> B["数据库唯一约束保证只有一个成功"] + B -- "胜出" --> C["创建正常买家账号"] + B -- "失败" --> Y["返回明确的重复注册提示"] + Y --> Y1["不泄露其他账号资料"] + A2["用户连续点击提交"] --> B2["前端防重按钮;服务端按字段级错误返回"] + B2 -- "无变化" --> N["不产生重复账号"] + A3["用户名生成多次冲突"] --> B3["在受控次数内重试"] + B3 -- "仍冲突" --> Z["提示系统繁忙,不创建账号"] + A4["客户端请求注入商家或管理员角色"] --> B4["忽略或拒绝角色字段"] + B4 --> C2["永远创建买家账号"] + A5["数据库或哈希计算失败"] --> B5["整体回滚"] + B5 --> R["提示稍后重试,不泄露内部错误"] +``` + +异常约束: + +- 重复注册不暴露已注册账号的用户名、创建时间或状态。 +- 用户名生成冲突必须显式可重试且不消耗未受控次数;最终不创建账号。 +- 注册事务任一步失败(手机号唯一性冲突、用户名冲突、哈希失败、数据库写入失败)必须整体回滚。 +- 网络重试或前端防抖失败时,重复提交按字段级错误返回,不创建第二张账号。 + +## 七、与核心模块的衔接 + +| 上游 | 入口事实 | 下游 | 出口结果 | +|---|---|---|---| +| 公共注册页 | 手机号、密码、确认密码 | M01-01 | 创建正常买家账号并返回账号摘要 | +| M01-01 | 正常买家账号摘要 | M01-02(F02) | 用户进入登录流程 | +| M01-01 | 注册成功事实 | M00 认证能力 | 用户名、密码哈希、默认资料登记到公共认证上下文 | +| M01-01 | 重复手机号、非法字段或角色注入 | M01-01 | 字段级错误或重复注册提示,不创建账号 | + +衔接约束: + +- 公开注册完成后必须立即进入 F02;不默认建立登录态,避免令牌与账号状态耦合。 +- 公开注册不写任何业务模块事实;M03、M04、M05、M06、M07、M08、M09 只能在用户后续登录后接入。 +- 注册事件事实目前不进入 M09;如未来纳入通知,需另行评审事件、接收人和文案。 + +## 八、由流程派生的接口契约映射 + +| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +|---|---|---|---| +| 公开注册创建买家账号 | A001 | 校验字段、手机号唯一性、密码强度、用户名生成和角色固定 | 待交叉评审 | + +接口仅承载“创建买家账号”这一业务结果;字段格式、错误码和 `traceId` 由接口设计在评审前统一,不得反向写入业务流程。 + +## 九、由流程反查出的接口与数据待评审项 + +1. 是否需要在 A001 响应中返回默认头像 URL;当前需求仅要求返回账号摘要。 +2. 用户名字符集需在接口设计中明确,避免与命名规范冲突。 +3. 注册成功是否同时建立短效令牌用于“注册即登录”;当前需求固定要求重新登录。 +4. 数据库是否需要为用户名预留历史记录字段;当前需求仅要求全局唯一。 +5. 注册事件是否进入 M09;当前需求未包含,本流程不预设接入。 + +## 十、验收证据清单 + +- [ ] 合法手机号和合规密码可以注册,返回自动生成用户名和默认头像摘要。 +- [ ] 7 位密码、纯字母、纯数字、等于手机号、等于手机号倒序、等于生成用户名的密码被拒绝。 +- [ ] 非法手机号、重复手机号和角色注入均被拦截。 +- [ ] 密码以可靠哈希存储,响应、日志和数据库中不出现明文密码。 +- [ ] 用户名和手机号全局唯一,并发注册不产生重复账号。 +- [ ] 重复点击、前端防抖失败和网络重试不创建第二张账号。 +- [ ] 保存正常与异常注册截图,并能说明凭据保护、唯一性和角色固定规则。 \ No newline at end of file diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" new file mode 100644 index 0000000..1dfc7de --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" @@ -0,0 +1,196 @@ +# M01-02 用户登录与退出流程 + +> - 覆盖:M01-02、F02 +> - 主责人:唐宇昊 +> - 需求来源:[《需求规格说明书》M01-02](../../../01-需求文档/需求规格说明书.md) 的“M01-02 用户登录与退出(F02)— 唐宇昊”完整七节 +> - 基础核心流程:F01、M00 公共认证;后续 F03、F04~F06、F07、F08、F09、F10、F11、F12、F13、X02 +> - 直接入口:游客或低登录态用户提交手机号和密码;前端刷新或退出动作触发登录态恢复与失效 +> - 直接出口:M01 返回有效登录凭证、服务端确认的角色与账号状态;下游模块据此决定是否继续受理 +> - 回归核心结果:F01 已注册账号、F03 个人资料、F13 账号治理结果保持不变 +> - 不得改变:账号、订单、支付事实与权限结果;本期不引入第三方登录或多端登录联动 + +## 一、范围与事实来源 + +本流程负责公开登录入口、统一收银台登录、商家端和管理端登录、登录态恢复、主动退出和令牌失效判定。它不提供用户名登录、第三方登录、找回密码、密码强度提示升级或多设备同步退出。 + +A002~A005 由本流程派生,仅在流程评审通过后用于契约映射;现有清单不能反向决定或拼接业务流程。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| M01-02/F02 需求 | 完整定义 | 作为业务语义事实源 | +| 本文业务流程 | 初稿 | 先确认角色、判断、状态、事务边界和模块出入口 | +| A002~A005 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| DBxxx 账号/令牌表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | +| C10 多实例认证 | 部分定义 | 只登记接入点;多实例令牌验证规则由 C10 评审 | + +## 二、模块直接出入口 + +```mermaid +flowchart LR + VIS["游客或登录过期用户"] -->|"登录入口提交"| LOG["M01-02 Identity
登录与退出"] + LOG -->|"有效 JWT + 角色 + 账号状态"| M03["M03 Cart"] + LOG -->|"有效 JWT + 角色 + 账号状态"| M04["M04 Ordering"] + LOG -->|"有效 JWT + 角色 + 账号状态"| M05["M05 Payment"] + LOG -->|"有效 JWT + 角色 + 账号状态"| M06["M06 后台"] + LOG -->|"有效 JWT + 角色 + 账号状态"| M08["M08 收藏与历史"] + LOG -->|"退出后令牌失效"| UI["前端清理登录态并返回登录页"] + LOG -. "登录成功事实" .-> LATER["M09 待登记:登录通知(非本期)"] + F13["M06-03 禁用/启用"] -->|"账号状态变更"| LOG + F01["M01-01 注册成功"] -->|"正常买家账号"| LOG + VIS -. "越权访问" .-> REJ["401/403,不泄露账号存在性"] + VIS -. "令牌失效或角色错误" .-> RLOG["受保护请求失败并引导重新登录"] +``` + +边界约束: + +- M01-02 只产出可被任一 API 实例验证的 JWT 和当前账号状态,不直接通知业务模块。 +- 令牌失效结果无法确认时,受保护请求必须失败关闭,不允许因依赖异常继续放行。 +- 退出只使当前令牌失效;多设备或全设备退出需另行评审并写入接口设计。 + +## 三、登录主流程 + +```mermaid +flowchart TD + A["用户进入统一登录入口"] --> B["提交手机号和密码"] + B --> C{"手机号格式正确?"} + C -- "否" --> X1["提示字段错误并保留输入"] + C -- "是" --> D["服务端按手机号定位账号"] + D --> E{"账号存在?"} + E -- "否" --> X2["统一提示账号或密码错误"] + E -- "是" --> F["比对密码哈希"] + F --> G{"密码正确?"} + G -- "否" --> X2 + G -- "是" --> H{"账号状态正常?"} + H -- "否" --> X3["拒绝登录并提示账号停用"] + H -- "是" --> I["签发有明确有效期的登录凭证"] + I --> J["返回账号摘要、服务端确认的角色和当前状态"] + J --> K["前端保存登录态,按角色进入对应端"] + K --> L{"按角色进入?"} + L -- "买家" --> M["进入购物端"] + L -- "商家" --> M1["进入商家端"] + L -- "管理员" --> M2["进入管理端"] +``` + +主流程要求: + +- 账号不存在和密码错误必须返回统一的“账号或密码错误”提示,禁止区分错误字段。 +- 账号禁用需明确告知用户联系管理员,但不暴露内部状态码或异常。 +- JWT 包含明确有效期;登录态有效期、刷新策略和签名配置由接口设计统一。 +- 多实例环境下,JWT 验签配置必须一致;任一实例可独立验证同一有效令牌。 + +## 四、登录态恢复与角色路由 + +```mermaid +flowchart TD + A["用户刷新页面或重新打开浏览器"] --> B["前端读取本地保存的令牌"] + B --> C{"令牌仍处于有效期?"} + C -- "否" --> X["清理登录态并引导重新登录"] + C -- "是" --> D["向后端发起身份校验"] + D --> E{"令牌有效且账号状态正常?"} + E -- "否" --> X + E -- "是" --> F["返回当前账号摘要与角色"] + F --> G{"路由是否匹配当前角色?"} + G -- "否" --> H["按服务端确认角色跳转对应端"] + G -- "是" --> I["继续展示当前页面"] + H --> I +``` + +恢复要求: + +- 刷新或重连后必须重新执行服务端身份校验,禁止仅凭前端缓存决定路由。 +- 角色路由以服务端确认结果为准;前端隐藏菜单不替代服务端授权。 +- 登录态恢复失败、令牌过期或账号被禁用时,立即清理失效登录态并提示用户重新登录。 + +## 五、退出与令牌失效 + +```mermaid +flowchart TD + A["用户在任一端点击退出"] --> B["前端清理本地登录态"] + B --> C["调用退出接口"] + C --> D["服务端登记当前令牌失效"] + D --> E["返回退出成功"] + E --> F["前端跳转登录页"] + A2["受保护接口仍使用旧令牌"] --> B2["校验时返回 401 或登录失效"] + B2 --> C2["前端清理登录态并引导重新登录"] + A3["M06-03 禁用账号"] --> B3["服务端撤销该账号全部令牌"] + B3 --> C3["受保护请求返回 401 或登录失效"] + A4["手机号修改成功"] --> B4["服务端撤销修改前签发的全部令牌"] + B4 --> C4["受保护请求返回 401 或登录失效"] +``` + +退出与失效约束: + +- 主动退出只影响当前令牌;本期不自动撤销同账号的其他设备令牌。 +- M06-03 禁用账号、手机号修改成功后必须撤销对应令牌;新令牌签发前用户必须重新登录。 +- 令牌失效结果需在多实例之间保持一致;失效状态无法确认时受保护请求失败关闭。 +- 退出后用户的所有个人页和受保护页必须退出到登录态,禁止出现“看似已退出但仍能访问”的状态。 + +## 六、并发、幂等与异常 + +```mermaid +flowchart TD + A1["密码错误连续尝试"] --> B1["统一返回账号或密码错误"] + B1 --> N1["不暴露账号存在性,不触发额外锁定(本期不实现登录限流)"] + A2["已禁用账号尝试登录"] --> B2["拒绝并提示账号停用"] + B2 --> N2["不签发新令牌"] + A3["令牌过期或被撤销"] --> B3["受保护请求 401 或登录失效"] + B3 --> N3["前端清理登录态并提示重新登录"] + A4["失效能力暂时不可用"] --> B4["无法确认登录态的受保护请求提示服务暂不可用"] + B4 --> N4["不得继续放行"] + A5["多实例验证不一致"] --> B5["视为令牌失效,受保护请求拒绝"] + B5 --> N5["由 C10 与 JWT 配置统一保证一致"] + A6["买家尝试访问商家或管理接口"] --> B6["返回 403,不泄露目标数据"] + A7["商家从购物端入口登录"] --> B7["登录成功后按角色跳转商家端"] + B7 --> N7["不误报密码错误"] +``` + +异常约束: + +- 401 表示未认证或登录已失效;403 表示身份有效但无权执行当前操作。 +- 失效能力不可用时不得返回虚假成功,也不得静默放行受保护请求。 +- 退出接口的幂等性由失效登记机制保证;重复退出返回一致结果。 + +## 七、与核心模块的衔接 + +| 上游 | 入口事实 | 下游 | 出口结果 | +|---|---|---|---| +| M01-01 注册成功 | 正常买家账号 | M01-02 | 用户进入登录流程 | +| 公开登录入口 | 手机号和密码 | M01-02 | JWT、角色与账号状态 | +| M01-02 | 有效令牌 + 角色 | M03、M04、M05、M06、M08 | 业务模块按各自资源规则受理 | +| M06-03 禁用/启用 | 账号状态变更 | M01-02 | 旧令牌失效或恢复正常登录 | +| M01-03 修改手机号 | 旧令牌集合 | M01-02 | 修改前全部令牌失效,需重新登录 | +| C10 多实例环境 | 共享 JWT 验签配置 | M01-02 | 任一实例可独立验证同一有效令牌 | + +衔接约束: + +- 下游模块只接收 JWT 解析后的身份和角色,不接受客户端自行声明的接收人。 +- 退出或令牌失效结果必须可被任何 API 实例在合理时间内观察到;具体延迟由 C10 和 Redis 失效能力共同确定。 +- 登录入口不区分 PC Web、Electron 和 Android;同一令牌在各客户端均有效,但路由和入口展示仍由前端按角色控制。 + +## 八、由流程派生的接口契约映射 + +| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +|---|---|---|---| +| 提交手机号和密码登录 | A002 | 校验凭据与账号状态、签发 JWT、返回角色与摘要 | 待交叉评审 | +| 刷新或重连时身份校验 | A003 | 校验令牌有效性并返回当前账号状态 | 待交叉评审 | +| 安全退出 | A004 | 登记当前令牌失效并清理登录态 | 待交叉评审 | +| 登录态恢复 | A005 | 按当前令牌恢复账号摘要和路由信息 | 待交叉评审 | + +接口必须承载“当前账号可登录”这一业务结果;HTTP 状态码、错误码和 `traceId` 由接口设计在评审前统一,不得反向写入业务流程。 + +## 九、由流程反查出的接口与数据待评审项 + +1. 多设备或全设备退出范围需另行确认;A004 当前仅承诺单令牌失效。 +2. 登录失败次数限制和锁定策略本期是否实现,需在接口设计中明确。 +3. 失效能力不可用时的降级策略需要在 C10 与 M00 协作下进一步评审。 +4. 角色路由在多端(PC Web、Electron、Android)上的跳转目标是否一致需另行确认。 +5. JWT 刷新令牌机制本期不实现,需明确提示用户到期重新登录。 + +## 十、验收证据清单 + +- [ ] 正确账号可登录,刷新后登录态保持,买家、商家和管理员落地路由正确。 +- [ ] 错误密码不泄露账号存在性,被禁用账号无法登录并获得清楚提示。 +- [ ] 退出后原令牌不能访问受保护接口;跨角色访问返回正确的 403。 +- [ ] 在两个 API 实例间切换请求时,同一有效令牌得到一致认证结果。 +- [ ] 失效能力不可用时,受保护请求提示服务暂不可用,不静默放行。 +- [ ] 保存登录、刷新、退出、禁用账号、登录凭证失效能力不可用和越权访问证据。 \ No newline at end of file diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" new file mode 100644 index 0000000..d15a891 --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" @@ -0,0 +1,198 @@ +# M01-03 个人信息与收货地址流程 + +> - 覆盖:M01-03、F03 +> - 主责人:唐宇昊 +> - 需求来源:[《需求规格说明书》M01-03](../../../01-需求文档/需求规格说明书.md) 的“M01-03 个人信息与收货地址(F03)— 唐宇昊”完整七节 +> - 基础核心流程:F02、F08 +> - 直接入口:M01-02 提供已登录买家身份;M04 在 F08 提交订单时按地址快照契约读取 +> - 直接出口:买家资料与地址的查看、修改和默认地址结果;M04 据此完成地址归属校验和快照 +> - 回归核心结果:F02 已签发登录态保持有效;F08 已下单订单的地址快照不变 +> - 不得改变:订单金额、商品快照、支付事实与他人资料归属 + +## 一、范围与事实来源 + +本流程负责买家个人中心的资料查看与维护、地址新增、查询、编辑、删除、默认地址切换和敏感修改的令牌处理。它不提供自定义头像上传、商家资料维护、管理员代修改或多地址簿切换。 + +A006~A014 由本流程派生,仅在流程评审通过后用于契约映射;现有清单不能反向决定或拼接业务流程。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| M01-03/F03 需求 | 完整定义 | 作为业务语义事实源 | +| 本文业务流程 | 初稿 | 先确认角色、判断、状态、事务边界和模块出入口 | +| A006~A014 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| DBxxx 用户资料/地址表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | +| M04 地址快照契约 | 部分定义 | 只登记接入点;快照字段由 Ordering 评审 | + +## 二、模块直接出入口 + +```mermaid +flowchart LR + B["已登录买家"] -->|"个人中心入口"| PROF["M01-03 Identity
资料与地址"] + PROF -->|"用户名、默认头像、掩码手机号"| UI["个人中心页"] + PROF -->|"地址列表 + 默认地址标记"| ADDR["地址管理页"] + PROF -->|"修改手机号撤销旧令牌"| TOK["M01-02 登录与退出"] + PROF -->|"地址归属校验和地址快照"| ORD["M04 Ordering
F08 提交订单"] + ORD -->|"地址快照随订单持久化"| SNAP["历史订单地址快照"] + GUEST["游客"] -. "访问个人中心" .-> REJ["引导登录并保留安全返回目标"] + MERCH["商家或管理员"] -. "访问买家资料" .-> FORB["403:拒绝访问买家私人资源"] +``` + +边界约束: + +- 资料与地址只能由当前买家修改;M04 只读取快照数据,不能修改地址。 +- 敏感修改(手机号)成功后必须撤销修改前签发的全部令牌,由 M01-02 处理登录态失效。 +- 地址列表与默认地址切换必须按当前买家 ID 隔离,禁止跨用户访问。 + +## 三、资料维护主流程 + +```mermaid +flowchart TD + A["买家进入个人中心"] --> B{"身份合法且账号状态正常?"} + B -- "否" --> X["拒绝访问并引导登录"] + B -- "是" --> C["返回用户名、默认头像和掩码手机号"] + C --> D{"选择资料操作?"} + D -- "重置用户名" --> E{"本项目期内是否仍有一次机会?"} + E -- "否" --> F["拒绝修改并说明次数已用完"] + E -- "是" --> G["服务端按 F01-FR05 重新生成全局唯一用户名"] + G --> H["保存并返回最新资料"] + D -- "修改手机号" --> I["要求重新验证当前密码"] + I --> I1{"当前密码正确?"} + I1 -- "否" --> K["保持原资料并提示原因"] + I1 -- "是" --> J{"新手机号格式正确且全局唯一?"} + J -- "否" --> K + J -- "是" --> L["保存新手机号并撤销修改前全部令牌"] + L --> M["M01-02 清理登录态并要求重新登录"] +``` + +资料维护约束: + +- 用户名只能由服务端重新生成,不接受客户端指定任意字符串。 +- 用户名自助修改次数仅一次,成功后页面立即刷新为新用户名。 +- 手机号修改必须验证当前密码;成功后修改前签发的全部令牌失效。 +- 资料修改不得在响应或日志中泄露完整手机号、密码哈希或内部异常。 + +## 四、地址维护主流程 + +```mermaid +flowchart TD + A["已登录买家进入地址管理"] --> B["按当前买家查询本人地址列表"] + B --> C{"选择操作?"} + C -- "新增" --> D["录入收件人、联系电话、省市区、详细地址"] + D --> E{"字段合法?"} + E -- "否" --> X1["保留输入并提示字段错误"] + E -- "是" --> F["保存地址并刷新列表"] + C -- "编辑" --> G{"地址属于当前买家?"} + G -- "否" --> Y["返回不存在或无权限,不泄露归属"] + G -- "是" --> D + C -- "设默认" --> H{"地址属于当前买家?"} + H -- "否" --> Y + H -- "是" --> I["原子切换:唯一默认地址"] + C -- "删除普通地址" --> J{"地址属于当前买家?"} + J -- "否" --> Y + J -- "是" --> K["删除地址并刷新列表"] + C -- "删除默认地址" --> L{"地址属于当前买家?"} + L -- "否" --> Y + L -- "是" --> M["删除地址,保持无默认地址"] + M --> N["提示买家后续下单前重新选择"] +``` + +地址约束: + +- 每名买家最多一个默认地址,切换操作需原子完成或提供等效一致性保障。 +- 删除默认地址后不自动选择其他地址,下单时由买家明确选择。 +- 同一买家对地址的读写始终按当前用户过滤,禁止仅凭资源 ID 跨用户访问。 +- 地址字段变更不影响历史订单的地址快照,下单时间点确定的快照始终保留。 + +## 五、地址归属校验与下单衔接 + +```mermaid +flowchart LR + A["买家在 F08 提交订单时选择地址 ID"] --> B["M04 向 M01-03 校验地址归属"] + B --> C{"地址属于当前买家且存在?"} + C -- "否" --> X["拒绝整次下单并提示原因"] + C -- "是" --> D["M01-03 返回地址快照数据"] + D --> E["M04 在同一事务内保存地址快照"] + E --> F["地址变更不影响历史订单"] + G["买家在地址管理中删除或修改地址"] --> H["M01-03 仅影响当前买家"] + H --> I["历史订单快照保持不变"] +``` + +衔接约束: + +- M04 必须在提交订单时再次校验地址归属,不能依赖前端传入或本地缓存。 +- 地址快照需保存下单时刻的完整收件人、联系电话、省市区和详细地址。 +- M01-03 不接收订单、支付或售后模块直接写入;资料和地址是买家私人数据。 + +## 六、并发、幂等与异常 + +```mermaid +flowchart TD + A1["并发设置多个默认地址"] --> B1["原子切换或等效机制保证最终唯一"] + B1 --> N1["不出现两个默认地址"] + A2["两个请求同时修改手机号"] --> B2["先成功者写入;后者按唯一性失败"] + B2 --> N2["保留当前有效登录信息"] + A3["敏感修改时密码错误"] --> B3["拒绝修改,不泄露账号存在性"] + B3 --> N3["登录态保持有效"] + A4["地址归属错误或他人地址"] --> B4["返回不存在或无权限"] + B4 --> N4["不泄露地址是否真实存在"] + A5["用户名重置次数已用完"] --> B5["拒绝并说明规则"] + B5 --> N5["不再次扣减次数"] + A6["删除默认地址后未选新地址直接下单"] --> B6["M04 拒绝并提示先选择地址"] + B6 --> N6["不自动选择其他地址"] + A7["资料字段格式非法"] --> B7["字段级错误,保留可恢复输入"] +``` + +异常约束: + +- 任何敏感修改的失败都必须保持现有账号状态、登录态和地址不变。 +- 资料和地址接口不允许通过仅凭资源 ID 跨用户访问,越权请求统一返回“资源不存在或无权限”。 +- 并发修改场景下,最终结果必须保持一致;不出现“两个默认地址”或“两个不同手机号同时生效”的状态。 + +## 七、与核心模块的衔接 + +| 上游 | 入口事实 | 下游 | 出口结果 | +|---|---|---|---| +| M01-02 | 已登录买家身份 | M01-03 | 资料与地址的读写权限 | +| M01-03 | 当前买家地址 ID | M04 Ordering | 地址归属校验和快照数据 | +| M04 | 已下单订单 | 历史订单 | 地址快照随订单持久化,不被后续修改覆盖 | +| M01-03 | 敏感修改成功 | M01-02 | 撤销修改前全部令牌,要求重新登录 | +| F13 M06-03 | 账号状态变更 | M01-03 | 禁用账号后不能再修改资料或地址 | + +衔接约束: + +- 资料与地址结果对 M04 来说只读快照;M04 不得反向修改 M01-03 的数据。 +- M01-03 不为商家或管理员提供读写入口;后台管理端不得通过本流程访问买家私人数据。 +- 任何地址快照写入必须发生于下单事务;下单失败时不能留下新的地址快照事实。 + +## 八、由流程派生的接口契约映射 + +| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +|---|---|---|---| +| 查看本人资料 | A006 | 返回用户名、默认头像和掩码手机号 | 待交叉评审 | +| 重置用户名 | A007 | 在限次规则内由服务端生成唯一用户名 | 待交叉评审 | +| 修改手机号 | A008 | 验证当前密码、校验格式与唯一性、撤销旧令牌 | 待交叉评审 | +| 查询本人地址 | A009 | 按当前买家分页返回地址列表与默认标记 | 待交叉评审 | +| 新增地址 | A010 | 校验字段、保存地址、刷新列表 | 待交叉评审 | +| 编辑地址 | A011 | 仅修改本人地址、重新校验字段 | 待交叉评审 | +| 删除地址 | A012 | 仅删除本人地址,删除默认地址时不自动指定其他地址 | 待交叉评审 | +| 切换默认地址 | A013 | 原子切换默认地址,保证最终唯一 | 待交叉评审 | +| F08 下单时地址归属校验与快照 | A014 | 仅返回当前买家地址快照,不修改地址 | 待交叉评审 | + +接口仅承载本流程结果;错误码、HTTP 状态码和 `traceId` 由接口设计统一,不得反向写入业务流程。 + +## 九、由流程反查出的接口与数据待评审项 + +1. 用户名自助重置次数的存储位置与查询语义需要数据库和接口设计统一。 +2. 地址省市区数据是否采用受控字典或开放输入,需另行评审。 +3. 修改手机号时旧令牌失效的并发场景(同一秒既有登录又有修改请求)需要在多实例下保持一致。 +4. 默认地址切换的“等效一致性”具体实现方式(事务、唯一约束、补偿)需在数据库设计中明确。 +5. 商家或管理员尝试调用 A006~A014 的具体错误码和返回体需在接口设计中定义。 + +## 十、验收证据清单 + +- [ ] 买家可以查看和修改允许变更的个人资料,并完成地址增删改查。 +- [ ] 用户名唯一和自助修改次数限制生效;手机号变更后旧登录态不能继续执行敏感操作。 +- [ ] 默认地址始终最多一个,删除默认地址后下单流程不会擅自选择其他地址。 +- [ ] 买家不能访问他人地址,游客、商家和管理员不能越权调用买家资料接口。 +- [ ] F08 提交订单时地址归属校验和快照写入正确;历史订单地址快照不被后续修改覆盖。 +- [ ] 保存资料修改、地址 CRUD、默认地址切换和越权拦截证据。 \ No newline at end of file diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" new file mode 100644 index 0000000..6c271c7 --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -0,0 +1,188 @@ +# M06-03 后台用户管理流程 + +> - 覆盖:M06-03、F13 +> - 主责人:唐宇昊 +> - 需求来源:[《需求规格说明书》M06-03](../../../01-需求文档/需求规格说明书.md) 的“M06-03 后台用户管理(F13)— 唐宇昊”完整七节 +> - 基础核心流程:F02、M06-01 与 M06-02 的后台角色入口 +> - 直接入口:管理员通过 Policy 进入管理端;M01-02 提供登录态校验与角色判断 +> - 直接出口:M01 账号状态变更和旧令牌失效;商家发货(M06-02)和商品维护(M06-01)继续遵守新状态 +> - 回归核心结果:买家订单、支付和售后事实不变;商品事实不变 +> - 不得改变:管理员账号不可操作,角色字段不可修改,禁用默认商家和仍有待处理业务的商家必须被拒绝 + +## 一、范围与事实来源 + +本流程负责管理员分页查看买家和商家账号、按受控条件筛选、对可禁用账号执行禁用或启用,并协调旧令牌失效和默认商家保护。它不提供管理员账号管理、角色修改、提权或普通用户资料编辑。 + +A015~A017 由本流程派生,仅在流程评审通过后用于契约映射;现有清单不能反向决定或拼接业务流程。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| M06-03/F13 需求 | 完整定义 | 作为业务语义事实源 | +| 本文业务流程 | 初稿 | 先确认角色、判断、状态、事务边界和模块出入口 | +| A015~A017 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| DBxxx 账号/操作记录表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | +| C10 多实例令牌失效 | 部分定义 | 只登记接入点;具体失效范围和延迟由 C10 评审 | + +## 二、模块直接出入口 + +```mermaid +flowchart LR + ADM["管理员
已认证、状态正常"] -->|"管理端用户管理"| MGMT["M06-03 Admin
账号治理"] + MGMT -->|"最新账号状态 + 旧令牌失效"| M01["M01 Identity
登录态、令牌"] + MGMT -->|"商家禁用约束"| M06P["M06-01 商家商品入口"] + MGMT -->|"商家禁用约束"| M06O["M06-02 商家履约入口"] + MGMT -->|"查询过滤"| DB["账号与角色事实"] + BUYER["买家"] -. "尝试进入" .-> FORB["403:拒绝进入后台"] + MERCH["商家"] -. "尝试进入" .-> FORB + GUEST["游客"] -. "尝试进入" .-> NOLOG["要求登录,不返回账号列表"] + MGMT -. "管理员账号不可操作" .-> NOADM["拒绝操作,不泄露管理员列表"] +``` + +边界约束: + +- M06-03 只对买家和商家账号执行禁用或启用,不允许修改角色或操作管理员账号。 +- 禁用账号必须立即撤销旧令牌;启用后旧令牌不恢复,用户必须重新登录。 +- 商家禁用约束由 M06-03 强制执行,具体可执行操作由 M06-01 和 M06-02 依据账号状态决定。 + +## 三、列表与筛选主流程 + +```mermaid +flowchart TD + A["管理员进入用户管理页"] --> B{"身份合法且账号状态正常?"} + B -- "否" --> X["拒绝访问并要求登录"] + B -- "是" --> C["按分页和筛选条件查询账号"] + C --> D{"筛选条件?"} + D -- "无" --> E["返回当前页买家和商家账号"] + D -- "用户名/手机号" --> F["按已确认字段精确或前缀匹配"] + D -- "角色" --> G["按买家或商家过滤"] + D -- "状态" --> H["按正常或禁用过滤"] + E --> I["返回账号摘要:用户名、掩码手机号、角色、状态、注册时间"] + F --> I + G --> I + H --> I + I --> J["手机号默认掩码,不返回密码哈希或私人业务数据"] +``` + +列表要求: + +- 分页参数必须校验:`page` 从 1 开始,`pageSize` 默认 10、上限 50。 +- 列表不返回密码哈希、完整手机号或非必要私人数据。 +- 筛选条件必须使用受控字段,禁止拼接任意字段作为查询条件。 +- 管理员账号不出现在列表中;不允许通过筛选绕过隐藏。 + +## 四、禁用账号 + +```mermaid +flowchart TD + A["管理员选择目标账号并确认禁用"] --> B{"目标账号合法?"} + B -- "否" --> X["拒绝操作并提示原因"] + B -- "是" --> C{"是否默认商家?"} + C -- "是" --> X1["拒绝禁用并提示默认商家不可禁用"] + C -- "否" --> D{"目标账号是否仍有待处理业务?"} + D -- "是" --> X2["拒绝禁用并提示仍有待处理业务"] + D -- "否" --> E{"目标账号当前状态?"} + E -- "已禁用" --> Y["幂等返回当前状态,不重复产生副作用"] + E -- "正常" --> F["开启账号治理事务"] + F --> G["更新账号状态为禁用并记录操作人和时间"] + G --> H["撤销该账号在多实例中已签发的全部令牌"] + H --> I{"事务提交成功?"} + I -- "否" --> R["整体回滚,不返回虚假成功"] + I -- "是" --> J["返回最新账号状态"] + J --> K["被禁账号再次登录或使用旧令牌被拒绝"] +``` + +禁用约束: + +- 默认商家和仍有待处理业务的商家必须拒绝禁用,账号及业务归属保持不变。 +- 禁用成功后必须撤销该账号已签发的全部令牌,且失效结果需在多实例之间一致。 +- 失效能力不可用时,禁用操作必须返回失败,不得返回虚假成功。 + +## 五、启用账号 + +```mermaid +flowchart TD + A["管理员选择目标账号并确认启用"] --> B{"目标账号合法?"} + B -- "否" --> X["拒绝操作并提示原因"] + B -- "是" --> C{"目标账号当前状态?"} + C -- "正常" --> Y["幂等返回当前状态,不重复产生副作用"] + C -- "已禁用" --> D["开启账号治理事务"] + D --> E["更新账号状态为正常并记录操作人和时间"] + E --> F["不恢复禁用前的旧令牌,用户必须重新登录"] + F --> G{"事务提交成功?"} + G -- "否" --> R["整体回滚,保持禁用状态"] + G -- "是" --> H["返回最新账号状态"] + H --> I["用户重新登录获取新令牌"] +``` + +启用约束: + +- 启用只改变账号状态,不恢复任何旧令牌;用户必须重新登录。 +- 重复启用必须幂等,不产生相互矛盾的状态或重复副作用。 + +## 六、并发、幂等与异常 + +```mermaid +flowchart TD + A1["两名管理员并发禁用同一账号"] --> B1["使用状态条件保证仅一次有效更新"] + B1 --> N1["其余请求返回当前最新状态"] + A2["同一账号在禁用与启用之间切换"] --> B2["按状态条件更新,最终状态唯一确定"] + B2 --> N2["不出现两种状态同时生效"] + A3["尝试修改角色或管理员账号"] --> B3["接口不接受相关字段或明确拒绝"] + B3 --> N3["不返回修改后的角色"] + A4["失效能力暂时不可用"] --> B4["无法确认登录态的受保护请求提示服务暂不可用"] + B4 --> N4["禁用操作不返回虚假成功"] + A5["商家仍有未完成售后或关联订单"] --> B5["拒绝禁用并说明原因"] + B5 --> N5["不自动改派业务归属"] + A6["非管理员访问管理端接口"] --> B6["返回 401 或 403,不返回账号列表"] + A7["筛选或分页参数非法"] --> B7["字段级错误,保留可恢复输入"] +``` + +异常约束: + +- 任何并发变更的最终结果必须保持一致;不允许出现“禁用后又启用”或反之的中间结果被外部观察。 +- 禁用或启用操作必须记录操作人、目标账号、原状态、新状态、时间和 `traceId`,不写入密码或完整 Token。 +- 重复禁用或启用必须幂等返回当前状态,不重复产生副作用。 + +## 七、与核心模块的衔接 + +| 上游 | 入口事实 | 下游 | 出口结果 | +|---|---|---|---| +| M01-02 | 管理员登录态 | M06-03 | 后台访问权限 | +| M06-03 | 禁用/启用命令 | M01 Identity | 账号状态与令牌失效结果 | +| M06-03 | 商家禁用约束 | M06-01 | 商品维护可继续遵守账号状态 | +| M06-03 | 商家禁用约束 | M06-02 | 商家发货和售后审核遵守账号状态 | +| M06-03 | 操作记录 | 管理员查询 | 操作人、时间、`traceId` 可追踪 | + +衔接约束: + +- 商家禁用约束由 M06-03 在治理事务中校验;M06-01 和 M06-02 仍需在自身业务动作前再次校验账号状态。 +- 禁用或启用结果必须在多实例之间一致;具体失效延迟由 C10 和 M00 共同保证。 +- 管理员账号治理不直接修改订单、支付或售后事实;只通过账号状态影响后续业务受理。 + +## 八、由流程派生的接口契约映射 + +| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +|---|---|---|---| +| 分页查询账号 | A015 | 返回买家和商家账号摘要与筛选结果 | 待交叉评审 | +| 禁用账号 | A016 | 校验可禁用条件并撤销旧令牌 | 待交叉评审 | +| 启用账号 | A017 | 校验账号并恢复状态,旧令牌不恢复 | 待交叉评审 | + +接口仅承载本流程结果;错误码、HTTP 状态码和 `traceId` 由接口设计统一,不得反向写入业务流程。 + +## 九、由流程反查出的接口与数据待评审项 + +1. 默认商家和“仍有待处理业务”的判断口径需要在数据库设计中确认:是否包含进行中售后、待发货订单、未结束秒杀活动等。 +2. 多实例令牌失效的可见延迟需在 C10 与 M00 评审中明确,本流程不预设具体延迟。 +3. 操作记录是否长期保留或定期归档需在数据库设计中明确,本流程只承诺最小记录字段。 +4. 禁用或启用操作是否需要支持批量,本期不实现。 +5. 商家禁用的“业务归属转移”本期不实现,需明确告知管理员原因。 + +## 十、验收证据清单 + +- [ ] 管理员可以分页筛选买家和商家账号,手机号默认掩码。 +- [ ] 失效能力正常时,禁用后不能再次登录,禁用前的登录凭证也不能继续访问;启用后旧凭证仍不可用,用户可重新登录。 +- [ ] 失效能力不可用时,无法确认登录态的受保护请求不得放行;恢复后多个服务实例必须得到一致结果。 +- [ ] 买家、商家和游客不能访问管理接口,管理员账号和角色字段不能通过本模块操作。 +- [ ] 重复与并发状态变更保持幂等,安全追踪记录不包含敏感凭据。 +- [ ] 默认商家和存在待处理业务的商家禁用保护可复现,普通可禁用商家仍可完成禁用/启用验收。 +- [ ] 保存列表、禁用、启用、旧令牌失效、越权和角色注入拦截证据。 \ No newline at end of file diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" new file mode 100644 index 0000000..461da0b --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" @@ -0,0 +1,182 @@ +# M08 商品收藏与浏览历史流程 + +> - 覆盖:M08、X02 +> - 主责人:唐宇昊 +> - 需求来源:[《需求规格说明书》M08](../../../01-需求文档/需求规格说明书.md) 的“M08 商品收藏与浏览历史(X02)— 唐宇昊”完整七节 +> - 基础核心流程:F02、F06;与 F04/F05 列表衔接 +> - 直接入口:M01-02 提供已登录买家身份;M02 在 F06 详情页提供商品事实 +> - 直接出口:买家个人收藏列表与浏览历史列表;下架商品保留记录并标记不可购买 +> - 回归核心结果:商品销售状态、价格和库存不变;游客不建立个人记录 +> - 不得改变:商品上下架、价格、库存与订单快照 + +## 一、范围与事实来源 + +本流程负责买家收藏商品、取消收藏、查看收藏列表、查看商品详情时记录或更新浏览时间、按上限保留历史记录并允许关闭或开启后续记录。它不提供收藏分组、分享、跨账号迁移或推荐。 + +A018~A025 由本流程派生,仅在流程评审通过后用于契约映射;现有清单不能反向决定或拼接业务流程。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| M08/X02 需求 | 完整定义 | 作为业务语义事实源 | +| 本文业务流程 | 初稿 | 先确认角色、判断、状态、事务边界和模块出入口 | +| A018~A025 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| DBxxx 收藏/浏览表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | +| M02 商品事实 | 部分定义 | 只登记接入点;价格、库存与销售状态由 Catalog 评审 | + +## 二、模块直接出入口 + +```mermaid +flowchart LR + B["已登录买家"] -->|"收藏/取消/列表入口"| COLL["M08 Identity
收藏与浏览历史"] + B -->|"查看商品详情"| COLL + COLL -->|"收藏列表 + 当前商品摘要"| UI["收藏与历史页"] + COLL -->|"更新浏览时间"| HIST["浏览历史"] + CAT["M02 Catalog
商品事实、销售状态"] -->|"实时读取"| COLL + F06["F06 商品详情"] -->|"浏览行为触发"| COLL + GUEST["游客"] -. "触发收藏或历史" .-> REJ["引导登录并保留安全返回目标"] + GUEST -. "未登录不建立记录" .-> NONE["不创建匿名历史"] + COLL -. "不修改商品事实" .-> CAT +``` + +边界约束: + +- M08 只读取商品事实,不修改商品销售状态、价格或库存。 +- 收藏与浏览历史均按当前买家 ID 隔离,禁止跨用户访问。 +- 游客不创建匿名记录;商家和管理员不能查看或维护买家私人数据。 + +## 三、收藏与取消主流程 + +```mermaid +flowchart TD + A["买家在商品详情或列表点击收藏"] --> B["服务端按买家和商品完成幂等写入"] + B --> C{"记录已存在?"} + C -- "否" --> D["新增收藏并记录收藏时间"] + C -- "是" --> E["保留原收藏时间,返回幂等成功"] + D --> F["返回最新收藏状态"] + E --> F + F --> G["收藏列表按最近收藏时间倒序展示"] + H["买家点击取消收藏"] --> I{"记录属于当前买家?"} + I -- "否" --> X["返回不存在或无权限,不泄露归属"] + I -- "是" --> J["删除收藏记录并刷新列表"] + J --> K{"取消成功?"} + K -- "是" --> L["页面立即更新为空"] + K -- "否" --> M["恢复原状态并允许重试"] +``` + +收藏约束: + +- 同一买家和同一商品最多一条收藏记录,重复点击按幂等处理。 +- 取消收藏同样按当前买家 ID 过滤,重复取消返回幂等成功。 +- 商品下架不删除收藏记录,但购买入口必须失效。 + +## 四、浏览记录与历史列表 + +```mermaid +flowchart TD + A["买家进入商品详情"] --> B{"历史记录开关是否开启?"} + B -- "否" --> X["不写入浏览记录"] + B -- "是" --> C{"该买家对当前商品已有浏览记录?"} + C -- "否" --> D["新增浏览记录,记录首次浏览时间"] + C -- "是" --> E["仅更新最近浏览时间,不新增记录"] + D --> F{"超过记录上限?"} + E --> F + F -- "是" --> G["移除最早一条浏览记录"] + F -- "否" --> H["保留全部记录"] + G --> I["按最近浏览时间倒序展示"] + H --> I + J["买家进入浏览历史页"] --> K["按当前买家分页返回"] + K --> L["下架商品继续展示并标记不可购买"] +``` + +浏览约束: + +- 浏览记录上限由接口设计在评审前确认,默认值在流程中只承诺“按已确认上限保留最近记录”。 +- 历史开关关闭后不删除已有记录;重新开启后继续按规则更新最近浏览时间。 +- 浏览历史不得影响商品事实;商品下架、库存变化或价格调整均由 M02 决定。 + +## 五、收藏与浏览列表的展示 + +```mermaid +flowchart TD + A["买家进入收藏列表"] --> B["按当前买家和最近收藏时间倒序分页"] + B --> C["读取商品摘要:名称、主图、当前价格、销售状态"] + C --> D{"商品是否仍可售?"} + D -- "是" --> E["展示可售状态和进入详情/加购入口"] + D -- "否" --> F["保留记录并标记不可购买"] + F --> G["提供返回列表或查看历史的入口"] + H["买家进入浏览历史列表"] --> I["按最近浏览时间倒序分页"] + I --> J["读取商品摘要与销售状态"] + J --> K["下架商品标记不可购买,重复浏览不新增重复记录"] +``` + +展示约束: + +- 列表按各自最近活动时间倒序,排序结果稳定。 +- 下架商品保留记录并明确标记不可购买,但不删除个人记录。 +- 收藏与浏览列表不返回他人或他人的历史条目,禁止仅凭记录 ID 跨用户访问。 + +## 六、并发、幂等与异常 + +```mermaid +flowchart TD + A1["重复收藏同一商品"] --> B1["保持单条记录,重复请求幂等成功"] + A2["重复浏览同一商品"] --> B2["仅更新时间,不新增重复记录"] + A3["重复取消收藏"] --> B3["幂等成功,不报系统异常"] + A4["商品不存在"] --> B4["拒绝新增记录,已有记录按不可用状态处理"] + A5["越权访问他人记录"] --> B5["返回资源不存在或无权限,不泄露归属"] + A6["历史记录超过上限"] --> B6["移除最早记录,保证数据完整"] + A7["网络或保存失败"] --> B7["恢复原状态,允许重试,不制造假成功"] +``` + +异常约束: + +- 重复操作必须保持幂等,不产生重复记录或重复副作用。 +- 商品下架不影响收藏和浏览记录的存在,但展示中必须标记不可购买。 +- 任何写操作的失败都必须可重试,不留下半成功的状态。 + +## 七、与核心模块的衔接 + +| 上游 | 入口事实 | 下游 | 出口结果 | +|---|---|---|---| +| M01-02 | 已登录买家身份 | M08 | 收藏与浏览的读写权限 | +| F06 商品详情 | 商品 ID 与当前摘要 | M08 | 更新或新增浏览记录 | +| M02 Catalog | 商品事实、销售状态、实时价格 | M08 | 收藏与历史列表的展示 | +| M08 | 收藏或浏览记录存在 | 列表页 | 下架商品标记不可购买 | + +衔接约束: + +- M08 不修改商品事实;所有展示字段以 M02 最新事实为准。 +- 收藏和浏览记录不参与订单、支付或售后业务;不写入任何业务模块的事实表。 +- 跨设备访问同一买家记录时,结果必须一致;记录以 PostgreSQL 为唯一事实来源。 + +## 八、由流程派生的接口契约映射 + +| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +|---|---|---|---| +| 收藏商品 | A018 | 按买家和商品幂等写入收藏记录 | 待交叉评审 | +| 取消收藏 | A019 | 仅删除本人收藏记录,重复取消幂等 | 待交叉评审 | +| 查询收藏列表 | A020 | 按当前买家分页返回收藏与商品摘要 | 待交叉评审 | +| 记录或更新浏览时间 | A021 | 在历史开启时写入或更新最近浏览时间 | 待交叉评审 | +| 查询浏览历史列表 | A022 | 按当前买家分页返回浏览历史与商品摘要 | 待交叉评审 | +| 历史开关设置 | A023 | 控制是否继续写入浏览记录,不删除已有数据 | 待交叉评审 | +| 浏览记录上限配置读取 | A024 | 返回当前生效的浏览记录上限 | 待交叉评审 | +| 收藏状态读取 | A025 | 在商品详情或列表上返回当前买家的收藏状态 | 待交叉评审 | + +接口仅承载本流程结果;错误码、HTTP 状态码和 `traceId` 由接口设计统一,不得反向写入业务流程。 + +## 九、由流程反查出的接口与数据待评审项 + +1. 浏览记录上限默认值需在接口设计中明确,本流程只承诺“按已确认上限保留”。 +2. 历史开关默认值(开启/关闭)需要在接口和数据库设计评审中确认。 +3. 下架商品的浏览历史是否需要自动清理,由 Catalog 与本流程协商,本期默认保留。 +4. 收藏与浏览列表的分页上限需统一,避免出现无分页返回。 +5. A025 在商品详情或列表上的展示语义需与 M02 协作确认,避免与公开摘要口径冲突。 + +## 十、验收证据清单 + +- [ ] 收藏、取消收藏、收藏列表、浏览记录和历史列表均正常工作。 +- [ ] 重复收藏、重复浏览和重复取消满足幂等要求,不产生重复记录。 +- [ ] 收藏和历史按最近时间倒序,数据严格按买家隔离。 +- [ ] 下架商品仍保留记录并明确不可购买;游客、商家和管理员不能越权访问。 +- [ ] 浏览记录超过上限后移除最早记录;历史开关切换不影响已有记录。 +- [ ] 保存正常操作、重复操作、下架占位、记录上限和跨账号隔离证据。 \ No newline at end of file diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" index 8f96055..30c96b4 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" @@ -4,7 +4,7 @@ > > 编写日期:2026-07-24 版本:v0.2 > -> 当前状态:部分定义;F01~F13 已对照总需求和教师验收完成基线校准,仍待各主责人交叉评审;X03、C06、C07、C10 已形成个人流程初稿,仍待主责自审与直接协作人交叉评审 +> 当前状态:部分定义;F01~F13 已对照总需求和教师验收完成基线校准,仍待各主责人交叉评审;X03、C06、C07、C10 已形成个人流程初稿,仍待主责自审与直接协作人交叉评审;唐宇昊(tyh)新增 F01/F02/F03/F13/X02 个人流程初稿,待主责自审与 M00/Ordering/Catalog 评审 ## 修订记录 @@ -12,6 +12,7 @@ |---|---|---|---| | v0.1 | 2026-07-24 | 全体成员(罗皓晨统稿) | 建立集中式业务流程设计,覆盖核心主链路,并对照 F01~F13 需求与验收校准状态、模块交接、X/C 扩展点和核心结果保护规则 | | v0.2 | 2026-07-24 | 罗皓晨 | 补充 M09、C06、C07、C10 个人流程入口,新增消息、缓存和单 API 实例故障的直接交接图,并更新扩展流程成熟度 | +| v0.3 | 2026-07-24 | 唐宇昊 | 在 tyh/ 新增 M01-01、M01-02、M01-03、M06-03、M08 五份个人流程文档,登记 F01/F02/F03/F13 和 X02 追踪矩阵链接 | ## 一、文档定位与事实来源 @@ -722,9 +723,10 @@ flowchart LR | 教师编号 | 核心状态或确定结果 | 直接入口 → 直接出口 | 本文流程 | 主责人 | 当前成熟度 | |---|---|---|---|---|---| -| F01 | 创建“正常”买家账号 | 游客注册 → F02 登录 | 3.1 | 唐宇昊 | 基线已校准,待主责确认 | -| F02 | 有效令牌 + 服务端角色;退出后当前令牌失效 | M01 → M03/M04/M05/M06 | 3.1、3.8.1 | 唐宇昊 | 基线已校准,待主责确认 | -| F03 | 本人资料与地址;敏感修改后令牌状态明确 | M01 Address → M04 地址快照 | 3.2、3.8.1 | 唐宇昊 | 基线已校准,待主责确认 | +| F01 | 创建“正常”买家账号 | 游客注册 → F02 登录 | [M01-01 用户注册流程](tyh/M01-01-用户注册流程.md)、3.1 | 唐宇昊 | 初稿,待主责自审及 M00 协作评审 | +| F02 | 有效令牌 + 服务端角色;退出后当前令牌失效 | M01 → M03/M04/M05/M06 | [M01-02 用户登录与退出流程](tyh/M01-02-用户登录与退出流程.md)、3.1、3.8.1 | 唐宇昊 | 初稿,待主责自审及 M00/C10 评审 | +| F03 | 本人资料与地址;敏感修改后令牌状态明确 | M01 Address → M04 地址快照 | [M01-03 个人信息与收货地址流程](tyh/M01-03-个人信息与收货地址流程.md)、3.2、3.8.1 | 唐宇昊 | 初稿,待主责自审及 Ordering 评审 | +| F13 | 买家/商家账号“正常 ↔ 禁用”,旧令牌结果明确 | M06-03 → M01 → 全部受保护入口 | [M06-03 后台用户管理流程](tyh/M06-03-后台用户管理流程.md)、3.1、3.7、3.8.4 | 唐宇昊 | 初稿,待主责自审及 M00/C10 评审 | | F04 | 只返回已上架商品的分页列表 | M02 → 购物端列表 | 3.3 | 顾欣月 | 基线已校准,待主责确认 | | F05 | 安全的关键词/组合查询结果 | 查询条件 → M02 → F04 列表 | 3.3 | 顾欣月 | 基线已校准,待主责确认 | | F06 | 公开详情、最新价格库存和明确可售状态 | F04 列表 → M02 详情 → M03 | 3.3、3.8.1 | 顾欣月 | 基线已校准,待主责确认 | @@ -743,7 +745,7 @@ flowchart LR | 编号 | 基础核心流程 | 直接扩展入口 → 出口 | 不可变核心结果 | 主责人 | 当前状态 | |---|---|---|---|---|---| | X01 | F09、F06 | `Completed` 订单项 → 评价记录 → F06 公开评价 | 订单保持 `Completed`;快照、商品状态、价格和库存不变;同一订单项最多一条评价 | 顾欣月 | 待细化 | -| X02 | F02、F06,展示衔接 F04 | 已登录买家查看详情 → 本人收藏/浏览记录 | 不修改商品事实;游客不产生个人记录;严格按买家隔离 | 唐宇昊 | 待细化 | +| X02 | F02、F06,展示衔接 F04 | 已登录买家查看详情 → 本人收藏/浏览记录 | 不修改商品事实;游客不产生个人记录;严格按买家隔离 | 唐宇昊 | [M08 商品收藏与浏览历史流程](tyh/M08-商品收藏与浏览历史流程.md):初稿,待主责自审及 Catalog 评审 | | X03 | F02、F13;F08、F09、F10、F12;X04 可追加来源 | 核心事务提交事件 → 消息落库/查询/已读/离线补查 | 消息失败不回滚核心事务,也不能反向修改订单、支付或售后状态 | 罗皓晨 | [M09 站内消息流程](lhc/M09-站内消息流程.md):初稿,待主责自审;来源事件、精确商家接收账号及核心流程边界待 Ordering、Payment、AfterSales、Identity 交叉评审 | | X04 | F09、F10、F12 | 本人 `Paid/Shipped/Completed` 订单项 → 独立售后状态 → 幂等退款 | 订单核心状态和快照不被“已退款”覆盖;退款不超实付且不重复入账 | 张海洋 | 待细化 | | C01 | F11 + F04/F06 → F08 → F10/F09/F12 | F11 商家创建/发布活动;F06 买家进入秒杀入口 → 独立库存条件扣减 → 汇入 `PendingPayment` | 后续复用核心支付和履约;取消只回补原秒杀库存;支付成功不得回补 | 朱惠惠 | 待细化;库存划拨口径待确认 | -- Gitee From e7d3d558e653476d7516fee903a86a097468d2e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=B5=B7=E6=B4=8B?= <2040454617@qq.com> Date: Fri, 24 Jul 2026 16:09:05 +0800 Subject: [PATCH 064/118] =?UTF-8?q?docs(process):=20fix=20C08/M10=20?= =?UTF-8?q?=E9=98=BB=E6=96=AD=E6=80=A7=E6=B5=81=E7=A8=8B=E6=BC=8F=E6=B4=9E?= =?UTF-8?q?=EF=BC=9B=E7=BB=9F=E4=B8=80=E4=BA=8B=E5=8A=A1=E8=BE=B9=E7=95=8C?= =?UTF-8?q?=E4=B8=8E=E5=A4=B1=E8=B4=A5=E7=8A=B6=E6=80=81=E5=8F=AF=E8=BE=BE?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C08 修复(3 个阻断性漏洞): - 回调接收事务拆断:把 Inbox 写入、订单状态条件更新、Outbox 事件并入单一 PostgreSQL 事务,通过 Ordering 公开应用契约锁定 orders 行 - 失败分支错误发布支付成功事实:拆分 PendingPayment 失败、Paid/Cancelled/其他非 PendingPayment 状态的 Inbox 终态路径,仅 PendingPayment + Success 写 Outbox 支付成功事实 - 对账批次漏掉 Difference:批次读取范围从 Processed 扩展为 Processed 或 Difference,确保'已取消订单收到迟到成功'等差异进入每日对账 M10 修复(5 个阻断性漏洞): - 创建申请并发风险:通过 Ordering 公开应用契约在同一 PostgreSQL 事务中锁定 orders 行,用条件聚合核算'剩余可售后数量 = 订单项数量 - 处理中数量 - 已退款数量' - RefundOnly 异步分段事务:审核通过时同事务内同步调用 IRefundService,成功 → Refunded,失败 → RefundFailed - 商家确认收货后异步退款:把库存回补、IRefundService 退款、状态终态合并到单一 PostgreSQL 事务;任一步失败整体回滚 - 快递单号全局唯一约束:删除全局唯一判断和约束说明,按 A434 总契约允许同订单多笔申请共享同一包裹 - RefundFailed 不可达:IRefundService 事务失败时不整体回滚 Refunding 申请,用独立失败记录置为 RefundFailed 供 A419 重试 依据:docs/02-设计文档/接口设计.md A412/A416/A417/A419/A421/A434 总契约,docs/02-设计文档/process/README.md 状态流转模板与图规范。 Refs: zhy 漏洞清单 C08 行 121/160/213 + M10 行 118/152/184/211/229 --- ...71\350\264\246\346\265\201\347\250\213.md" | 40 ++++++------ ...56\345\220\216\346\265\201\347\250\213.md" | 62 +++++++++++-------- 2 files changed, 59 insertions(+), 43 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" index bb04c88..4bd4738 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" @@ -125,13 +125,13 @@ flowchart TD D -- "否" --> E["命中 Inbox:返回首次处理结果"] D -- "是" --> F{"字段合法?"} F -- "否" --> Y["拒绝:字段错误或缺失"] - F -- "是" --> G["开启事务"] - G --> H["写入 Inbox 记录(状态 Processing)"] - H --> I["查询订单当前状态"] - I --> J["判定下一动作(见第五章 幂等与乱序处理)"] + F -- "是" --> G["开启单一 PostgreSQL 事务"] + G --> H["通过 Ordering 公开应用契约锁定 orders 行 + 写入 Inbox 记录(状态 Processing)"] + H --> I["查询订单当前状态 + 判定下一动作(见第五章 幂等与乱序处理)"] + I --> J["同事务内:支付记录 + 订单状态条件更新 + Inbox 终态 + Outbox 事件"] J --> K{"事务提交成功?"} - K -- "否" --> KR["整体回滚,Inbox 回退到 Received"] - K -- "是" --> Z["进入事务一致性处理"] + K -- "否" --> KR["整体回滚,Inbox 保持 Processing,等待安全重试"] + K -- "是" --> Z["事务一致性已完成:Outbox 由 M09 可靠推送"] ``` 签名与字段校验: @@ -149,7 +149,7 @@ flowchart TD B -- "否" --> C{"订单当前状态?"} C -- "PendingPayment" --> D{"回调结果?"} D -- "Success" --> E["条件推进 PendingPayment → Paid"] - D -- "Failed" --> E2["写入失败记录,订单保持 PendingPayment"] + D -- "Failed" --> E2["写入失败记录,订单保持 PendingPayment
不写 Outbox 支付成功事实"] C -- "Paid" --> F{"回调结果?"} F -- "Success" --> FX["已支付成功回调,标记 Ignored,不重复写支付记录"] F -- "Failed" --> FY["登记为差异:支付记录重复但订单已支付"] @@ -157,16 +157,20 @@ flowchart TD G -- "Success" --> GX["重要:已取消订单收到迟到成功 → 标记 Difference,不改为 Paid"] G -- "Failed" --> GY["失败回调到达已取消订单,标记 Ignored"] C -- "其他不可支付状态" --> H["标记 Ignored"] - E --> I["写入支付记录、订单状态、Outbox 支付成功事实"] - E2 --> I - FX --> I - FY --> I - GX --> I - GY --> I - H --> I + E --> I["同事务内:写入支付记录、订单状态 Paid、Outbox 支付成功事实"] + FX --> IFX["同事务内:仅推进 Inbox 至 Ignored,不写支付记录、不发 Outbox"] + FY --> IFY["同事务内:推进 Inbox 至 Difference,登记差异条目"] + GX --> IGX["同事务内:推进 Inbox 至 Difference,登记差异条目,不改为 Paid"] + GY --> IGY["同事务内:推进 Inbox 至 Ignored"] + H --> IH["同事务内:推进 Inbox 至 Ignored"] I --> J{"事务提交成功?"} - J -- "否" --> JR["整体回滚,Inbox 状态回退"] - J -- "是" --> K["更新 Inbox 状态为最终态(Processed/Ignored/Difference)"] + IFX --> J + IFY --> J + IGX --> J + IGY --> J + IH --> J + J -- "否" --> JR["整体回滚,Inbox 保持 Processing,等待安全重试"] + J -- "是" --> K["提交事务:Outbox 支付成功事实由 M09 在事务外可靠推送"] ``` 乱序关键规则: @@ -210,7 +214,7 @@ flowchart TD A --> B{"该日期+范围已存在批次?"} B -- "是" --> BX["跳过:不重复生成"] B -- "否" --> C["开启批次事务"] - C --> D["读取支付记录、订单状态、状态为 Processed 的回调 Inbox"] + C --> D["读取支付记录、订单状态、终态为 Processed 或 Difference 的回调 Inbox"] D --> E["读取 M10 退款记录、wallet_ledgers 中退款入账记录"] E --> F["比对支付记录 vs 订单状态"] F --> G["比对退款成功 vs 退款流水 vs 钱包入账"] @@ -225,7 +229,7 @@ flowchart TD 关键规则: - 同一日期同一范围不重复生成矛盾批次;以 `(date, range)` 唯一约束去重。 -- 批次范围只包含已提交事务的支付记录(对应 `payment.status = 'Succeeded'`)、M10 已退款的记录和 `Processed` 状态的回调 Inbox,不包含处理中或失败中的回调。 +- 批次范围只包含已提交事务的支付记录(对应 `payment.status = 'Succeeded'`)、M10 已退款的记录和终态为 `Processed` 或 `Difference` 的回调 Inbox,不包含 `Processing`、`Failed` 等中间态;`Difference` 状态承担"已取消订单收到迟到成功"等需管理员闭环的差异暴露,由 A424/A425 处理。 - 资金类比对必须用 PostgreSQL 条件查询与聚合;不在应用层先读后算。 ## 八、差异识别与闭环 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" index 4627399..25d43af 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" @@ -112,14 +112,14 @@ flowchart TD D -- "是" --> E{"类型与状态匹配?"} E -- "仅退款 + Paid" --> F["确定类型为 RefundOnly"] E -- "退款/退货 + Shipped/Completed" --> G["确定类型为 ReturnAndRefund"] - F --> H["开启创建事务"] + F --> H["通过 Ordering 公开应用契约在同一 PostgreSQL 事务中锁定 orders 行"] G --> H - H --> I["计算金额 = 实付单价 × 申请数量"] - I --> J{"同一订单项已有 PendingReview?"} - J -- "是" --> JZ["拒绝重复申请"] - J -- "否" --> K["写入申请、申请单状态置 PendingReview、记录审计日志"] + H --> I["在锁定行内用 PostgreSQL 条件聚合核算:
剩余可售后数量 = 订单项数量 − 处理中数量 − 已退款数量"] + I --> J{"申请数量 ≤ 剩余可售后数量(同事务内复算)?"} + J -- "否" --> JZ["拒绝:剩余可售后数量不足"] + J -- "是" --> K["写入申请、申请单状态置 PendingReview、记录审计日志"] K --> L{"事务提交成功?"} - L -- "否" --> LR["整体回滚,不创建申请"] + L -- "否" --> LR["整体回滚,不创建申请;锁随事务结束释放"] L -- "是" --> M["记录待发布申请提交事实"] M --> N["通知商家审核"] M --> O["买家可查看本人申请详情"] @@ -148,11 +148,15 @@ flowchart TD F --> G["记录待发布审核拒绝事实"] D -- "Approve" --> H["记录审核意见"] H --> I{"申请类型?"} - I -- "RefundOnly" --> J["状态条件推进 PendingReview → Refunding"] - J --> K["异步调用 IRefundService 触发退款"] + I -- "RefundOnly" --> J["开启审核事务"] + J --> J1["同事务内调用 IRefundService:原子写入退款记录 + 钱包入账 + 钱包流水"] + J1 --> J2{"IRefundService 返回结果?"} + J2 -- "退款成功" --> J3["状态推进 PendingReview → Refunded"] + J2 -- "退款失败" --> J4["用独立失败记录保留状态 RefundFailed
供 A419 失败重试使用"] I -- "ReturnAndRefund" --> L["状态条件推进 PendingReview → PendingReturn"] L --> L1["保留审核意见,等待买家提交退货物流"] - K --> MR + J3 --> MR + J4 --> MR L --> MR G --> MR MR{"事务提交成功?"} @@ -177,13 +181,12 @@ flowchart TD B -- "是" --> C["输入快递公司、快递单号、寄出时间与备注"] C --> D{"字段合法?"} D -- "否" --> DZ["保留输入并提示字段错误"] - D -- "是" --> E{"快递单号已被使用?"} - E -- "是" --> EZ["拒绝重复使用同一单号"] - E -- "否" --> H["开启事务"] - H --> I["状态条件推进 PendingReturn → PendingReceipt"] + D -- "是" --> H["开启单一 PostgreSQL 事务"] + H --> H0["通过 Ordering 公开应用契约在同一 PostgreSQL 事务中锁定 orders 行"] + H0 --> I["状态条件推进 PendingReturn → PendingReceipt"] I --> J["记录退货物流信息、audit_log"] J --> K{"事务提交成功?"} - K -- "否" --> KR["整体回滚,状态保持 PendingReturn"] + K -- "否" --> KR["整体回滚,状态保持 PendingReturn;锁随事务结束释放"] K -- "是" --> L["记录待发布退货物流提交事实"] L --> M["通知商家待收货"] ``` @@ -191,7 +194,7 @@ flowchart TD 退货物规则: - 状态条件 `WHERE status = 'PendingReturn' AND buyer_id = currentBuyerId` 唯一推进。 -- 快递单号全局唯一约束,避免多笔售后重复登记同一运单。 +- 不对快递单号施加全局唯一约束:同一包裹可承载同一订单的多笔退货申请(A434 总契约);重复提交由申请状态、`Idempotency-Key` 和单号在 `audit_log` 中的处理说明控制。 - 仅退款(`RefundOnly`)申请不需要走本流程。 ## 七、商家确认收货与退款入账 @@ -204,13 +207,19 @@ flowchart TD B -- "是" --> C["输入收到数量与备注"] C --> D{"收到数量合法?"} D -- "否" --> DZ["保留输入并提示错误"] - D -- "是" --> E["开启事务"] - E --> F["状态条件推进 PendingReceipt → Refunding"] + D -- "是" --> E["开启单一 PostgreSQL 事务"] + E --> F["状态条件推进 PendingReceipt"] F --> G["按退货数量回补订单项原库存通道"] - G --> H["记录回补事实与确认收货审计日志"] - H --> I["事务提交成功后异步调用 IRefundService 触发退款"] - I --> J["记录待发布退款事实"] - J --> K["通知买家退款处理中"] + G --> H["同事务内调用 IRefundService:原子写入退款记录 + 钱包入账 + 钱包流水"] + H --> H1{"IRefundService 返回结果?"} + H1 -- "退款成功" --> H2["状态推进 PendingReceipt → Refunded"] + H1 -- "退款失败" --> H3["用独立失败记录保留状态 RefundFailed
供 A419 失败重试使用"] + H2 --> I + H3 --> I + I["记录待发布退款事实 + 确认收货审计日志"] + I --> J{"事务提交成功?"} + J -- "否" --> JR["整体回滚:库存不回补、状态保持 PendingReceipt,锁随事务结束释放"] + J -- "是" --> K["通知买家退款处理中"] ``` 退款入账流程(`IRefundService`,内部应用能力,必走): @@ -224,11 +233,14 @@ flowchart TD C -- "同 Key 同金额" --> D["返回首次结果"] C -- "同 Key 不同金额" --> CZ["抛 IdempotencyKeyReusedException"] C -- "否" --> E["开启退款事务"] - E --> F["原子写入:退款记录 + 钱包入账 + 钱包流水 + 申请状态 Refunding → Refunded"] + E --> F["原子写入:退款记录 + 钱包入账 + 钱包流水"] F --> G{"事务提交成功?"} - G -- "否" --> GR["整体回滚,状态保持 Refunding"] - G -- "是" --> H["记录待发布退款完成事实"] + G -- "是" --> GS["申请状态推进 Refunding → Refunded"] + GS --> H["记录待发布退款完成事实"] H --> I["通知买家退款成功"] + G -- "否" --> GF["不整体回滚:用独立失败记录保留状态 Refunded 申请保持 Refunding,
未推进申请置为 RefundFailed"] + GF --> GFA["独立失败记录由 A419 失败重试入口处理"] + GFA --> IFAIL["通知相关方失败原因,等待重试"] ``` 退款入账原子结果: @@ -351,7 +363,7 @@ flowchart TD 5. 账号禁用售后:流程要求禁用账号不能新建或主动操作售后;A412 / A415 必须在账号禁用时拒绝;已有申请可被商家和系统继续处理。 6. 库存回补口径:流程要求按订单项原库存来源通道回补;具体库存通道归属由 M02 协作时确认;M10 不替代 M02 决定库存通道。 7. 状态机不可逆:流程要求"已退款 / 已拒绝 / 已撤销"为终止状态;A419 退款失败重试只能从 `退款失败` 推进,不能从"已退款"或"已拒绝"推进。 -8. 退货快递单号全局唯一:流程要求同一快递单号只能用于一笔售后;A434 提交退货物流要建唯一约束 DBxxx(待评审)。 +8. 退货快递单号共享:流程要求同一快递单号可承载同一订单多笔退货申请(A434 总契约);不建快递单号全局唯一约束;重复提交由申请状态和 `Idempotency-Key` 控制。 9. 商家并发审核:流程要求状态条件唯一胜出;A416 审核必须检查 `WHERE status = 'PendingReview'` 条件更新,失败方收到 `409 + AFTER_SALES.INVALID_STATUS`。 10. 库存回补时机:流程要求未发货仅退款 + 退货退款确认收货都回补库存;A431 内部应用能力(退款入账)不负责库存回补,由 M04 / M02 在确认收货时完成。 11. DB081~DB100 尚未形成可实施的完整表定义,数据库字段、约束和索引必须由数据库设计任务另行确认。 -- Gitee From 1b48ae912e09075f8d536f8d7ff7a4c8bb061d2f Mon Sep 17 00:00:00 2001 From: FISH SOUP Date: Fri, 24 Jul 2026 16:21:57 +0800 Subject: [PATCH 065/118] =?UTF-8?q?docs(process):=20=E4=BF=AE=E5=A4=8D=20t?= =?UTF-8?q?yh=20=E6=B5=81=E7=A8=8B=E6=96=87=E6=A1=A3=E7=9A=84=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E9=94=99=E4=BD=8D=E3=80=81=E4=BA=8B=E5=8A=A1=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E4=B8=8E=E5=9B=BE=E7=A4=BA=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...50\345\206\214\346\265\201\347\250\213.md" | 31 ++--- ...00\345\207\272\346\265\201\347\250\213.md" | 124 +++++++++--------- ...60\345\235\200\346\265\201\347\250\213.md" | 108 ++++++++------- ...41\347\220\206\346\265\201\347\250\213.md" | 122 ++++++++++------- ...06\345\217\262\346\265\201\347\250\213.md" | 77 ++++++----- 5 files changed, 258 insertions(+), 204 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" index 2cc1c50..4686363 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" @@ -3,15 +3,15 @@ > - 覆盖:M01-01、F01 > - 主责人:唐宇昊 > - 需求来源:[《需求规格说明书》M01-01](../../../01-需求文档/需求规格说明书.md) 的“M01-01 用户注册(F01)— 唐宇昊”完整七节 -> - 基础核心流程:F02(账号体系入口) +> - 基础核心流程:F02(账号体系入口);为 M03、M04、M05、M06、M08、M09 提供“正常”买家账号前提 > - 直接入口:游客在公共注册入口提交手机号、密码和确认密码 > - 直接出口:M01 创建“正常”买家账号,引导进入 F02 登录流程;账号事实后续供 M03、M04、M05、M06、M07、M08、M09 复用 -> - 回归核心结果:账号状态为“正常”,角色固定为买家,全站不出现管理员或商家账号 -> - 不得改变:F02 已签发登录态、未注册的购物车或浏览历史归属 +> - 回归核心结果:F02 已签发登录态保持有效;F03 已维护资料与地址不变;F13 账号治理结果不变 +> - 不得改变:管理员和商家账号必须由初始化或受控流程创建;公开注册流程不能创建管理员或商家账号 ## 一、范围与事实来源 -本流程负责公开注册页面、服务端校验、用户名生成和默认资料创建。它不提供商家或管理员在线注册、不发送短信验证码、不支持自定义头像、不提供找回密码或账号合并。 +本流程负责公开注册页面、服务端校验、用户名生成、默认资料创建和后续引导登录。它不提供商家或管理员在线注册、不发送短信验证码、不支持自定义头像、不提供找回密码或账号合并。 A001 由本流程派生,仅在流程评审通过后用于契约映射;现有清单不能反向决定或拼接业务流程。 @@ -27,17 +27,17 @@ A001 由本流程派生,仅在流程评审通过后用于契约映射;现有 ```mermaid flowchart LR - VIS["游客
未认证、无任何身份"] -->|"公开注册入口"| REG["M01-01 Identity
注册事务"] + VIS["游客"] -->|"公共注册入口"| REG["M01-01 Identity
注册事务"] REG -->|"正常买家账号 + 默认资料"| F02["M01-02 登录流程"] - REG -->|"账号摘要(不含敏感字段)"| UI["注册成功页"] - REG -. "注册事件事实" .-> LATER["M09 待登记:注册成功通知(非本期)"] + REG -->|"账号摘要"| UI["注册成功页"] + REG -. "失败字段级提示" .-> FAIL["保留非敏感输入并提示原因"] VIS -. "尝试指定商家或管理员" .-> REJ["忽略或拒绝角色字段"] - VIS -. "重复手机号或非法输入" .-> FAIL["返回字段级错误,不创建账号"] + VIS -. "重复手机号或非法输入" .-> FAIL ``` 边界约束: -- 注册只产出买家账号,不接受客户端指定角色。 +- 公开注册流程只能产出买家账号;管理员和商家账号由初始化或另行确认的受控流程提供。 - 公开注册不与任何受保护业务共享事务边界;账号创建成功后由 F02 独立负责登录态。 - 默认资料由系统生成,不允许用户上传头像或指定用户名。 @@ -77,7 +77,7 @@ flowchart TD ```mermaid flowchart TD A["接收注册请求"] --> B["读取手机号、密码、确认密码"] - B --> C{"手机号匹配 ^1[3-9]\d{9}$ 且无前后空白?"} + B --> C{"手机号符合中国大陆手机号格式且无前后空白?"} C -- "否" --> X1["拒绝:手机号格式错误"] C -- "是" --> D{"密码长度 8~16 且同时包含字母和数字?"} D -- "否" --> X2["拒绝:密码强度不足"] @@ -90,7 +90,7 @@ flowchart TD 校验要求: -- 拒绝 `+86`、`0086`、空格、连字符或固话号码;只接受中国大陆 11 位手机号。 +- 拒绝国际区号、空格、连字符或固话号码;只接受中国大陆 11 位手机号(具体格式校验在接口设计中给出,本流程不展开)。 - 不得强制特殊字符、大小写混合或验证码;本期不存储历史密码。 - 校验失败时,前端保留已填写的手机号和提示,密码字段不回显。 - 字段级错误不得泄露其他账号是否存在或格式细节。 @@ -99,7 +99,7 @@ flowchart TD ```mermaid flowchart TD - A["字段校验通过且手机号唯一"] --> B["按 F01-FR05 生成用户名:u_ + 8 位不易混淆字符"] + A["字段校验通过且手机号唯一"] --> B["按注册规则生成用户名:u_ + 8 位不易混淆字符"] B --> C{"全局唯一?"} C -- "否,受控次数内重试" --> B C -- "仍冲突" --> X["提示系统繁忙,请稍后重试"] @@ -118,9 +118,9 @@ flowchart TD ```mermaid flowchart TD - A["两个请求同时注册同一手机号"] --> B["数据库唯一约束保证只有一个成功"] - B -- "胜出" --> C["创建正常买家账号"] - B -- "失败" --> Y["返回明确的重复注册提示"] + A1["两个请求同时注册同一手机号"] --> B1["数据库唯一约束保证只有一个成功"] + B1 -- "胜出" --> C["创建正常买家账号"] + B1 -- "失败" --> Y["返回明确的重复注册提示"] Y --> Y1["不泄露其他账号资料"] A2["用户连续点击提交"] --> B2["前端防重按钮;服务端按字段级错误返回"] B2 -- "无变化" --> N["不产生重复账号"] @@ -178,4 +178,5 @@ flowchart TD - [ ] 密码以可靠哈希存储,响应、日志和数据库中不出现明文密码。 - [ ] 用户名和手机号全局唯一,并发注册不产生重复账号。 - [ ] 重复点击、前端防抖失败和网络重试不创建第二张账号。 +- [ ] 系统仍存在由初始化或受控流程创建的商家和管理员账号;公开注册流程不能创建这些账号。 - [ ] 保存正常与异常注册截图,并能说明凭据保护、唯一性和角色固定规则。 \ No newline at end of file diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" index 1dfc7de..7f3cb12 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" @@ -7,7 +7,7 @@ > - 直接入口:游客或低登录态用户提交手机号和密码;前端刷新或退出动作触发登录态恢复与失效 > - 直接出口:M01 返回有效登录凭证、服务端确认的角色与账号状态;下游模块据此决定是否继续受理 > - 回归核心结果:F01 已注册账号、F03 个人资料、F13 账号治理结果保持不变 -> - 不得改变:账号、订单、支付事实与权限结果;本期不引入第三方登录或多端登录联动 +> - 不得改变:账号、订单、支付事实与权限结果;本期不引入第三方登录或多设备登录联动 ## 一、范围与事实来源 @@ -28,23 +28,22 @@ A002~A005 由本流程派生,仅在流程评审通过后用于契约映射 ```mermaid flowchart LR VIS["游客或登录过期用户"] -->|"登录入口提交"| LOG["M01-02 Identity
登录与退出"] - LOG -->|"有效 JWT + 角色 + 账号状态"| M03["M03 Cart"] - LOG -->|"有效 JWT + 角色 + 账号状态"| M04["M04 Ordering"] - LOG -->|"有效 JWT + 角色 + 账号状态"| M05["M05 Payment"] - LOG -->|"有效 JWT + 角色 + 账号状态"| M06["M06 后台"] - LOG -->|"有效 JWT + 角色 + 账号状态"| M08["M08 收藏与历史"] - LOG -->|"退出后令牌失效"| UI["前端清理登录态并返回登录页"] - LOG -. "登录成功事实" .-> LATER["M09 待登记:登录通知(非本期)"] + LOG -->|"有效登录态 + 角色 + 账号状态"| M03["M03 Cart"] + LOG -->|"有效登录态 + 角色 + 账号状态"| M04["M04 Ordering"] + LOG -->|"有效登录态 + 角色 + 账号状态"| M05["M05 Payment"] + LOG -->|"有效登录态 + 角色 + 账号状态"| M06["M06 后台"] + LOG -->|"有效登录态 + 角色 + 账号状态"| M08["M08 收藏与历史"] + LOG -->|"退出后当前令牌失效"| UI["前端清理登录态并返回登录页"] F13["M06-03 禁用/启用"] -->|"账号状态变更"| LOG F01["M01-01 注册成功"] -->|"正常买家账号"| LOG - VIS -. "越权访问" .-> REJ["401/403,不泄露账号存在性"] + VIS -. "越权访问" .-> REJ["按角色规则拒绝访问"] VIS -. "令牌失效或角色错误" .-> RLOG["受保护请求失败并引导重新登录"] ``` 边界约束: -- M01-02 只产出可被任一 API 实例验证的 JWT 和当前账号状态,不直接通知业务模块。 -- 令牌失效结果无法确认时,受保护请求必须失败关闭,不允许因依赖异常继续放行。 +- M01-02 只产出可被任一 API 实例验证的登录态,并返回当前账号状态,不直接通知业务模块。 +- 登录态失效结果无法确认时,受保护请求必须失败关闭,不允许因依赖异常继续放行。 - 退出只使当前令牌失效;多设备或全设备退出需另行评审并写入接口设计。 ## 三、登录主流程 @@ -62,7 +61,7 @@ flowchart TD G -- "否" --> X2 G -- "是" --> H{"账号状态正常?"} H -- "否" --> X3["拒绝登录并提示账号停用"] - H -- "是" --> I["签发有明确有效期的登录凭证"] + H -- "是" --> I["签发有明确有效期的登录态和刷新凭证"] I --> J["返回账号摘要、服务端确认的角色和当前状态"] J --> K["前端保存登录态,按角色进入对应端"] K --> L{"按角色进入?"} @@ -75,18 +74,18 @@ flowchart TD - 账号不存在和密码错误必须返回统一的“账号或密码错误”提示,禁止区分错误字段。 - 账号禁用需明确告知用户联系管理员,但不暴露内部状态码或异常。 -- JWT 包含明确有效期;登录态有效期、刷新策略和签名配置由接口设计统一。 -- 多实例环境下,JWT 验签配置必须一致;任一实例可独立验证同一有效令牌。 +- 登录态包含明确有效期;登录态有效期、刷新策略和签名配置由接口设计统一。 +- 多实例环境下,登录态验签配置必须一致;任一实例可独立验证同一有效登录态。 ## 四、登录态恢复与角色路由 ```mermaid flowchart TD - A["用户刷新页面或重新打开浏览器"] --> B["前端读取本地保存的令牌"] - B --> C{"令牌仍处于有效期?"} + A["用户刷新页面或重新打开浏览器"] --> B["前端读取本地保存的登录态"] + B --> C{"登录态仍处于有效期?"} C -- "否" --> X["清理登录态并引导重新登录"] C -- "是" --> D["向后端发起身份校验"] - D --> E{"令牌有效且账号状态正常?"} + D --> E{"登录态有效且账号状态正常?"} E -- "否" --> X E -- "是" --> F["返回当前账号摘要与角色"] F --> G{"路由是否匹配当前角色?"} @@ -99,30 +98,32 @@ flowchart TD - 刷新或重连后必须重新执行服务端身份校验,禁止仅凭前端缓存决定路由。 - 角色路由以服务端确认结果为准;前端隐藏菜单不替代服务端授权。 -- 登录态恢复失败、令牌过期或账号被禁用时,立即清理失效登录态并提示用户重新登录。 +- 登录态恢复失败、登录态过期或账号被禁用时,立即清理失效登录态并提示用户重新登录。 -## 五、退出与令牌失效 +## 五、退出与登录态失效 ```mermaid flowchart TD - A["用户在任一端点击退出"] --> B["前端清理本地登录态"] - B --> C["调用退出接口"] - C --> D["服务端登记当前令牌失效"] - D --> E["返回退出成功"] - E --> F["前端跳转登录页"] - A2["受保护接口仍使用旧令牌"] --> B2["校验时返回 401 或登录失效"] + A["用户在任一端点击退出"] --> B["前端使用当前登录态调用退出动作"] + B --> C{"退出动作成功?"} + C -- "否" --> C1["保留本地登录态,提示重试并保留返回入口"] + C -- "是" --> D["服务端登记当前登录态失效"] + D --> E["前端清理本地登录态"] + E --> F["跳转登录页"] + A2["受保护接口仍使用旧登录态"] --> B2["校验时返回登录失效"] B2 --> C2["前端清理登录态并引导重新登录"] - A3["M06-03 禁用账号"] --> B3["服务端撤销该账号全部令牌"] - B3 --> C3["受保护请求返回 401 或登录失效"] - A4["手机号修改成功"] --> B4["服务端撤销修改前签发的全部令牌"] - B4 --> C4["受保护请求返回 401 或登录失效"] + A3["M06-03 禁用账号"] --> B3["服务端提升账号令牌版本,旧登录态按版本失效"] + B3 --> C3["受保护请求返回登录失效"] + A4["手机号修改成功"] --> B4["服务端提升账号令牌版本,修改前签发的全部登录态失效"] + B4 --> C4["受保护请求返回登录失效"] ``` 退出与失效约束: -- 主动退出只影响当前令牌;本期不自动撤销同账号的其他设备令牌。 -- M06-03 禁用账号、手机号修改成功后必须撤销对应令牌;新令牌签发前用户必须重新登录。 -- 令牌失效结果需在多实例之间保持一致;失效状态无法确认时受保护请求失败关闭。 +- 退出顺序必须先使用当前登录态调用退出动作,收到服务端撤销结果后再清理本地登录态;服务端未成功时不得丢弃本地登录态,避免出现“看似已退出但服务端仍有效”。 +- 主动退出只影响当前登录态;本期不自动撤销同账号的其他设备登录态。 +- M06-03 禁用账号、手机号修改成功后必须提升对应账号令牌版本,使修改前签发的登录态失效;新登录态签发前用户必须重新登录。 +- 登录态失效结果需在多实例之间保持一致;失效状态无法确认时受保护请求失败关闭。 - 退出后用户的所有个人页和受保护页必须退出到登录态,禁止出现“看似已退出但仍能访问”的状态。 ## 六、并发、幂等与异常 @@ -132,65 +133,68 @@ flowchart TD A1["密码错误连续尝试"] --> B1["统一返回账号或密码错误"] B1 --> N1["不暴露账号存在性,不触发额外锁定(本期不实现登录限流)"] A2["已禁用账号尝试登录"] --> B2["拒绝并提示账号停用"] - B2 --> N2["不签发新令牌"] - A3["令牌过期或被撤销"] --> B3["受保护请求 401 或登录失效"] + B2 --> N2["不签发新登录态"] + A3["登录态过期或被撤销"] --> B3["受保护请求按登录失效处理"] B3 --> N3["前端清理登录态并提示重新登录"] - A4["失效能力暂时不可用"] --> B4["无法确认登录态的受保护请求提示服务暂不可用"] + A4["撤销状态共享暂时不可用"] --> B4["无法确认登录态的受保护请求提示服务暂不可用"] B4 --> N4["不得继续放行"] - A5["多实例验证不一致"] --> B5["视为令牌失效,受保护请求拒绝"] - B5 --> N5["由 C10 与 JWT 配置统一保证一致"] - A6["买家尝试访问商家或管理接口"] --> B6["返回 403,不泄露目标数据"] + A5["多实例验证不一致"] --> B5["视为登录态失效,受保护请求拒绝"] + B5 --> N5["由 C10 与登录态验签配置统一保证一致"] + A6["买家尝试访问商家或管理入口"] --> B6["按角色规则拒绝访问,不泄露目标数据"] A7["商家从购物端入口登录"] --> B7["登录成功后按角色跳转商家端"] B7 --> N7["不误报密码错误"] + A8["登录态即将过期"] --> B8["调用刷新凭证换取新登录态"] + B8 --> N8["旧登录态随刷新撤销"] ``` 异常约束: -- 401 表示未认证或登录已失效;403 表示身份有效但无权执行当前操作。 -- 失效能力不可用时不得返回虚假成功,也不得静默放行受保护请求。 -- 退出接口的幂等性由失效登记机制保证;重复退出返回一致结果。 +- 未认证或登录已失效与身份有效但无权执行当前操作是两类不同的拒绝结果;具体错误码和 HTTP 状态由接口设计统一。 +- 撤销状态共享不可用时不得返回虚假成功,也不得静默放行受保护请求。 +- 退出动作的幂等性由撤销登记机制保证;重复退出返回一致结果。 ## 七、与核心模块的衔接 | 上游 | 入口事实 | 下游 | 出口结果 | |---|---|---|---| | M01-01 注册成功 | 正常买家账号 | M01-02 | 用户进入登录流程 | -| 公开登录入口 | 手机号和密码 | M01-02 | JWT、角色与账号状态 | -| M01-02 | 有效令牌 + 角色 | M03、M04、M05、M06、M08 | 业务模块按各自资源规则受理 | -| M06-03 禁用/启用 | 账号状态变更 | M01-02 | 旧令牌失效或恢复正常登录 | -| M01-03 修改手机号 | 旧令牌集合 | M01-02 | 修改前全部令牌失效,需重新登录 | -| C10 多实例环境 | 共享 JWT 验签配置 | M01-02 | 任一实例可独立验证同一有效令牌 | +| 公开登录入口 | 手机号和密码 | M01-02 | 登录态、角色与账号状态 | +| M01-02 | 有效登录态 + 角色 | M03、M04、M05、M06、M08 | 业务模块按各自资源规则受理 | +| M06-03 禁用/启用 | 账号状态变更 | M01-02 | 旧登录态失效或恢复正常登录 | +| M01-03 修改手机号 | 旧登录态集合 | M01-02 | 修改前全部登录态失效,需重新登录 | +| C10 多实例环境 | 共享登录态验签配置 | M01-02 | 任一实例可独立验证同一有效登录态 | 衔接约束: -- 下游模块只接收 JWT 解析后的身份和角色,不接受客户端自行声明的接收人。 -- 退出或令牌失效结果必须可被任何 API 实例在合理时间内观察到;具体延迟由 C10 和 Redis 失效能力共同确定。 -- 登录入口不区分 PC Web、Electron 和 Android;同一令牌在各客户端均有效,但路由和入口展示仍由前端按角色控制。 +- 下游模块只接收登录态解析后的身份和角色,不接受客户端自行声明的接收人。 +- 退出或登录态失效结果必须可被任何 API 实例在合理时间内观察到;具体延迟由 C10 和共享撤销能力共同确定。 +- 登录入口不区分 PC Web、Electron 和 Android;同一登录态在各客户端均有效,但路由和入口展示仍由前端按角色控制。 ## 八、由流程派生的接口契约映射 | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 提交手机号和密码登录 | A002 | 校验凭据与账号状态、签发 JWT、返回角色与摘要 | 待交叉评审 | -| 刷新或重连时身份校验 | A003 | 校验令牌有效性并返回当前账号状态 | 待交叉评审 | -| 安全退出 | A004 | 登记当前令牌失效并清理登录态 | 待交叉评审 | -| 登录态恢复 | A005 | 按当前令牌恢复账号摘要和路由信息 | 待交叉评审 | +| 提交手机号和密码登录 | A002 | 校验凭据与账号状态、签发登录态与刷新凭证、返回角色与摘要 | 待交叉评审 | +| 主动退出当前登录态 | A003 | 登记当前登录态与刷新凭证失效 | 待交叉评审 | +| 登录态恢复与当前账号查询 | A004 | 校验登录态有效性并返回当前账号状态 | 待交叉评审 | +| 使用刷新凭证换取新登录态 | A005 | 校验刷新凭证有效性并签发新登录态,旧凭证同步撤销 | 待交叉评审 | 接口必须承载“当前账号可登录”这一业务结果;HTTP 状态码、错误码和 `traceId` 由接口设计在评审前统一,不得反向写入业务流程。 ## 九、由流程反查出的接口与数据待评审项 -1. 多设备或全设备退出范围需另行确认;A004 当前仅承诺单令牌失效。 +1. 多设备或全设备退出范围需另行确认;A003 当前仅承诺单登录态失效。 2. 登录失败次数限制和锁定策略本期是否实现,需在接口设计中明确。 -3. 失效能力不可用时的降级策略需要在 C10 与 M00 协作下进一步评审。 +3. 撤销状态共享不可用时的降级策略需要在 C10 与 M00 协作下进一步评审。 4. 角色路由在多端(PC Web、Electron、Android)上的跳转目标是否一致需另行确认。 -5. JWT 刷新令牌机制本期不实现,需明确提示用户到期重新登录。 +5. 登录态即将过期时的静默刷新策略需在接口和前端设计中明确。 ## 十、验收证据清单 - [ ] 正确账号可登录,刷新后登录态保持,买家、商家和管理员落地路由正确。 - [ ] 错误密码不泄露账号存在性,被禁用账号无法登录并获得清楚提示。 -- [ ] 退出后原令牌不能访问受保护接口;跨角色访问返回正确的 403。 -- [ ] 在两个 API 实例间切换请求时,同一有效令牌得到一致认证结果。 -- [ ] 失效能力不可用时,受保护请求提示服务暂不可用,不静默放行。 -- [ ] 保存登录、刷新、退出、禁用账号、登录凭证失效能力不可用和越权访问证据。 \ No newline at end of file +- [ ] 退出时先由服务端登记当前登录态失效,再清理本地登录态;服务端未成功时本地登录态保留并提示重试。 +- [ ] 退出后旧登录态不能再访问受保护入口;跨角色访问按角色规则被拒绝。 +- [ ] 在两个 API 实例间切换请求时,同一有效登录态得到一致认证结果。 +- [ ] 撤销状态共享不可用时,受保护请求提示服务暂不可用,不静默放行。 +- [ ] 保存登录、刷新、退出、禁用账号、登录态失效能力不可用和越权访问证据。 \ No newline at end of file diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" index d15a891..c9d2b79 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" @@ -6,12 +6,12 @@ > - 基础核心流程:F02、F08 > - 直接入口:M01-02 提供已登录买家身份;M04 在 F08 提交订单时按地址快照契约读取 > - 直接出口:买家资料与地址的查看、修改和默认地址结果;M04 据此完成地址归属校验和快照 -> - 回归核心结果:F02 已签发登录态保持有效;F08 已下单订单的地址快照不变 +> - 回归核心结果:F02 已签发登录态在非敏感修改后保持有效;F08 已下单订单的地址快照不变;修改手机号后必须重新登录 > - 不得改变:订单金额、商品快照、支付事实与他人资料归属 ## 一、范围与事实来源 -本流程负责买家个人中心的资料查看与维护、地址新增、查询、编辑、删除、默认地址切换和敏感修改的令牌处理。它不提供自定义头像上传、商家资料维护、管理员代修改或多地址簿切换。 +本流程负责买家个人中心的资料查看与维护、地址新增、查询、编辑、删除、默认地址切换和敏感修改的登录态处理。它不提供自定义头像上传、商家资料维护、管理员代修改或多地址簿切换。 A006~A014 由本流程派生,仅在流程评审通过后用于契约映射;现有清单不能反向决定或拼接业务流程。 @@ -30,17 +30,17 @@ flowchart LR B["已登录买家"] -->|"个人中心入口"| PROF["M01-03 Identity
资料与地址"] PROF -->|"用户名、默认头像、掩码手机号"| UI["个人中心页"] PROF -->|"地址列表 + 默认地址标记"| ADDR["地址管理页"] - PROF -->|"修改手机号撤销旧令牌"| TOK["M01-02 登录与退出"] + PROF -->|"敏感修改后提升令牌版本"| TOK["M01-02 登录与退出"] PROF -->|"地址归属校验和地址快照"| ORD["M04 Ordering
F08 提交订单"] ORD -->|"地址快照随订单持久化"| SNAP["历史订单地址快照"] GUEST["游客"] -. "访问个人中心" .-> REJ["引导登录并保留安全返回目标"] - MERCH["商家或管理员"] -. "访问买家资料" .-> FORB["403:拒绝访问买家私人资源"] + MERCH["商家或管理员"] -. "访问买家资料" .-> FORB["拒绝访问买家私人资源"] ``` 边界约束: - 资料与地址只能由当前买家修改;M04 只读取快照数据,不能修改地址。 -- 敏感修改(手机号)成功后必须撤销修改前签发的全部令牌,由 M01-02 处理登录态失效。 +- 敏感修改(手机号)成功后必须提升账号令牌版本,使修改前签发的全部登录态失效,由 M01-02 处理登录态失效。 - 地址列表与默认地址切换必须按当前买家 ID 隔离,禁止跨用户访问。 ## 三、资料维护主流程 @@ -51,25 +51,30 @@ flowchart TD B -- "否" --> X["拒绝访问并引导登录"] B -- "是" --> C["返回用户名、默认头像和掩码手机号"] C --> D{"选择资料操作?"} - D -- "重置用户名" --> E{"本项目期内是否仍有一次机会?"} - E -- "否" --> F["拒绝修改并说明次数已用完"] - E -- "是" --> G["服务端按 F01-FR05 重新生成全局唯一用户名"] - G --> H["保存并返回最新资料"] - D -- "修改手机号" --> I["要求重新验证当前密码"] - I --> I1{"当前密码正确?"} - I1 -- "否" --> K["保持原资料并提示原因"] - I1 -- "是" --> J{"新手机号格式正确且全局唯一?"} - J -- "否" --> K - J -- "是" --> L["保存新手机号并撤销修改前全部令牌"] - L --> M["M01-02 清理登录态并要求重新登录"] + D -- "查看资料" --> E["返回展示资料和自助重置用户名状态"] + D -- "修改展示资料" --> F{"字段合法?"} + F -- "否" --> F1["字段级错误,保留可恢复输入"] + F -- "是" --> F2["保存展示资料并返回最新结果"] + D -- "重置用户名" --> G{"本项目期内是否仍有一次机会?"} + G -- "否" --> H["拒绝修改并说明次数已用完"] + G -- "是" --> I["服务端按注册规则重新生成全局唯一用户名"] + I --> J["保存并返回最新资料"] + D -- "修改手机号" --> K["要求重新验证当前密码"] + K --> K1{"当前密码正确?"} + K1 -- "否" --> L["保持原资料并提示原因"] + K1 -- "是" --> M{"新手机号格式正确且全局唯一?"} + M -- "否" --> L + M -- "是" --> N["保存新手机号并提升账号令牌版本"] + N --> O["M01-02:修改前签发的全部登录态失效,需重新登录"] ``` 资料维护约束: - 用户名只能由服务端重新生成,不接受客户端指定任意字符串。 - 用户名自助修改次数仅一次,成功后页面立即刷新为新用户名。 -- 手机号修改必须验证当前密码;成功后修改前签发的全部令牌失效。 +- 手机号修改必须验证当前密码;成功后提升账号令牌版本,使修改前签发的全部登录态失效。 - 资料修改不得在响应或日志中泄露完整手机号、密码哈希或内部异常。 +- 修改展示资料不改变登录态;只有修改手机号需要重新登录。 ## 四、地址维护主流程 @@ -77,29 +82,34 @@ flowchart TD flowchart TD A["已登录买家进入地址管理"] --> B["按当前买家查询本人地址列表"] B --> C{"选择操作?"} - C -- "新增" --> D["录入收件人、联系电话、省市区、详细地址"] - D --> E{"字段合法?"} - E -- "否" --> X1["保留输入并提示字段错误"] - E -- "是" --> F["保存地址并刷新列表"] - C -- "编辑" --> G{"地址属于当前买家?"} - G -- "否" --> Y["返回不存在或无权限,不泄露归属"] - G -- "是" --> D - C -- "设默认" --> H{"地址属于当前买家?"} - H -- "否" --> Y - H -- "是" --> I["原子切换:唯一默认地址"] - C -- "删除普通地址" --> J{"地址属于当前买家?"} - J -- "否" --> Y - J -- "是" --> K["删除地址并刷新列表"] - C -- "删除默认地址" --> L{"地址属于当前买家?"} - L -- "否" --> Y - L -- "是" --> M["删除地址,保持无默认地址"] - M --> N["提示买家后续下单前重新选择"] + C -- "新增" --> D{"当前地址数量已达上限?"} + D -- "是" --> D1["拒绝新增并提示已达上限"] + D -- "否" --> E["录入收件人、联系电话、省市区、详细地址和可选默认标记"] + E --> E1{"字段合法?"} + E1 -- "否" --> E2["保留输入并提示字段错误"] + E1 -- "是" --> E3["保存地址并刷新列表"] + C -- "编辑" --> F{"地址属于当前买家?"} + F -- "否" --> Y["返回不存在或无权限,不泄露归属"] + F -- "是" --> E + C -- "设默认" --> G{"地址属于当前买家?"} + G -- "否" --> Y + G -- "是" --> H["原子切换:唯一默认地址"] + C -- "删除普通地址" --> I{"地址属于当前买家?"} + I -- "否" --> Y + I -- "是" --> J["删除地址并刷新列表"] + C -- "删除默认地址" --> K{"地址属于当前买家?"} + K -- "否" --> Y + K -- "是" --> L["删除地址,保持无默认地址"] + L --> N["提示买家后续下单前重新选择"] ``` 地址约束: - 每名买家最多一个默认地址,切换操作需原子完成或提供等效一致性保障。 - 删除默认地址后不自动选择其他地址,下单时由买家明确选择。 +- 新增地址时可携带默认地址标记;在同一事务内将其他地址的默认标记取消,保证唯一性。 +- 编辑地址不允许直接修改默认标记;默认地址切换必须通过设默认动作完成。 +- 单买家地址上限暂定 20 条;超出时拒绝新增并提示已达上限。 - 同一买家对地址的读写始终按当前用户过滤,禁止仅凭资源 ID 跨用户访问。 - 地址字段变更不影响历史订单的地址快照,下单时间点确定的快照始终保留。 @@ -130,7 +140,7 @@ flowchart TD A1["并发设置多个默认地址"] --> B1["原子切换或等效机制保证最终唯一"] B1 --> N1["不出现两个默认地址"] A2["两个请求同时修改手机号"] --> B2["先成功者写入;后者按唯一性失败"] - B2 --> N2["保留当前有效登录信息"] + B2 --> N2["提升令牌版本只发生一次"] A3["敏感修改时密码错误"] --> B3["拒绝修改,不泄露账号存在性"] B3 --> N3["登录态保持有效"] A4["地址归属错误或他人地址"] --> B4["返回不存在或无权限"] @@ -140,6 +150,8 @@ flowchart TD A6["删除默认地址后未选新地址直接下单"] --> B6["M04 拒绝并提示先选择地址"] B6 --> N6["不自动选择其他地址"] A7["资料字段格式非法"] --> B7["字段级错误,保留可恢复输入"] + A8["地址数量已达上限仍尝试新增"] --> B8["拒绝新增并提示已达上限"] + A9["修改展示资料时夹带手机号或用户名"] --> B9["字段被忽略,不修改对应内容"] ``` 异常约束: @@ -147,6 +159,7 @@ flowchart TD - 任何敏感修改的失败都必须保持现有账号状态、登录态和地址不变。 - 资料和地址接口不允许通过仅凭资源 ID 跨用户访问,越权请求统一返回“资源不存在或无权限”。 - 并发修改场景下,最终结果必须保持一致;不出现“两个默认地址”或“两个不同手机号同时生效”的状态。 +- 修改展示资料不允许改变手机号、用户名、角色、状态、用户标识或头像;这些字段只能通过专门接口修改。 ## 七、与核心模块的衔接 @@ -155,7 +168,7 @@ flowchart TD | M01-02 | 已登录买家身份 | M01-03 | 资料与地址的读写权限 | | M01-03 | 当前买家地址 ID | M04 Ordering | 地址归属校验和快照数据 | | M04 | 已下单订单 | 历史订单 | 地址快照随订单持久化,不被后续修改覆盖 | -| M01-03 | 敏感修改成功 | M01-02 | 撤销修改前全部令牌,要求重新登录 | +| M01-03 | 敏感修改成功 | M01-02 | 提升账号令牌版本,使修改前签发的全部登录态失效 | | F13 M06-03 | 账号状态变更 | M01-03 | 禁用账号后不能再修改资料或地址 | 衔接约束: @@ -168,15 +181,15 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 查看本人资料 | A006 | 返回用户名、默认头像和掩码手机号 | 待交叉评审 | +| 修改手机号 | A006 | 验证当前密码、校验格式与唯一性、提升令牌版本 | 待交叉评审 | | 重置用户名 | A007 | 在限次规则内由服务端生成唯一用户名 | 待交叉评审 | -| 修改手机号 | A008 | 验证当前密码、校验格式与唯一性、撤销旧令牌 | 待交叉评审 | -| 查询本人地址 | A009 | 按当前买家分页返回地址列表与默认标记 | 待交叉评审 | -| 新增地址 | A010 | 校验字段、保存地址、刷新列表 | 待交叉评审 | -| 编辑地址 | A011 | 仅修改本人地址、重新校验字段 | 待交叉评审 | -| 删除地址 | A012 | 仅删除本人地址,删除默认地址时不自动指定其他地址 | 待交叉评审 | -| 切换默认地址 | A013 | 原子切换默认地址,保证最终唯一 | 待交叉评审 | -| F08 下单时地址归属校验与快照 | A014 | 仅返回当前买家地址快照,不修改地址 | 待交叉评审 | +| 获取本人资料 | A008 | 返回展示资料、掩码手机号和自助重置状态 | 待交叉评审 | +| 修改本人展示资料 | A009 | 维护展示名、简介等展示字段;不接受手机号、用户名、角色、状态等敏感字段 | 待交叉评审 | +| 查询本人地址列表 | A010 | 按当前买家分页返回地址列表与默认标记 | 待交叉评审 | +| 新增地址 | A011 | 校验字段、检查地址数量上限、支持默认地址标记 | 待交叉评审 | +| 编辑地址 | A012 | 仅修改本人地址、重新校验字段、不允许修改默认标记 | 待交叉评审 | +| 删除地址 | A013 | 仅删除本人地址,删除默认地址时不自动指定其他地址 | 待交叉评审 | +| 设置默认地址 | A014 | 原子切换默认地址,保证最终唯一 | 待交叉评审 | 接口仅承载本流程结果;错误码、HTTP 状态码和 `traceId` 由接口设计统一,不得反向写入业务流程。 @@ -184,15 +197,20 @@ flowchart TD 1. 用户名自助重置次数的存储位置与查询语义需要数据库和接口设计统一。 2. 地址省市区数据是否采用受控字典或开放输入,需另行评审。 -3. 修改手机号时旧令牌失效的并发场景(同一秒既有登录又有修改请求)需要在多实例下保持一致。 +3. 修改手机号时令牌版本提升的并发场景(同一秒既有登录又有修改请求)需要在多实例下保持一致。 4. 默认地址切换的“等效一致性”具体实现方式(事务、唯一约束、补偿)需在数据库设计中明确。 5. 商家或管理员尝试调用 A006~A014 的具体错误码和返回体需在接口设计中定义。 +6. 地址数量上限(20 条)的最终值需在数据库约束和接口响应中确认一致。 ## 十、验收证据清单 - [ ] 买家可以查看和修改允许变更的个人资料,并完成地址增删改查。 - [ ] 用户名唯一和自助修改次数限制生效;手机号变更后旧登录态不能继续执行敏感操作。 - [ ] 默认地址始终最多一个,删除默认地址后下单流程不会擅自选择其他地址。 +- [ ] 单买家地址数量不超过上限;超出时拒绝新增。 +- [ ] 新增地址时携带默认地址标记能在同一事务内取消旧默认地址。 +- [ ] 编辑地址不能直接修改默认地址标记;只能通过设默认动作完成切换。 +- [ ] 修改展示资料时不接受手机号、用户名、角色、状态等敏感字段。 - [ ] 买家不能访问他人地址,游客、商家和管理员不能越权调用买家资料接口。 - [ ] F08 提交订单时地址归属校验和快照写入正确;历史订单地址快照不被后续修改覆盖。 - [ ] 保存资料修改、地址 CRUD、默认地址切换和越权拦截证据。 \ No newline at end of file diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" index 6c271c7..5c957c3 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -4,14 +4,14 @@ > - 主责人:唐宇昊 > - 需求来源:[《需求规格说明书》M06-03](../../../01-需求文档/需求规格说明书.md) 的“M06-03 后台用户管理(F13)— 唐宇昊”完整七节 > - 基础核心流程:F02、M06-01 与 M06-02 的后台角色入口 -> - 直接入口:管理员通过 Policy 进入管理端;M01-02 提供登录态校验与角色判断 -> - 直接出口:M01 账号状态变更和旧令牌失效;商家发货(M06-02)和商品维护(M06-01)继续遵守新状态 +> - 直接入口:管理员在管理端入口发起账号治理;M01-02 提供登录态校验与角色判断 +> - 直接出口:M01 账号状态变更和令牌版本提升;商家发货(M06-02)和商品维护(M06-01)继续遵守新状态 > - 回归核心结果:买家订单、支付和售后事实不变;商品事实不变 > - 不得改变:管理员账号不可操作,角色字段不可修改,禁用默认商家和仍有待处理业务的商家必须被拒绝 ## 一、范围与事实来源 -本流程负责管理员分页查看买家和商家账号、按受控条件筛选、对可禁用账号执行禁用或启用,并协调旧令牌失效和默认商家保护。它不提供管理员账号管理、角色修改、提权或普通用户资料编辑。 +本流程负责管理员分页查看买家和商家账号、按受控条件筛选、对可禁用账号执行禁用或启用,并协调令牌版本提升和默认商家保护。它不提供管理员账号管理、角色修改、提权或普通用户资料编辑。 A015~A017 由本流程派生,仅在流程评审通过后用于契约映射;现有清单不能反向决定或拼接业务流程。 @@ -27,12 +27,15 @@ A015~A017 由本流程派生,仅在流程评审通过后用于契约映射 ```mermaid flowchart LR - ADM["管理员
已认证、状态正常"] -->|"管理端用户管理"| MGMT["M06-03 Admin
账号治理"] - MGMT -->|"最新账号状态 + 旧令牌失效"| M01["M01 Identity
登录态、令牌"] + ADM["管理员"] -->|"管理端用户管理"| MGMT["M06-03 Admin
账号治理"] + MGMT -->|"最新账号状态 + 令牌版本提升"| M01["M01 Identity
登录态、令牌"] MGMT -->|"商家禁用约束"| M06P["M06-01 商家商品入口"] MGMT -->|"商家禁用约束"| M06O["M06-02 商家履约入口"] MGMT -->|"查询过滤"| DB["账号与角色事实"] - BUYER["买家"] -. "尝试进入" .-> FORB["403:拒绝进入后台"] + MGMT -->|"业务归属复核"| ORD["M04 Ordering
应用契约"] + MGMT -->|"业务归属复核"| AFT["M10 AfterSales
应用契约"] + MGMT -->|"业务归属复核"| SCK["C01 Seckill
应用契约"] + BUYER["买家"] -. "尝试进入" .-> FORB["拒绝进入后台"] MERCH["商家"] -. "尝试进入" .-> FORB GUEST["游客"] -. "尝试进入" .-> NOLOG["要求登录,不返回账号列表"] MGMT -. "管理员账号不可操作" .-> NOADM["拒绝操作,不泄露管理员列表"] @@ -41,7 +44,7 @@ flowchart LR 边界约束: - M06-03 只对买家和商家账号执行禁用或启用,不允许修改角色或操作管理员账号。 -- 禁用账号必须立即撤销旧令牌;启用后旧令牌不恢复,用户必须重新登录。 +- 禁用账号必须提升账号令牌版本,使修改前签发的全部登录态失效;启用后旧登录态不恢复,用户必须重新登录。 - 商家禁用约束由 M06-03 强制执行,具体可执行操作由 M06-01 和 M06-02 依据账号状态决定。 ## 三、列表与筛选主流程 @@ -53,7 +56,7 @@ flowchart TD B -- "是" --> C["按分页和筛选条件查询账号"] C --> D{"筛选条件?"} D -- "无" --> E["返回当前页买家和商家账号"] - D -- "用户名/手机号" --> F["按已确认字段精确或前缀匹配"] + D -- "用户名/手机号关键词" --> F["按已确认字段精确或模糊匹配"] D -- "角色" --> G["按买家或商家过滤"] D -- "状态" --> H["按正常或禁用过滤"] E --> I["返回账号摘要:用户名、掩码手机号、角色、状态、注册时间"] @@ -74,28 +77,37 @@ flowchart TD ```mermaid flowchart TD - A["管理员选择目标账号并确认禁用"] --> B{"目标账号合法?"} - B -- "否" --> X["拒绝操作并提示原因"] - B -- "是" --> C{"是否默认商家?"} - C -- "是" --> X1["拒绝禁用并提示默认商家不可禁用"] - C -- "否" --> D{"目标账号是否仍有待处理业务?"} - D -- "是" --> X2["拒绝禁用并提示仍有待处理业务"] - D -- "否" --> E{"目标账号当前状态?"} - E -- "已禁用" --> Y["幂等返回当前状态,不重复产生副作用"] - E -- "正常" --> F["开启账号治理事务"] - F --> G["更新账号状态为禁用并记录操作人和时间"] - G --> H["撤销该账号在多实例中已签发的全部令牌"] - H --> I{"事务提交成功?"} - I -- "否" --> R["整体回滚,不返回虚假成功"] - I -- "是" --> J["返回最新账号状态"] - J --> K["被禁账号再次登录或使用旧令牌被拒绝"] + A[“管理员选择目标账号并确认禁用”] --> B{“目标账号合法?”} + B -- “否” --> X[“拒绝操作并提示原因”] + B -- “是” --> C[“读取目标账号当前状态并锁定本流程内的复核时刻”] + C --> D{“当前状态?”} + D -- “已禁用” --> Y[“幂等返回当前状态,不重复产生副作用”] + D -- “正常” --> E[“调用 Ordering 公开应用契约查询该商家的待支付/待履约订单”] + E --> F[“调用 AfterSales 公开应用契约查询未结束的售后申请或售后窗口”] + F --> G[“调用 Seckill 公开应用契约查询未结束的秒杀活动”] + G --> H{“是否存在待处理业务?”} + H -- “是” --> Z[“拒绝禁用,账号及业务归属保持不变,返回原因”] + Z --> Z1[“将复核失败原因写入结构化日志,等待管理员决策”] + H -- “否” --> I[“开启 PostgreSQL 事务”] + I --> J[“条件更新:仅在状态为正常且复核时刻匹配时改为禁用并提升令牌版本号”] + J --> K{“条件更新是否影响行?”} + K -- “否” --> K1[“说明从复核到提交之间账号状态被他人改变,拒绝并返回最新状态”] + K -- “是” --> L[“提交 PostgreSQL 事务”] + L --> M[“PostgreSQL 为事实,Redis 撤销集合随后失效旧登录态”] + M --> N{“Redis 撤销状态共享是否可用?”} + N -- “否” --> N1[“返回服务暂不可用,账号状态和令牌版本仍按已提交结果生效”] + N -- “是” --> O[“记录操作人、目标账号、原状态、新状态、时间和 traceId”] + O --> P[“返回最新账号状态”] + P --> Q[“被禁账号再次登录或使用旧登录态被拒绝”] ``` 禁用约束: - 默认商家和仍有待处理业务的商家必须拒绝禁用,账号及业务归属保持不变。 -- 禁用成功后必须撤销该账号已签发的全部令牌,且失效结果需在多实例之间一致。 -- 失效能力不可用时,禁用操作必须返回失败,不得返回虚假成功。 +- 业务归属复核必须在 PostgreSQL 提交之前完成,按 Ordering → AfterSales → Seckill 的顺序在流程内集中复核;任一返回”存在待处理业务”则直接拒绝,不进入 PostgreSQL。 +- 复核到 PostgreSQL 提交之间存在并发窗口:必须使用 `WHERE status = 正常 AND 复核时刻 = :readAt` 形式的条件更新,并匹配复核阶段读取的状态;条件更新不命中时说明状态已被他人改变,必须拒绝并返回最新状态,避免”复核通过却被并发修改绕过”的漏洞。 +- PostgreSQL 与 Redis 不能组成同一事务;PostgreSQL 提交后 Redis 撤销不可用时,必须返回服务暂不可用,不允许回滚已经持久化的状态变更(避免出现”禁用后又回滚导致旧登录态生效”的更大问题),同时也不允许返回虚假成功。 +- 状态变更可追踪:操作人、目标账号、原状态、新状态、时间和 `traceId` 写入结构化日志,不写入通用操作审计。 ## 五、启用账号 @@ -103,44 +115,49 @@ flowchart TD flowchart TD A["管理员选择目标账号并确认启用"] --> B{"目标账号合法?"} B -- "否" --> X["拒绝操作并提示原因"] - B -- "是" --> C{"目标账号当前状态?"} - C -- "正常" --> Y["幂等返回当前状态,不重复产生副作用"] - C -- "已禁用" --> D["开启账号治理事务"] - D --> E["更新账号状态为正常并记录操作人和时间"] - E --> F["不恢复禁用前的旧令牌,用户必须重新登录"] - F --> G{"事务提交成功?"} - G -- "否" --> R["整体回滚,保持禁用状态"] - G -- "是" --> H["返回最新账号状态"] - H --> I["用户重新登录获取新令牌"] + B -- "是" --> C["开启 PostgreSQL 事务"] + C --> D["条件更新:仅在状态为禁用时改为正常"] + D --> E{"条件更新是否影响行?"} + E -- "否且当前已正常" --> Y["回滚事务,幂等返回当前状态"] + E -- "否且当前已是其他状态" --> Y1["回滚事务,拒绝并说明原因"] + E -- "是" --> F["提交 PostgreSQL 事务"] + F --> G["不恢复任何旧登录态,账号令牌版本保持当前值"] + G --> H["记录操作人、目标账号、原状态、新状态、时间和 traceId"] + H --> I["返回最新账号状态"] + I --> J["用户重新登录获取新登录态"] ``` 启用约束: -- 启用只改变账号状态,不恢复任何旧令牌;用户必须重新登录。 +- 启用只改变账号状态,不恢复任何旧登录态;用户必须重新登录。 +- 启用不修改 Redis 撤销集合;旧登录态即使未过期也无法继续使用。 - 重复启用必须幂等,不产生相互矛盾的状态或重复副作用。 ## 六、并发、幂等与异常 ```mermaid flowchart TD - A1["两名管理员并发禁用同一账号"] --> B1["使用状态条件保证仅一次有效更新"] - B1 --> N1["其余请求返回当前最新状态"] + A1["两名管理员并发禁用同一账号"] --> B1["先读取并锁定复核时刻;后提交者条件更新不命中"] + B1 --> N1["返回当前最新状态,不重复产生副作用"] A2["同一账号在禁用与启用之间切换"] --> B2["按状态条件更新,最终状态唯一确定"] B2 --> N2["不出现两种状态同时生效"] A3["尝试修改角色或管理员账号"] --> B3["接口不接受相关字段或明确拒绝"] B3 --> N3["不返回修改后的角色"] - A4["失效能力暂时不可用"] --> B4["无法确认登录态的受保护请求提示服务暂不可用"] - B4 --> N4["禁用操作不返回虚假成功"] - A5["商家仍有未完成售后或关联订单"] --> B5["拒绝禁用并说明原因"] - B5 --> N5["不自动改派业务归属"] - A6["非管理员访问管理端接口"] --> B6["返回 401 或 403,不返回账号列表"] + A4["Redis 撤销状态共享暂时不可用"] --> B4["返回服务暂不可用"] + B4 --> N4["账号状态仍按已提交结果生效,不允许回滚"] + A5["商家在复核到提交之间新产生订单或售后"] --> B5["PostgreSQL 条件更新不命中复核时刻"] + B5 --> N5["拒绝并返回最新状态,账号及业务归属保持不变"] + A6["非管理员访问管理端入口"] --> B6["拒绝访问,不返回账号列表"] A7["筛选或分页参数非法"] --> B7["字段级错误,保留可恢复输入"] + A8["商家仍有未完成售后或关联订单"] --> B8["业务归属复核返回存在待处理业务"] + B8 --> N8["拒绝禁用并说明原因,不自动改派业务归属"] ``` 异常约束: - 任何并发变更的最终结果必须保持一致;不允许出现“禁用后又启用”或反之的中间结果被外部观察。 -- 禁用或启用操作必须记录操作人、目标账号、原状态、新状态、时间和 `traceId`,不写入密码或完整 Token。 +- 禁用或启用操作必须记录操作人、目标账号、原状态、新状态、时间和 `traceId`,不写入密码或完整登录态。 +- PostgreSQL 与 Redis 的状态变更天然不在同一事务,必须以 PostgreSQL 状态和令牌版本为安全事实;Redis 撤销集合只在 PostgreSQL 提交成功后追加。 - 重复禁用或启用必须幂等返回当前状态,不重复产生副作用。 ## 七、与核心模块的衔接 @@ -148,24 +165,26 @@ flowchart TD | 上游 | 入口事实 | 下游 | 出口结果 | |---|---|---|---| | M01-02 | 管理员登录态 | M06-03 | 后台访问权限 | -| M06-03 | 禁用/启用命令 | M01 Identity | 账号状态与令牌失效结果 | +| M06-03 | 禁用/启用命令 | M01 Identity | 账号状态与令牌版本提升 | | M06-03 | 商家禁用约束 | M06-01 | 商品维护可继续遵守账号状态 | | M06-03 | 商家禁用约束 | M06-02 | 商家发货和售后审核遵守账号状态 | +| M06-03 | 业务归属复核 | Ordering、AfterSales、Seckill 公开应用契约 | 最新待支付/待履约/售后/活动状态 | | M06-03 | 操作记录 | 管理员查询 | 操作人、时间、`traceId` 可追踪 | 衔接约束: - 商家禁用约束由 M06-03 在治理事务中校验;M06-01 和 M06-02 仍需在自身业务动作前再次校验账号状态。 -- 禁用或启用结果必须在多实例之间一致;具体失效延迟由 C10 和 M00 共同保证。 +- 禁用或启用结果必须在多实例之间一致;具体失效延迟由 C10 和 Redis 撤销能力共同保证。 - 管理员账号治理不直接修改订单、支付或售后事实;只通过账号状态影响后续业务受理。 +- 业务归属复核必须使用目标模块的公开应用契约,不能直接读取目标模块内部表。 ## 八、由流程派生的接口契约映射 | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| | 分页查询账号 | A015 | 返回买家和商家账号摘要与筛选结果 | 待交叉评审 | -| 禁用账号 | A016 | 校验可禁用条件并撤销旧令牌 | 待交叉评审 | -| 启用账号 | A017 | 校验账号并恢复状态,旧令牌不恢复 | 待交叉评审 | +| 禁用账号 | A016 | 校验可禁用条件、提升令牌版本、按 Redis 可用性返回结果 | 待交叉评审 | +| 启用账号 | A017 | 校验账号并恢复状态,旧登录态不恢复 | 待交叉评审 | 接口仅承载本流程结果;错误码、HTTP 状态码和 `traceId` 由接口设计统一,不得反向写入业务流程。 @@ -176,13 +195,16 @@ flowchart TD 3. 操作记录是否长期保留或定期归档需在数据库设计中明确,本流程只承诺最小记录字段。 4. 禁用或启用操作是否需要支持批量,本期不实现。 5. 商家禁用的“业务归属转移”本期不实现,需明确告知管理员原因。 +6. Redis 撤销不可用时的返回码(503 / AUTH.TOKEN_SERVICE_UNAVAILABLE)和前端降级策略需在接口设计中明确。 ## 十、验收证据清单 - [ ] 管理员可以分页筛选买家和商家账号,手机号默认掩码。 -- [ ] 失效能力正常时,禁用后不能再次登录,禁用前的登录凭证也不能继续访问;启用后旧凭证仍不可用,用户可重新登录。 -- [ ] 失效能力不可用时,无法确认登录态的受保护请求不得放行;恢复后多个服务实例必须得到一致结果。 +- [ ] 失效能力正常时,禁用后不能再次登录,禁用前的登录态也不能继续访问;启用后旧登录态仍不可用,用户可重新登录。 +- [ ] Redis 撤销不可用时返回服务暂不可用,不返回虚假成功;恢复后多个服务实例必须得到一致结果。 +- [ ] 业务归属复核在禁用提交前完成;存在待处理业务的商家禁用被拒绝,且不影响业务归属。 +- [ ] PostgreSQL 状态和令牌版本与 Redis 撤销集合明确分工:PostgreSQL 为安全事实,Redis 为共享加速,PostgreSQL 提交后不允许回滚。 - [ ] 买家、商家和游客不能访问管理接口,管理员账号和角色字段不能通过本模块操作。 - [ ] 重复与并发状态变更保持幂等,安全追踪记录不包含敏感凭据。 - [ ] 默认商家和存在待处理业务的商家禁用保护可复现,普通可禁用商家仍可完成禁用/启用验收。 -- [ ] 保存列表、禁用、启用、旧令牌失效、越权和角色注入拦截证据。 \ No newline at end of file +- [ ] 保存列表、禁用、启用、旧登录态失效、越权和角色注入拦截证据。 \ No newline at end of file diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" index 461da0b..9b0641f 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" @@ -11,7 +11,7 @@ ## 一、范围与事实来源 -本流程负责买家收藏商品、取消收藏、查看收藏列表、查看商品详情时记录或更新浏览时间、按上限保留历史记录并允许关闭或开启后续记录。它不提供收藏分组、分享、跨账号迁移或推荐。 +本流程负责买家收藏商品、取消收藏、查看收藏列表、查看商品详情时记录或更新浏览时间、按上限保留历史记录、清空历史、开启或关闭后续记录。它不提供收藏分组、分享、跨账号迁移或推荐。 A018~A025 由本流程派生,仅在流程评审通过后用于契约映射;现有清单不能反向决定或拼接业务流程。 @@ -73,25 +73,24 @@ flowchart TD ```mermaid flowchart TD - A["买家进入商品详情"] --> B{"历史记录开关是否开启?"} - B -- "否" --> X["不写入浏览记录"] - B -- "是" --> C{"该买家对当前商品已有浏览记录?"} - C -- "否" --> D["新增浏览记录,记录首次浏览时间"] - C -- "是" --> E["仅更新最近浏览时间,不新增记录"] - D --> F{"超过记录上限?"} - E --> F - F -- "是" --> G["移除最早一条浏览记录"] - F -- "否" --> H["保留全部记录"] - G --> I["按最近浏览时间倒序展示"] - H --> I - J["买家进入浏览历史页"] --> K["按当前买家分页返回"] - K --> L["下架商品继续展示并标记不可购买"] + A["买家成功打开已上架商品详情"] --> B{"历史记录开关是否开启?"} + B -- "否" --> X["不写入浏览记录,返回开关关闭提示"] + B -- "是" --> C["开启浏览记录写入事务"] + C --> D["按买家和商品唯一约束写入或更新最近浏览时间"] + D --> E["按稳定排序裁剪到本买家上限以内(默认 200 条)"] + E --> F["提交事务,返回本次写入结果和清理条数"] + F --> G["浏览历史列表按最近浏览时间倒序展示"] + H["买家进入浏览历史列表"] --> I{"历史记录开关是否开启?"} + I -- "否" --> I1["返回空列表,关闭不等于删除已有历史"] + I -- "是" --> J["按当前买家分页返回浏览历史"] ``` 浏览约束: -- 浏览记录上限由接口设计在评审前确认,默认值在流程中只承诺“按已确认上限保留最近记录”。 -- 历史开关关闭后不删除已有记录;重新开启后继续按规则更新最近浏览时间。 +- 浏览记录上限默认 200 条,超出后按稳定排序裁剪;裁剪必须在同一事务内完成,避免多个详情并发写入时基于旧数量判断导致超限。 +- 历史开关关闭时浏览历史列表返回空数组;调用修改开关动作可重新开启。 +- 历史开关关闭不等于删除已有历史;重新开启后继续按规则更新最近浏览时间。 +- 同一买家和同一商品最多一条浏览记录;按唯一约束写入或更新。 - 浏览历史不得影响商品事实;商品下架、库存变化或价格调整均由 M02 决定。 ## 五、收藏与浏览列表的展示 @@ -104,9 +103,11 @@ flowchart TD D -- "是" --> E["展示可售状态和进入详情/加购入口"] D -- "否" --> F["保留记录并标记不可购买"] F --> G["提供返回列表或查看历史的入口"] - H["买家进入浏览历史列表"] --> I["按最近浏览时间倒序分页"] - I --> J["读取商品摘要与销售状态"] - J --> K["下架商品标记不可购买,重复浏览不新增重复记录"] + H["买家进入浏览历史列表"] --> I{"历史记录开关是否开启?"} + I -- "否" --> I1["返回空列表"] + I -- "是" --> J["按最近浏览时间倒序分页"] + J --> K["读取商品摘要与销售状态"] + K --> L["下架商品标记不可购买,重复浏览不新增重复记录"] ``` 展示约束: @@ -124,8 +125,12 @@ flowchart TD A3["重复取消收藏"] --> B3["幂等成功,不报系统异常"] A4["商品不存在"] --> B4["拒绝新增记录,已有记录按不可用状态处理"] A5["越权访问他人记录"] --> B5["返回资源不存在或无权限,不泄露归属"] - A6["历史记录超过上限"] --> B6["移除最早记录,保证数据完整"] + A6["浏览记录超过上限"] --> B6["在同一事务内按稳定排序裁剪到上限以内"] + B6 --> N6["并发写入不会超过上限"] A7["网络或保存失败"] --> B7["恢复原状态,允许重试,不制造假成功"] + A8["历史开关关闭时仍尝试记录浏览"] --> B8["不写入新记录,不返回错误"] + A9["清空浏览历史"] --> B9["按当前买家物理删除全部历史记录"] + B9 --> N9["不影响历史开关状态"] ``` 异常约束: @@ -133,6 +138,8 @@ flowchart TD - 重复操作必须保持幂等,不产生重复记录或重复副作用。 - 商品下架不影响收藏和浏览记录的存在,但展示中必须标记不可购买。 - 任何写操作的失败都必须可重试,不留下半成功的状态。 +- 浏览记录写入与裁剪必须在同一事务内完成;不能先写入后异步清理。 +- 清空浏览历史后再次浏览商品仍按当前开关决定是否写入。 ## 七、与核心模块的衔接 @@ -153,30 +160,32 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 收藏商品 | A018 | 按买家和商品幂等写入收藏记录 | 待交叉评审 | -| 取消收藏 | A019 | 仅删除本人收藏记录,重复取消幂等 | 待交叉评审 | -| 查询收藏列表 | A020 | 按当前买家分页返回收藏与商品摘要 | 待交叉评审 | -| 记录或更新浏览时间 | A021 | 在历史开启时写入或更新最近浏览时间 | 待交叉评审 | -| 查询浏览历史列表 | A022 | 按当前买家分页返回浏览历史与商品摘要 | 待交叉评审 | -| 历史开关设置 | A023 | 控制是否继续写入浏览记录,不删除已有数据 | 待交叉评审 | -| 浏览记录上限配置读取 | A024 | 返回当前生效的浏览记录上限 | 待交叉评审 | -| 收藏状态读取 | A025 | 在商品详情或列表上返回当前买家的收藏状态 | 待交叉评审 | +| 收藏列表 | A018 | 按当前买家分页返回收藏与商品摘要 | 待交叉评审 | +| 收藏商品 | A019 | 按买家和商品幂等写入收藏记录 | 待交叉评审 | +| 取消收藏 | A020 | 仅删除本人收藏记录,重复取消幂等 | 待交叉评审 | +| 浏览历史列表 | A021 | 按当前买家分页返回浏览历史,开关关闭时返回空列表 | 待交叉评审 | +| 修改浏览记录开关 | A022 | 开启或关闭后续浏览记录写入,不删除已有历史 | 待交叉评审 | +| 清空浏览历史 | A023 | 按当前买家物理删除全部历史记录,不影响开关状态 | 待交叉评审 | +| 记录浏览历史 | A024 | 在历史开启时写入或更新最近浏览时间,并按稳定排序裁剪到上限以内 | 待交叉评审 | +| 查询浏览记录开关 | A025 | 返回当前开关值,用于前端初始化控件状态 | 待交叉评审 | 接口仅承载本流程结果;错误码、HTTP 状态码和 `traceId` 由接口设计统一,不得反向写入业务流程。 ## 九、由流程反查出的接口与数据待评审项 -1. 浏览记录上限默认值需在接口设计中明确,本流程只承诺“按已确认上限保留”。 -2. 历史开关默认值(开启/关闭)需要在接口和数据库设计评审中确认。 +1. 浏览记录上限默认值(200 条)需要在数据库约束和接口响应中确认一致。 +2. 历史开关默认值(开启)需在接口和数据库设计评审中确认。 3. 下架商品的浏览历史是否需要自动清理,由 Catalog 与本流程协商,本期默认保留。 4. 收藏与浏览列表的分页上限需统一,避免出现无分页返回。 -5. A025 在商品详情或列表上的展示语义需与 M02 协作确认,避免与公开摘要口径冲突。 +5. 列表展示中下架商品标记的字段口径需与 M02 协作确认。 ## 十、验收证据清单 -- [ ] 收藏、取消收藏、收藏列表、浏览记录和历史列表均正常工作。 +- [ ] 收藏、取消收藏、收藏列表、浏览记录、历史列表、清空历史和开关切换均正常工作。 - [ ] 重复收藏、重复浏览和重复取消满足幂等要求,不产生重复记录。 - [ ] 收藏和历史按最近时间倒序,数据严格按买家隔离。 - [ ] 下架商品仍保留记录并明确不可购买;游客、商家和管理员不能越权访问。 -- [ ] 浏览记录超过上限后移除最早记录;历史开关切换不影响已有记录。 -- [ ] 保存正常操作、重复操作、下架占位、记录上限和跨账号隔离证据。 \ No newline at end of file +- [ ] 浏览记录超过上限后按稳定排序在同一事务内裁剪,并发写入不会超过上限。 +- [ ] 历史开关关闭时浏览历史列表返回空列表;重新开启后恢复写入。 +- [ ] 清空浏览历史不影响开关状态,清空后再次浏览按开关决定是否写入。 +- [ ] 保存正常操作、重复操作、下架占位、记录上限、并发裁剪、开关切换和跨账号隔离证据。 \ No newline at end of file -- Gitee From 35d678c22e1624aba9a3c577d9ab5f495d3f6d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A1=BE=E6=AC=A3=E6=9C=88?= Date: Fri, 24 Jul 2026 16:23:53 +0800 Subject: [PATCH 066/118] =?UTF-8?q?fix(process):=20gxy=20=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E7=BC=96=E5=8F=B7=E3=80=81Mermaid=E8=A7=84=E8=8C=83?= =?UTF-8?q?=E3=80=81=E5=85=AC=E5=BC=80=E8=BA=AB=E4=BB=BD=E4=B8=8E=E8=AF=84?= =?UTF-8?q?=E4=BB=B7=E8=B6=8A=E7=95=8C=E6=BC=8F=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - M02:接口编号 A0xx → A101~A108;公开浏览不再因令牌失效而拒绝,401/403 仅作用于受保护写操作;Mermaid 节点剔除 IProductSearch / Cache-Aside 等技术细节 - M06-01:接口编号 A2xx → A111~A117;Mermaid 节点剔除 Cache-Aside、pg_trgm/GIN;并发保护方式移至待评审项 - M07:接口编号 A3xx → A121~A125;删除'订单项变为已评价'等越界表述,评价事实改为由 Review 唯一评价记录派生;Mermaid 节点清理 - C04:Mermaid 节点统一为业务动作(搜索索引 / 倒排索引 / 基础模糊查询基线);复用 M02 A105 契约;降级口径收紧 依据本轮漏洞评审与命名规范要求重排,待 Cart/Ordering/Identity/C07 主责确认边界。 --- ...34\347\264\242\346\265\201\347\250\213.md" | 44 ++++++------- ...06\345\223\201\346\265\201\347\250\213.md" | 62 +++++++++++-------- ...41\347\220\206\346\265\201\347\250\213.md" | 30 ++++----- ...04\344\273\267\346\265\201\347\250\213.md" | 34 +++++----- 4 files changed, 93 insertions(+), 77 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" index 4bc1e57..cce33cc 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" @@ -14,7 +14,7 @@ C04 在 M02-01 的基础搜索入口上增加中文分词模糊搜索、多条 本期使用字符 N-gram 等效分词和 PostgreSQL 倒排索引完成中文模糊搜索,不引入独立搜索引擎。本期不实现个性化排序、搜索广告、热词榜、搜索审核、同义词词典或后台全状态商品检索。 -本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。C04 不新增 Axxx 接口编号,而是替换 M02-01 列表查询的底层搜索实现。 +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。C04 不新增独立 Axxx 接口编号,而是替换 M02-01 列表查询(A101~A108)的底层搜索实现,对外接口契约完全沿用 M02 的 A105 等编号。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| @@ -28,11 +28,11 @@ C04 在 M02-01 的基础搜索入口上增加中文分词模糊搜索、多条 ```mermaid flowchart LR - UI["购物端商品列表与搜索页"] -->|"统一搜索契约 IProductSearch"| AD["C04 搜索适配器"] - AD -->|"字符 N-gram 与倒排索引"| DB["PostgreSQL 商品事实与 pg_trgm/GIN 索引"] + UI["购物端商品列表与搜索页"] -->|"调用统一搜索能力"| AD["C04 进阶搜索实现"] + AD -->|"使用中文分词与倒排索引召回"| DB["PostgreSQL 商品事实"] AD -->|"返回口径一致的搜索结果"| UI - BASE["M02-01 基础模糊查询(LIKE/ILIKE)"] -. "降级回退" .-> AD - CACHE["C07 缓存(Cache-Aside)"] -. "读取前缓存" .-> AD + BASE["M02-01 基础模糊查询"] -. "降级回退" .-> AD + CACHE["C07 性能缓存层"] -. "读取前缓存" .-> AD M06["M06-01 商家商品事务"] -->|"商品事实与索引同步"| DB AD -->|"调用方读取最新已上架商品"| LIST["M02-01 列表与 F04 公开浏览"] @@ -58,12 +58,12 @@ flowchart LR ```mermaid flowchart TD - A["用户输入中文关键词并按需选择分类、价格、库存和排序"] --> B["服务端校验输入并调用统一搜索契约 IProductSearch"] - B --> C["搜索适配器对关键词进行字符 N-gram 等效分词"] + A["用户输入中文关键词并按需选择分类、价格、库存和排序"] --> B["服务端校验输入并调用统一搜索能力"] + B --> C["搜索实现对关键词进行中文分词"] C --> D["按分词结果召回候选商品"] D --> E{"是否触发进阶查询?"} - E -- "是" --> F["基于 pg_trgm/GIN 倒排索引完成相关度排序"] - E -- "否" --> BAS["回退基础模糊查询(LIKE/ILIKE)"] + E -- "是" --> F["基于倒排索引完成相关度排序"] + E -- "否" --> BAS["回退基础模糊查询"] F --> G["应用多条件筛选:关键词、分类、价格区间、仅看有货、已上架"] BAS --> G G --> H{"结果是否需要再过滤已上架状态?"} @@ -88,7 +88,7 @@ flowchart TD ```mermaid flowchart TD A["M06-01 商家商品事务提交"] --> B["商品事实写入 PostgreSQL"] - B --> C["pg_trgm/GIN 数据库索引随商品数据同步更新"] + B --> C["搜索索引随商品数据同步更新"] C --> D["下一次搜索查询使用最新商品状态和检索文本"] D --> E{"下架商品是否仍出现在索引中?"} E -- "是" --> F["结果过滤阶段强制按已上架状态过滤"] @@ -106,11 +106,11 @@ flowchart TD ```mermaid flowchart TD - A["搜索请求进入 IProductSearch 适配器"] --> B{"进阶搜索是否可用?"} + A["搜索请求进入搜索实现"] --> B{"进阶搜索是否可用?"} B -- "是" --> C["执行进阶查询并返回结果"] B -- "否" --> D["记录降级原因(索引缺失、查询失败等)"] D --> E{"基础模糊查询能否保证已上架过滤和参数安全?"} - E -- "是" --> F["执行基础模糊查询(LIKE/ILIKE)"] + E -- "是" --> F["执行基础模糊查询"] E -- "否" --> G["返回明确提示稍后重试,不返回错误数据"] F --> H["返回结果前再次过滤已上架商品"] H --> I["分页返回数据并标注降级原因"] @@ -130,7 +130,7 @@ flowchart TD flowchart TD A["生成不少于 10000 条商品演示数据"] --> B["固定查询词、筛选条件和并发参数"] B --> C["使用 50 并发持续 60 秒执行进阶搜索"] - C --> D["使用 50 并发持续 60 秒执行 LIKE/ILIKE 基线"] + C --> D["使用 50 并发持续 60 秒执行基础模糊查询基线"] D --> E["汇总成功率、P95、QPS、CPU 和 I/O"] E --> F{"进阶搜索对比基线是否满足目标?"} F -- "是" --> G["记录原始数据、环境参数和命令"] @@ -208,9 +208,9 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 中文分词模糊搜索 | 复用 M02-01 列表接口底层 | 按统一搜索契约返回与基础模糊查询同口径的结果 | 待交叉评审 | -| 多条件筛选与排序 | 复用 M02-01 列表接口参数 | 关键词、分类、价格、库存和排序组合生效 | 待交叉评审 | -| 进阶搜索降级 | 由 M02-01 接口返回结果 | 降级时返回结果与原因日志,不改变公开口径 | 待交叉评审 | +| 中文分词模糊搜索 | 复用 M02-01 A105 搜索接口 | 按统一搜索契约返回与基础模糊查询同口径的结果 | 待交叉评审 | +| 多条件筛选与排序 | 复用 M02-01 A101/A105 接口参数 | 关键词、分类、价格、库存和排序组合生效 | 待交叉评审 | +| 进阶搜索降级 | 由 M02-01 A105 接口返回结果 | 降级时返回结果与原因日志,不改变公开口径 | 待交叉评审 | | 性能对比压测 | 不通过业务接口暴露 | 保留测试脚本与原始结果 | 待交叉评审 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 @@ -227,8 +227,8 @@ flowchart TD ## 十一、由流程反查出的接口与数据待评审项 1. 字符 N-gram 的最小长度和最大长度参数需要在数据库设计中明确,避免过短导致误命中或过长导致索引过大。 -2. pg_trgm/GIN 索引的具体列(商品名称、分类名称、描述)需要在数据库设计中确认是否全列建立或部分建立。 -3. 相关度排序的“相同条件下顺序应稳定”需要明确次级排序字段,建议在数据库设计中统一。 +2. 倒排索引的具体列(商品名称、分类名称、描述)需要在数据库设计中确认是否全列建立或部分建立;具体索引类型(pg_trgm/GIN 等)由数据库设计统一约定,本流程图与文字不重复枚举。 +3. 相关度排序的”相同条件下顺序应稳定”需要明确次级排序字段,建议在数据库设计中统一。 4. 进阶搜索降级是否需要在响应中携带降级原因字段,需要与前端展示要求对齐。 5. 性能对比环境的固定参数(CPU、内存、PostgreSQL 配置、连接池大小)需要在执行前统一记录,避免环境差异影响结论。 6. 缓存命中与索引缺失同时发生时是否需要主动清除缓存,避免长时间返回错误结果。 @@ -255,9 +255,11 @@ flowchart TD - [ ] Mermaid 图内部包含直接上游模块输入(M02-01、M06-01)和直接下游模块出口(F04、F06)。 - [ ] 主流程、降级分支、索引更新与一致性、性能对比齐全。 - [ ] 状态名称与需求规格说明书一致;没有新增商品状态或排序项。 -- [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径或 DTO 代替流程;没有自行发明接口编号、字段名或数据库状态码。 +- [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径、DTO、技术模式名称(如 N-gram、pg_trgm/GIN、LIKE/ILIKE)或 SQL 片段代替流程;没有自行发明接口编号、字段名或数据库状态码。 +- [ ] C04 不另起独立接口,复用 M02-01 A105 搜索接口契约;进阶实现替换时参数白名单与返回口径保持一致。 +- [ ] 降级不是静默失败:日志或可观测性指标需暴露降级原因,不在前端构造虚假”全部成功”反馈。 - [ ] 已对照根文档 3.3 节中搜索接入点校准入口位置,C04 不另起一套主链路。 -升级到“待交叉评审”的条件:自检完成、主流程与降级分支齐全、性能压测脚本可执行、第六章“结果回填位”保留、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 +升级到”待交叉评审”的条件:自检完成、主流程与降级分支齐全、性能压测脚本可执行、第六章”结果回填位”保留、Mermaid 节点已剔除技术细节、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 -升级到“已确认”的条件:Catalog 主责确认统一搜索契约与参数白名单,Ordering 主责确认下单重读不受搜索实现影响,C07 主责确认缓存失效责任,根文档 3.3 节中搜索接入点对应追踪项成熟度同步更新;性能压测结果按口径填入第六章“结果回填位”。 \ No newline at end of file +升级到”已确认”的条件:Catalog 主责确认统一搜索契约与参数白名单,Ordering 主责确认下单重读不受搜索实现影响,C07 主责确认缓存失效责任,根文档 3.3 节中搜索接入点对应追踪项成熟度同步更新;性能压测结果按口径填入第六章”结果回填位”。 \ No newline at end of file diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" index 2281805..c6ac561 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" @@ -12,7 +12,7 @@ 本模块负责购物端商品发现入口,覆盖分类筛选、关键词搜索、价格区间、库存条件、排序与组合查询,以及商品详情页的信息展示、可售状态判断、买家/游客操作衔接和评价入口衔接。它不承接商家后台的商品维护(属于 M06-01),不替代购物车的库存和归属校验,也不修改商品在历史订单中的快照。 -本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。A0xx 只用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。Catalog 接口编号落在 A101~A120 范围(M02 公开浏览 A101~A108,M06-01 后台写操作 A111~A120);该编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| @@ -28,10 +28,10 @@ ```mermaid flowchart LR - ID["M01 Identity
已认证买家、游客角色识别"] -->|"BuyerOnly/游客均允许"| CAT["M02 Catalog
已上架商品、分类、实时价格与库存"] + ID["M01 Identity
已认证买家、游客角色识别"] -->|"公开浏览对游客和买家均开放"| CAT["M02 Catalog
已上架商品、分类、实时价格与库存"] ADM["M06-01 商家后台商品管理"] -->|"分类与商品维护命令(事务提交后)"| CAT - CACHE["C07 Cache-Aside
缓存命中则直返
事务提交后失效"] -. "读取前缓存" .-> CAT - SEARCH["C04 进阶搜索适配器
IProductSearch"] -. "替换 F05 底层实现" .-> CAT + CACHE["C07 性能缓存层
命中则直返
商品事务提交后失效"] -. "读取前缓存" .-> CAT + SEARCH["C04 进阶搜索实现
替换 F05 底层查询"] -. "替换 F05 底层实现" .-> CAT CAT -->|"已上架商品与实时价格库存"| CART["M03 Cart 加购与失效标记"] CAT -->|"下单重读与库存条件更新"| ORD["M04 Ordering"] @@ -42,20 +42,21 @@ flowchart LR CART -. "收藏、加购或购买意图从详情页进入" .-> CAT REV["M07 商品评价"] -->|"公开评价汇总与列表"| DET - ID -->|"账号禁用、令牌失效或角色越权"| X["拒绝访问,不返回商品数据"] + ID -->|"账号禁用或角色越权"| X["按身份禁止越权操作"] ADM -->|"草稿、下架或已删除商品"| Y["购物端不得出现在公开浏览结果中"] CAT -->|"商品不存在、已下架或库存归零"| Z["详情显示不可售,不提供购买入口"] - CACHE -->|"缓存不可用或数据陈旧"| W["回退 PostgreSQL 直读,不返回旧值"] + CACHE -->|"缓存不可用或数据陈旧"| W["回退事实源直读,不返回旧值"] SEARCH -->|"进阶查询失败或索引损坏"| V["回退基础模糊查询并记录降级原因"] ``` 边界约束: - 公开浏览只暴露已上架商品;草稿、下架或已删除商品不得通过搜索参数绕过。 +- **公开浏览对游客和买家均开放**:携带过期或无效令牌访问公开商品接口时,按游客处理并正常返回商品数据,不得因令牌状态拒绝。401/403 仅在受保护写操作(收藏、加购、购买、评价提交)出现。 - 购物端的价格和库存只能作为浏览口径,下单与购物车写入必须由服务端在 M03、M04 中重新校验。 - 商品模块不直接访问用户私有数据;买家专属操作(收藏、加购、购买)由 M08、M03、M04 提供,商品模块只提供事实输入和入口。 - 评价(X01)汇总来自 M07,商品详情只做公开读取,不修改评价事实。 -- C07 缓存只能放在事实查询路径之前;缓存失效或不可用时,必须回退到 PostgreSQL 直读,不返回旧数据冒充成功;缓存写入与失效由缓存主责统一约定,本文不擅自规定 TTL。 +- C07 缓存只能放在事实查询路径之前;缓存失效或不可用时,必须回退到事实源(PostgreSQL)直读,不返回旧数据冒充成功;缓存写入与失效由缓存主责统一约定,本文不擅自规定 TTL。 - C04 在 M02-01 列表入口上替换底层搜索实现,对外参数白名单、已上架过滤和返回口径与本文保持一致;接口与数据待评审项在第十章集中登记。 - 扩展完成后回到的核心结果:F04 公开列表仍只返回已上架商品、F05 关键词查询仍按统一搜索契约返回、F06 详情仍按服务端最新状态展示。扩展失败不能改变上述核心结果。 @@ -160,15 +161,17 @@ flowchart TD | 场景 | M02 处理 | 最终状态/责任 | |---|---|---| -| 游客、商家或管理员无越权入口 | 按角色提供或隐藏入口 | 服务端始终按 JWT 和 Policy 校验 | -| 商品不存在 | 返回“商品不存在”,提供返回列表入口 | 不暴露内部异常 | -| 商品已下架或被删除 | 显示“暂不可售”,禁用购买 | 历史订单快照仍可读 | +| 公开浏览携带过期/无效令牌 | 按游客处理并正常返回公开商品数据 | 公开接口不依赖有效令牌 | +| 公开浏览携带账号禁用令牌 | 按游客处理并正常返回公开商品数据;保护写操作时返回 401 | 公开接口与受保护接口分开校验 | +| 商家或管理员在购物端尝试越权操作 | 服务端按 Policy 拒绝;前端隐藏入口不替代后端 | 401/403 由对应模块返回 | +| 商品不存在 | 返回”商品不存在”,提供返回列表入口 | 不暴露内部异常 | +| 商品已下架或被删除 | 显示”暂不可售”,禁用购买 | 历史订单快照仍可读 | | 库存为零或数量超限 | 显示售罄或拒绝购买 | 由 M03/M04 决定是否调大或重新选择 | | 关键词、分类、价格或排序非法 | 字段级错误,保留查询条件 | 不执行查询 | | 图片加载失败 | 使用占位图 | 不阻断价格、库存和描述浏览 | | 加载失败或网络中断 | 保留当前页面,允许重试 | 不把旧缓存价格当作最新价格 | | 商品在浏览瞬间被下架 | 服务端按最新状态返回不可售 | 不复用缓存 | -| C07 缓存失效或不可用 | 回退 PostgreSQL 直读 | 不返回旧值冒充成功 | +| C07 缓存失效或不可用 | 回退事实源直读 | 不返回旧值冒充成功 | ## 八、由流程派生的接口契约映射 @@ -176,16 +179,18 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 查询商品列表与组合筛选 | A0xx Catalog 列表 | 强制已上架过滤、分页、白名单排序、组合筛选和空结果正常返回 | 待交叉评审 | -| 查询商品详情 | A0xx Catalog 详情 | 仅返回当前已上架商品的最新价格、库存、图片和描述 | 待交叉评审 | -| 查询有效分类 | A0xx Catalog 分类 | 返回购物端筛选入口使用的有效分类,停用分类不出现在筛选入口 | 待交叉评审 | -| 评价公开汇总与列表(X01 衔接) | 由 M07 派生 | 商品详情只读取 M07 公开结果,不在此模块内实现评价提交 | 待交叉评审 | +| 查询商品列表与组合筛选 | A101 Catalog 列表 | 强制已上架过滤、分页、白名单排序、组合筛选和空结果正常返回 | 待交叉评审 | +| 查询商品详情 | A102 Catalog 详情 | 仅返回当前已上架商品的最新价格、库存、图片和描述 | 待交叉评审 | +| 查询有效分类 | A103 Catalog 分类 | 返回购物端筛选入口使用的有效分类,停用分类不出现在筛选入口 | 待交叉评审 | +| 查询筛选条件元数据 | A104 Catalog 筛选 | 返回价格区间、排序项白名单、库存条件等元数据 | 待交叉评审 | +| 关键词搜索 | A105 Catalog 搜索 | F05 基础模糊查询与 C04 进阶实现共用同一接口契约 | 待交叉评审 | +| 公开评价汇总与列表(X01 衔接) | 由 M07 派生(A121~) | 商品详情只读取 M07 公开结果,不在此模块内实现评价提交 | 待交叉评审 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 ## 九、扩展接入边界 -- C07 缓存:在商品详情和分类/列表查询路径前使用 Cache-Aside 读取;M06-01 提交商品事务后失效缓存,TTL 与主动失效策略由缓存主责人统一确认。PostgreSQL 仍是事实来源;缓存不可用时回退数据库直读,不掩盖错误。 +- C07 缓存:在商品详情和分类/列表查询路径前使用缓存层读取公开商品;M06-01 提交商品事务后失效缓存,TTL 与主动失效策略由缓存主责人统一确认。PostgreSQL 仍是事实来源;缓存不可用时回退数据库直读,不掩盖错误。 - C04 中文搜索:在 M02-01 列表查询入口上替换底层搜索实现,返回口径与基础模糊查询一致;公开浏览口径、参数白名单和已上架过滤不变。 - M03 购物车:只接收本模块输出的已上架商品与实时价格库存;下架或库存归零由 M03 标记失效,不反向修改商品状态。 - M04 订单:下单时由 M04 重读本模块的最新事实进行条件扣减,不信任购物端传入的金额和库存。 @@ -193,14 +198,15 @@ flowchart TD ## 十、由流程反查出的接口与数据待评审项 -1. 列表与详情对已上架过滤必须服务端强制;接口需要确认是否在响应中显式携带“不可售原因”或仅按 HTTP 状态码区分,由 M02 与接口设计共同决定。 -2. 排序白名单字段集尚未在需求中枚举;接口设计前需要 M02 与评审人员确认价格、时间、相关度的默认与可选顺序。 -3. 图片合规校验在前端完成上传限制后仍需服务端再次校验;接口字段需要明确“主图”与“附加图”的上传顺序和替换规则。 -4. 商品详情是否暴露最新库存数或仅暴露“有货/无货”摘要,由需求决定展示口径;接口返回字段需要和前端展示要求对齐。 -5. 评价公开汇总字段(平均分、总条数的计算时机)与缓存策略相关,需要与 M07、C07 共同确认。 -6. C04 进阶搜索替换 F05 基础模糊查询时,需要保留旧接口的请求字段和返回口径,避免前端按字段名硬编码查询逻辑。 -7. DBxxx 分类/商品表字段尚未形成可实施的完整定义,数据库字段、约束、索引和 GIN/pg_trgm 维护策略必须由数据库设计任务另行确认。 -8. C07 的 TTL 与主动失效上限需要在本流程评审前完成,避免缓存值与商品最新事实长期不一致。 +1. 公开浏览接口(A101~A103、A105)必须明确”无登录或令牌失效时按游客返回”的契约;接口设计需与 M01 的 JWT 鉴权边界统一,避免公开接口误判为受保护接口。 +2. 列表与详情对已上架过滤必须服务端强制;接口需要确认是否在响应中显式携带”不可售原因”或仅按 HTTP 状态码区分,由 M02 与接口设计共同决定。 +3. 排序白名单字段集尚未在需求中枚举;接口设计前需要 M02 与评审人员确认价格、时间、相关度的默认与可选顺序。 +4. 图片合规校验在前端完成上传限制后仍需服务端再次校验;接口字段需要明确”主图”与”附加图”的上传顺序和替换规则。 +5. 商品详情是否暴露最新库存数或仅暴露”有货/无货”摘要,由需求决定展示口径;接口返回字段需要和前端展示要求对齐。 +6. 评价公开汇总字段(平均分、总条数的计算时机)与缓存策略相关,需要与 M07、C07 共同确认。 +7. C04 进阶搜索替换 F05 基础模糊查询时,需要保留旧接口(A105)的请求字段和返回口径,避免前端按字段名硬编码查询逻辑。 +8. DBxxx 分类/商品表字段尚未形成可实施的完整定义,数据库字段、约束、索引和 GIN/pg_trgm 维护策略必须由数据库设计任务另行确认。 +9. C07 的 TTL 与主动失效上限需要在本流程评审前完成,避免缓存值与商品最新事实长期不一致。 ## 十一、验收证据清单 @@ -222,9 +228,11 @@ flowchart TD - [ ] Mermaid 图内部包含直接上游模块输入(M01、M06-01)和直接下游模块出口(M03、M04、M07)。 - [ ] 主流程、拒绝分支、失败分支和最终结果齐全;扩展不破坏核心权限、金额、库存、快照和事实来源。 - [ ] 状态名称与需求规格说明书一致;没有新增核心商品状态。 -- [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径或 DTO 代替流程;没有自行发明接口编号、字段名或数据库状态码。 +- [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径、DTO、技术模式名称(如 Cache-Aside)或 SQL 片段代替流程;没有自行发明接口编号、字段名或数据库状态码。 +- [ ] 公开浏览接口不因令牌失效而拒绝;身份逻辑与 M01 Policy 边界一致。 +- [ ] 接口编号落在 Catalog 范围 A101~A120(M02 占 A101~A108,M06-01 占 A111~A120,M07 占 A121~A128),不混用其他模块编号。 - [ ] 已对照根文档 3.3 校准主流程、状态机和模块出入口,与 M06-01 边界一致。 -升级到“待交叉评审”的条件:自检完成、主流程与异常分支齐全、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 +升级到”待交叉评审”的条件:自检完成、主流程与异常分支齐全、公开身份逻辑已对齐 M01、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 -升级到“已确认”的条件:直接协作人(Cart、Ordering、Identity)共同确认边界,C07 主责确认缓存接入边界,根文档 3.3 中对应追踪项成熟度同步更新。 \ No newline at end of file +升级到”已确认”的条件:直接协作人(Cart、Ordering、Identity)共同确认边界,C07 主责确认缓存接入边界,根文档 3.3 中对应追踪项成熟度同步更新。 \ No newline at end of file diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" index dfdd101..b9dccdd 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -14,7 +14,7 @@ 本模块不包含多商家数据隔离、批量导入导出、定时上架、复杂审批流、商品操作审计功能或管理员代商家修改商品。 -本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。A2xx 只用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。M06-01 商家写操作接口编号落在 A111~A120 范围,与 M02 公开浏览 A101~A108、M07 评价 A121~A128 同属 Catalog/Review 编号段(A101~A200);该编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| @@ -34,8 +34,8 @@ flowchart LR CAT -->|"最新商品销售状态、分类与价格库存"| LIST["F04~F06 购物端浏览"] CAT -->|"商品事实被 M06-01 修改"| CART["M03 Cart 失效条目重检"] CAT -->|"下单重读与库存条件更新"| ORD["M04 Ordering"] - CAT -. "事务提交后事件" .-> CACHE["C07 缓存失效与重建
(缓存主责统一执行)"] - CAT -. "事务提交后数据库索引同步" .-> SEARCH["C04 中文搜索索引
(pg_trgm/GIN 由 PG 同步)"] + CAT -. "事务提交后事件" .-> CACHE["C07 性能缓存层
缓存主责统一失效与重建"] + CAT -. "事务提交后索引同步" .-> SEARCH["C04 进阶搜索索引
由事实源数据库同步维护"] ID -->|"游客、买家、管理员或账号禁用"| X["403 或 401,拒绝后台访问"] ADM -->|"字段非法或并发冲突"| Y["拒绝保存并保留表单内容"] @@ -51,7 +51,7 @@ flowchart LR - 商品名称、有效分类、当前价格、库存和主图为必填;价格不得为负,库存必须为非负整数。 - 商品事务提交后才允许触发缓存失效与搜索索引同步;事务失败时不发起任何外部动作。 - 删除约束:存在历史订单关联时禁止破坏性删除,由系统建议改为下架。 -- 并发保护:编辑与上下架使用并发标记(version/etag)或 `WHERE updated_at` 条件更新,与 M04 订单并发口径由接口设计统一。 +- 并发保护:编辑与上下架使用并发标记(version/etag)或数据库条件更新防止静默覆盖,与 M04 订单并发口径由接口设计统一;具体技术实现见接口与数据库设计。 - 缓存与索引责任划分:缓存失效与重建由 C07 主责统一执行,本模块只提交“商品事务已提交”信号;搜索索引由 PostgreSQL 同步维护,本模块不创建独立同步任务。 - 扩展完成后回到的核心结果:F04~F06 公开浏览口径、M02 商品销售状态机、M03 购物车失效标记契约、M04 下单重读条件均不变;商家写操作不能绕过这些核心结果。 @@ -201,13 +201,13 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 商家分页查询商品 | A2xx 商品后台列表 | 按商家身份过滤、分页、关键词、分类和上下架状态组合查询 | 待交叉评审 | -| 新建商品 | A2xx 商品创建 | 校验字段、分类、图片和并发状态,事务内保存商品事实 | 待交叉评审 | -| 编辑商品 | A2xx 商品编辑 | 并发保护、字段校验、事务保存并返回最新商品事实 | 待交叉评审 | -| 上下架切换 | A2xx 商品上下架 | 校验上架完整性;事务内条件更新销售状态 | 待交叉评审 | -| 商品后台删除 | A2xx 商品删除 | 仅允许无历史关联时物理删除;存在历史订单时返回拒绝并建议下架 | 待交叉评审 | -| 分类查询与维护 | A2xx 分类维护 | 提供分类列表、新增、编辑、启停和受限删除 | 待交叉评审 | -| 商品图片上传 | A2xx 商品图片 | 按合规要求上传到对象存储,返回主图与附加图标识 | 待交叉评审 | +| 商家分页查询商品 | A111 商品后台列表 | 按商家身份过滤、分页、关键词、分类和上下架状态组合查询 | 待交叉评审 | +| 新建商品 | A112 商品创建 | 校验字段、分类、图片和并发状态,事务内保存商品事实 | 待交叉评审 | +| 编辑商品 | A113 商品编辑 | 并发保护、字段校验、事务保存并返回最新商品事实 | 待交叉评审 | +| 上下架切换 | A114 商品上下架 | 校验上架完整性;事务内条件更新销售状态 | 待交叉评审 | +| 商品后台删除 | A115 商品删除 | 仅允许无历史关联时物理删除;存在历史订单时返回拒绝并建议下架 | 待交叉评审 | +| 分类查询与维护 | A116 分类维护 | 提供分类列表、新增、编辑、启停和受限删除 | 待交叉评审 | +| 商品图片上传 | A117 商品图片 | 按合规要求上传到对象存储,返回主图与附加图标识 | 待交叉评审 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 @@ -250,9 +250,11 @@ flowchart TD - [ ] Mermaid 图内部包含直接上游模块输入(M01)和直接下游模块出口(M02、M03、M04、C07、C04)。 - [ ] 主流程、拒绝分支、失败分支和最终结果齐全;并发保护与缓存失效责任划分清晰。 - [ ] 状态名称与需求规格说明书一致;没有新增核心商品状态。 -- [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径或 DTO 代替流程;没有自行发明接口编号、字段名或数据库状态码。 +- [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径、DTO、技术模式名称(如 Cache-Aside、pg_trgm/GIN)或 SQL 片段代替流程;没有自行发明接口编号、字段名或数据库状态码。 +- [ ] 接口编号落在 Catalog 范围 A101~A120(M06-01 占 A111~A120),不混用其他模块编号。 +- [ ] MerchantOnly 鉴权链路由 M01 提供,M06-01 不重复定义角色判断规则。 - [ ] 已对照根文档 3.7 校准后台角色与操作边界,与 M06-02/M06-03 边界一致。 -升级到“待交叉评审”的条件:自检完成、主流程与异常分支齐全、并发与缓存责任划分清楚、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 +升级到”待交叉评审”的条件:自检完成、主流程与异常分支齐全、并发与缓存责任划分清楚、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 -升级到“已确认”的条件:Identity 主责确认 MerchantOnly 鉴权链路,C07 主责确认缓存失效责任,M02 与 M03 主责确认下游事实回退口径,根文档 3.7 中对应追踪项成熟度同步更新。 \ No newline at end of file +升级到”已确认”的条件:Identity 主责确认 MerchantOnly 鉴权链路,C07 主责确认缓存失效责任,M02 与 M03 主责确认下游事实回退口径,根文档 3.7 中对应追踪项成熟度同步更新。 \ No newline at end of file diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" index a7ba5fe..ce0d3e4 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" @@ -14,7 +14,7 @@ 本模块不包含追评、评价点赞、买家自删、匿名评价、商家回复或隐藏、自动内容审核和评价运营后台。 -本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。A3xx 只用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。M07 评价接口编号落在 A121~A128 范围,与 M02 公开浏览 A101~A108、M06-01 商家写操作 A111~A120 同属 Catalog/Review 编号段(A101~A200);该编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| @@ -31,7 +31,7 @@ flowchart LR ID["M01 Identity
BuyerOnly 认证与账号状态"] -->|"BuyerOnly + 账号正常"| RV["M07 Review
本人已完成订单项的评价"] ORD["M04 Ordering
Completed 订单项与归属事实"] -->|"本人订单项归属与完成状态"| RV DET["F06 商品详情"] -->|"读取公开评价与评分汇总"| RV - CACHE["C07 Cache-Aside
评价读取缓存"] -. "读取前缓存" .-> RV + CACHE["C07 评价读取缓存"] -. "读取前缓存" .-> RV RV -->|"本人评价记录"| ORD RV -->|"公开评价与评分汇总"| DET @@ -49,6 +49,7 @@ flowchart LR - 同一订单项只能形成一条评价,数据库唯一约束或等效机制必须作为最终保障。 - 评价公开展示时不得返回手机号、邮箱、内部用户标识等不必要的敏感信息。 - 公开展示名在提交评价时形成脱敏快照;用户以后修改资料不改变历史评价展示,也不得为评价列表逐条查询用户资料。 +- **M07 不修改 M04 订单项状态**:"订单项是否已评价"由 M07 的唯一评价事实派生(按订单项 ID 关联查询得到是否已有评价记录);订单项本身的状态机只由 M04 维护,不允许 Review 越界修改 Ordering 的内部订单项状态。 - 商品详情只读取公开评价,不在本模块内实现评价提交。 - 缓存只能放在公开评价读取路径之前;评价事务提交后由缓存主责统一失效,缓存不可用时回退数据库直读,不返回旧数据。 - 扩展完成后回到的核心结果:F06 商品详情仍只读取公开评价,F09 Completed 订单项状态保持不变,订单快照和支付事实不被评价结果覆盖。 @@ -57,19 +58,19 @@ flowchart LR ```mermaid flowchart TD - A["买家进入订单详情"] --> B{"订单项已完成且未评价?"} + A["买家进入订单详情"] --> B{"订单项已完成且尚未提交评价?"} B -- "否" --> X["不展示评价入口或拒绝提交"] B -- "是" --> C["展示评价表单:评分、文字和最多 6 张可选晒图"] C --> D["买家填写评分(1~5)、文字(1~500 字)并按需上传图片"] D --> E{"评分、文字和图片均合规?"} E -- "否" --> X1["字段级错误,保留已填内容"] E -- "是" --> F["买家提交并附带防重复标识"] - F --> G{"同一订单项是否已有评价?"} + F --> G{"同一订单项是否已存在评价记录?"} G -- "是" --> Y["返回已评价结果,不新增重复记录"] G -- "否" --> H["开启事务并保存评价及图片关联"] H --> I{"事务提交成功?"} I -- "否" --> Z["整体回滚,提示稍后重试并保留已填内容"] - I -- "是" --> J["订单项变为已评价,提交后展示最新评价"] + I -- "是" --> J["评价记录落库,订单项状态由 M04 维护,不在 M07 中改动"] J --> K["商品详情公开评价和评分汇总最终更新"] ``` @@ -133,7 +134,7 @@ stateDiagram-v2 并发与一致性: - 同一订单项重复评价或重复点击必须由数据库唯一约束或等效机制阻止,不能依赖前端去重。 -- 同一订单项在两个浏览器同时提交时,仅一个事务成功,另一个由唯一约束或 `WHERE NOT EXISTS` 条件返回已评价结果。 +- 同一订单项在两个浏览器同时提交时,仅一个事务成功,另一个由数据库唯一约束或等效机制返回已评价结果。 - 同一订单的多订单项并发提交评价:每个订单项独立判断资格与唯一性,互不影响;任一订单项评价事务失败不影响其他订单项。 - 评价事务与图片关联在同一受控事务内提交,任一写入失败时整体回滚,不留下“评价已存但图片缺失”的部分结果。 - 评价公开读取最终以 PostgreSQL 为事实来源;缓存失效或读取失败时回退到数据库直读,不返回旧数据。 @@ -143,7 +144,7 @@ stateDiagram-v2 ```mermaid flowchart TD A["买家在订单详情完成评价提交"] --> B{"提交结果"} - B -- "成功" --> C["订单项标记已评价;商品详情评价列表与评分汇总最终更新"] + B -- "成功" --> C["评价记录落库;商品详情评价列表与评分汇总最终更新"] B -- "已评价" --> D["提示已评价,不重复写入"] B -- "字段或图片不合规" --> E["字段级错误并保留可恢复的表单内容"] B -- "部分图片上传失败" --> F["标记失败项,允许重试或移除"] @@ -184,10 +185,11 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 提交评价 | A3xx 评价提交 | 校验身份、订单项归属、完成状态、唯一性、字段与图片并原子写入 | 待交叉评审 | -| 查询本人可评价订单项 | A3xx 评价资格 | 按当前买家返回 Completed 且未评价的订单项 | 待交叉评审 | -| 查询商品公开评价 | A3xx 公开评价列表 | 分页返回评分、文字、图片、时间和脱敏展示名 | 待交叉评审 | -| 查询商品评分汇总 | A3xx 评分汇总 | 返回有效评价总数与平均分 | 待交叉评审 | +| 提交评价 | A121 评价提交 | 校验身份、订单项归属、完成状态、唯一性、字段与图片并原子写入 | 待交叉评审 | +| 查询本人可评价订单项 | A122 评价资格 | 按当前买家返回 Completed 且尚未提交评价的订单项 | 待交叉评审 | +| 查询商品公开评价 | A123 公开评价列表 | 分页返回评分、文字、图片、时间和脱敏展示名 | 待交叉评审 | +| 查询商品评分汇总 | A124 评分汇总 | 返回有效评价总数与平均分 | 待交叉评审 | +| 查询本人已提交评价 | A125 我的评价 | 按当前买家返回本人历史评价列表 | 待交叉评审 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 @@ -196,7 +198,7 @@ flowchart TD - F06 商品详情:只读取 M07 公开评价与评分汇总,不在商品详情页内提交评价;评价提交入口由订单详情提供。 - F09 订单完成:评价入口只能从本人 Completed 订单项接入,不能由商品详情绕过。 - M09 站内消息:评价成功落库属于已确认业务事实,通知发送由 M09 决定;本模块不直接发送通知。 -- C07 缓存:评价公开读取可使用 Cache-Aside 加速;评价事务提交后由架构确定的可靠机制失效缓存,缓存不可用时回退数据库直读。 +- C07 缓存:评价公开读取可使用缓存层加速;评价事务提交后由架构确定的可靠机制失效缓存,缓存不可用时回退数据库直读。 - 商家回复、隐藏或点赞:本期不实现;后续如需扩展,必须先修订主需求和本文档的边界约束。 ## 十、由流程反查出的接口与数据待评审项 @@ -231,9 +233,11 @@ flowchart TD - [ ] Mermaid 图内部包含直接上游模块输入(M01、M04)和直接下游模块出口(M04、F06)。 - [ ] 主流程、拒绝分支、失败分支和最终结果齐全;并发场景下唯一约束生效。 - [ ] 状态名称与需求规格说明书一致;没有新增订单状态。 -- [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径或 DTO 代替流程;没有自行发明接口编号、字段名或数据库状态码。 +- [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径、DTO、技术模式名称或 SQL 片段代替流程;没有自行发明接口编号、字段名或数据库状态码。 +- [ ] **M07 不修改 M04 订单项状态**:流程图与状态机中不出现”订单项变为已评价”,已评价事实由 Review 唯一评价记录派生;订单项状态机只由 Ordering 维护。 +- [ ] 接口编号落在 Review 范围 A121~A128,不混用其他模块编号。 - [ ] 已对照根文档 3.6 节中评价接入点校准入口位置,X01 不可绕过订单资格。 -升级到“待交叉评审”的条件:自检完成、主流程与异常分支齐全、并发场景验证、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 +升级到”待交叉评审”的条件:自检完成、主流程与异常分支齐全、并发场景验证、订单项越界问题已修正、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 -升级到“已确认”的条件:Ordering 主责确认订单项归属与完成状态口径,Identity 主责确认买家脱敏快照生成时机,根文档 3.6 节中评价接入点对应追踪项成熟度同步更新。 \ No newline at end of file +升级到”已确认”的条件:Ordering 主责确认订单项归属与完成状态口径并认可”已评价由 Review 派生”的设计,Identity 主责确认买家脱敏快照生成时机,根文档 3.6 节中评价接入点对应追踪项成熟度同步更新。 \ No newline at end of file -- Gitee From 72941096ea4dd22b97500cfb0720b94125bac341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=83=A0=E6=83=A0?= <2205590672@qq.com> Date: Fri, 24 Jul 2026 16:29:49 +0800 Subject: [PATCH 067/118] docs(api): align seckill order queries with A302/A303 --- ...22\346\235\200\346\265\201\347\250\213.md" | 73 +++++++++++-------- ...51\350\275\246\346\265\201\347\250\213.md" | 42 ++++++----- 2 files changed, 64 insertions(+), 51 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" index ce13ea9..724f714 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" @@ -4,14 +4,14 @@ > 覆盖:C01-01、M03-01 与 M04-01 的秒杀衔接、X04 不参与秒杀取消回补 > 基础核心流程:F11(商品上下架)、F04/F06(活动浏览)、F08(下单)、F10(支付)、F09/F12(取消 / 发货) > 直接协作:顾欣月(M02 Catalog 与 M06-01 活动维护)、韦乾强(M04 Ordering 与 C03 超时取消)、张海洋(M05 Payment 与 C08 回调) -> 文档状态:初稿,待朱惠惠自审及 Catalog/Ordering 交叉评审;库存划拨口径与 C08 异步替换边界按根文档要求保持“待决” +> 文档状态:初稿,待朱惠惠自审及 Catalog/Ordering 交叉评审;C08 异步回调替换边界按根文档要求保持“待决” > 需求事实源:[需求规格说明书 C01](../../../01-需求文档/需求规格说明书.md) 的“C01 秒杀与防超卖”完整七节 ## 一、范围与事实来源 本扩展在面向买家的高并发秒杀场景下保证库存“只减不超、不少不丢”:每一份秒杀库存只能被一名买家以一份订单成功购买,重复请求不能产生重复扣减或重复订单,所有失败请求不得留下半扣减、未提交订单或孤立记录。本期秒杀以“限时一口价活动”为模型,关联一个已上架的普通商品和一份独立维护的秒杀库存。活动期内买家点击“立即抢购”直接提交秒杀订单,跳过普通加车流程;活动结束或库存耗尽后入口立刻失效,进入商品详情时只能看到普通购买。 -数据库事务是秒杀正确性的唯一事实来源。Redis、消息队列、Nginx 限流和前端防抖只承担性能与体验,不得用作并发正确性边界。本文按“先确认业务参与者和原子结果,再确定流程步骤和不可变核心事实”的顺序编写;A8xx(A801 浏览活动、A802 抢购提交、A803 抢购结果查询、A804 商家维护活动)只用于流程完成后的契约映射与缺口检查,不能反向拼接流程。 +数据库事务是秒杀正确性的唯一事实来源。Redis、消息队列、Nginx 限流和前端防抖只承担性能与体验,不得用作并发正确性边界。本文按“先确认业务参与者和原子结果,再确定流程步骤和不可变核心事实”的顺序编写;A22x(A220 浏览活动、A221 抢购提交、A222 抢购结果查询、A223 商家维护活动、A224 库存回补;项目约定的 C01 接口范围为 A220~A228)只用于流程完成后的契约映射与缺口检查,不能反向拼接流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| @@ -21,8 +21,8 @@ | C08 异步回调 | 待细化(张海洋负责) | 仅引用其对秒杀订单的幂等规则 | | C10 高可用 | 待细化(罗皓晨负责) | 仅引用其实例分发的最终一致性 | | C07 缓存 | 待细化(罗皓晨、顾欣月负责) | 仅引用其对活动列表的失效策略 | -| A8xx 接口 | 部分定义、未冻结 | 由流程派生并做映射 | -| DB201~DB210(C01 表) | 模板/占位 | 本文不发明字段、约束和索引 | +| A22x 接口 | 部分定义、未冻结 | 由流程派生并做映射 | +| 秒杀相关表(活动、库存、限购配额、幂等记录) | 模板/占位 | 本文不发明表名、字段、约束和索引 | | X04 售后退款 | 独立扩展 | 仅登记边界,不卷入秒杀取消回补 | ## 二、扩展直接出入口 @@ -40,10 +40,10 @@ flowchart LR SEC -. "事务提交后" .-> MSG["M09 消息持久化 / 通知"] ID -->|"游客、商家或账号禁用"| X["拒绝抢购,不创建独立秒杀订单"] - ORD -->|"支付回调回写、售后退款"| Y["按 M04/M05/M10 公开契约处理,不允许直接 UPDATE 秒杀库存"] + ORD -->|"支付回调回写、售后退款"| Y["按 M04/M05/M10 公开契约处理,不允许直接改写秒杀库存"] C08["C08 异步回调"] -. "作用于秒杀订单的幂等回写" .-> ORD - RATE["Nginx / API / 应用 / 连接池限流"] -->|"429 / 409 快速失败"| SEC + RATE["入口侧限流"] -->|"超过承载阈值时快速失败"| SEC CA["C07 缓存"] -->|"活动列表、商品基础信息读取"| PUBLIC SEC -. "库存事实" .-> CA ``` @@ -69,7 +69,13 @@ flowchart TD EE -- "Ended / Cancelled" --> X["拒绝编辑"] E --> F{"手动发布?"} F -- "否" --> EE1["保持 Draft"] - F -- "是" --> G["状态变为 Published,并写入活动开始 UTC 时间"] + F -- "是" --> G1["开启发布事务,先按当前秒杀库存量条件扣减普通商品可售库存"] + G1 --> G2{"商品当前可售库存 ≥ 秒杀库存总量?"} + G2 -- "否" --> X2["拒绝发布,回滚事务,并指出普通库存不足"] + G2 -- "是" --> G3["写入秒杀库存初始事实:剩余可售 = 秒杀量,已售 = 0"] + G3 --> G4{"发布事务整体提交?"} + G4 -- "否" --> X3["整体回滚:普通库存未被扣减、秒杀库存未被创建、活动保持 Draft"] + G4 -- "是" --> G["状态变为 Published,并写入活动开始 UTC 时间"] G --> H["基于数据库 UTC now() 自动判定:start ≤ now < end → Running;now ≥ end → Ended"] H --> I["活动详情可在卖家与买家入口查询"] I --> J["剩余库存售罄时立即标记 Sold Out 并禁用抢购入口"] @@ -93,6 +99,8 @@ stateDiagram-v2 关键约束: +- **发布即原子划拨**:商家将活动由 Draft 推进为 Published 时,必须在同一数据库事务内把等量普通商品可售库存条件扣减,并创建秒杀库存初始事实;事务失败整笔回滚,普通库存和秒杀库存均不留半改;只有事务整体提交后才能把活动状态推进为 Published。 +- **库存通道互不混淆**:普通下单只能扣减普通库存,秒杀下单只能扣减秒杀库存;发布时一次性划拨后,活动期间普通下单不会消耗已被划走的那部分库存。 - 活动状态字段由数据库维护并参与所有业务校验;商家只能在 Draft / Published / Running 时执行取消,Ended / Cancelled 拒绝重复状态变更。 - 状态推进在数据库侧以 UTC `now()` 为权威,避免应用实例时钟漂移造成提早或延后成功。 - 活动取消后已存在订单继续走完;未提交请求直接拒绝;本期不回收已分配秒杀库存,避免被普通订单夹带走量。 @@ -119,6 +127,7 @@ flowchart TD - 倒计时统一以数据库 UTC 时间计算;同 / 跨实例用户看到一致的剩余库存和倒计时。 - 前端轮询或服务端推送给出的剩余库存必须与数据库实际一致,禁止相反情景。 - 活动列表与商品基础信息读取可经 C07 缓存,但秒杀库存本身不进入缓存。 +- 活动详情暴露的剩余库存只能来源于发布时落地的事务事实;发布事务未提交的草稿不在抢购入口暴露可售数。 ## 五、立即抢购主流程 @@ -126,7 +135,7 @@ flowchart TD flowchart TD A["买家点击立即抢购,携带商品 ID、活动 ID、数量、幂等键"] --> B["服务端解析登录身份(JWT + role=buyer)"] B --> C{"身份合法?"} - C -- "否" --> X["401,引导登录"] + C -- "否" --> X["登录失效,引导登录"] C -- "是" --> D{"活动存在且状态为 Running?"} D -- "否" --> Y["按不存在 / 已结束 / 未开始返回明确原因"] D -- "是" --> E{"当前 UTC 时间落在开始和结束之间?"} @@ -141,9 +150,9 @@ flowchart TD H -- "是" --> I{"单用户当前限购内?"} I -- "否" --> Z2["超过单用户限购"] I -- "是" --> J["开启秒杀事务"] - J --> K["条件更新秒杀库存:remaining -= qty AND 状态/窗口/活动 ID 条件命中,期望受影响行数 = 1"] - K --> K1{"受影响行数符合预期?"} - K1 -- "否" --> ZR["并发竞争失败:按 409 / 已售罄返回"] + J --> K["条件扣减秒杀库存,联合校验活动状态、窗口、活动 ID 与剩余可售量,期望条件命中一次"] + K --> K1{"条件是否命中?"} + K1 -- "否" --> ZR["并发竞争失败:按已售罄或状态竞争失败返回"] K1 -- "是" --> L["原子占用 (activity_id, buyer_id) 限购配额行"] L --> M["通过 Ordering 公开应用契约写入共享 orders / order_items,带 orderType=Seckill、activityId、秒杀价快照"] M --> N["写入待发布订单创建事实(OrderCreatedIntegrationEvent)"] @@ -155,8 +164,8 @@ flowchart TD 不可变核心事实: -- 条件更新 `remaining = remaining - :qty`、`sold = sold + :qty` 必须使用 `WHERE remaining >= :qty AND status = 'Running' AND now() BETWEEN start_at AND end_at AND activity_id = :id` 的更新语句;命中受影响行数 = 1 才算扣减成功。 -- 事务短小:秒杀事务只覆盖秒杀库存行、限购配额占用、订单写入和必要快照;事务内禁止调用外部 HTTP、等待用户输入或长计算。 +- 抢购条件扣减使用一条带状态/窗口/活动 ID/剩余可售量联合判定的数据库更新;只有当前请求数量、买家限购与状态版本完全命中可售条件时才算扣减成功,未命中条件时立即拒绝并按既定失败分支返回。 +- 事务短小:秒杀事务只覆盖秒杀库存行、限购配额占用、订单写入和必要快照;事务内禁止远程调用、等待用户输入或长计算。 - 锁粒度按活动 ID 单行:抢购事务只对单个活动的库存和订单写入加锁,不全表扫描。 ## 六、取消、回补与限购名额释放 @@ -169,7 +178,7 @@ flowchart TD C --> D["条件推进 PendingPayment → Cancelled,影响订单状态"] D --> E{"订单状态条件更新成功?"} E -- "否" --> Y["返回失败,已支付或状态竞争"] - E -- "是" --> F["按订单项 (activityId, qty) 条件回补秒杀库存:remaining += qty、sold -= qty,影响行数 = 1"] + E -- "是" --> F["按订单项 (活动 ID, 数量) 联合条件回补秒杀库存剩余可售量并核减已售数,期望条件命中一次"] F --> G["释放 (activity_id, buyer_id) 限购配额:已用数量 -= qty,影响行数 ≥ 1"] G --> H["记录取消时间和待发布订单取消事实"] H --> I{"取消事务整体提交?"} @@ -181,10 +190,10 @@ flowchart TD 关键约束: - 秒杀取消回补必须落到原活动的秒杀可售库存;不得回补到普通商品库存。 -- 同一笔订单的重复取消请求只能回补一次:以 `(order_id, 'CANCELLED')` 或订单状态条件更新作为幂等保障。 +- 同一笔订单的重复取消请求只能回补一次:以订单状态条件推进为唯一幂等保障(同一订单的多次取消只能产生一次回补)。 - 买家主动取消、C03 超时取消的库存回补在同一事务内完成;任何一步失败整体回滚,不产生“库存已回补但订单仍为 PendingPayment”的部分结果。 - 已 `Paid` 订单不走取消回补;后续退款 / 退货按 M10 售后流程处理。 -- 秒杀库存回补以条件更新回写到 `remaining` 并扣减 `sold`;活动结束后回补仍允许,只是不再允许新抢购。 +- 秒杀库存回补以联合条件更新回写到剩余可售量并核减已售数;活动结束后回补仍允许,只是不再允许新抢购。 ## 七、与其他挑战模块的衔接 @@ -208,11 +217,12 @@ flowchart LR | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 浏览秒杀活动 | A801 | 仅返回当前请求可见的活动状态、剩余库存、已售数量和倒计时 | 待交叉评审 | -| 秒杀立即抢购 | A802 | 校验身份、活动状态、窗口、数量、限购、幂等;以条件更新原子扣减并生成订单 | 待交叉评审 | -| 查询抢购结果 | A803 | 返回本人订单号、活动 ID 和购买结果;非本人返回“不存在 / 无权限” | 待交叉评审 | -| 商家维护秒杀活动 | A804 | 在 Draft / Published / Running 允许创建、发布、取消;Ended / Cancelled 拒绝重复变更 | 待交叉评审 | -| 秒杀库存回补 | A805 | 按订单项 `(activityId, qty)` 条件回补;同订单多次取消只回补一次 | 待交叉评审 | +| 浏览秒杀活动 | A221 | 仅返回当前请求可见的活动状态、剩余库存、已售数量和倒计时 | 待交叉评审 | +| 秒杀立即抢购 | A222 | 校验身份、活动状态、窗口、数量、限购、幂等;以条件更新原子扣减并生成订单 | 待交叉评审 | +| 查询抢购结果 | A223 | 返回本人订单号、活动 ID 和购买结果;非本人返回“不存在 / 无权限” | 待交叉评审 | +| 商家维护秒杀活动 | A224 | 在 Draft / Published / Running 允许创建、发布、取消;Ended / Cancelled 拒绝重复变更 | 待交叉评审 | +| 秒杀库存回补 | A226 | 按订单项 `(活动 ID, 数量)` 联合条件回补秒杀库存剩余可售量并核减已售数,期望条件命中一次 | 待交叉评审 | +| 发布时原子划拨 | A225 | 商家将活动 Draft 推进为 Published 时,同一数据库事务内条件扣减普通商品可售库存并创建秒杀库存初始事实 | 待交叉评审 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码与 OpenAPI、ProblemDetails 不得反向写入业务图;接口设计 1.12 通用幂等规则与 4.6 资金类幂等约束同样适用于秒杀订单。 @@ -227,21 +237,22 @@ flowchart LR ## 十、由流程反查出的接口与数据待评审项 -1. 抢购接口 A802 的最终扣减库存与价格必须走服务端重读:客户端不得指定秒杀价或库存;当前 A802 草案若允许 `expectedAmount / expectedQty` 参与业务判定,需要明确“仅作为客户端旧值冲突保护”,不得成为扣减事实。 -2. 单用户限购以 `(activity_id, buyer_id)` 唯一配额事实为准;A802 必须先占用配额再进入主流程;同 Key 同请求重放首配结果,不得再次增加限额。 -3. 库存语义:`remaining + sold + frozen = 初始总量`;本期不启用 `frozen`,所有提交要么直接成功,要么立即失败;若后续启用 `frozen`,A802 需要回看本文第五节并保留 6.6 异常分支。 +1. 抢购接口 A222 的最终扣减库存与价格必须走服务端重读:客户端不得指定秒杀价或库存;当前 A222 草案若允许 `expectedAmount / expectedQty` 参与业务判定,需要明确“仅作为客户端旧值冲突保护”,不得成为扣减事实。 +2. 单用户限购以 `(activity_id, buyer_id)` 唯一配额事实为准;A222 必须先占用配额再进入主流程;同 Key 同请求重放首配结果,不得再次增加限额。 +3. 库存语义:`remaining + sold + frozen = 初始总量`;本期不启用 `frozen`,所有提交要么直接成功,要么立即失败;若后续启用 `frozen`,A222 需要回看本文第五节并保留 6.6 异常分支。 4. C08 异步回调:若 C08 替换 F10 的部分支付确认步骤,必须先在根文档决定其接入 F10 的哪个同步步骤;不能让同步支付和异步回调同时成为最终支付事实。 -5. C03 超时取消:必须在 M04 完成条件推进 `PendingPayment → Cancelled` 后进入秒杀回补通道;不能绕过 M04 直接 UPDATE 秒杀库存。 -6. 活动取消运营动作:A804 的“取消”动作只对 Draft / Published / Running 生效;取消后已存在订单继续按既有流程走完,本期不回收已分配库存,避免与普通订单混淆。 -7. 限流分级:A802 与 A801 / A8xx(普通购物车接口)应分桶限流,秒杀高并发不得拖垮普通商品查询;Nginx / API / 应用 / 连接池任意一层都能给出 429。 +5. C03 超时取消:必须在 M04 完成条件推进 `PendingPayment → Cancelled` 后进入秒杀回补通道;不能绕过 M04 直接改写秒杀库存。 +6. 活动取消运营动作:A224 的“取消”动作只对 Draft / Published / Running 生效;取消后已存在订单继续按既有流程走完,本期不回收已分配库存,避免与普通订单混淆。 +7. 限流分级:A222 与 A221 / M03 的 A2xx 应分桶限流,秒杀高并发不得拖垮普通商品查询;入口侧任意一层(网关、API、应用、连接池)触发后都能快速失败。 8. 时间口径:服务端使用 UTC 写入;状态推进与活动判断以数据库 `now()` 为权威;应用节点间的时钟轻微漂移不影响业务结果。 -9. 数据隔离:A801~A805 全部按 `(buyer_id, activity_id)` 或 `(订单 ID, 活动 ID)` 双重过滤;越权返回“不存在 / 无权限”统一错误,不暴露记录是否存在。 -10. DB201~DB210 尚未形成可实施的完整表定义;秒杀活动表、库存表、限购配额表、幂等记录表的字段、约束和索引必须由数据库设计任务另行确认。 +9. 数据隔离:A221~A226 全部按 `(买家 ID, 活动 ID)` 或 `(订单 ID, 活动 ID)` 双重过滤;越权返回“不存在 / 无权限”统一错误,不暴露记录是否存在。 +10. 秒杀活动表、库存表、限购配额表、幂等记录表的字段、约束和索引必须由数据库设计任务另行确认;本文不发明表名或字段。 ## 十一、验收证据清单 -- [ ] 100 并发请求抢 10 件库存:成功订单数 = 10,剩余可售 = 0,已售 = 10;其余 90 个请求以 409 / 已售罄或 429 / 限流明确失败;无 5xx 长期堆积。 -- [ ] 校验数据库一致性:`remaining + sold + frozen = 初始总量`;成功订单一一对应一次库存扣减;失败请求无扣减记录、无订单。 +- [ ] 发布时原子划拨:商家将秒杀活动 Draft → Published 时,数据库事务内同一次提交完成“普通商品可售库存 -= 秒杀量、秒杀库存初始事实写入”;普通库存不足时整笔拒绝,普通库存和秒杀库存均无半改;活动状态推进到 Published 的唯一条件是事务整体提交。 +- [ ] 100 并发请求抢 10 件库存:成功订单数 = 10,剩余可售 = 0,已售 = 10;其余 90 个请求以已售罄或限流快速失败;不存在长期堆积的服务异常。 +- [ ] 校验数据库一致性:秒杀库存剩余可售 + 已售 = 初始总量;成功订单一一对应一次库存扣减;失败请求无扣减记录、无订单。 - [ ] 单用户限购:同一买家连续两次抢购仅一笔成功;第二次返回“超过单用户限购”或“已售罄”;数据库同一 `(activity_id, buyer_id)` 仅一笔秒杀订单。 - [ ] 时间窗口:把开始时间改为未来 1 分钟后立刻抢购 → 全部返回“活动未开始”,库存不变;到达开始时间后可正常抢购;把结束时间改为过去 1 分钟后立刻抢购 → 全部返回“活动已结束”,库存不变。 - [ ] 幂等:在约定窗口内用同一标识连续提交两次 → 仅生成一笔订单;剩余库存只扣减一次;两次响应携带同一订单号。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" index e9241b3..e7378ef 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" @@ -11,14 +11,14 @@ 本模块负责买家在登录态下维护本人购物车,覆盖查看列表、加入商品、修改数量、删除条目、单选 / 全选、选择失效处理、服务端计价和下单前 / 下单事务内的购物车清理。购物车只承担“下单前的暂存区”,不承载营销、优惠、推荐、凑单,也不维护独立状态机;选中状态、价格、库存上限由服务端实时派生,客户端不得越权决定订单金额。 -本文先依据需求确定业务参与者、上游输入、状态派生口径、原子结果和模块出入口,再由这些动作派生接口契约。A1xx(A101 加购、A102 查看、A103 改数量、A104 删除、A105 选中、A106 服务端计价、A107 清空、A108 幂等记录)只用于流程完成后的契约映射与缺口检查,不能反向拼接流程,也不能用现有 Axxx 反向覆盖业务语义。 +本文先依据需求确定业务参与者、上游输入、状态派生口径、原子结果和模块出入口,再由这些动作派生接口契约。A2xx(A201 加购、A202 查看、A203 改数量、A204 删除、A205 选中、A206 服务端计价、A207 清空、A208 幂等记录)只用于流程完成后的契约映射与缺口检查,不能反向拼接流程,也不能用现有 Axxx 反向覆盖业务语义。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M03-01/F07 需求 | 完整定义 | 作为购物车业务语义事实源 | | 本文业务流程 | 初稿 | 先确认参与者、上游输入、状态派生、原子结果和模块出入口 | -| A1xx 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | -| DB0xx(cart_items、cart_idem 等) | 模板/占位 | 本文不发明字段、约束和索引 | +| A2xx 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| 购物车相关表(条目、幂等记录) | 模板/占位 | 本文不发明表名、字段、约束或索引 | | X02 收藏与浏览历史 | 独立扩展 | 仅登记边界,不混入 F07 主流程 | | C01 秒杀 | 独立扩展 | 立即抢购绕过购物车,C01 仅与本文确定“不读写购物车”的边界 | @@ -198,7 +198,9 @@ flowchart TD | 订单主动取消 / C03 超时 | 本期不恢复购物车条目 | 避免与重新加入状态混淆 | | 失效条目清理 | 仅随用户主动删除 / 清空 | 后台不主动清理,便于排查 | | C01 秒杀并发抢购 | 不涉及购物车 | 秒杀走独立库存与限购通道 | -| 网络失败、500、401 | 给出明确错误码:401 引导登录、400 字段问题、5xx 提供重试入口 | 不依赖前端缓存重建购物车 | +| 登录态失效(令牌过期 / 被撤销) | 引导重新登录,本地保留购物车输入 | 不依赖前端缓存重建购物车 | +| 字段值非法或缺失 | 保留用户输入,按字段逐项提示错误 | 不在此处修改条目 | +| 服务暂时不可用 | 给出明确可重试提示;同一请求幂等键可原样重放 | 不得因前端缓存或猜测状态变更条目 | ## 八、由流程派生的接口契约映射 @@ -206,35 +208,35 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 加入购物车(可选幂等) | A101 | 校验数量 + 库存,同 `(买家+商品)` 累加,幂等不重复累加 | 待交叉评审 | -| 查看本人购物车 | A102 | 仅返回本人条目,含可结算 / 失效标记与失效原因 | 待交叉评审 | -| 修改本人条目数量 | A103 | 实时校验库存上限;调小始终允许;返回最新数量与最大可设值 | 待交叉评审 | -| 删除本人条目 / 清空 | A104 | 按 `(买家+条目ID)` 过滤;幂等;多次删除同一 ID 结果一致 | 待交叉评审 | -| 切换单选 / 全选 / 反选 | A105 | 服务端持久化选中;失效条目不允许被选中 | 待交叉评审 | -| 服务端计价(结算预览) | A106 | 实时重读单价并计算总额;前端不能指定金额 | 待交叉评审 | -| 一键清空购物车 | A107 | 幂等;只影响本人;失效条目一并清理 | 待交叉评审 | -| 加购幂等记录 | A108 | 按 `(买家+幂等键)` 持久化记录;同键同请求重放首次结果 | 待交叉评审 | +| 加入购物车(可选幂等) | A201 | 校验数量 + 库存,同 `(买家+商品)` 累加,幂等不重复累加 | 待交叉评审 | +| 查看本人购物车 | A202 | 仅返回本人条目,含可结算 / 失效标记与失效原因 | 待交叉评审 | +| 修改本人条目数量 | A203 | 实时校验库存上限;调小始终允许;返回最新数量与最大可设值 | 待交叉评审 | +| 删除本人条目 / 清空 | A204 | 按 `(买家+条目ID)` 过滤;幂等;多次删除同一 ID 结果一致 | 待交叉评审 | +| 切换单选 / 全选 / 反选 | A205 | 服务端持久化选中;失效条目不允许被选中 | 待交叉评审 | +| 服务端计价(结算预览) | A206 | 实时重读单价并计算总额;前端不能指定金额 | 待交叉评审 | +| 一键清空购物车 | A207 | 幂等;只影响本人;失效条目一并清理 | 待交叉评审 | +| 加购幂等记录 | A208 | 按 `(买家+幂等键)` 持久化记录;同键同请求重放首次结果 | 待交叉评审 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段、错误码与幂等键传递方式不得反向写入业务图,接口设计 1.12 通用幂等规则统一承载。 ## 九、扩展接入边界 - C01 秒杀:立即抢购绕过购物车,Seckill 与 Ordering 自行完成资格、限购、活动库存和订单校验;本文购物车不参与秒杀扣减 / 回补,也不被秒杀回补触发的库存变化影响。M04 必须按 `orderType` 与 `seckillActivityId` 区分库存回补通道,确保秒杀回补不误增普通库存。 -- X02 收藏 / 浏览历史:仅引用商品 ID 维度的公开数据,不读写购物车条目;用户在收藏页点击“加入购物车”时调用本文 A101,遵循同一所有权与库存上限校验。 +- X02 收藏 / 浏览历史:仅引用商品 ID 维度的公开数据,不读写购物车条目;用户在收藏页点击“加入购物车”时调用本文 A201,遵循同一所有权与库存上限校验。 - M09 站内消息:仅消费 M04 下单事务提交后发布的 `OrderCreatedIntegrationEvent`;M03 不主动发布消息,也不依赖消息反馈修改条目。 - M06-01 后台商品上下架:通过商品销售状态变更触发购物车失效标记,不直接修改他人购物车条目。 ## 十、由流程反查出的接口与数据待评审项 -1. 本文要求购物车主键为 `(buyer_id, product_id)`,单一组合唯一;若现有 A1xx 在多次加购时按 “条目 ID 自增” 创建多条记录,接口语义必须改为“按 `(买家, 商品)` 唯一累加”。 -2. 数量上下限必须在服务端实时校验并返回最大可设值;A103 必须区分“调大拒绝”和“调小允许”,不能统一返回字段错误导致前端无法截断。 +1. 本文要求购物车主键为 `(buyer_id, product_id)`,单一组合唯一;若现有 A2xx 在多次加购时按 “条目 ID 自增” 创建多条记录,接口语义必须改为“按 `(买家, 商品)` 唯一累加”。 +2. 数量上下限必须在服务端实时校验并返回最大可设值;A203 必须区分“调大拒绝”和“调小允许”,不能统一返回字段错误导致前端无法截断。 3. 加购幂等键的窗口期需要与库存 / 上限校验配合:同一幂等键只能重放首次成功结果,不同请求体携带同键视为标识复用并被拒绝。 -4. 下单成功后,订单模块在事务内清理购物车条目;若现有 A1xx 与 A8xx(下单)跨事务异步清理,必须先改为同事务清理,保证事务回滚不丢条目。 -5. 结算预览返回的服务端金额是“可选预览”,下单时必须再重读一次商品与库存;A8xx 不能复用 A106 的金额作为最终扣款事实。 -6. 失效条目清理:本流程要求“仅随用户主动删除 / 清空”,A104 必须不复用物理删除批量逻辑;后台清理需另起保留期规则,不在本文范围。 -7. 购物车数据归属全部按 `(买家 ID, 商品 ID)` 或 `(买家 ID, 条目 ID)` 双重过滤;A1xx 不能仅按条目 ID 给出可访问性。 +4. 下单成功后,订单模块在事务内清理购物车条目;若现有 A2xx 与订单模块的下单接口跨事务异步清理,必须先改为同事务清理,保证事务回滚不丢条目。 +5. 结算预览返回的服务端金额是“可选预览”,下单时订单模块必须再重读一次商品与库存,不能直接复用 A206 的金额作为最终扣款事实。 +6. 失效条目清理:本流程要求“仅随用户主动删除 / 清空”,A204 必须不复用物理删除批量逻辑;后台清理需另起保留期规则,不在本文范围。 +7. 购物车数据归属全部按 `(买家 ID, 商品 ID)` 或 `(买家 ID, 条目 ID)` 双重过滤;A2xx 不能仅按条目 ID 给出可访问性。 8. 价格变动:商品改价后购物车再次展示用实时单价;现有接口若缓存条目的小计或反推金额,需在列表时重算并返回最新单价。 -9. DB0xx 尚未形成可实施的完整表定义,购物车表、幂等记录表的字段、约束和索引必须由数据库设计任务另行确认。 +9. 购物车表、幂等记录表的字段、约束和索引必须由数据库设计任务另行确认,本文档不发明。 ## 十一、验收证据清单 -- Gitee From 6bbddd50de172cca8430045ac5a032fdecbac34d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9F=A6=E4=B9=BE=E5=BC=BA?= <3195306445@qq.com> Date: Fri, 24 Jul 2026 16:30:50 +0800 Subject: [PATCH 068/118] =?UTF-8?q?docs(process):=20=E6=B6=A6=E8=89=B2=20M?= =?UTF-8?q?04=20=E8=AE=A2=E5=8D=95=E6=B5=81=E7=A8=8B=E4=B8=8E=20C03=20?= =?UTF-8?q?=E8=B6=85=E6=97=B6=E5=8F=96=E6=B6=88=E6=B5=81=E7=A8=8B=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修正 M04 流程图中 ORD 站内消息通知 BUYER 的重复边 - 统一事件命名为 OrderCreatedIntegrationEvent(与需求规格说明书一致) - 补充 assignedMerchantUserId 解析与保存步骤,修正节点编号 - 修正 C03 流程图中 STOCK 无效出边 - Mermaid 图语法修复 --- ...05\346\227\266\346\265\201\347\250\213.md" | 179 ++++++++++++ ...42\345\215\225\346\265\201\347\250\213.md" | 270 ++++++++++++++++++ 2 files changed, 449 insertions(+) create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" new file mode 100644 index 0000000..835dd48 --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" @@ -0,0 +1,179 @@ +# C03 订单超时自动取消流程 + +> 负责人:韦乾强 +> 覆盖:C03 订单超时自动取消 +> 基础核心流程:M04 订单状态机(F08/F09)、M05 支付(F10) +> 直接协作:罗皓晨(M00 Worker 基础设施)、张海洋(M05 支付)、朱惠惠(M03 购物车) +> 文档状态:初稿 +> 需求事实源:[需求规格说明书 C03 订单超时自动取消](../../../01-需求文档/需求规格说明书.md) 的"C03 订单超时自动取消"完整章节 + +## 一、范围与事实来源 + +本挑战模块负责在买家未按时支付时,由系统自动取消订单并回补库存。核心目标是"超时即取消、取消即回补、并发安全"。Worker 定时扫描 PendingPayment 订单,对超时订单执行与买家主动取消相同的状态变更和库存回补逻辑。 + +本文先确认超时触发条件、超时时间配置、扫描策略、与支付模块的状态竞争处理,再登记对 M04 订单状态机的复用边界和对 M09 站内消息的输出接口。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| C03 需求 | 完整定义 | 作为超时取消业务语义事实源 | +| 超时时间配置 | 正式环境 30 分钟,演示环境可配置 | 由部署配置注入,不硬编码 | +| 超时扫描与取消事务 | 初稿 | 确认 Worker 调度、批次上限、重试策略 | +| 与支付的状态竞争 | 完整定义 | 条件更新保证幂等 | +| 站内消息通知 | M09 承载 | 登记输出事件类型和消息内容要求 | + +## 二、基础核心流程依赖 + +```mermaid +flowchart LR + ORDER["M04 Ordering
PendingPayment 订单创建"] --> TIMEOUT["C03 超时 Worker
定时扫描 PendingPayment 订单"] + ORDER --> PAY["M05 Payment
买家主动支付"] + TIMEOUT -->|"超时取消事务|回补库存"| STOCK["M02 Catalog
库存回补"] + TIMEOUT -->|"OrderCancelledEvent
超时取消通知"| MSG["M09 站内消息"] + PAY -->|"OrderPaidEvent
支付成功通知"| MSG + MSG -->|"超时取消通知
买家站内消息"| BUYER["买家消息中心"] +``` + +关键约束: + +- C03 复用 M04 的订单状态变更和库存回补逻辑,不独立发明新事务。 +- C03 由 Worker 后台任务触发,不提供买家主动接口。 +- 超时时间从订单 `created_at` 计算,不从其他时间点计算。 + +## 三、超时时间配置 + +```mermaid +flowchart TD + A["Worker 启动"] --> B["读取配置 OrderTimeoutMinutes"] + B --> C{"配置存在?"} + C -- "否" --> D["使用默认值 30 分钟"] + C -- "是" --> E["使用配置值"] + D --> F["订单超时时间已确认"] + E --> F +``` + +关键约束: + +- 超时时间通过配置项 `OrderTimeoutMinutes` 管理。 +- 正式环境默认 30 分钟。 +- 演示环境可通过配置调整为更短时间(如 5 分钟),但需说明与正式参数的对应关系。 +- 配置不得硬编码。 + +## 四、Worker 超时扫描流程 + +```mermaid +flowchart TD + A["Worker 定时触发(建议间隔 ≤ 超时时间/2)"] --> B["查询超时订单:status = PendingPayment AND created_at + timeout < NOW()"] + B --> C{"有待处理订单?"} + C -- "否" --> Z["本次扫描结束"] + C -- "是" --> D["按批次处理(建议每批 ≤ 100 条)"] + D --> E["对每条超时订单开启独立事务"] + E --> F["条件更新状态为 Cancelled:WHERE status = PendingPayment"] + F --> G{"影响行数 = 1?"} + G -- "否" --> H["订单已被其他操作处理(如已支付/已取消),跳过"] + G -- "是" --> I["回补库存:stock = stock + quantity"] + I --> J["记录 cancelled_at = NOW() 和 cancel_reason = TIMEOUT"] + J --> K["写入 Outbox:OrderCancelledEvent(cancel_reason = TIMEOUT)"] + K --> L["提交事务"] + L --> M{"提交成功?"} + M -- "否" --> N["记录错误日志,重试(最多 3 次)"] + N --> O{"超过重试次数?"} + O -- "是" --> P["发送告警,记录失败订单列表"] + O -- "否" --> E + M -- "是" --> Q["继续处理下一条"] + P --> Q + Q --> R{"批次处理完成?"} + R -- "否" --> D + R -- "是" --> Z +``` + +关键约束: + +- 扫描间隔建议 ≤ 超时时间/2(如超时 30 分钟,扫描间隔 ≤ 15 分钟)。 +- 每批次处理上限 100 条,避免长时间锁表。 +- 使用条件更新 `WHERE status = 'PendingPayment'` 保证幂等。 +- 失败重试 3 次后告警,订单保留待处理状态。 + +## 五、与支付的状态竞争处理 + +```mermaid +flowchart TD + subgraph 并发竞争 + A["C03 Worker:超时扫描并尝试取消"] --> B["条件更新状态为 Cancelled"] + C["买家:主动点击支付"] --> D["M05 支付事务:条件更新状态为 Paid"] + end + + B --> E{"乐观锁结果"} + D --> F{"乐观锁结果"} + + E -- "取消成功,行数=1" --> G["库存已回补,订单已取消"] + F -- "支付成功,行数=1" --> H["余额已扣减,订单已支付"] + E -- "行数=0:已被支付" --> I["取消跳过,返回幂等成功"] + F -- "行数=0:已被取消" --> J["支付跳过,返回余额未扣减"] +``` + +关键约束: + +- 取消与支付使用相同的条件更新 `WHERE status = 'PendingPayment'`。 +- 最终只有一个操作成功,避免"又支付又取消"的矛盾状态。 +- 两者竞争时数据库事务隔离保证最终一致性。 + +## 六、事件输出 + +```mermaid +flowchart TD + A["超时取消事务提交成功"] --> B["发布 OrderCancelledEvent(cancel_reason = TIMEOUT)"] + B --> C["Outbox 投递到 MQ"] + C --> D["M09 消费事件"] + D --> E["生成站内消息:订单超时取消通知"] + E --> F["买家查看消息中心"] +``` + +事件内容要求: + +- 消息标题:订单超时取消 +- 消息内容:您的订单 {订单号} 因超时未支付已自动取消,库存已回补,如有需要可重新下单。 +- cancel_reason = 'TIMEOUT' 用于 M09 生成差异化文案。 + +## 七、异常与边界场景 + +| 场景 | C03 处理 | 最终状态/责任 | +|---|---|---| +| Worker 停止 | 重启后继续扫描 | 不漏扫,不重复取消 | +| 数据库连接短暂中断 | 记录错误日志,下次扫描重试 | 最多延迟一个扫描周期 | +| 扫描时订单已被支付 | 条件更新影响行数=0,跳过 | 订单保持 Paid | +| 扫描时订单已被买家取消 | 条件更新影响行数=0,跳过 | 订单保持 Cancelled | +| 库存回补时商品已删除 | 记录警告日志,跳过该商品 | 订单仍变为 Cancelled | +| 多实例 Worker 并发扫描 | 使用 SELECT FOR UPDATE SKIP LOCKED | 同一订单只被一个 Worker 处理 | +| 连续失败超过阈值 | 告警,人工介入 | 订单保留 PendingPayment | + +## 八、与 M04 订单模块的复用关系 + +C03 复用的 M04 逻辑: + +| M04 逻辑 | C03 复用方式 | +|---|---| +| 取消条件校验 | 直接复用订单归属和状态判断 | +| 库存回补事务 | 直接复用库存回补 SQL | +| 订单状态变更 | 直接复用条件更新 SQL | +| cancelled_at 和 cancel_reason | 直接复用字段写入 | +| OrderCancelledEvent | 复用事件结构,cancel_reason = TIMEOUT | + +C03 不改变的 M04 逻辑: + +- 订单创建逻辑 +- 订单查询逻辑 +- 买家主动取消的具体接口契约 + +## 九、验收证据清单 + +- [ ] 超时订单被自动取消,状态变为 Cancelled +- [ ] 库存正确回补,回补量 = 订单项数量 +- [ ] cancelled_at 和 cancel_reason = TIMEOUT 已写入 +- [ ] OrderCancelledEvent(TIMEOUT)已发布到 Outbox +- [ ] 买家收到站内消息通知 +- [ ] 买家在超时前支付成功,取消被跳过 +- [ ] 并发取消与支付只有一个成功,不出现矛盾状态 +- [ ] 重复取消返回幂等成功,库存只回补一次 +- [ ] Worker 重启后继续扫描,不漏扫 +- [ ] 多实例 Worker 不重复处理同一订单 +- [ ] 演示环境超时时间可配置(如 30 秒、5 分钟) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" new file mode 100644 index 0000000..5029bae --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" @@ -0,0 +1,270 @@ +# M04 订单流程 + +> 负责人:韦乾强 +> 覆盖:M04-01 提交订单(F08)、M04-02 订单列表与详情(F09)、M04-03 取消订单(F09) +> 基础核心流程:M01 身份与鉴权、M02 分类与商品、M03 购物车、M05 支付、M06-02 后台订单管理 +> 直接协作:朱惠惠(M03 购物车)、张海洋(M05 支付)、顾欣月(M02 商品)、罗皓晨(M09 站内消息) +> 文档状态:初稿 +> 需求事实源:[需求规格说明书 M04 订单模块](../../../01-需求文档/需求规格说明书.md) 的"M04-01、M04-02、M04-03"完整七节 + +## 一、范围与事实来源 + +本模块负责买家在已登录态下完成订单创建、订单查询、订单取消,以及商家在后台完成订单发货。订单模块承担"交易确认与履约的核心状态机",连接买家购物车结算、支付模拟、商家履约和管理员监督。 + +本文先依据需求确定业务参与者、上游输入、状态派生口径、原子结果和模块出入口,再由这些动作派生接口契约。A301~A308 只用于流程完成后的契约映射与缺口检查,不能反向拼接流程,也不能用现有 Axxx 反向覆盖业务语义。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| M04-01/F08、M04-02/F09、M04-03/F09 需求 | 完整定义 | 作为订单业务语义事实源 | +| 本文业务流程 | 初稿 | 先确认参与者、上游输入、状态派生、原子结果和模块出入口 | +| A301~A308 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| DBxxx(orders、order_items) | 模板/占位 | 本文不发明字段、约束和索引 | +| C03 订单超时取消 | 独立扩展 | 登记边界,不混入 F08/F09 主流程 | +| M06-02 后台订单管理 | F12 基础履约 | 仅登记与 M04 共享的订单状态机边界 | + +## 二、模块直接出入口 + +```mermaid +flowchart LR + ID["M01 Identity
已认证买家、角色、账号状态"] -->|"BuyerOnly 通过"| ORD["M04 Ordering
订单状态机:PendingPayment → Paid → Shipped → Completed"] + CART["M03 Cart
已选中条目、服务端金额"] -->|"提交结算:商品+数量+地址+幂等键"| ORD + CAT["M02 Catalog
实时价格、实时库存、上架状态"] -->|"价格快照、库存扣减/回补"| ORD + PAY["M05 Payment
模拟支付状态、余额"] -->|"支付成功/失败、余额变化"| ORD + ORD -->|"订单列表、详情、取消/确认动作"| BUYER["买家订单页"] + ORD -->|"发货操作、物流信息"| MERCHANT["商家后台订单页"] + ORD -->|"站内消息通知"| MSG["M09 站内消息"] + MSG -->|"订单状态变更通知"| BUYER + MSG -->|"待发货通知"| MERCHANT + + ID -. "买家取消 / 超时取消".-> ORD + CAT -. "商品下架/改价".-> ORD +``` + +边界约束: + +- M04 不接受前端传入最终金额,订单总额由服务端按快照计算。 +- M04 库存扣减/回补在事务内完成,使用条件更新防止超卖。 +- M04 不承担购物车、支付、站内消息的内部状态,只消费外部输入并输出确定性结果。 +- C03 超时取消复用 M04 的状态机与库存回补逻辑,但由 Worker 触发,不走买家主动接口。 + +## 三、提交订单(M04-01 / F08) + +```mermaid +flowchart TD + ID["M01:已认证且状态正常的买家"] --> A["买家从购物车提交订单:商品 ID 列表 + 地址 ID + 幂等键"] + CART["M03 Cart:读取选中条目"] --> B + CAT["M02 Catalog:读取实时价格、实时库存、上架状态"] --> B + A --> B{"全部商品可售且库存充足?"} + B -- "否" --> X["整单拒绝,返回问题商品和当前可购数量"] + B -- "是" --> C{"地址归属当前买家?"} + C -- "否" --> Y["拒绝:地址无效"] + C -- "是" --> D["服务端计算订单总额 = Σ(实时单价 × 数量)"] + D --> E["开启订单创建事务"] + E --> F["条件扣减库存:WHERE stock >= quantity"] + F --> G["创建订单主记录(PendingPayment)+ 订单项快照"] + G --> H["解析并保存 assignedMerchantUserId"] + H --> I["删除已下单的购物车条目"] + I --> J["写入 Outbox:OrderCreatedIntegrationEvent"] + J --> K{"事务提交成功?"} + K -- "否" --> R["库存回滚、订单不创建"] + K -- "是" --> L["返回订单号、应付金额、PendingPayment 状态"] +``` + +关键约束: + +- 同一幂等键 `(buyer_id, idempotency_key)` 只创建一张订单,重复请求返回首次成功结果。 +- 库存扣减使用条件更新 `WHERE stock >= quantity`,避免并发超卖。 +- 订单项保存商品名称、图片、成交单价快照,后续改价不影响已有订单。 +- 地址保存快照,后续修改不影响已有订单。 +- 订单金额由服务端计算,不接受客户端传入。 +- 购物车清理在同事务内完成;清理失败不影响订单有效性。 + +## 四、订单列表与详情(M04-02 / F09) + +### 4.1 订单列表 + +```mermaid +flowchart TD + ID["M01:已认证买家"] --> A["买家请求订单列表:分页 + 可选状态筛选"] + A --> B["按 buyer_id = 当前用户过滤"] + B --> C["按状态筛选(PendingPayment/Paid/Shipped/Completed/Cancelled)"] + C --> D["按创建时间倒序返回列表"] + D --> E["返回订单号、状态、总额、创建时间、商品摘要"] +``` + +关键约束: + +- 买家只能查看本人订单,按 buyer_id 过滤。 +- 订单号可脱敏展示,商品摘要最多 3 个。 +- 分页参数 page ≥ 1,pageSize 默认 10,上限 50。 + +### 4.2 订单详情 + +```mermaid +flowchart TD + ID["M01:已认证买家"] --> A["买家请求订单详情:orderId"] + A --> B{"订单存在且归属当前买家?"} + B -- "否" --> X["返回 404 或 403,不泄露归属"] + B -- "是" --> C["返回完整订单信息"] + C --> D["地址快照:收件人、手机号(脱敏)、省市区、详细地址"] + D --> E["订单项快照:商品名称/图片/单价/数量/小计"] + E --> F["状态时间线:创建/支付/发货/完成/取消时间"] + F --> G["可用操作入口:根据状态展示 cancel/confirm/pay"] +``` + +关键约束: + +- 订单项为快照,不读取商品实时价格。 +- 地址为快照,不读取地址实时状态。 +- 状态时间线展示所有状态变更节点。 + +## 五、取消订单(M04-03 / F09) + +```mermaid +flowchart TD + ID["M01:已认证买家"] --> A["买家请求取消订单:orderId"] + A --> B{"订单存在且归属当前买家?"} + B -- "否" --> X["返回 404 或 403"] + B -- "是" --> C{"订单状态为 PendingPayment?"} + C -- "否" --> Y["返回 409:状态不允许取消"] + C -- "是" --> D["开启取消事务"] + D --> E["条件更新状态为 Cancelled:WHERE status = PendingPayment"] + E --> F["回补库存:stock = stock + quantity"] + F --> G["记录 cancelled_at 和 cancel_reason = BUYER_CANCELLED"] + G --> H["写入 Outbox:OrderCancelledEvent"] + H --> I["事务提交成功?"] + I -- "否" --> R["返回错误,不回补库存"] + I -- "是" --> J["返回取消成功"] +``` + +关键约束: + +- 只有 PendingPayment 状态可取消。 +- 使用条件更新 `WHERE status = 'PendingPayment'` 保证幂等。 +- 重复取消返回成功,不重复回补库存。 +- 取消事务回滚时库存不变化。 + +## 六、与支付模块的协作(M05) + +```mermaid +flowchart TD + PENDING["PendingPayment 订单"] --> PAY["M05 Payment:买家确认支付"] + PAY --> A{"余额充足?"} + A -- "否" --> X["提示余额不足,引导充值"] + A -- "是" --> B["开启支付事务"] + B --> C["扣减钱包余额"] + C --> D["写入 payment 记录"] + D --> E["更新订单状态为 Paid"] + E --> F["写入 Outbox:OrderPaidEvent"] + F --> G{"事务提交成功?"} + G -- "否" --> R["余额回滚,订单保持 PendingPayment"] + G -- "是" --> H["返回支付成功"] + H --> I["通知 M04 更新订单状态为 Paid"] + I --> J["M09 发送站内消息给买家"] +``` + +关键约束: + +- 支付使用条件更新 `WHERE status = 'PendingPayment'` 保证幂等。 +- M05 与 M04 的状态更新共享同一 PendingPayment 条件,避免"又支付又取消"。 +- 支付成功后订单状态流转为 Paid,进入待发货阶段。 + +## 七、与商家发货的协作(M06-02 / F12) + +```mermaid +flowchart TD + PAID["Paid 订单"] --> SHIP["商家请求发货:orderId + 物流信息"] + SHIP --> A{"订单存在且与商家商品相关?"} + A -- "否" --> X["返回 404 或 403"] + A -- "是" --> B{"订单状态为 Paid?"} + B -- "否" --> Y["返回 409:状态不允许发货"] + B -- "是" --> C["更新状态为 Shipped,记录 shipped_at、物流信息"] + C --> D["写入 Outbox:OrderShippedEvent"] + D --> E["事务提交成功?"] + E -- "否" --> R["返回错误"] + E -- "是" --> F["返回发货成功"] + F --> G["M09 发送站内消息给买家"] +``` + +关键约束: + +- 商家只能操作与其商品相关的订单。 +- 发货使用条件更新 `WHERE status = 'Paid'` 保证幂等。 +- 重复发货返回成功,不重复变更状态。 + +## 八、买家确认收货 + +```mermaid +flowchart TD + SHIPPED["Shipped 订单"] --> CONFIRM["买家请求确认收货:orderId"] + CONFIRM --> A{"订单存在且归属当前买家?"} + A -- "否" --> X["返回 404 或 403"] + A -- "是" --> B{"订单状态为 Shipped?"} + B -- "否" --> Y["返回 409:状态不允许确认"] + B -- "是" --> C["更新状态为 Completed,记录 completed_at 和 completed_by = BUYER_CONFIRMED"] + C --> D["写入 Outbox:OrderConfirmedEvent"] + D --> E["事务提交成功?"] + E -- "否" --> R["返回错误"] + E -- "是" --> F["返回确认成功"] + F --> G["M09 发送站内消息给买家"] + G --> H["X01 开放评价入口(若已实现)"] +``` + +关键约束: + +- 只有 Shipped 状态可确认收货。 +- 确认收货后买家可对订单项进行评价(X01)。 +- 使用条件更新 `WHERE status = 'Shipped'` 保证幂等。 + +## 九、异常、状态竞争与责任 + +| 场景 | M04 处理 | 最终状态/责任 | +|---|---|---| +| 游客、商家、管理员访问买家订单接口 | 拒绝 | 401/403,不返回数据 | +| 订单不存在或不属于买家 | 404/403 | 不泄露归属 | +| 库存不足 | 整单拒绝 | 事务回滚,库存不扣减 | +| 商品下架 | 整单拒绝 | 事务回滚 | +| 地址无效或不归属 | 整单拒绝 | 事务回滚 | +| 幂等键重复提交 | 返回首次成功结果 | 不重复扣库存,不重复创建订单 | +| 支付时余额不足 | 支付失败 | 订单保持 PendingPayment | +| 取消时状态已变更(已支付/已发货/已完成/已取消) | 条件更新影响行数=0,返回幂等成功 | 不重复取消,不重复回补库存 | +| 并发取消与支付 | 条件更新竞争,最终只有一个成功 | 不会出现"又支付又取消" | +| 商家发货时状态已变更 | 条件更新影响行数=0,返回幂等成功 | 不重复变更 | +| C03 超时取消与支付竞争 | 条件更新竞争,最终只有一个成功 | 不会出现矛盾状态 | + +## 十、由流程派生的接口映射 + +| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +|---|---|---|---| +| 提交订单(可选幂等) | A301 | 校验库存/地址/商品状态,原子扣减,创建订单快照,返回订单号和 PendingPayment | 待评审 | +| 查询订单列表(分页+筛选) | A302 | 仅返回当前买家订单,按创建时间倒序 | 待评审 | +| 查询订单详情 | A303 | 返回地址快照、订单项快照、状态时间线 | 待评审 | +| 买家取消订单 | A304 | 条件更新状态为 Cancelled,回补库存,写入 cancelled_at | 待评审 | +| 商家查询订单列表 | A305 | 仅返回与商家商品相关的订单 | 待评审 | +| 商家查询订单详情 | A306 | 返回商家可见的订单信息 | 待评审 | +| 商家发货 | A307 | 条件更新状态为 Shipped,记录物流信息 | 待评审 | +| 买家确认收货 | A308 | 条件更新状态为 Completed,记录 completed_at | 待评审 | + +## 十一、扩展接入边界 + +- C03 订单超时自动取消:复用取消事务逻辑,Worker 触发,不走买家主动接口;C03 复用 M04 的库存回补和状态变更逻辑。 +- X01 商品评价:Completed 状态后开放评价入口,评价模块消费 OrderConfirmedEvent。 +- M09 站内消息:消费 OrderCreatedIntegrationEvent、OrderCancelledEvent、OrderShippedEvent、OrderConfirmedEvent。 +- M05 支付:消费 OrderPaidEvent 更新订单状态为 Paid。 + +## 十二、验收证据清单 + +- [ ] 正常下单:返回订单号、PendingPayment 状态、订单总额 +- [ ] 库存不足:整单拒绝,库存不扣减 +- [ ] 地址无效:整单拒绝 +- [ ] 幂等键重复:返回首次成功结果,不重复扣库存 +- [ ] 订单列表:仅返回当前买家订单,分页正确 +- [ ] 订单详情:地址快照、订单项快照、状态时间线正确 +- [ ] 买家取消:状态变为 Cancelled,库存回补,站内消息通知 +- [ ] 重复取消:幂等成功,库存只回补一次 +- [ ] 已支付/已发货/已完成/已取消订单取消被拒绝 +- [ ] 商家发货:状态变为 Shipped +- [ ] 重复发货:幂等成功 +- [ ] 买家确认收货:状态变为 Completed,站内消息通知 +- [ ] 支付与取消/发货竞争:条件更新保证最终只有一个成功 +- [ ] C03 超时取消:状态变为 Cancelled,库存回补,站内消息通知 -- Gitee From f8165b1a43511725ef2d3815e65c622ef28ebb8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9F=A6=E4=B9=BE=E5=BC=BA?= <3195306445@qq.com> Date: Fri, 24 Jul 2026 16:46:06 +0800 Subject: [PATCH 069/118] =?UTF-8?q?docs(daily):=20=E6=B7=BB=E5=8A=A0=20202?= =?UTF-8?q?6-07-24=20=E6=97=A5=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...724-\351\237\246\344\271\276\345\274\272.md" | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 "reports/daily/20260724-\351\237\246\344\271\276\345\274\272.md" diff --git "a/reports/daily/20260724-\351\237\246\344\271\276\345\274\272.md" "b/reports/daily/20260724-\351\237\246\344\271\276\345\274\272.md" new file mode 100644 index 0000000..b2735cf --- /dev/null +++ "b/reports/daily/20260724-\351\237\246\344\271\276\345\274\272.md" @@ -0,0 +1,17 @@ +# 日报 - 韦乾强 - 2026-07-24 + +## 今日完成 + +1. 将数据库设计和项目接口设计编写完成2.对所负责模块M04-订单和C03-订单超时自动取消完成这两个模块的设计文档 + +## 遇到的问题 + +无 + +## 明日计划 + +1. 确认后完成对应文档调整。 + +## 今日工时 + +约 6 小时 -- Gitee From 10d01752a9a69c2ace579a8e767b9f3d2e789de0 Mon Sep 17 00:00:00 2001 From: FISH SOUP Date: Fri, 24 Jul 2026 16:51:46 +0800 Subject: [PATCH 070/118] =?UTF-8?q?docs(process):=20=E4=BF=AE=E5=A4=8D=20t?= =?UTF-8?q?yh=20=E6=B5=81=E7=A8=8B=E6=96=87=E6=A1=A3=E7=9A=84=20TOCTOU?= =?UTF-8?q?=E3=80=81=E9=80=80=E5=87=BA=E6=B8=85=E7=90=86=E3=80=81=E5=9C=B0?= =?UTF-8?q?=E5=9D=80=E7=BC=96=E8=BE=91=E4=B8=8E=E6=A8=A1=E5=9D=97=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...00\345\207\272\346\265\201\347\250\213.md" | 31 +++++---- ...60\345\235\200\346\265\201\347\250\213.md" | 12 +++- ...41\347\220\206\346\265\201\347\250\213.md" | 68 ++++++++++--------- ...06\345\217\262\346\265\201\347\250\213.md" | 2 +- 4 files changed, 64 insertions(+), 49 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" index 7f3cb12..2f3e18d 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" @@ -104,27 +104,30 @@ flowchart TD ```mermaid flowchart TD - A["用户在任一端点击退出"] --> B["前端使用当前登录态调用退出动作"] - B --> C{"退出动作成功?"} - C -- "否" --> C1["保留本地登录态,提示重试并保留返回入口"] - C -- "是" --> D["服务端登记当前登录态失效"] - D --> E["前端清理本地登录态"] - E --> F["跳转登录页"] - A2["受保护接口仍使用旧登录态"] --> B2["校验时返回登录失效"] - B2 --> C2["前端清理登录态并引导重新登录"] - A3["M06-03 禁用账号"] --> B3["服务端提升账号令牌版本,旧登录态按版本失效"] - B3 --> C3["受保护请求返回登录失效"] - A4["手机号修改成功"] --> B4["服务端提升账号令牌版本,修改前签发的全部登录态失效"] - B4 --> C4["受保护请求返回登录失效"] + A[“用户在任一端点击退出”] --> B[“前端使用当前登录态调用退出动作”] + B --> C{“退出动作成功?”} + C -- “是” --> D[“服务端登记当前登录态失效,返回成功”] + D --> E[“前端清理本地登录态和刷新凭证”] + C -- “否” --> F[“前端无论结果如何都清理本地登录态和刷新凭证”] + F --> G[“前端提示服务端撤销状态待确认,建议用户假设凭据已泄露并尽快修改密码”] + E --> H[“跳转登录页”] + G --> H + A2[“受保护接口仍使用旧登录态”] --> B2[“校验时返回登录失效”] + B2 --> C2[“前端清理登录态并引导重新登录”] + A3[“M06-03 禁用账号”] --> B3[“服务端提升账号令牌版本,旧登录态按版本失效”] + B3 --> C3[“受保护请求返回登录失效”] + A4[“手机号修改成功”] --> B4[“服务端提升账号令牌版本,修改前签发的全部登录态失效”] + B4 --> C4[“受保护请求返回登录失效”] ``` 退出与失效约束: -- 退出顺序必须先使用当前登录态调用退出动作,收到服务端撤销结果后再清理本地登录态;服务端未成功时不得丢弃本地登录态,避免出现“看似已退出但服务端仍有效”。 +- 退出顺序必须先使用当前登录态调用退出动作;前端无论服务端是否成功,都必须清理本地登录态和刷新凭证,避免公共电脑等场景保留凭据造成安全风险。 +- 服务端撤销失败时前端必须提示”服务端撤销状态待确认”,并建议用户假设凭据已泄露、尽快修改密码;不允许保留本地登录态等待重试。 - 主动退出只影响当前登录态;本期不自动撤销同账号的其他设备登录态。 - M06-03 禁用账号、手机号修改成功后必须提升对应账号令牌版本,使修改前签发的登录态失效;新登录态签发前用户必须重新登录。 - 登录态失效结果需在多实例之间保持一致;失效状态无法确认时受保护请求失败关闭。 -- 退出后用户的所有个人页和受保护页必须退出到登录态,禁止出现“看似已退出但仍能访问”的状态。 +- 退出后用户的所有个人页和受保护页必须退出到登录态,禁止出现”看似已退出但仍能访问”的状态。 ## 六、并发、幂等与异常 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" index c9d2b79..4f2001a 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" @@ -87,10 +87,18 @@ flowchart TD D -- "否" --> E["录入收件人、联系电话、省市区、详细地址和可选默认标记"] E --> E1{"字段合法?"} E1 -- "否" --> E2["保留输入并提示字段错误"] - E1 -- "是" --> E3["保存地址并刷新列表"] + E1 -- "是" --> E3{"isDefault = true?"} + E3 -- "是" --> E4["在同一事务内取消旧默认地址"] + E3 -- "否" --> E5["保存地址"] + E4 --> E6["保存地址"] + E6 --> E7["刷新列表"] + E5 --> E7 C -- "编辑" --> F{"地址属于当前买家?"} F -- "否" --> Y["返回不存在或无权限,不泄露归属"] - F -- "是" --> E + F -- "是" --> F1["录入收件人、联系电话、省市区、详细地址(不含默认标记)"] + F1 --> F2{"字段合法?"} + F2 -- "否" --> F3["保留输入并提示字段错误"] + F2 -- "是" --> F4["保存地址,默认标记保持原值不变"] C -- "设默认" --> G{"地址属于当前买家?"} G -- "否" --> Y G -- "是" --> H["原子切换:唯一默认地址"] diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" index 5c957c3..b18e0a2 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -79,19 +79,19 @@ flowchart TD flowchart TD A[“管理员选择目标账号并确认禁用”] --> B{“目标账号合法?”} B -- “否” --> X[“拒绝操作并提示原因”] - B -- “是” --> C[“读取目标账号当前状态并锁定本流程内的复核时刻”] - C --> D{“当前状态?”} - D -- “已禁用” --> Y[“幂等返回当前状态,不重复产生副作用”] - D -- “正常” --> E[“调用 Ordering 公开应用契约查询该商家的待支付/待履约订单”] - E --> F[“调用 AfterSales 公开应用契约查询未结束的售后申请或售后窗口”] - F --> G[“调用 Seckill 公开应用契约查询未结束的秒杀活动”] - G --> H{“是否存在待处理业务?”} - H -- “是” --> Z[“拒绝禁用,账号及业务归属保持不变,返回原因”] + B -- “是” --> C[“开启 PostgreSQL 事务并按目标账号条件锁”] + C --> D[“读取当前账号状态和令牌版本号,记录为禁用版本基准”] + D --> E{“当前状态?”} + E -- “已禁用” --> Y[“回滚事务,幂等返回当前状态,不重复产生副作用”] + E -- “正常” --> F[“在同一受控事务内调用 Ordering 公开应用契约复核阻断条件”] + F --> G[“在同一受控事务内调用 AfterSales 公开应用契约复核阻断条件”] + G --> H[“在同一受控事务内调用 Seckill 公开应用契约复核阻断条件”] + H --> I{“是否存在待支付/待履约订单、未结束售后或未结束活动?”} + I -- “是” --> Z[“回滚事务,拒绝禁用,账号及业务归属保持不变”] Z --> Z1[“将复核失败原因写入结构化日志,等待管理员决策”] - H -- “否” --> I[“开启 PostgreSQL 事务”] - I --> J[“条件更新:仅在状态为正常且复核时刻匹配时改为禁用并提升令牌版本号”] + I -- “否” --> J[“条件更新:仅在状态为正常且令牌版本匹配禁用版本基准时改为禁用并提升令牌版本号”] J --> K{“条件更新是否影响行?”} - K -- “否” --> K1[“说明从复核到提交之间账号状态被他人改变,拒绝并返回最新状态”] + K -- “否” --> K1[“回滚事务,拒绝并返回最新状态”] K -- “是” --> L[“提交 PostgreSQL 事务”] L --> M[“PostgreSQL 为事实,Redis 撤销集合随后失效旧登录态”] M --> N{“Redis 撤销状态共享是否可用?”} @@ -104,8 +104,8 @@ flowchart TD 禁用约束: - 默认商家和仍有待处理业务的商家必须拒绝禁用,账号及业务归属保持不变。 -- 业务归属复核必须在 PostgreSQL 提交之前完成,按 Ordering → AfterSales → Seckill 的顺序在流程内集中复核;任一返回”存在待处理业务”则直接拒绝,不进入 PostgreSQL。 -- 复核到 PostgreSQL 提交之间存在并发窗口:必须使用 `WHERE status = 正常 AND 复核时刻 = :readAt` 形式的条件更新,并匹配复核阶段读取的状态;条件更新不命中时说明状态已被他人改变,必须拒绝并返回最新状态,避免”复核通过却被并发修改绕过”的漏洞。 +- 业务归属复核必须发生在 PostgreSQL 事务内部,与账号状态条件更新共享同一事务边界;Ordering、AfterSales、Seckill 必须提供能在该受控事务内阻止新业务归属的版本事实或行锁,否则本流程不能消除 TOCTOU 漏洞。具体事务边界由系统架构设计承接,本流程不规定应用层 HTTP 调用顺序。 +- PostgreSQL 事务以目标账号为锁起点;条件更新除匹配 `status = 正常` 外还必须匹配复核阶段读取的令牌版本号,避免复核到提交之间状态被并发改变。 - PostgreSQL 与 Redis 不能组成同一事务;PostgreSQL 提交后 Redis 撤销不可用时,必须返回服务暂不可用,不允许回滚已经持久化的状态变更(避免出现”禁用后又回滚导致旧登录态生效”的更大问题),同时也不允许返回虚假成功。 - 状态变更可追踪:操作人、目标账号、原状态、新状态、时间和 `traceId` 写入结构化日志,不写入通用操作审计。 @@ -115,42 +115,46 @@ flowchart TD flowchart TD A["管理员选择目标账号并确认启用"] --> B{"目标账号合法?"} B -- "否" --> X["拒绝操作并提示原因"] - B -- "是" --> C["开启 PostgreSQL 事务"] - C --> D["条件更新:仅在状态为禁用时改为正常"] - D --> E{"条件更新是否影响行?"} - E -- "否且当前已正常" --> Y["回滚事务,幂等返回当前状态"] - E -- "否且当前已是其他状态" --> Y1["回滚事务,拒绝并说明原因"] - E -- "是" --> F["提交 PostgreSQL 事务"] - F --> G["不恢复任何旧登录态,账号令牌版本保持当前值"] - G --> H["记录操作人、目标账号、原状态、新状态、时间和 traceId"] - H --> I["返回最新账号状态"] - I --> J["用户重新登录获取新登录态"] + B -- "是" --> C["开启 PostgreSQL 事务并按目标账号条件锁"] + C --> D["读取当前状态并记录为启用版本基准"] + D --> E["条件更新:仅在状态为禁用且令牌版本匹配启用版本基准时改为正常"] + E --> F{"条件更新是否影响行?"} + F -- "否且当前已正常" --> Y["回滚事务,幂等返回当前状态"] + F -- "否且当前已是其他状态" --> Y1["回滚事务,拒绝并说明原因"] + F -- "是" --> G["提交 PostgreSQL 事务"] + G --> H["不恢复任何旧登录态,账号令牌版本保持当前值"] + H --> I["记录操作人、目标账号、原状态、新状态、时间和 traceId"] + I --> J["返回最新账号状态"] + J --> K["用户重新登录获取新登录态"] ``` 启用约束: - 启用只改变账号状态,不恢复任何旧登录态;用户必须重新登录。 - 启用不修改 Redis 撤销集合;旧登录态即使未过期也无法继续使用。 +- 启用与禁用共用同一受控事务边界,按目标账号条件锁起始;条件更新除匹配状态外还匹配启用版本基准,避免并发覆盖。 - 重复启用必须幂等,不产生相互矛盾的状态或重复副作用。 ## 六、并发、幂等与异常 ```mermaid flowchart TD - A1["两名管理员并发禁用同一账号"] --> B1["先读取并锁定复核时刻;后提交者条件更新不命中"] - B1 --> N1["返回当前最新状态,不重复产生副作用"] - A2["同一账号在禁用与启用之间切换"] --> B2["按状态条件更新,最终状态唯一确定"] + A1["两名管理员并发禁用同一账号"] --> B1["目标账号条件锁串行化;后提交者条件更新不命中令牌版本"] + B1 --> N1["回滚事务并返回最新状态,不重复产生副作用"] + A2["同一账号在禁用与启用之间切换"] --> B2["按状态与令牌版本条件更新,最终状态唯一确定"] B2 --> N2["不出现两种状态同时生效"] A3["尝试修改角色或管理员账号"] --> B3["接口不接受相关字段或明确拒绝"] B3 --> N3["不返回修改后的角色"] A4["Redis 撤销状态共享暂时不可用"] --> B4["返回服务暂不可用"] B4 --> N4["账号状态仍按已提交结果生效,不允许回滚"] - A5["商家在复核到提交之间新产生订单或售后"] --> B5["PostgreSQL 条件更新不命中复核时刻"] - B5 --> N5["拒绝并返回最新状态,账号及业务归属保持不变"] - A6["非管理员访问管理端入口"] --> B6["拒绝访问,不返回账号列表"] - A7["筛选或分页参数非法"] --> B7["字段级错误,保留可恢复输入"] - A8["商家仍有未完成售后或关联订单"] --> B8["业务归属复核返回存在待处理业务"] - B8 --> N8["拒绝禁用并说明原因,不自动改派业务归属"] + A5["商家在事务内新产生订单或售后"] --> B5["Ordering、AfterSales、Seckill 必须提供能阻止新业务归属的版本事实或行锁"] + B5 --> N5["本事务已锁定的版本事实保证新业务不会绕过阻断条件"] + A6["应用契约不能在同一受控事务内阻止新业务归属"] --> B6["视为架构级能力缺口,必须在系统架构和接口设计中补齐"] + B6 --> N6["否则本流程不能消除 TOCTOU,本流程文档不构成完成"] + A7["非管理员访问管理端入口"] --> B7["拒绝访问,不返回账号列表"] + A8["筛选或分页参数非法"] --> B8["字段级错误,保留可恢复输入"] + A9["商家仍有未完成售后或关联订单"] --> B9["业务归属复核返回存在待处理业务"] + B9 --> N9["拒绝禁用并说明原因,不自动改派业务归属"] ``` 异常约束: diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" index 9b0641f..97885d6 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" @@ -27,7 +27,7 @@ A018~A025 由本流程派生,仅在流程评审通过后用于契约映射 ```mermaid flowchart LR - B["已登录买家"] -->|"收藏/取消/列表入口"| COLL["M08 Identity
收藏与浏览历史"] + B["已登录买家"] -->|"收藏/取消/列表入口"| COLL["M08 Engagement
收藏与浏览历史"] B -->|"查看商品详情"| COLL COLL -->|"收藏列表 + 当前商品摘要"| UI["收藏与历史页"] COLL -->|"更新浏览时间"| HIST["浏览历史"] -- Gitee From f27c48edffd3bb65637449f830a6cb63f2ecee13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A1=BE=E6=AC=A3=E6=9C=88?= Date: Fri, 24 Jul 2026 16:51:54 +0800 Subject: [PATCH 071/118] =?UTF-8?q?docs(docs):=20=E6=8F=90=E4=BA=A42026?= =?UTF-8?q?=E5=B9=B47=E6=9C=8824=E6=97=A5=E4=B8=AA=E4=BA=BA=E6=97=A5?= =?UTF-8?q?=E6=8A=A5=EF=BC=9B=E8=AE=B0=E5=BD=95=E6=8E=A5=E5=8F=A3=E4=B8=8E?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E8=AE=BE=E8=AE=A1=E3=80=81=E5=8E=9F?= =?UTF-8?q?=E5=9E=8B=E5=88=9D=E7=89=88=E5=8F=8A=E5=91=A8=E6=8A=A5=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- ...4-\351\241\276\346\254\243\346\234\210.md" | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 "reports/daily/20260724-\351\241\276\346\254\243\346\234\210.md" diff --git "a/reports/daily/20260724-\351\241\276\346\254\243\346\234\210.md" "b/reports/daily/20260724-\351\241\276\346\254\243\346\234\210.md" new file mode 100644 index 0000000..29d9aa6 --- /dev/null +++ "b/reports/daily/20260724-\351\241\276\346\254\243\346\234\210.md" @@ -0,0 +1,33 @@ +# 日报 - 顾欣月 - 2026-07-24 + + + +## 今日完成 + +1. **接口设计**:完成本人负责内容的接口设计整理,形成后续评审与开发对接所需的设计材料。 +2. **数据库设计**:完成本人负责内容的数据库设计整理,为后续实体、接口与业务数据关系的进一步核对提供依据。 +3. **原型设计**:完成项目原型设计初版,初步搭建主要页面和交互的大致框架,后续仍需继续细化。 +4. **项目 Agent 与 Skill 更新**:根据当前项目协作和文档工作需要,更新项目级 Agent 与 Skill 配置。 +5. **周报撰写**:整理本周工作进展并完成周报编写。 + + + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式/求助对象 | +|----------|----------|-------------------| +| 原型目前仅完成初版大致框架,页面细节和交互流程仍需继续完善 | 未解决 | 后续结合组内意见继续调整和细化 | +| 接口设计与数据库设计已完成本轮整理,但仍需结合后续评审结果检查一致性 | 未解决 | 后续与小组成员交叉核对设计内容并按评审意见修改 | + + + +## 明日计划 + +1. 继续细化原型页面布局与主要交互流程,完善初版中尚未明确的内容。 +2. 对接口设计和数据库设计进行交叉检查,根据组内反馈补充或调整设计细节。 + + + +## 今日工时 + +约6.5小时(上午8:00-11:20,下午14:20-18:00) -- Gitee From 963c87f502e58c5d1f50c4bd3e1dd014a7f55ff2 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 16:53:15 +0800 Subject: [PATCH 072/118] =?UTF-8?q?docs(report):=20add=20=E7=BD=97?= =?UTF-8?q?=E7=9A=93=E6=99=A8=202026-07-24=20daily=20report?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...3-\347\275\227\347\232\223\346\231\250.md" | 6 +++-- ...4-\347\275\227\347\232\223\346\231\250.md" | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 "reports/daily/20260724-\347\275\227\347\232\223\346\231\250.md" diff --git "a/reports/daily/20260723-\347\275\227\347\232\223\346\231\250.md" "b/reports/daily/20260723-\347\275\227\347\232\223\346\231\250.md" index e12f5a9..aecf90c 100644 --- "a/reports/daily/20260723-\347\275\227\347\232\223\346\231\250.md" +++ "b/reports/daily/20260723-\347\275\227\347\232\223\346\231\250.md" @@ -13,8 +13,10 @@ ## 明日计划 -1. 跟进系统架构中的待确认事项,确认后完成对应文档调整。 -2. 继续检查需求、架构和接口文档中的命名与边界是否一致。 +1. 初始化前后端基础工程,形成可继续开发的 Vue/Vite 与 .NET 解决方案基础结构。 +2. 整理接口与数据库协作规则,补充消息和基础设施接口契约,并汇总团队接口文档以处理跨模块口径问题。 +3. 建立业务流程文档的路由与维护方式,完成本人站内消息、实时推送、缓存和高可用相关流程设计。 +4. 完善项目工作流、文档路由和阶段提交边界规则,便于后续按统一规范协作。 ## 今日工时 diff --git "a/reports/daily/20260724-\347\275\227\347\232\223\346\231\250.md" "b/reports/daily/20260724-\347\275\227\347\232\223\346\231\250.md" new file mode 100644 index 0000000..b8e5423 --- /dev/null +++ "b/reports/daily/20260724-\347\275\227\347\232\223\346\231\250.md" @@ -0,0 +1,23 @@ +# 日报 - 罗皓晨 - 2026-07-24 + +## 今日完成 + +1. 初始化前后端基础工程:新增 Vue 3/Vite 前端项目,以及包含 API、Application、Domain、Infrastructure、Worker 和测试项目的 .NET 解决方案基础结构(`fc9ed60`)。 +2. 整理接口与数据库协作规则,补充消息与基础设施接口契约;汇总团队接口文档,并对齐需求、架构与跨模块接口口径(`7569005`、`74415cc`、`9c25f26`、`94e2528`)。 +3. 建立业务流程文档的路由与维护方式,完成本人站内消息、实时推送、缓存和高可用相关流程设计(`d6e4255`、`aa2ecda`)。 +4. 完善项目工作流、文档路由和阶段提交边界规则,明确阶段成果的提交范围(`b9ca003`、`6200df7`)。 + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式/求助对象 | +|----------|----------|-------------------| +| 接口契约与需求、架构之间存在跨模块口径不一致,业务流程文档也缺少统一路由。 | 已解决 | 通过汇总接口契约、补充协作规则和建立流程文档路由处理(`94e2528`、`d6e4255`、`6200df7`)。 | + +## 明日计划 + +1. 基于已初始化的前后端工程,继续按实际模块需求补充可验证的实现与测试。 +2. 持续复核接口契约、需求和架构文档之间的边界与命名一致性。 + +## 今日工时 + +约 12 小时 -- Gitee From b2e02e9d2890e016dc504e5772aeebaa744f295b Mon Sep 17 00:00:00 2001 From: FISH SOUP Date: Fri, 24 Jul 2026 16:58:41 +0800 Subject: [PATCH 073/118] =?UTF-8?q?docs(daily):=20add=20=E5=94=90=E5=AE=87?= =?UTF-8?q?=E6=98=8A=202026-07-24=20daily=20report?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...4-\345\224\220\345\256\207\346\230\212.md" | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 "reports/daily/20260724-\345\224\220\345\256\207\346\230\212.md" diff --git "a/reports/daily/20260724-\345\224\220\345\256\207\346\230\212.md" "b/reports/daily/20260724-\345\224\220\345\256\207\346\230\212.md" new file mode 100644 index 0000000..fdfa4f4 --- /dev/null +++ "b/reports/daily/20260724-\345\224\220\345\256\207\346\230\212.md" @@ -0,0 +1,41 @@ +# 日报 - 唐宇昊 - 2026-07-24 + +## 今日完成 + +1. **新增唐宇昊负责模块流程文档(F01/F02/F03/F13/X02)**:在 `docs/02-设计文档/process/tyh/` 下落地五条端到端流程文件,并同步刷新 `docs/02-设计文档/process/README.md` 与 `业务流程设计.md` 索引,覆盖买家 / 管理员 / 四身份差异化下的鉴权边界与异常分支: + - `M01-01-用户注册流程.md`(F01) + - `M01-02-用户登录与退出流程.md`(F02) + - `M01-03-个人信息与收货地址流程.md`(F03) + - `M06-03-后台用户管理流程.md`(F13) + - `M08-商品收藏与浏览历史流程.md`(X02) + - 提交:`e9a095e` docs(process): 新增唐宇昊 F01/F02/F03/F13/X02 流程文档。 +2. **修复 tyh 流程文档的接口错位、事务安全与图示规范**:基于今日新增的接口契约(`interface-tyh.md`)回填流程图中的 A 编号引用,统一事务边界与失败状态可达性,纠正 Mermaid 图示规范(字符命名、形状、连线方向): + - 提交:`1b48ae9` docs(process): 修复 tyh 流程文档的接口错位、事务安全与图示规范;5 个流程文件均被改动。 +3. **修复 tyh 流程文档的 TOCTOU、退出清理、地址编辑与模块名**:针对身份切换 / 并发场景下的 TOCTOU 风险、登录退出链路的状态清理、地址编辑校验与流程图中的模块命名规范进行统一修订: + - 提交:`10d0175` docs(process): 修复 tyh 流程文档的 TOCTOU、退出清理、地址编辑与模块名。 +4. **登记 tyh 个人接口契约(Identity & Engagement)**:在 `docs/02-设计文档/interface-tyh.md` 新增 A001~A023 接口清单与详细定义,覆盖 F01 注册、F02 登录退出、F03 资料与地址、F13 后台账号治理、X02 收藏与浏览历史;遵守 `docs/02-设计文档/接口设计.md` 2.2 节要求的个人协作阶段材料格式(仅用于贡献与交叉评审追踪,未作为实现事实源): + - 提交:`2c79bd5` docs(api): add Identity & Engagement interface contract for tyh;累计 +1307 行。 +5. **补登 A024/A025 并收紧 Identity 鉴权范围**: + - 补登 `A024 记录浏览历史` 与 `A025 查询浏览记录开关`,闭合 X02 浏览历史"写 + 读开关"两端。 + - 将 `A006/A007` 路径从 `/api/auth` 迁回 `/api/users/me` 资源域,避免个人资料变更混入认证域。 + - 按 F03 把 `A010~A014` 收货地址接口鉴权收紧为 `BuyerOnly`,与昨日日报中落地的"M01-03 仅买家可访问"与"管理员不通过本模块查看或修改买家资料"边界保持一致。 + - 提交:`eef2f43` docs(api): 补登 A024/A025 并收紧 Identity 鉴权范围。 + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式/求助对象 | +|----------|----------|-------------------| +| 流程文档首次落地时未对照最新接口契约,引用 A 编号与后续 `interface-tyh.md` 存在错位;同时 Mermaid 图示字符命名与连线方向不符合团队规范 | 已解决 | `1b48ae9` 一次性回填五条流程图的 A 编号,统一事务边界与失败状态可达性,并按 Mermaid 规范修正命名 / 形状 / 连线方向 | +| F02 登录退出链路未显式处理身份切换 / 并发的 TOCTOU 场景;F03 地址编辑校验与流程图模块命名存在进一步瑕疵 | 已解决 | `10d0175` 对 TOCTOU、退出状态清理、地址编辑校验与流程图模块命名做统一修订 | +| `A006/A007` 个人资料变更接口最初落在 `/api/auth` 路径下,与"个人资料隶属资源域而非认证域"的边界冲突;`A010~A014` 收货地址接口原鉴权范围宽于 F03 业务要求 | 已解决 | `eef2f43` 把 A006/A007 迁回 `/api/users/me` 资源域,并把 A010~A014 收紧为 `BuyerOnly`;以 `M01-03` 四身份差异化规则为依据 | +| X02 浏览历史读 / 写两端的接口契约在 A001~A023 中缺失,流程图里仅有"写"动作,缺少"读 + 开关读取" | 已解决 | `eef2f43` 补登 A024(记录浏览历史)与 A025(查询浏览记录开关),闭合 X02 闭环 | + +## 明日计划 + +1. 基于今日 `1b48ae9` 与 `eef2f43` 修复的鉴权收紧与接口契约回填,复核 `docs/02-设计文档/process/tyh/M06-03-后台用户管理流程.md` 是否对 `M06-03 BuyerOnly` / `Role!=Admin` 边界有显式分支图,必要时补一版修正 commit。 +2. 在 `docs/02-设计文档/interface-tyh.md` 已落地 A024/A025 的基础上,准备 `docs/02-设计文档/接口设计.md`(总文档)的"A024 / A025 详细定义"与"BuyerOnly 鉴权收紧"两条评审条目;待评审后由总文档统一纳入。 +3. 跟进组内六人分工确认进度,确定后续 `/eshop-deliver-feature` 阶段第一个接入接口(候选:`M01-02-A002 买家登录`),与罗皓晨同步 `auth` 模块边界。 + +## 今日工时 + +约 9 小时 \ No newline at end of file -- Gitee From 7367922056f3290882742e986f174c3fb232a387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A1=BE=E6=AC=A3=E6=9C=88?= Date: Fri, 24 Jul 2026 17:00:00 +0800 Subject: [PATCH 074/118] =?UTF-8?q?fix(process-gxy):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=20gxy=20=E6=B5=81=E7=A8=8B=E7=A8=BF=E7=BC=96=E5=8F=B7=E6=98=A0?= =?UTF-8?q?=E5=B0=84=E3=80=81=E7=8A=B6=E6=80=81=E6=9C=BA=E3=80=81=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E5=8F=A3=E5=BE=84=E4=B8=8E=E5=AD=A4=E5=84=BF=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E6=B8=85=E7=90=86=EF=BC=9B=E5=B0=86=20A101/A102/A103?= =?UTF-8?q?=E3=80=81A110~A128=E3=80=81A140~A144=20=E5=AF=B9=E9=BD=90?= =?UTF-8?q?=E5=88=B0=E6=8E=A5=E5=8F=A3=E8=AE=BE=E8=AE=A1=E4=BA=8B=E5=AE=9E?= =?UTF-8?q?=E6=BA=90=EF=BC=9BM02/M06=20=E7=8A=B6=E6=80=81=E6=9C=BA?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=B7=B2=E4=B8=8A=E6=9E=B6=E2=86=92=E5=B7=B2?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=9B=B4=E6=8E=A5=E7=AE=AD=E5=A4=B4=EF=BC=9B?= =?UTF-8?q?M02=20=E7=BC=93=E5=AD=98=E4=B8=80=E8=87=B4=E6=80=A7=E6=89=BF?= =?UTF-8?q?=E8=AE=A4=E5=8F=97=20TTL=20=E7=BA=A6=E6=9D=9F=E7=9A=84=E7=9F=AD?= =?UTF-8?q?=E6=9A=82=E6=97=A7=E5=80=BC=E7=AA=97=E5=8F=A3=EF=BC=9BM06-01=20?= =?UTF-8?q?pg=5Ftrgm/GIN=20=E6=94=B9=E4=B8=BA=20PostgreSQL=20=E4=BA=8B?= =?UTF-8?q?=E5=8A=A1=E5=86=85=E5=90=8C=E6=AD=A5=E7=BB=B4=E6=8A=A4=EF=BC=9B?= =?UTF-8?q?M07=20=E6=96=B0=E5=A2=9E=E6=9A=82=E5=AD=98=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E5=AD=A4=E5=84=BF=E6=B8=85=E7=90=86=E7=AD=96=E7=95=A5=EF=BC=88?= =?UTF-8?q?24h=20=E4=BF=9D=E7=95=99=E7=AA=97=E5=8F=A3=E3=80=815=20?= =?UTF-8?q?=E6=AC=A1=E9=87=8D=E8=AF=95=E4=B8=8A=E9=99=90=EF=BC=89=EF=BC=9B?= =?UTF-8?q?C04=20=E5=A4=8D=E7=94=A8=20A102=20=E6=90=9C=E7=B4=A2=E5=B9=B6?= =?UTF-8?q?=E6=94=B9=E5=86=99=20Top-N=20=E6=8E=92=E5=BA=8F=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E6=80=A7=E7=9A=84=E4=B8=8D=E6=AD=A3=E7=A1=AE=E6=A0=87?= =?UTF-8?q?=E5=87=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...34\347\264\242\346\265\201\347\250\213.md" | 14 +++---- ...06\345\223\201\346\265\201\347\250\213.md" | 29 +++++++-------- ...41\347\220\206\346\265\201\347\250\213.md" | 34 ++++++++++------- ...04\344\273\267\346\265\201\347\250\213.md" | 37 ++++++++++++++----- 4 files changed, 67 insertions(+), 47 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" index cce33cc..e713797 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" @@ -14,7 +14,7 @@ C04 在 M02-01 的基础搜索入口上增加中文分词模糊搜索、多条 本期使用字符 N-gram 等效分词和 PostgreSQL 倒排索引完成中文模糊搜索,不引入独立搜索引擎。本期不实现个性化排序、搜索广告、热词榜、搜索审核、同义词词典或后台全状态商品检索。 -本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。C04 不新增独立 Axxx 接口编号,而是替换 M02-01 列表查询(A101~A108)的底层搜索实现,对外接口契约完全沿用 M02 的 A105 等编号。 +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。C04 不新增独立 Axxx 接口编号,而是替换 M02-01 列表查询(A101~A103)的底层搜索实现,对外接口契约完全沿用 M02 的 A102 商品列表/搜索等编号。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| @@ -147,7 +147,7 @@ flowchart TD | 持续时间 | 60 秒 | 60 秒 | 包含预热与正式采样两段 | | 成功率 | ≥ 99% | 记录基线 | 含 5xx 视为失败 | | P95 延迟 | ≤ 500 ms | 记录基线 | 与基线对比应降低至少 30% | -| 结果正确性 | 与基线核对 | 基线 | 关键查询词集合返回的 Top-N 完全一致 | +| 结果正确性 | 进阶实现独立验证 | 基线 | 不要求 Top-N 排序完全一致;按已上架过滤、参数安全、预期相关结果集合和分页一致性核对 | 结果回填位(执行后填入真实数据): @@ -165,7 +165,7 @@ flowchart TD - 进阶搜索成功率不低于 99%。 - P95 不高于 500 ms。 - 与 `LIKE/ILIKE` 基线相比,P95 应降低至少 30%。 -- 结果正确性需要核对,避免“更快但不准”的错误对比。 +- 结果正确性独立验证,不要求两个不同算法排序完全一致:核对已上架强制过滤生效、关键词与筛选条件均生效、参数白名单与 SQL 注入防护到位、预期相关结果出现在 Top-N 中(可解释的命中样例)、分页参数一致且不漏不重。进阶搜索的核心价值是召回和相关度,不是否定基础模糊查询的排序。 ## 七、结果反馈与页面衔接 @@ -208,9 +208,9 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 中文分词模糊搜索 | 复用 M02-01 A105 搜索接口 | 按统一搜索契约返回与基础模糊查询同口径的结果 | 待交叉评审 | -| 多条件筛选与排序 | 复用 M02-01 A101/A105 接口参数 | 关键词、分类、价格、库存和排序组合生效 | 待交叉评审 | -| 进阶搜索降级 | 由 M02-01 A105 接口返回结果 | 降级时返回结果与原因日志,不改变公开口径 | 待交叉评审 | +| 中文分词模糊搜索 | 复用 M02-01 A102 商品列表/搜索接口 | 按统一搜索契约返回与基础模糊查询同口径的结果 | 待交叉评审 | +| 多条件筛选与排序 | 复用 M02-01 A102 接口参数 | 关键词、分类、价格、库存和排序组合生效 | 待交叉评审 | +| 进阶搜索降级 | 由 M02-01 A102 接口返回结果 | 降级时返回结果与原因日志,不改变公开口径 | 待交叉评审 | | 性能对比压测 | 不通过业务接口暴露 | 保留测试脚本与原始结果 | 待交叉评审 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 @@ -256,7 +256,7 @@ flowchart TD - [ ] 主流程、降级分支、索引更新与一致性、性能对比齐全。 - [ ] 状态名称与需求规格说明书一致;没有新增商品状态或排序项。 - [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径、DTO、技术模式名称(如 N-gram、pg_trgm/GIN、LIKE/ILIKE)或 SQL 片段代替流程;没有自行发明接口编号、字段名或数据库状态码。 -- [ ] C04 不另起独立接口,复用 M02-01 A105 搜索接口契约;进阶实现替换时参数白名单与返回口径保持一致。 +- [ ] C04 不另起独立接口,复用 M02-01 A102 商品列表/搜索接口契约;进阶实现替换时参数白名单与返回口径保持一致。 - [ ] 降级不是静默失败:日志或可观测性指标需暴露降级原因,不在前端构造虚假”全部成功”反馈。 - [ ] 已对照根文档 3.3 节中搜索接入点校准入口位置,C04 不另起一套主链路。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" index c6ac561..f84ec8c 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" @@ -12,7 +12,7 @@ 本模块负责购物端商品发现入口,覆盖分类筛选、关键词搜索、价格区间、库存条件、排序与组合查询,以及商品详情页的信息展示、可售状态判断、买家/游客操作衔接和评价入口衔接。它不承接商家后台的商品维护(属于 M06-01),不替代购物车的库存和归属校验,也不修改商品在历史订单中的快照。 -本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。Catalog 接口编号落在 A101~A120 范围(M02 公开浏览 A101~A108,M06-01 后台写操作 A111~A120);该编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。Catalog 接口编号落在 A101~A128 范围(M02 公开浏览 A101~A103,M06-01 后台写操作 A110~A128,M07 评价读取 A140~A144 引用);该编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| @@ -45,7 +45,7 @@ flowchart LR ID -->|"账号禁用或角色越权"| X["按身份禁止越权操作"] ADM -->|"草稿、下架或已删除商品"| Y["购物端不得出现在公开浏览结果中"] CAT -->|"商品不存在、已下架或库存归零"| Z["详情显示不可售,不提供购买入口"] - CACHE -->|"缓存不可用或数据陈旧"| W["回退事实源直读,不返回旧值"] + CACHE -->|"缓存不可用或命中陈旧"| W["回退事实源直读;缓存命中时无法可靠感知数据库已变更,存在受 TTL 约束的短暂旧值窗口"] SEARCH -->|"进阶查询失败或索引损坏"| V["回退基础模糊查询并记录降级原因"] ``` @@ -56,7 +56,7 @@ flowchart LR - 购物端的价格和库存只能作为浏览口径,下单与购物车写入必须由服务端在 M03、M04 中重新校验。 - 商品模块不直接访问用户私有数据;买家专属操作(收藏、加购、购买)由 M08、M03、M04 提供,商品模块只提供事实输入和入口。 - 评价(X01)汇总来自 M07,商品详情只做公开读取,不修改评价事实。 -- C07 缓存只能放在事实查询路径之前;缓存失效或不可用时,必须回退到事实源(PostgreSQL)直读,不返回旧数据冒充成功;缓存写入与失效由缓存主责统一约定,本文不擅自规定 TTL。 +- C07 缓存只能放在事实查询路径之前;缓存命中时无法可靠感知底层 PostgreSQL 已发生的变更,因此存在受 TTL 约束的短暂旧值窗口;缓存失效或不可用时回退到事实源(PostgreSQL)直读,由缓存主责约定 TTL 与主动失效策略,本文不擅自承诺"绝不返回旧值"。 - C04 在 M02-01 列表入口上替换底层搜索实现,对外参数白名单、已上架过滤和返回口径与本文保持一致;接口与数据待评审项在第十章集中登记。 - 扩展完成后回到的核心结果:F04 公开列表仍只返回已上架商品、F05 关键词查询仍按统一搜索契约返回、F06 详情仍按服务端最新状态展示。扩展失败不能改变上述核心结果。 @@ -118,7 +118,6 @@ stateDiagram-v2 草稿 --> 已上架: 完整性校验通过并主动上架 草稿 --> 已删除: 无历史关联且确认删除 已上架 --> 已下架: 商家主动下架 - 已上架 --> 已删除: 无历史关联且确认删除 已下架 --> 已上架: 重新校验通过并上架 已下架 --> 已删除: 无历史关联且确认删除 已删除 --> [*] @@ -128,7 +127,7 @@ stateDiagram-v2 并发与一致性: -- 商品事务提交后由 M06-01 触发缓存失效;缓存不可用或失效失败时按 M02 直读 PostgreSQL 处理,不返回旧值冒充成功。 +- 商品事务提交后由 M06-01 触发缓存失效;缓存命中时无法可靠感知数据库已变更,存在受 TTL 约束的短暂旧值窗口;缓存不可用或失效失败时按 M02 直读 PostgreSQL 处理,不掩饰错误。 - 购物端读取始终以 PostgreSQL 为事实来源;Redis 仅承担性能缓冲,不得覆盖浏览口径。 - 商品在买家浏览瞬间被下架,详情页必须按服务端最新状态展示暂不可售,不复用缓存中的已上架结果。 @@ -171,7 +170,7 @@ flowchart TD | 图片加载失败 | 使用占位图 | 不阻断价格、库存和描述浏览 | | 加载失败或网络中断 | 保留当前页面,允许重试 | 不把旧缓存价格当作最新价格 | | 商品在浏览瞬间被下架 | 服务端按最新状态返回不可售 | 不复用缓存 | -| C07 缓存失效或不可用 | 回退事实源直读 | 不返回旧值冒充成功 | +| C07 缓存失效或不可用 | 回退事实源直读 | 缓存命中时存在受 TTL 约束的短暂旧值窗口,不掩饰错误 | ## 八、由流程派生的接口契约映射 @@ -179,12 +178,10 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 查询商品列表与组合筛选 | A101 Catalog 列表 | 强制已上架过滤、分页、白名单排序、组合筛选和空结果正常返回 | 待交叉评审 | -| 查询商品详情 | A102 Catalog 详情 | 仅返回当前已上架商品的最新价格、库存、图片和描述 | 待交叉评审 | -| 查询有效分类 | A103 Catalog 分类 | 返回购物端筛选入口使用的有效分类,停用分类不出现在筛选入口 | 待交叉评审 | -| 查询筛选条件元数据 | A104 Catalog 筛选 | 返回价格区间、排序项白名单、库存条件等元数据 | 待交叉评审 | -| 关键词搜索 | A105 Catalog 搜索 | F05 基础模糊查询与 C04 进阶实现共用同一接口契约 | 待交叉评审 | -| 公开评价汇总与列表(X01 衔接) | 由 M07 派生(A121~) | 商品详情只读取 M07 公开结果,不在此模块内实现评价提交 | 待交叉评审 | +| 查询有效分类 | A101 Catalog 分类 | 返回购物端筛选入口使用的有效分类,停用分类不出现在筛选入口 | 待交叉评审 | +| 查询商品列表与组合筛选/关键词搜索 | A102 Catalog 列表/搜索 | 强制已上架过滤、分页、白名单排序、组合筛选和空结果正常返回;F05 基础模糊查询与 C04 进阶实现共用同一接口契约 | 待交叉评审 | +| 查询商品详情 | A103 Catalog 详情 | 仅返回当前已上架商品的最新价格、库存、图片和描述 | 待交叉评审 | +| 公开评价汇总与列表(X01 衔接) | 由 M07 派生(A140~A144) | 商品详情只读取 M07 公开结果,不在此模块内实现评价提交 | 待交叉评审 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 @@ -198,13 +195,13 @@ flowchart TD ## 十、由流程反查出的接口与数据待评审项 -1. 公开浏览接口(A101~A103、A105)必须明确”无登录或令牌失效时按游客返回”的契约;接口设计需与 M01 的 JWT 鉴权边界统一,避免公开接口误判为受保护接口。 +1. 公开浏览接口(A101~A103)必须明确"无登录或令牌失效时按游客返回"的契约;接口设计需与 M01 的 JWT 鉴权边界统一,避免公开接口误判为受保护接口。 2. 列表与详情对已上架过滤必须服务端强制;接口需要确认是否在响应中显式携带”不可售原因”或仅按 HTTP 状态码区分,由 M02 与接口设计共同决定。 3. 排序白名单字段集尚未在需求中枚举;接口设计前需要 M02 与评审人员确认价格、时间、相关度的默认与可选顺序。 4. 图片合规校验在前端完成上传限制后仍需服务端再次校验;接口字段需要明确”主图”与”附加图”的上传顺序和替换规则。 5. 商品详情是否暴露最新库存数或仅暴露”有货/无货”摘要,由需求决定展示口径;接口返回字段需要和前端展示要求对齐。 6. 评价公开汇总字段(平均分、总条数的计算时机)与缓存策略相关,需要与 M07、C07 共同确认。 -7. C04 进阶搜索替换 F05 基础模糊查询时,需要保留旧接口(A105)的请求字段和返回口径,避免前端按字段名硬编码查询逻辑。 +7. C04 进阶搜索替换 F05 基础模糊查询时,需要保留 A102 商品列表/搜索接口的请求字段和返回口径,避免前端按字段名硬编码查询逻辑。 8. DBxxx 分类/商品表字段尚未形成可实施的完整定义,数据库字段、约束、索引和 GIN/pg_trgm 维护策略必须由数据库设计任务另行确认。 9. C07 的 TTL 与主动失效上限需要在本流程评审前完成,避免缓存值与商品最新事实长期不一致。 @@ -218,7 +215,7 @@ flowchart TD - [ ] N04:购物端任何身份均无法搜索到草稿、下架或已删除商品;游客、买家、商家和管理员看到符合权限的操作入口,服务端鉴权生效。 - [ ] N02:图片失败或接口失败时页面仍可理解、可返回或可重试,不出现空白页。 - [ ] N05:Chrome / Edge 最新版正常显示,无明显样式错乱。 -- [ ] 缓存:缓存失效或不可用时,公开浏览口径不返回旧值;缓存命中但底层数据已变更时按 M06-01 失效结果回退。 +- [ ] 缓存:缓存命中时承认存在受 TTL 约束的短暂旧值窗口;缓存失效或不可用时回退事实源直读,不掩饰错误。 - [ ] X01 衔接:已选 X01 的评分与评价入口展示正常,但未满足条件的用户不能从详情页绕过订单资格提交评价。 ## 十二、提交前自检与升级路径 @@ -230,7 +227,7 @@ flowchart TD - [ ] 状态名称与需求规格说明书一致;没有新增核心商品状态。 - [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径、DTO、技术模式名称(如 Cache-Aside)或 SQL 片段代替流程;没有自行发明接口编号、字段名或数据库状态码。 - [ ] 公开浏览接口不因令牌失效而拒绝;身份逻辑与 M01 Policy 边界一致。 -- [ ] 接口编号落在 Catalog 范围 A101~A120(M02 占 A101~A108,M06-01 占 A111~A120,M07 占 A121~A128),不混用其他模块编号。 +- [ ] 接口编号落在 Catalog/Review 范围 A101~A200(M02 公开浏览 A101~A103,M06-01 后台写操作 A110~A128,M07 评价 A140~A144),不混用其他模块编号。 - [ ] 已对照根文档 3.3 校准主流程、状态机和模块出入口,与 M06-01 边界一致。 升级到”待交叉评审”的条件:自检完成、主流程与异常分支齐全、公开身份逻辑已对齐 M01、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" index b9dccdd..9292cf2 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -14,7 +14,7 @@ 本模块不包含多商家数据隔离、批量导入导出、定时上架、复杂审批流、商品操作审计功能或管理员代商家修改商品。 -本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。M06-01 商家写操作接口编号落在 A111~A120 范围,与 M02 公开浏览 A101~A108、M07 评价 A121~A128 同属 Catalog/Review 编号段(A101~A200);该编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。M06-01 商家写操作接口编号落在 A110~A128 范围(A110~A114 后台分类、A120~A128 后台商品),与 M02 公开浏览 A101~A103、M07 评价 A140~A144 同属 Catalog/Review 编号段(A101~A200);该编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| @@ -99,7 +99,7 @@ flowchart TD F -- "是" --> G["开启商品事务并保存商品事实"] G --> H{"事务提交成功?"} H -- "否" --> Z["整体回滚,提示保存失败并允许安全重试"] - H -- "是" --> I["提交后触发缓存失效与搜索索引同步
返回最新商品事实"] + H -- "是" --> I["提交后触发缓存失效;pg_trgm/GIN 由 PostgreSQL 事务内同步维护
返回最新商品事实"] ``` 商品字段与图片校验: @@ -124,7 +124,7 @@ flowchart TD G -- "是" --> H["先执行下架,状态变为已下架"] G -- "否" --> I["完成物理删除,进入终止结果"] H --> I - D --> J["提交后触发缓存失效与搜索索引同步"] + D --> J["提交后触发缓存失效;pg_trgm/GIN 由 PostgreSQL 事务内同步维护,不发起独立同步任务"] E --> J I --> J ``` @@ -143,7 +143,6 @@ stateDiagram-v2 草稿 --> 已上架: 完整性校验通过并主动上架 草稿 --> 已删除: 无历史关联且确认删除 已上架 --> 已下架: 商家主动下架 - 已上架 --> 已删除: 无历史关联且确认删除 已下架 --> 已上架: 重新校验通过并上架 已下架 --> 已删除: 无历史关联且确认删除 已删除 --> [*] @@ -201,20 +200,27 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 商家分页查询商品 | A111 商品后台列表 | 按商家身份过滤、分页、关键词、分类和上下架状态组合查询 | 待交叉评审 | -| 新建商品 | A112 商品创建 | 校验字段、分类、图片和并发状态,事务内保存商品事实 | 待交叉评审 | -| 编辑商品 | A113 商品编辑 | 并发保护、字段校验、事务保存并返回最新商品事实 | 待交叉评审 | -| 上下架切换 | A114 商品上下架 | 校验上架完整性;事务内条件更新销售状态 | 待交叉评审 | -| 商品后台删除 | A115 商品删除 | 仅允许无历史关联时物理删除;存在历史订单时返回拒绝并建议下架 | 待交叉评审 | -| 分类查询与维护 | A116 分类维护 | 提供分类列表、新增、编辑、启停和受限删除 | 待交叉评审 | -| 商品图片上传 | A117 商品图片 | 按合规要求上传到对象存储,返回主图与附加图标识 | 待交叉评审 | +| 商家分页查询商品(全状态) | A120 后台商品列表 | 按商家身份过滤、分页、关键词、分类和上下架状态组合查询 | 待交叉评审 | +| 后台商品详情 | A121 后台商品详情 | 返回含 version 字段的全状态商品事实,供编辑并发校验 | 待交叉评审 | +| 新建商品 | A122 商品创建 | 校验字段、分类、图片和并发状态,事务内保存商品事实 | 待交叉评审 | +| 编辑商品 | A123 商品编辑 | 乐观并发保护、字段校验、事务保存并返回最新商品事实 | 待交叉评审 | +| 商品上架 | A125 商品上架 | 校验上架完整性;事务内将状态置为已上架 | 待交叉评审 | +| 商品下架 | A126 商品下架 | 事务内将状态置为已下架;购物端列表与搜索立即不再返回 | 待交叉评审 | +| 商品后台删除 | A124 商品删除 | 仅允许无历史关联时物理删除;存在历史订单时返回拒绝并建议下架 | 待交叉评审 | +| 后台分类列表 | A110 后台分类列表 | 返回全状态分类,购物端只返回启用分类 | 待交叉评审 | +| 新建分类 | A111 新建分类 | 校验名称、父级、排序和初始状态,事务内保存 | 待交叉评审 | +| 编辑分类 | A112 编辑分类 | 校验字段和依赖,事务内保存并返回最新分类 | 待交叉评审 | +| 启用分类 | A113 启用分类 | 切换启停状态并校验依赖 | 待交叉评审 | +| 停用分类 | A114 停用分类 | 切换启停状态并校验依赖;停用后 A101/A102 不再以其作为筛选入口 | 待交叉评审 | +| 上传商品图片 | A127 商品图片上传 | 按合规要求上传到对象存储,返回主图与附加图标识 | 待交叉评审 | +| 删除商品图片 | A128 商品图片删除 | 删除商品图片关联与对象存储对象,保持引用一致 | 待交叉评审 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 ## 十、扩展接入边界 - C07 缓存:商家端商品事务提交后由架构确定的可靠机制触发缓存失效;缓存不可用时不影响商品事务,由缓存处理器重试失效动作。 -- C04 搜索:商品事务提交后由 PostgreSQL 同步维护 `pg_trgm`/GIN 索引,不建设独立的索引同步任务;进阶搜索暂时不可用时回退基础模糊查询。 +- C04 搜索:`pg_trgm`/GIN 由 PostgreSQL 事务内同步维护;本模块不建设独立的索引同步任务,事务回滚时索引同样回滚;进阶搜索暂时不可用时回退基础模糊查询。 - M03 购物车:商品下架、库存归零或被禁用后由购物车模块按 M03 规则标记失效,不反向写入商品状态。 - M04 订单:商品事务不修改历史订单的地址或商品快照;价格或上下架变更不影响已有订单。 - M01 Identity:本模块不修改账号、角色或令牌状态;账号禁用由 M06-03 独立流程处理。 @@ -238,7 +244,7 @@ flowchart TD - [ ] F11:下架商品不会出现在购物端列表和搜索结果,旧链接不再允许购买;历史订单快照保持可读。 - [ ] N04:游客、买家和管理员无法进入或调用商家写操作,后端返回正确的认证或授权结果。 - [ ] N02:并发编辑、图片失败、保存失败和删除受限时均有明确反馈,已填写内容不会无故丢失。 -- [ ] 索引:商品变更后,PostgreSQL `pg_trgm`/GIN 索引随数据同步保持一致。 +- [ ] 索引:商品变更随事务提交后,PostgreSQL `pg_trgm`/GIN 索引在事务内同步保持一致;事务回滚时索引同样回滚,本模块不建设独立索引同步任务。 - [ ] 缓存:缓存失效失败不回滚商品事务,由缓存处理器重试并记录可追踪错误。 - [ ] 保存正常和异常操作的页面截图、并发冲突提示和图片失败标记证据。 - [ ] 答辩能够说明商家事务与缓存失效的边界,以及搜索索引同步维护的责任划分。 @@ -251,7 +257,7 @@ flowchart TD - [ ] 主流程、拒绝分支、失败分支和最终结果齐全;并发保护与缓存失效责任划分清晰。 - [ ] 状态名称与需求规格说明书一致;没有新增核心商品状态。 - [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径、DTO、技术模式名称(如 Cache-Aside、pg_trgm/GIN)或 SQL 片段代替流程;没有自行发明接口编号、字段名或数据库状态码。 -- [ ] 接口编号落在 Catalog 范围 A101~A120(M06-01 占 A111~A120),不混用其他模块编号。 +- [ ] 接口编号落在 Catalog/Review 范围 A101~A200(M06-01 占 A110~A128),不混用其他模块编号。 - [ ] MerchantOnly 鉴权链路由 M01 提供,M06-01 不重复定义角色判断规则。 - [ ] 已对照根文档 3.7 校准后台角色与操作边界,与 M06-02/M06-03 边界一致。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" index ce0d3e4..4640c25 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" @@ -14,7 +14,7 @@ 本模块不包含追评、评价点赞、买家自删、匿名评价、商家回复或隐藏、自动内容审核和评价运营后台。 -本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。M07 评价接口编号落在 A121~A128 范围,与 M02 公开浏览 A101~A108、M06-01 商家写操作 A111~A120 同属 Catalog/Review 编号段(A101~A200);该编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。M07 评价接口编号落在 A140~A144 范围,与 M02 公开浏览 A101~A103、M06-01 商家写操作 A110~A128 同属 Catalog/Review 编号段(A101~A200);该编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| @@ -136,9 +136,21 @@ stateDiagram-v2 - 同一订单项重复评价或重复点击必须由数据库唯一约束或等效机制阻止,不能依赖前端去重。 - 同一订单项在两个浏览器同时提交时,仅一个事务成功,另一个由数据库唯一约束或等效机制返回已评价结果。 - 同一订单的多订单项并发提交评价:每个订单项独立判断资格与唯一性,互不影响;任一订单项评价事务失败不影响其他订单项。 -- 评价事务与图片关联在同一受控事务内提交,任一写入失败时整体回滚,不留下“评价已存但图片缺失”的部分结果。 +- 评价事务与图片关联在同一受控事务内提交,任一写入失败时整体回滚,不留下"评价已存但图片缺失"的部分结果。 - 评价公开读取最终以 PostgreSQL 为事实来源;缓存失效或读取失败时回退到数据库直读,不返回旧数据。 +**暂存图片孤儿清理**: + +评价图片采用"上传即暂存"模式(A141 返回对象键),买家在评价事务提交前可能放弃提交、关闭页面、评价事务失败或被防重复规则拦截,导致对象存储里出现未被评价引用的对象键。这些对象不属于业务事实,但持续占用对象存储容量且无任何业务用途,必须按以下策略清理: + +- 每张暂存图片在上传时记录 `uploadedAt`、`uploadedBy`、`status`(暂存/已绑定/已移除),写入图片暂存表(与评价主表分离)。 +- 提交评价事务在绑定图片成功后才将 `status` 由"暂存"切到"已绑定",并把对象键写入评价图片关联表;切换在同一受控事务内完成,整体回滚时 `status` 不变。 +- 评价事务失败、用户主动移除图片或买家放弃提交时,相关图片 `status` 标记为"已移除",但对象键保留到过期清理窗口。 +- 后台清理任务:定时(建议每日凌晨)扫描 `status='已移除'` 且 `uploadedAt` 早于保留窗口(建议 24 小时)的记录,调用对象存储删除接口并物理删除暂存表记录。 +- 暂存保留窗口内,已移除图片对应的对象键可被同一买家在原订单项上重新上传复用,不立即物理删除以避免误删正在重试的对象。 +- 用户主动"移除失败图片"立即标记 `status='已移除'` 并进入清理窗口;用户移除成功图片若已绑定到评价,则按 M07 删除评价图片契约处理,删除关联表记录并删除对象存储对象,不走暂存清理。 +- 清理任务失败的对象键需记录重试次数和最后错误,超过重试上限(建议 5 次)后由可观测性告警并保留记录供人工排查,不允许静默吞掉。 + ## 六、结果反馈与页面衔接 ```mermaid @@ -173,6 +185,9 @@ flowchart TD | 同一订单项重复评价或重复点击 | 返回已评价结果 | 不新增重复记录 | | 评分、文字或图片不合规 | 字段级错误 | 保留可恢复的表单内容 | | 部分图片上传失败 | 标记失败项 | 允许重试或移除 | +| 评价事务失败、用户放弃提交或防重复拦截 | 暂存图片标记为"已移除" | 保留至暂存保留窗口后由清理任务删除对象存储对象 | +| 暂存保留窗口内同一买家重新上传 | 复用原对象键 | 不重复上传、不重复计费 | +| 清理任务失败 | 记录重试次数与最后错误 | 超过重试上限由可观测性告警并保留记录,不静默吞掉 | | 提交时登录失效 | 引导重新登录 | 保留未提交内容,重新提交时执行完整资格校验 | | 评价事务失败 | 整体回滚 | 提示稍后重试 | | 评价列表为空或加载失败 | 友好空状态或重试入口 | 不显示空白页 | @@ -185,11 +200,11 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 提交评价 | A121 评价提交 | 校验身份、订单项归属、完成状态、唯一性、字段与图片并原子写入 | 待交叉评审 | -| 查询本人可评价订单项 | A122 评价资格 | 按当前买家返回 Completed 且尚未提交评价的订单项 | 待交叉评审 | -| 查询商品公开评价 | A123 公开评价列表 | 分页返回评分、文字、图片、时间和脱敏展示名 | 待交叉评审 | -| 查询商品评分汇总 | A124 评分汇总 | 返回有效评价总数与平均分 | 待交叉评审 | -| 查询本人已提交评价 | A125 我的评价 | 按当前买家返回本人历史评价列表 | 待交叉评审 | +| 上传评价图片(提交前暂存) | A141 评价图片上传 | 校验合规、上传到对象存储并写入暂存表,返回对象键与暂存记录 | 待交叉评审 | +| 提交商品评价(幂等) | A142 评价提交 | 校验身份、订单项归属、完成状态、唯一性、字段与图片并原子写入;图片在同一事务内由暂存切换为已绑定 | 待交叉评审 | +| 查询订单项评价资格/结果 | A143 评价资格 | 按当前买家返回 Completed 订单项的可评价状态与已提交评价 | 待交叉评审 | +| 商品公开评价分页 + 评分汇总 | A140 公开评价 | 分页返回评分、文字、图片、时间和脱敏展示名,并返回总数与平均分汇总 | 待交叉评审 | +| 单条公开评价详情查询 | A144 评价详情 | 返回单条公开评价的完整字段,供评价详情或举报链路使用 | 待交叉评审 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 @@ -210,7 +225,8 @@ flowchart TD 5. 商品详情读取评价是否要求登录状态、是否区分登录与游客可见范围,需要在接口设计中明确。 6. 评价事务失败的回滚语义需要与图片上传失败处理保持一致,避免“评价已存但图片缺失”的部分结果。 7. DBxxx 评价表字段尚未形成可实施的完整定义,数据库字段、约束和索引必须由数据库设计任务另行确认。 -8. 评价与图片上传的字段命名(`reviews`、`review_images`)已与命名规范统一,需在数据库设计中按词根实现。 +8. 评价与图片上传的字段命名(`reviews`、`review_images`、`review_image_staging`)已与命名规范统一,需在数据库设计中按词根实现;暂存表与评价图片关联表分离,孤儿清理任务依赖此分离结构。 +9. 暂存图片保留窗口(建议 24 小时)、清理任务调度频率(建议每日凌晨)、清理重试上限(建议 5 次)需要在接口和运维设计中明确,避免对象存储无限增长或被激进清理误删。 ## 十一、验收证据清单 @@ -224,7 +240,8 @@ flowchart TD - [ ] N04:评价提交与图片关联在同一事务内提交,任一步失败整体回滚;评分与文字字段级错误不写入数据库。 - [ ] N02:评价列表为空或加载失败时展示友好空状态或重试入口,不显示空白页。 - [ ] 多订单项并发:同一订单的多个订单项独立评价互不影响;两个浏览器同时提交同一订单项时仅一个成功。 -- [ ] 答辩能够说明唯一约束或等效机制如何阻止重复评价、脱敏快照生成时机、评价事务与图片关联的原子性。 +- [ ] 孤儿图片:评价失败、用户放弃提交或防重复拦截产生的暂存图片在保留窗口后被清理任务删除;清理任务失败时记录重试次数与最后错误。 +- [ ] 答辩能够说明唯一约束或等效机制如何阻止重复评价、脱敏快照生成时机、评价事务与图片关联的原子性、暂存图片孤儿清理策略。 ## 十二、提交前自检与升级路径 @@ -235,7 +252,7 @@ flowchart TD - [ ] 状态名称与需求规格说明书一致;没有新增订单状态。 - [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径、DTO、技术模式名称或 SQL 片段代替流程;没有自行发明接口编号、字段名或数据库状态码。 - [ ] **M07 不修改 M04 订单项状态**:流程图与状态机中不出现”订单项变为已评价”,已评价事实由 Review 唯一评价记录派生;订单项状态机只由 Ordering 维护。 -- [ ] 接口编号落在 Review 范围 A121~A128,不混用其他模块编号。 +- [ ] 接口编号落在 Review 范围 A140~A144,不混用其他模块编号。 - [ ] 已对照根文档 3.6 节中评价接入点校准入口位置,X01 不可绕过订单资格。 升级到”待交叉评审”的条件:自检完成、主流程与异常分支齐全、并发场景验证、订单项越界问题已修正、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 -- Gitee From d62ce21a9806f6bc5e7f3ad212a44d0d3920b0da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9F=A6=E4=B9=BE=E5=BC=BA?= <3195306445@qq.com> Date: Fri, 24 Jul 2026 17:02:31 +0800 Subject: [PATCH 075/118] =?UTF-8?q?docs(process):=20=E4=BF=AE=E6=AD=A3=20M?= =?UTF-8?q?04/C03=20=E8=AE=A2=E5=8D=95=E6=B5=81=E7=A8=8B=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E7=9A=84=2012=20=E9=A1=B9=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - M04-7: 购物车清理失败改为事务整体回滚 - M04-8: 库存扣减条件增加 status='OnSale' 防止下架后下单 - M04-9: 取消订单区分普通/秒杀库存通道 - M04-10: 不可取消状态返回 409 而非幂等成功 - M04-11: 移除支付流程中重复的 M04 状态更新 - M04-12: 商家发货权限改用 assignedMerchantUserId,加入售后竞争校验 - M04-17: 全部事件名称统一为 OrderXxxIntegrationEvent - C03-13: 商品删除时库存回补失败改为事务整体回滚 - C03-14: 区分普通/秒杀订单库存回补通道 - C03-15: 扫描查询增加 FOR UPDATE SKIP LOCKED - C03-16: 重试次数需持久化 - C03-17: 事件名称统一为 IntegrationEvent --- ...05\346\227\266\346\265\201\347\250\213.md" | 30 +++++---- ...42\345\215\225\346\265\201\347\250\213.md" | 66 ++++++++++--------- 2 files changed, 53 insertions(+), 43 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" index 835dd48..66e591f 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" @@ -28,8 +28,8 @@ flowchart LR ORDER["M04 Ordering
PendingPayment 订单创建"] --> TIMEOUT["C03 超时 Worker
定时扫描 PendingPayment 订单"] ORDER --> PAY["M05 Payment
买家主动支付"] TIMEOUT -->|"超时取消事务|回补库存"| STOCK["M02 Catalog
库存回补"] - TIMEOUT -->|"OrderCancelledEvent
超时取消通知"| MSG["M09 站内消息"] - PAY -->|"OrderPaidEvent
支付成功通知"| MSG + TIMEOUT -->|"OrderCancelledIntegrationEvent
超时取消通知"| MSG["M09 站内消息"] + PAY -->|"OrderPaidIntegrationEvent
支付成功通知"| MSG MSG -->|"超时取消通知
买家站内消息"| BUYER["买家消息中心"] ``` @@ -62,7 +62,7 @@ flowchart TD ```mermaid flowchart TD - A["Worker 定时触发(建议间隔 ≤ 超时时间/2)"] --> B["查询超时订单:status = PendingPayment AND created_at + timeout < NOW()"] + A["Worker 定时触发(建议间隔 ≤ 超时时间/2)"] --> B["查询超时订单(FOR UPDATE SKIP LOCKED):status = PendingPayment AND created_at + timeout < NOW()"] B --> C{"有待处理订单?"} C -- "否" --> Z["本次扫描结束"] C -- "是" --> D["按批次处理(建议每批 ≤ 100 条)"] @@ -70,9 +70,12 @@ flowchart TD E --> F["条件更新状态为 Cancelled:WHERE status = PendingPayment"] F --> G{"影响行数 = 1?"} G -- "否" --> H["订单已被其他操作处理(如已支付/已取消),跳过"] - G -- "是" --> I["回补库存:stock = stock + quantity"] - I --> J["记录 cancelled_at = NOW() 和 cancel_reason = TIMEOUT"] - J --> K["写入 Outbox:OrderCancelledEvent(cancel_reason = TIMEOUT)"] + G -- "是" --> I{"订单类型?"} + I -- "普通订单" --> J1["回补普通库存:stock = stock + quantity"] + I -- "秒杀订单" --> J2["回补秒杀活动库存 + 释放买家限购额度"] + J1 --> J3["记录 cancelled_at = NOW() 和 cancel_reason = TIMEOUT"] + J2 --> J3 + J3 --> K["写入 Outbox:OrderCancelledIntegrationEvent(cancel_reason = TIMEOUT)"] K --> L["提交事务"] L --> M{"提交成功?"} M -- "否" --> N["记录错误日志,重试(最多 3 次)"] @@ -91,7 +94,8 @@ flowchart TD - 扫描间隔建议 ≤ 超时时间/2(如超时 30 分钟,扫描间隔 ≤ 15 分钟)。 - 每批次处理上限 100 条,避免长时间锁表。 - 使用条件更新 `WHERE status = 'PendingPayment'` 保证幂等。 -- 失败重试 3 次后告警,订单保留待处理状态。 +- 库存回补与订单状态变更、Outbox 写入处于同一事务;任一失败则整体回滚,不允许出现"订单已取消但库存未回补"的部分成功状态。 +- 失败重试 3 次后告警,订单保留 PendingPayment,重试次数需持久化以支持跨 Worker 重启后继续。 ## 五、与支付的状态竞争处理 @@ -121,7 +125,7 @@ flowchart TD ```mermaid flowchart TD - A["超时取消事务提交成功"] --> B["发布 OrderCancelledEvent(cancel_reason = TIMEOUT)"] + A["超时取消事务提交成功"] --> B["发布 OrderCancelledIntegrationEvent(cancel_reason = TIMEOUT)"] B --> C["Outbox 投递到 MQ"] C --> D["M09 消费事件"] D --> E["生成站内消息:订单超时取消通知"] @@ -142,9 +146,9 @@ flowchart TD | 数据库连接短暂中断 | 记录错误日志,下次扫描重试 | 最多延迟一个扫描周期 | | 扫描时订单已被支付 | 条件更新影响行数=0,跳过 | 订单保持 Paid | | 扫描时订单已被买家取消 | 条件更新影响行数=0,跳过 | 订单保持 Cancelled | -| 库存回补时商品已删除 | 记录警告日志,跳过该商品 | 订单仍变为 Cancelled | -| 多实例 Worker 并发扫描 | 使用 SELECT FOR UPDATE SKIP LOCKED | 同一订单只被一个 Worker 处理 | -| 连续失败超过阈值 | 告警,人工介入 | 订单保留 PendingPayment | +| 库存回补时商品已删除或秒杀活动已结束 | 事务整体回滚,记录错误日志,重试(最多 3 次);3 次仍失败则告警,订单保留 PendingPayment | 订单不变更,库存不丢失,等待下次扫描重新处理 | +| 多实例 Worker 并发扫描 | 扫描查询使用 SELECT FOR UPDATE SKIP LOCKED,后续条件更新依赖行锁;领取与处理在同一事务内完成 | 同一订单只被一个 Worker 处理 | +| 连续失败超过阈值 | 告警,订单保留 PendingPayment,重试次数持久化;下次扫描时继承次数并继续重试 | 人工可查告警记录,订单最终仍会被处理或人工介入 | ## 八、与 M04 订单模块的复用关系 @@ -156,7 +160,7 @@ C03 复用的 M04 逻辑: | 库存回补事务 | 直接复用库存回补 SQL | | 订单状态变更 | 直接复用条件更新 SQL | | cancelled_at 和 cancel_reason | 直接复用字段写入 | -| OrderCancelledEvent | 复用事件结构,cancel_reason = TIMEOUT | +| OrderCancelledIntegrationEvent | 复用事件结构,cancel_reason = TIMEOUT | C03 不改变的 M04 逻辑: @@ -169,7 +173,7 @@ C03 不改变的 M04 逻辑: - [ ] 超时订单被自动取消,状态变为 Cancelled - [ ] 库存正确回补,回补量 = 订单项数量 - [ ] cancelled_at 和 cancel_reason = TIMEOUT 已写入 -- [ ] OrderCancelledEvent(TIMEOUT)已发布到 Outbox +- [ ] OrderCancelledIntegrationEvent(TIMEOUT)已发布到 Outbox - [ ] 买家收到站内消息通知 - [ ] 买家在超时前支付成功,取消被跳过 - [ ] 并发取消与支付只有一个成功,不出现矛盾状态 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" index 5029bae..ddde240 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" @@ -60,7 +60,7 @@ flowchart TD C -- "否" --> Y["拒绝:地址无效"] C -- "是" --> D["服务端计算订单总额 = Σ(实时单价 × 数量)"] D --> E["开启订单创建事务"] - E --> F["条件扣减库存:WHERE stock >= quantity"] + E --> F["条件扣减库存:WHERE stock >= quantity AND status = 'OnSale'"] F --> G["创建订单主记录(PendingPayment)+ 订单项快照"] G --> H["解析并保存 assignedMerchantUserId"] H --> I["删除已下单的购物车条目"] @@ -73,11 +73,11 @@ flowchart TD 关键约束: - 同一幂等键 `(buyer_id, idempotency_key)` 只创建一张订单,重复请求返回首次成功结果。 -- 库存扣减使用条件更新 `WHERE stock >= quantity`,避免并发超卖。 +- 库存扣减使用条件更新 `WHERE stock >= quantity AND status = 'OnSale'`,避免并发超卖和商品下架后仍被下单。 - 订单项保存商品名称、图片、成交单价快照,后续改价不影响已有订单。 - 地址保存快照,后续修改不影响已有订单。 - 订单金额由服务端计算,不接受客户端传入。 -- 购物车清理在同事务内完成;清理失败不影响订单有效性。 +- 购物车清理在同事务内完成;清理失败时事务整体回滚,库存不扣减,订单不创建。 ## 四、订单列表与详情(M04-02 / F09) @@ -129,12 +129,15 @@ flowchart TD C -- "否" --> Y["返回 409:状态不允许取消"] C -- "是" --> D["开启取消事务"] D --> E["条件更新状态为 Cancelled:WHERE status = PendingPayment"] - E --> F["回补库存:stock = stock + quantity"] - F --> G["记录 cancelled_at 和 cancel_reason = BUYER_CANCELLED"] - G --> H["写入 Outbox:OrderCancelledEvent"] - H --> I["事务提交成功?"] - I -- "否" --> R["返回错误,不回补库存"] - I -- "是" --> J["返回取消成功"] + E --> F{"订单类型?"} + F -- "普通订单" --> G1["回补普通库存:stock = stock + quantity"] + F -- "秒杀订单" --> G2["回补秒杀活动库存 + 释放买家限购额度"] + G1 --> H["记录 cancelled_at 和 cancel_reason = BUYER_CANCELLED"] + G2 --> H + H --> I["写入 Outbox:OrderCancelledIntegrationEvent"] + I --> J{"事务提交成功?"} + J -- "否" --> R["返回错误,不回补库存"] + J -- "是" --> K["返回取消成功"] ``` 关键约束: @@ -155,12 +158,11 @@ flowchart TD B --> C["扣减钱包余额"] C --> D["写入 payment 记录"] D --> E["更新订单状态为 Paid"] - E --> F["写入 Outbox:OrderPaidEvent"] + E --> F["写入 Outbox:OrderPaidIntegrationEvent"] F --> G{"事务提交成功?"} G -- "否" --> R["余额回滚,订单保持 PendingPayment"] G -- "是" --> H["返回支付成功"] - H --> I["通知 M04 更新订单状态为 Paid"] - I --> J["M09 发送站内消息给买家"] + H --> I["M09 消费 OrderPaidIntegrationEvent,发送站内消息"] ``` 关键约束: @@ -171,25 +173,28 @@ flowchart TD ## 七、与商家发货的协作(M06-02 / F12) -```mermaid +\`\`\`mermaid flowchart TD PAID["Paid 订单"] --> SHIP["商家请求发货:orderId + 物流信息"] - SHIP --> A{"订单存在且与商家商品相关?"} + SHIP --> A{"订单存在且 assignedMerchantUserId == currentUserId?"} A -- "否" --> X["返回 404 或 403"] - A -- "是" --> B{"订单状态为 Paid?"} - B -- "否" --> Y["返回 409:状态不允许发货"] - B -- "是" --> C["更新状态为 Shipped,记录 shipped_at、物流信息"] - C --> D["写入 Outbox:OrderShippedEvent"] - D --> E["事务提交成功?"] - E -- "否" --> R["返回错误"] - E -- "是" --> F["返回发货成功"] - F --> G["M09 发送站内消息给买家"] -``` + A -- "是" --> B{"存在未完结售后申请锁定该订单行?"} + B -- "是" --> Y["返回 409:存在进行中售后,暂不允许发货"] + B -- "否" --> C{"订单状态为 Paid?"} + C -- "否" --> Z["返回 409:状态不允许发货"] + C -- "是" --> D["更新状态为 Shipped,记录 shipped_at、物流信息"] + D --> E["写入 Outbox:OrderShippedIntegrationEvent"] + E --> F{"事务提交成功?"} + F -- "否" --> R["返回错误"] + F -- "是" --> G["返回发货成功"] + G --> H["M09 消费 OrderShippedIntegrationEvent,发送站内消息"] +\`\`\` 关键约束: -- 商家只能操作与其商品相关的订单。 -- 发货使用条件更新 `WHERE status = 'Paid'` 保证幂等。 +- 商家只能操作 \`assignedMerchantUserId == currentUserId\` 的订单(单店 B2C 严格校验)。 +- 发货前须检查是否存在未完结的售后申请(AfterSales 锁定同一订单行)。 +- 发货使用条件更新 \`WHERE status = 'Paid'\` 保证幂等。 - 重复发货返回成功,不重复变更状态。 ## 八、买家确认收货 @@ -202,7 +207,7 @@ flowchart TD A -- "是" --> B{"订单状态为 Shipped?"} B -- "否" --> Y["返回 409:状态不允许确认"] B -- "是" --> C["更新状态为 Completed,记录 completed_at 和 completed_by = BUYER_CONFIRMED"] - C --> D["写入 Outbox:OrderConfirmedEvent"] + C --> D["写入 Outbox:OrderCompletedIntegrationEvent"] D --> E["事务提交成功?"] E -- "否" --> R["返回错误"] E -- "是" --> F["返回确认成功"] @@ -227,7 +232,8 @@ flowchart TD | 地址无效或不归属 | 整单拒绝 | 事务回滚 | | 幂等键重复提交 | 返回首次成功结果 | 不重复扣库存,不重复创建订单 | | 支付时余额不足 | 支付失败 | 订单保持 PendingPayment | -| 取消时状态已变更(已支付/已发货/已完成/已取消) | 条件更新影响行数=0,返回幂等成功 | 不重复取消,不重复回补库存 | +| 取消时订单已为 Cancelled | 条件更新影响行数=0,返回幂等成功 | 不重复取消,不重复回补库存 | +| 取消时订单为 Paid/Shipped/Completed | 条件更新影响行数=0,返回 409 状态冲突 | 订单保持不变 | | 并发取消与支付 | 条件更新竞争,最终只有一个成功 | 不会出现"又支付又取消" | | 商家发货时状态已变更 | 条件更新影响行数=0,返回幂等成功 | 不重复变更 | | C03 超时取消与支付竞争 | 条件更新竞争,最终只有一个成功 | 不会出现矛盾状态 | @@ -248,9 +254,9 @@ flowchart TD ## 十一、扩展接入边界 - C03 订单超时自动取消:复用取消事务逻辑,Worker 触发,不走买家主动接口;C03 复用 M04 的库存回补和状态变更逻辑。 -- X01 商品评价:Completed 状态后开放评价入口,评价模块消费 OrderConfirmedEvent。 -- M09 站内消息:消费 OrderCreatedIntegrationEvent、OrderCancelledEvent、OrderShippedEvent、OrderConfirmedEvent。 -- M05 支付:消费 OrderPaidEvent 更新订单状态为 Paid。 +- X01 商品评价:Completed 状态后开放评价入口,评价模块消费 OrderCompletedIntegrationEvent。 +- M09 站内消息:消费 OrderCreatedIntegrationEvent、OrderCancelledIntegrationEvent、OrderShippedIntegrationEvent、OrderCompletedIntegrationEvent。 +- M05 支付:消费 OrderPaidIntegrationEvent 更新订单状态为 Paid。 ## 十二、验收证据清单 -- Gitee From bd5256db21624fded46b54462a55c096a76f2aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A1=BE=E6=AC=A3=E6=9C=88?= Date: Fri, 24 Jul 2026 17:05:29 +0800 Subject: [PATCH 076/118] =?UTF-8?q?docs(docs):=20=E8=A1=A5=E5=85=857?= =?UTF-8?q?=E6=9C=8822=E6=97=A5=E9=A1=B9=E7=9B=AE=E5=A4=8D=E7=9B=98?= =?UTF-8?q?=E4=BC=9A=E8=AE=AE=E7=BA=AA=E8=A6=81=EF=BC=9B=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E5=95=86=E5=9F=8E=E8=A7=84=E5=88=92=E3=80=81Gitee=E5=8D=8F?= =?UTF-8?q?=E4=BD=9C=E8=A6=81=E6=B1=82=E4=B8=8E=E8=A1=8C=E5=8A=A8=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- ...57\345\212\250\350\247\204\345\210\222.md" | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 "docs/04-\344\274\232\350\256\256\350\256\260\345\275\225/2026-07-22-\344\273\277\351\243\236\344\271\246\351\241\271\347\233\256\345\244\215\347\233\230\344\270\216\347\224\265\345\255\220\345\225\206\345\237\216\351\241\271\347\233\256\345\220\257\345\212\250\350\247\204\345\210\222.md" diff --git "a/docs/04-\344\274\232\350\256\256\350\256\260\345\275\225/2026-07-22-\344\273\277\351\243\236\344\271\246\351\241\271\347\233\256\345\244\215\347\233\230\344\270\216\347\224\265\345\255\220\345\225\206\345\237\216\351\241\271\347\233\256\345\220\257\345\212\250\350\247\204\345\210\222.md" "b/docs/04-\344\274\232\350\256\256\350\256\260\345\275\225/2026-07-22-\344\273\277\351\243\236\344\271\246\351\241\271\347\233\256\345\244\215\347\233\230\344\270\216\347\224\265\345\255\220\345\225\206\345\237\216\351\241\271\347\233\256\345\220\257\345\212\250\350\247\204\345\210\222.md" new file mode 100644 index 0000000..4d249e5 --- /dev/null +++ "b/docs/04-\344\274\232\350\256\256\350\256\260\345\275\225/2026-07-22-\344\273\277\351\243\236\344\271\246\351\241\271\347\233\256\345\244\215\347\233\230\344\270\216\347\224\265\345\255\220\345\225\206\345\237\216\351\241\271\347\233\256\345\220\257\345\212\250\350\247\204\345\210\222.md" @@ -0,0 +1,38 @@ +# 会议纪要:仿飞书项目复盘与电子商城项目启动规划 + +| 项目 | 内容 | +|------|------| +| 会议时间 | 2026-07-22 | +| 参会人员 | 顾欣月、罗皓晨、唐宇昊、张海洋、朱惠惠、韦乾强 | +| 缺席人员及原因 | 无 | +| 记录人 | 顾欣月 | + +## 一、会议议题 + +1. 总结上一阶段仿飞书项目中暴露的问题,并提炼本项目需要改进的协作方式。 +2. 讨论电子商城项目的范围、技术路线、阶段计划和成员分工原则。 +3. 明确 Gitee 仓库的分支、提交、推送和 PR/MR 要求。 +4. 确认电子商城项目应采用的基础模板和文档模板。 + +## 二、讨论内容与结论 + +| 议题 | 讨论要点 | 结论/决策 | +|------|----------|-----------| +| 仿飞书项目问题复盘 | 1. 前期需求边界和成员职责不够清晰,部分功能在开发后期反复调整。
2. 接口、数据库和页面之间缺少提前约定,联调时容易出现字段、状态和交互不一致。
3. 分支使用、提交说明和文件上传不够规范,出现过任务混杂、推错分支或提交内容难以追踪的问题。
4. 进度同步和阶段检查不够及时,部分问题集中到项目后期才暴露。
5. 套用模板或使用 AI 生成内容后缺少人工复核,成员对部分实现的理解和说明不够充分。 | 新项目先明确需求、分工和接口,再进入编码;采用按业务模块纵向负责的方式,每名成员负责对应模块的数据库、后端接口、前端页面、测试和必要文档。任务应拆小并及时提交,通过会议、日报、PR/MR 和交叉 Code Review 持续同步,避免问题积压到最后。 | +| 电子商城总体规划 | 项目按 4 周推进:第 1 周完成需求与设计,第 2 周开发用户、商品等核心功能,第 3 周完成购物车、订单、支付及选做功能,第 4 周进行测试、部署和答辩准备。 | 先完成需求规格说明书、系统架构、数据库设计、接口设计和成员分工,再开始代码实现。项目保持模块化单体结构,不提前拆分微服务;各成员按模块完成纵向链路,公共能力由组长或架构负责人协调。 | +| Gitee 提交与协作要求 | `master` 用于稳定发布,`dev` 用于日常集成,两个长期分支均不得直接开发或直接 Push。普通任务从最新 `dev` 创建短生命周期任务分支,一个任务对应一个分支和一个 PR/MR。提交前依次检查 `git status`、`git diff`、选择性暂存和暂存区差异,禁止未经检查执行 `git add .`。提交信息应说明实际改动,每人每天至少完成一次真实、有效、可追踪的提交。PR/MR 合入 `dev` 前须完成真实验证,并至少由一名其他成员交叉 Review。不得提交密码、Token、生产配置及 `node_modules`、`dist`、`bin`、`obj` 等生成文件。 | 全组统一按《Git 团队协作流程》执行,具体分支命名、提交格式和 PR/MR 模板以后续仓库规范为准。每次推送前确认当前分支和目标远程分支;首次使用 Gitee 的成员应先完成 HTTPS 凭据配置和远程访问验证。 | + +## 三、行动项 + +| 事项 | 负责人 | 截止时间 | 状态 | +|------|--------|----------|------| +| 整理仿飞书项目问题清单,并将改进措施落实到电子商城协作流程中 | 组长及全体成员 | 2026-07-23 | 进行中 | +| 确认六名成员的模块分工、功能边界及交叉 Review 关系 | 组长及全体成员 | 2026-07-24 | 已完成 | +| 完善需求规格说明书、系统架构设计、数据库设计和接口设计 | 各模块负责人 | 第 1 周结束前 | 进行中 | +| 完成 Gitee 凭据、远程仓库和个人任务分支操作检查 | 全体成员 | 2026-07-23 | 已完成 | +| 按要求提交个人日报,并保证提交记录与本人真实工作一致 | 全体成员 | 每个工作日结束前 | 进行中 | + +## 四、遗留问题 + +1. 前端 UI 组件库、页面视觉模板及部分基础设施的精确版本尚未最终确定。 +2. Gitee 的 `master`、`dev` 分支保护、PR/MR 审批和 CI 检查规则仍需仓库管理员在平台侧完成配置并验证。 -- Gitee From 8f9cce4cc1ce9c9abb4666ab6d53563cabccc9fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=B5=B7=E6=B4=8B?= <2040454617@qq.com> Date: Fri, 24 Jul 2026 17:05:29 +0800 Subject: [PATCH 077/118] =?UTF-8?q?docs(process):=20fix=20C08/M10=20?= =?UTF-8?q?=E7=AC=AC=E4=BA=8C=E8=BD=AE=E9=98=BB=E6=96=AD=E6=80=A7=E6=BC=8F?= =?UTF-8?q?=E6=B4=9E=EF=BC=9B=E7=8A=B6=E6=80=81=E6=9C=BA/Outbox/=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=A5=91=E7=BA=A6=E4=B8=80=E8=87=B4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C08 修复(2 个): - §3.1 状态机与流程图矛盾:删除 Received→Rejected 箭头(签名错误不写入 Inbox);合法转换回执明确'签名错误或伪造回调不写入 Inbox,由 §四 流程图 X 节点处理' - §3.2 批次状态机与流程图矛盾:删除 [*]→Pending 中间态;改为 [*]→Matched 和 [*]→HasDifferences 两条直接路径,与 §七 对账批次生成流程图 I 节点'同事务内生成'一致 M10 修复(4 个 + 2 处遗留): - §1 范围引用 A431:改为 A411~A419 与 A432、A434(A431 已取消并入 IRefundService) - §3 状态机 Refunding 矛盾:合法转换回执加'Refunding 是事务内瞬间中间状态,事务提交后立即转为 Refunded 或 RefundFailed;外部观察和监控按终态过滤' - §8 撤销申请 Outbox 事务外:D 改开启单一 PostgreSQL 事务;G 改为'同事务内:写入审计日志 + Outbox 申请撤销事实';删除事务外 I 节点;J 直接通知(Outbox 由 M09 事务外可靠推送) - §13 第 357 行 A431 措辞:改为'原 A431 内部应用能力(已并入 IRefundService)的 expectedAmount' - §1 第 21 行事实表遗留:'A431 内部应用能力' 改为 'IRefundService 内部应用能力(A431 已取消并入)' - §13 第 366 行待评审项 10 遗留:'A431 内部应用能力(退款入账)' 改为 'IRefundService 内部应用能力(A431 已取消并入,仅承担退款入账)' 依据:docs/02-设计文档/接口设计.md A411-A419/A421-A425/A432-A434 总契约(明确 A418 / A431 已取消)+ docs/02-设计文档/process/README.md 状态机与图规范 + Outbox 必须在原业务事务内写入。 Refs: zhy 第二轮漏洞清单 C08 §3.1/§3.2 + M10 §1/§3/§8/§13 --- ...71\350\264\246\346\265\201\347\250\213.md" | 41 +++++++-------- ...56\345\220\216\346\265\201\347\250\213.md" | 50 +++++++++---------- 2 files changed, 43 insertions(+), 48 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" index 4bd4738..9b14db0 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" @@ -61,14 +61,12 @@ flowchart LR stateDiagram-v2 [*] --> Received: 通道发送回调 Received --> Processing: 签名校验通过 - Received --> Rejected: 签名错误或伪造 Processing --> Processed: 事务提交成功 Processing --> Ignored: 订单状态拒绝目标结果 Processing --> Difference: 已取消订单收到迟到成功 Processing --> Failed: 事务中断或字段错误 Failed --> Processing: 安全重试 Processed --> [*] - Rejected --> [*] Ignored --> [*] Difference --> [*] ``` @@ -79,14 +77,14 @@ stateDiagram-v2 - `Ignored` 表示回调信号被业务规则主动忽略(如订单已 `Paid` 又收到重复成功)。 - `Difference` 表示已登记差异,等待对账阶段暴露并由管理员处理。 - 任一终态都保留 Inbox 记录,可重复分析但不可再修改业务状态。 +- 签名错误或伪造回调**不写入 Inbox**(由 §四 流程图 X 节点处理),不进入 Inbox 状态机的任何状态;如需审计追踪,由网关层或安全日志保留。 ### 3.2 对账批次状态 ```mermaid stateDiagram-v2 - [*] --> Pending: Worker 开始生成 - Pending --> Matched: 全部匹配无差异 - Pending --> HasDifferences: 存在差异 + [*] --> Matched: Worker 同事务内生成,全部匹配无差异 + [*] --> HasDifferences: Worker 同事务内生成,存在差异 HasDifferences --> Resolved: 管理员闭环所有差异 Matched --> [*] Resolved --> [*] @@ -95,6 +93,7 @@ stateDiagram-v2 合法转换回执: - 同一日期同一范围不重复生成矛盾批次;Worker 重复执行以 `date + range` 唯一约束去重。 +- 批次状态由 Worker 在同事务内根据差异结果直接定为 `Matched`(全部匹配)或 `HasDifferences`(存在差异),不经过 `Pending` 中间态;这与 §七 对账批次生成流程图 I 节点"同事务内生成批次记录"一致。 - `HasDifferences` 必须保留所有差异条目和处理状态,差异未闭环时批次仍处于 `HasDifferences`。 ### 3.3 差异处理状态 @@ -103,9 +102,7 @@ stateDiagram-v2 stateDiagram-v2 [*] --> Pending: 批次生成时登记 Pending --> InProgress: 管理员开始处理 - InProgress --> Resolved: 管理员闭环处理 - Pending --> [*] - InProgress --> [*] + InProgress --> Resolved: 管理员闭环处理(必须先填写处理说明) Resolved --> [*] ``` @@ -113,6 +110,7 @@ stateDiagram-v2 - 状态条件 `WHERE status = ?` 唯一推进,避免并发处理同一差异。 - `Resolved` 必须保留处理说明和处理人,不得靠直接改库绕开记录。 +- `Pending` 和 `InProgress` 都是未完成状态,不允许直接终止;只有填完处理说明并提交事务后才能进入 `Resolved` 终止。 ## 四、回调接收与鉴别 @@ -150,6 +148,7 @@ flowchart TD C -- "PendingPayment" --> D{"回调结果?"} D -- "Success" --> E["条件推进 PendingPayment → Paid"] D -- "Failed" --> E2["写入失败记录,订单保持 PendingPayment
不写 Outbox 支付成功事实"] + E2 --> IE2["同事务内:推进 Inbox 至 Failed(保留失败结果,不停留在 Processing)"] C -- "Paid" --> F{"回调结果?"} F -- "Success" --> FX["已支付成功回调,标记 Ignored,不重复写支付记录"] F -- "Failed" --> FY["登记为差异:支付记录重复但订单已支付"] @@ -164,6 +163,7 @@ flowchart TD GY --> IGY["同事务内:推进 Inbox 至 Ignored"] H --> IH["同事务内:推进 Inbox 至 Ignored"] I --> J{"事务提交成功?"} + IE2 --> J IFX --> J IFY --> J IGX --> J @@ -213,17 +213,16 @@ flowchart TD W["Worker 定时触发"] --> A["确定对账日期与范围"] A --> B{"该日期+范围已存在批次?"} B -- "是" --> BX["跳过:不重复生成"] - B -- "否" --> C["开启批次事务"] + B -- "否" --> C["开启单一 PostgreSQL 事务"] C --> D["读取支付记录、订单状态、终态为 Processed 或 Difference 的回调 Inbox"] D --> E["读取 M10 退款记录、wallet_ledgers 中退款入账记录"] E --> F["比对支付记录 vs 订单状态"] F --> G["比对退款成功 vs 退款流水 vs 钱包入账"] - G --> H["发现差异则逐条登记"] - H --> I["生成批次记录"] + G --> H["发现差异则同事务内逐条登记为待处理差异条目"] + H --> I["同事务内生成批次记录,状态按差异结果为 Matched 或 HasDifferences"] I --> J{"事务提交成功?"} - J -- "否" --> JR["整体回滚,批次未生成"] - J -- "是" --> K["创建差异条目(若存在)"] - K --> L["通知管理员有批次生成"] + J -- "否" --> JR["整体回滚:批次和差异明细均未生成,避免'有差异统计但没有差异明细'"] + J -- "是" --> L["通知管理员有批次生成"] ``` 关键规则: @@ -241,15 +240,13 @@ flowchart TD C --> D{"选择单条差异"} D -- "查看详情" --> E["展示订单号、支付/退款记录、状态时间线、Inbox 历史"] D -- "开始处理" --> F["状态条件推进 Pending → InProgress"] - D -- "标记已解决" --> G["状态条件推进 InProgress → Resolved"] - D -- "已解决" --> H["填写处理说明"] - H --> I["开启事务"] - I --> J["写入处理说明、处理人、处理时间"] - J --> K["推进差异状态"] + F --> H["填写处理说明(必填)"] + H --> I["开启单一 PostgreSQL 事务"] + I --> J["同事务内:写入处理说明 + 处理人 + 处理时间"] + J --> K["同事务内:推进差异状态 InProgress → Resolved"] K --> L{"事务提交成功?"} - L -- "否" --> LR["整体回滚,差异状态保持"] - L -- "是" --> M["记录处理审计日志"] - G --> M + L -- "否" --> LR["整体回滚,处理说明和 Resolved 状态均未生效;状态保持 InProgress"] + L -- "是" --> M["同事务内:记录处理审计日志"] M --> N{"批次所有差异都已 Resolved?"} N -- "是" --> O["批次状态推进 HasDifferences → Resolved"] N -- "否" --> P["保持 HasDifferences"] diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" index 25d43af..f4be56a 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" @@ -11,14 +11,14 @@ 本模块负责买家针对本人已支付、已发货或完成后 7 天内的订单项发起退款或退货申请,商家在后台审核并推进售后状态,退款采用模拟处理并退回买家小金库。它不接入真实退款渠道、不参与 C03 待支付订单超时扫描、不主动修改订单的核心履约状态。 -本文先依据需求确定业务参与者、状态、判断分支、事务结果和模块出入口,再由这些流程步骤派生接口能力。A411~A419 与 A431~A434 只用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 +本文先依据需求确定业务参与者、状态、判断分支、事务结果和模块出入口,再由这些流程步骤派生接口能力。A411~A419 与 A432、A434 只用于流程完成后的契约映射和缺口检查(A431 已取消并入 `IRefundService` 内部应用能力),不能反向决定或拼接业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M10/X04 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 初稿 | 先确认角色、状态、分支、事务边界和模块出入口 | | A411~A419、A432、A434 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | -| A431 内部应用能力 | 内部契约 | 作为退款入账的内部服务,不占 Axxx HTTP 编号 | +| IRefundService 内部应用能力(A431 已取消并入) | 内部契约 | 作为退款入账的内部服务,不占 Axxx HTTP 编号 | | DB081~DB100 数据库设计 | 模板/占位 | 本文不发明表字段、枚举编码和索引 | | C08 退款对账 | 独立扩展 | 只登记接入点,不混入 F10 核心状态机 | | C03 超时取消 | 独立流程 | 售后申请与退款处理不参与 C03 扫描 | @@ -75,6 +75,7 @@ stateDiagram-v2 - 申请只能由买家提交;只有 `PendingReview` 状态可被买家主动撤销。 - 商家只能在 `PendingReview` 状态下审核;审核拒绝后不可再次审核。 - 退货退款必须经历 `PendingReturn → PendingReceipt → Refunding`;商家确认收货是退款前置条件。 +- `Refunding` 是事务内瞬间中间状态,事务提交后立即转为 `Refunded`(退款成功)或 `RefundFailed`(退款失败);外部观察和监控按终态(`Refunded` / `RefundFailed`)过滤。 - 退款失败**不是**终止状态,可通过幂等重试回到 `Refunding`。 - 重复退款请求返回首次结果,不重复写入钱包流水。 @@ -117,12 +118,11 @@ flowchart TD H --> I["在锁定行内用 PostgreSQL 条件聚合核算:
剩余可售后数量 = 订单项数量 − 处理中数量 − 已退款数量"] I --> J{"申请数量 ≤ 剩余可售后数量(同事务内复算)?"} J -- "否" --> JZ["拒绝:剩余可售后数量不足"] - J -- "是" --> K["写入申请、申请单状态置 PendingReview、记录审计日志"] + J -- "是" --> K["同事务内:写入申请 + 申请单状态 PendingReview + audit_log + Outbox 申请提交事实"] K --> L{"事务提交成功?"} - L -- "否" --> LR["整体回滚,不创建申请;锁随事务结束释放"] - L -- "是" --> M["记录待发布申请提交事实"] - M --> N["通知商家审核"] - M --> O["买家可查看本人申请详情"] + L -- "否" --> LR["整体回滚:申请、Outbox 事实均未写入;锁随事务结束释放"] + L -- "是" --> N["通知商家审核(Outbox 由 M09 在事务外可靠推送)"] + N --> O["买家可查看本人申请详情"] ``` 关键规则: @@ -149,10 +149,11 @@ flowchart TD D -- "Approve" --> H["记录审核意见"] H --> I{"申请类型?"} I -- "RefundOnly" --> J["开启审核事务"] - J --> J1["同事务内调用 IRefundService:原子写入退款记录 + 钱包入账 + 钱包流水"] + J --> J0["同事务内:按普通/秒杀原通道回补未发货库存(保留库存可售数量)"] + J0 --> J1["同事务内调用 IRefundService:原子写入退款记录 + 钱包入账 + 钱包流水"] J1 --> J2{"IRefundService 返回结果?"} - J2 -- "退款成功" --> J3["状态推进 PendingReview → Refunded"] - J2 -- "退款失败" --> J4["用独立失败记录保留状态 RefundFailed
供 A419 失败重试使用"] + J2 -- "退款成功" --> J3["同事务内:状态推进 PendingReview → Refunded"] + J2 -- "退款失败" --> J4["同事务内:用独立失败记录保留状态 RefundFailed
供 A419 失败重试使用"] I -- "ReturnAndRefund" --> L["状态条件推进 PendingReview → PendingReturn"] L --> L1["保留审核意见,等待买家提交退货物流"] J3 --> MR @@ -184,11 +185,10 @@ flowchart TD D -- "是" --> H["开启单一 PostgreSQL 事务"] H --> H0["通过 Ordering 公开应用契约在同一 PostgreSQL 事务中锁定 orders 行"] H0 --> I["状态条件推进 PendingReturn → PendingReceipt"] - I --> J["记录退货物流信息、audit_log"] + I --> J["同事务内:写入退货物流信息 + audit_log + Outbox 退货物流提交事实"] J --> K{"事务提交成功?"} - K -- "否" --> KR["整体回滚,状态保持 PendingReturn;锁随事务结束释放"] - K -- "是" --> L["记录待发布退货物流提交事实"] - L --> M["通知商家待收货"] + K -- "否" --> KR["整体回滚:状态、物流信息、Outbox 事实均未生效;锁随事务结束释放"] + K -- "是" --> M["通知商家待收货(Outbox 由 M09 在事务外可靠推送)"] ``` 退货物规则: @@ -238,7 +238,7 @@ flowchart TD G -- "是" --> GS["申请状态推进 Refunding → Refunded"] GS --> H["记录待发布退款完成事实"] H --> I["通知买家退款成功"] - G -- "否" --> GF["不整体回滚:用独立失败记录保留状态 Refunded 申请保持 Refunding,
未推进申请置为 RefundFailed"] + G -- "否" --> GF["不整体回滚:用独立失败记录保留状态 RefundFailed(钱包入账失败/退款记录失败等中间失败时),由 A419 失败重试入口处理"] GF --> GFA["独立失败记录由 A419 失败重试入口处理"] GFA --> IFAIL["通知相关方失败原因,等待重试"] ``` @@ -254,7 +254,7 @@ flowchart TD = 同一事务提交成功 ``` -任一步失败时整体回滚,不允许出现"钱包已入账但退款记录缺失"或"申请已退款但订单被覆盖"的部分结果。 +任一步失败时按失败子操作分别处理:钱包入账或退款记录中间失败时**不整体回滚**,用独立失败记录保留 `RefundFailed` 供 A419 重试;其他非法状态变更(订单被覆盖、申请被回退到非终态)才整体回滚。不允许出现"钱包已入账但退款记录缺失"或"申请已退款但订单被覆盖"的部分结果。 ## 八、买家撤销申请 @@ -265,14 +265,13 @@ flowchart TD B -- "否" --> X["拒绝访问"] B -- "是" --> C{"状态为 PendingReview?"} C -- "否" --> CY["拒绝:审核后不允许撤销"] - C -- "是" --> D["开启事务"] + C -- "是" --> D["开启单一 PostgreSQL 事务"] D --> E["释放订单项已占用的未售后数量"] E --> F["状态条件推进 PendingReview → Cancelled"] - F --> G["记录审计日志"] + F --> G["同事务内:写入审计日志 + Outbox 申请撤销事实"] G --> H{"事务提交成功?"} - H -- "否" --> HR["整体回滚,状态保持 PendingReview"] - H -- "是" --> I["记录待发布申请撤销事实"] - I --> J["通知商家申请已撤销"] + H -- "否" --> HR["整体回滚:状态变更、审计日志、Outbox 事实均未生效"] + H -- "是" --> J["通知商家申请已撤销(Outbox 由 M09 在事务外可靠推送)"] ``` 撤销售后规则: @@ -333,14 +332,13 @@ flowchart TD | 售后资格预检 | A411 | 按订单项、状态、时限和剩余可售后数量判断资格 | 待交叉评审 | | 提交售后申请 | A412 | 校验归属、类型、金额和防重复,并写入 PendingReview | 待交叉评审 | | 申请列表 | A413 | 按本人或授权范围分页返回申请 | 待交叉评审 | -| 申请详情 | A414 | 展示订单项快照、实付金额、申请内容、审核意见、状态时间线 | 待交叉评审 | +| 申请详情(含状态时间线 + 审核日志) | A414 | 展示订单项快照、实付金额、申请内容、审核意见、状态时间线、audit_log 时序 | 待交叉评审 | | 撤销申请 | A415 | 仅 PendingReview 可撤销,状态条件推进 | 待交叉评审 | | 商家审核 | A416 | 通过业务能力推进状态并发退款或转待退货 | 待交叉评审 | | 提交退货物流 | A434 | 写入快递公司与运单,状态推进 PendingReturn → PendingReceipt | 待交叉评审 | | 商家确认收货 | A417 | 状态条件推进 PendingReceipt → Refunding + 库存回补 | 待交叉评审 | -| 审核日志 | A418 | 按 audit_log 时序展示状态变更 | 待交叉评审 | | 退款失败重试 | A419 | 状态条件推进 RefundFailed → Refunding | 待交叉评审 | -| 退款入账 | A431 内部应用能力 | 不占 Axxx HTTP 编号,进程内调用 IRefundService | 待交叉评审 | +| 退款入账 | `IRefundService` 内部应用能力 | 不占 Axxx HTTP 编号(A431 已取消并入),进程内调用 `IRefundService.CreateRefundAsync` | 待交叉评审 | | 退款详情 | A432 | 展示单笔退款金额、状态、流水编号 | 待交叉评审 | | 退款列表 | A433 | 按本人或授权范围分页返回退款记录 | 待交叉评审 | @@ -356,7 +354,7 @@ flowchart TD ## 十三、由流程反查出的接口与数据待评审项 -1. 退款金额不一致:流程要求金额由服务端按实付单价 × 申请数量计算;A431 内部应用能力的 `expectedAmount` 只能承担客户端旧值冲突保护,不能成为扣款事实。 +1. 退款金额不一致:流程要求金额由服务端按实付单价 × 申请数量计算;原 A431 内部应用能力(已并入 `IRefundService`)的 `expectedAmount` 只能承担客户端旧值冲突保护,不能成为扣款事实。 2. 部分退款后订单状态:流程要求订单保持原核心履约状态;A407 支付记录列表与 A432 退款详情必须分别提供"未退款金额"和"已退款金额",不能合并为"已退款"状态。 3. 卖家超时未处理:流程不自动同意或拒绝;A416 审核接口必须保留超时仍未处理的 PendingReview 状态,不能引入"超时自动拒绝"。 4. 退货拦截发货:流程要求处理中申请阻断发货;M04 的发货接口必须能识别订单项未售后数量,禁止对未售后数量不足的订单项发货。 @@ -365,7 +363,7 @@ flowchart TD 7. 状态机不可逆:流程要求"已退款 / 已拒绝 / 已撤销"为终止状态;A419 退款失败重试只能从 `退款失败` 推进,不能从"已退款"或"已拒绝"推进。 8. 退货快递单号共享:流程要求同一快递单号可承载同一订单多笔退货申请(A434 总契约);不建快递单号全局唯一约束;重复提交由申请状态和 `Idempotency-Key` 控制。 9. 商家并发审核:流程要求状态条件唯一胜出;A416 审核必须检查 `WHERE status = 'PendingReview'` 条件更新,失败方收到 `409 + AFTER_SALES.INVALID_STATUS`。 -10. 库存回补时机:流程要求未发货仅退款 + 退货退款确认收货都回补库存;A431 内部应用能力(退款入账)不负责库存回补,由 M04 / M02 在确认收货时完成。 +10. 库存回补时机:流程要求未发货仅退款 + 退货退款确认收货都回补库存;`IRefundService` 内部应用能力(A431 已取消并入,仅承担退款入账)不负责库存回补,由 M04 / M02 在确认收货时完成。 11. DB081~DB100 尚未形成可实施的完整表定义,数据库字段、约束和索引必须由数据库设计任务另行确认。 ## 十四、验收证据清单 -- Gitee From 92f936616413d3e3a9fdd1715338de4721c3b258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=83=A0=E6=83=A0?= <2205590672@qq.com> Date: Fri, 24 Jul 2026 16:56:38 +0800 Subject: [PATCH 078/118] =?UTF-8?q?docs(process-zhh):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20M03/C01=20=E6=B5=81=E7=A8=8B=E6=96=87=E6=A1=A3=E7=9A=84?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E9=94=99=E4=BD=8D=E4=B8=8E=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=9BM03=20=E9=87=8D=E6=96=B0=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=20A2xx=20=E6=98=A0=E5=B0=84=E5=B9=B6=E6=8A=8A?= =?UTF-8?q?=E6=95=B0=E9=87=8F=E6=B5=81=E7=A8=8B=E6=8B=86=E5=88=86=E4=B8=BA?= =?UTF-8?q?=E8=B0=83=E5=A4=A7/=E8=B0=83=E5=B0=8F=E4=B8=A4=E6=9D=A1?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=EF=BC=9BC01=20=E9=87=8D=E6=96=B0=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=20A22x=20=E6=98=A0=E5=B0=84=E3=80=81=E6=94=B6?= =?UTF-8?q?=E7=B4=A7=E7=BC=96=E8=BE=91=E5=8F=AA=E5=85=81=E8=AE=B8=20Draft?= =?UTF-8?q?=20=E6=88=96=E5=B0=9A=E6=9C=AA=E5=BC=80=E5=A7=8B=E3=80=81?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E6=94=B9=E4=B8=BA=E6=8C=89=20activityId=20?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6=E6=BF=80=E6=B4=BB=E5=B7=B2=E5=86=99=E8=AE=A1?= =?UTF-8?q?=E5=88=92=E9=85=8D=E9=A2=9D=E3=80=81=E6=8A=8A=E7=A7=92=E6=9D=80?= =?UTF-8?q?=E5=BA=93=E5=AD=98=E5=89=8D=E5=90=8E=E7=AB=AF=E4=B8=80=E8=87=B4?= =?UTF-8?q?=E6=94=B9=E5=86=99=E4=B8=BA=E9=A1=B5=E9=9D=A2=E4=BB=85=E6=8F=90?= =?UTF-8?q?=E7=A4=BA/=E6=8F=90=E4=BA=A4=E6=8C=89=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E6=9D=A1=E4=BB=B6=E6=9B=B4=E6=96=B0=EF=BC=9B=E5=85=B6?= =?UTF-8?q?=E4=BB=96=E6=A8=A1=E5=9D=97=E6=8E=A5=E5=8F=A3=E6=98=A0=E5=B0=84?= =?UTF-8?q?=E3=80=81=E5=85=AC=E5=85=B1=E5=A5=91=E7=BA=A6=E4=B8=8E=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E6=8F=8F=E8=BF=B0=E4=B8=8D=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...22\346\235\200\346\265\201\347\250\213.md" | 52 +++++++++++-------- ...51\350\275\246\346\265\201\347\250\213.md" | 23 ++++---- 2 files changed, 43 insertions(+), 32 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" index 724f714..b00dd67 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" @@ -11,7 +11,7 @@ 本扩展在面向买家的高并发秒杀场景下保证库存“只减不超、不少不丢”:每一份秒杀库存只能被一名买家以一份订单成功购买,重复请求不能产生重复扣减或重复订单,所有失败请求不得留下半扣减、未提交订单或孤立记录。本期秒杀以“限时一口价活动”为模型,关联一个已上架的普通商品和一份独立维护的秒杀库存。活动期内买家点击“立即抢购”直接提交秒杀订单,跳过普通加车流程;活动结束或库存耗尽后入口立刻失效,进入商品详情时只能看到普通购买。 -数据库事务是秒杀正确性的唯一事实来源。Redis、消息队列、Nginx 限流和前端防抖只承担性能与体验,不得用作并发正确性边界。本文按“先确认业务参与者和原子结果,再确定流程步骤和不可变核心事实”的顺序编写;A22x(A220 浏览活动、A221 抢购提交、A222 抢购结果查询、A223 商家维护活动、A224 库存回补;项目约定的 C01 接口范围为 A220~A228)只用于流程完成后的契约映射与缺口检查,不能反向拼接流程。 +数据库事务是秒杀正确性的唯一事实来源。Redis、消息队列、Nginx 限流和前端防抖只承担性能与体验,不得用作并发正确性边界。本文按“先确认业务参与者和原子结果,再确定流程步骤和不可变核心事实”的顺序编写;A22x(A220 创建活动、A221 更新活动、A222 发布活动、A223 取消活动、A224 商家活动列表、A225 商家活动详情、A226 买家活动列表、A227 买家活动详情、A228 秒杀下单;项目约定的 C01 接口范围为 A220~A228)只用于流程完成后的契约映射与缺口检查,不能反向拼接流程。库存回补、抢购结果查询与库存划拨不占用 A22x;其中库存回补走 M04 取消事务通过 Ordering 公开应用契约回写到 `seckill_inventory`,抢购结果由 Ordering 的 A302/A303 承担,发布划拨走 A222 在同一事务内落 `seckill_inventory.activated_at`。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| @@ -63,22 +63,21 @@ flowchart TD B -- "新建" --> C["绑定已上架商品、设定开始时间、结束时间、秒杀价、库存总量和单用户限购"] C --> D{"开始 ≥ 当前 UTC、结束 > 开始、库存 ≤ 商品当前可售库存且为正整数?"} D -- "否" --> X["拒绝保存并指出缺失项"] - D -- "是" --> E["保存为草稿,状态 Draft"] + D -- "是" --> E["保存为草稿,状态 Draft,并在 seckill_inventory 写入计划配额行:remainingStock=totalStock、soldCount=0、frozenCount=0、activatedAt=NULL"] B -- "编辑" --> EE{"活动状态?"} - EE -- "Draft / Published / Running" --> FF["允许修改开始前 / 限购字段;运行中开始时间不得回拨"] - EE -- "Ended / Cancelled" --> X["拒绝编辑"] + EE -- "Draft / 尚未开始的 Published" --> FF["允许修改名称 / 价格 / 时间 / 限购等可调字段;totalStock 仅在 Draft 可调"] + EE -- "Ongoing / Ended / Cancelled" --> X["拒绝编辑(已锁定)"] E --> F{"手动发布?"} F -- "否" --> EE1["保持 Draft"] - F -- "是" --> G1["开启发布事务,先按当前秒杀库存量条件扣减普通商品可售库存"] + F -- "是" --> G1["开启发布事务,先按 totalStock 条件扣减普通商品可售库存"] G1 --> G2{"商品当前可售库存 ≥ 秒杀库存总量?"} G2 -- "否" --> X2["拒绝发布,回滚事务,并指出普通库存不足"] - G2 -- "是" --> G3["写入秒杀库存初始事实:剩余可售 = 秒杀量,已售 = 0"] + G2 -- "是" --> G3["按 activityId 条件激活已存在的 seckill_inventory 计划配额行:activatedAt=now(),并按数据库 now() 把活动从 Draft 条件推进为 Published 或 Ongoing"] G3 --> G4{"发布事务整体提交?"} - G4 -- "否" --> X3["整体回滚:普通库存未被扣减、秒杀库存未被创建、活动保持 Draft"] - G4 -- "是" --> G["状态变为 Published,并写入活动开始 UTC 时间"] - G --> H["基于数据库 UTC now() 自动判定:start ≤ now < end → Running;now ≥ end → Ended"] - H --> I["活动详情可在卖家与买家入口查询"] - I --> J["剩余库存售罄时立即标记 Sold Out 并禁用抢购入口"] + G4 -- "否" --> X3["整体回滚:普通库存未被扣减、秒杀计划配额保持未激活、活动保持 Draft"] + G4 -- "是" --> G["状态写为 Published 或 Ongoing,并由数据库 UTC now() 后续推进到 Ended"] + G --> H["活动详情可在卖家与买家入口查询"] + H --> I["剩余库存售罄时立即标记 Sold Out 并禁用抢购入口"] ``` 状态机: @@ -99,9 +98,10 @@ stateDiagram-v2 关键约束: -- **发布即原子划拨**:商家将活动由 Draft 推进为 Published 时,必须在同一数据库事务内把等量普通商品可售库存条件扣减,并创建秒杀库存初始事实;事务失败整笔回滚,普通库存和秒杀库存均不留半改;只有事务整体提交后才能把活动状态推进为 Published。 +- **草稿即落计划配额**:商家保存草稿时,必须在同一数据库事务内按 `seckill_inventory (activity_id)` 唯一行写入计划配额 `remainingStock=totalStock`、`soldCount=0`、`frozenCount=0`、`activatedAt=NULL`,为后续 A222 发布提供唯一的事实锚点;不允许草稿与发布两个阶段重复创建秒杀库存行。 +- **发布即原子激活与划拨**:商家将活动由 Draft 推进为 Published(或在开始时间 ≤ 数据库 `now()` 时直接为 Ongoing)时,必须在同一数据库事务内按 `activityId` 把等量普通商品可售库存条件扣减,再按同一 `activityId` 把计划配额的 `activatedAt` 写为数据库 `now()`;不允许在发布阶段重新插入秒杀库存行。事务失败整笔回滚,普通库存和秒杀计划配额均不留半改;只有事务整体提交后才能把活动状态推进为 Published 或 Ongoing。 - **库存通道互不混淆**:普通下单只能扣减普通库存,秒杀下单只能扣减秒杀库存;发布时一次性划拨后,活动期间普通下单不会消耗已被划走的那部分库存。 -- 活动状态字段由数据库维护并参与所有业务校验;商家只能在 Draft / Published / Running 时执行取消,Ended / Cancelled 拒绝重复状态变更。 +- 活动状态字段由数据库维护并参与所有业务校验;商家只能在 Draft 或尚未开始的 Published 时执行编辑,运行中 / 结束 / 已取消一律拒绝;取消动作只对 Draft / Published / Ongoing 生效,Ended / Cancelled 拒绝重复状态变更。 - 状态推进在数据库侧以 UTC `now()` 为权威,避免应用实例时钟漂移造成提早或延后成功。 - 活动取消后已存在订单继续走完;未提交请求直接拒绝;本期不回收已分配秒杀库存,避免被普通订单夹带走量。 @@ -118,15 +118,16 @@ flowchart TD C -- "Running" --> D{"剩余可售库存?"} D -- "= 0" --> X5["入口标记已售罄,按钮置灰"] D -- "> 0" --> E["倒计时至结束时间,按钮可点击"] - E --> F["前端轮询 / 缓存刷新需要与数据库 remaining 一致"] - F --> G["不允许“前端还有库存但下单失败”或“前端售罄但实际还能抢”"] + E --> F["页面仅展示当前轮询/缓存视图"] + F --> G["提交时以数据库条件更新原子扣减为准:竞争失败按已售罄或状态冲突返回"] ``` 关键约束: - 倒计时统一以数据库 UTC 时间计算;同 / 跨实例用户看到一致的剩余库存和倒计时。 -- 前端轮询或服务端推送给出的剩余库存必须与数据库实际一致,禁止相反情景。 -- 活动列表与商品基础信息读取可经 C07 缓存,但秒杀库存本身不进入缓存。 +- 页面库存仅作提示,不作为最终抢锁事实:高并发秒杀下页面视图天然可能短暂落后;最终结果必须以提交时数据库条件更新为准。 +- 库存与限购的正确性边界只由数据库条件更新承担,不依赖页面显示与 Redis。竞争失败按 `SOLD_OUT` / `PER_BUYER_LIMIT_EXCEEDED` / 状态冲突返回,不允许产生超卖或重复订单。 +- 活动列表与商品基础信息读取可经 C07 缓存,但秒杀库存本身不进入缓存;C07 失效时降级为实时读 + 短 TTL,售罄状态以数据库为准重新加载活动详情。 - 活动详情暴露的剩余库存只能来源于发布时落地的事务事实;发布事务未提交的草稿不在抢购入口暴露可售数。 ## 五、立即抢购主流程 @@ -217,12 +218,17 @@ flowchart LR | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 浏览秒杀活动 | A221 | 仅返回当前请求可见的活动状态、剩余库存、已售数量和倒计时 | 待交叉评审 | -| 秒杀立即抢购 | A222 | 校验身份、活动状态、窗口、数量、限购、幂等;以条件更新原子扣减并生成订单 | 待交叉评审 | -| 查询抢购结果 | A223 | 返回本人订单号、活动 ID 和购买结果;非本人返回“不存在 / 无权限” | 待交叉评审 | -| 商家维护秒杀活动 | A224 | 在 Draft / Published / Running 允许创建、发布、取消;Ended / Cancelled 拒绝重复变更 | 待交叉评审 | -| 秒杀库存回补 | A226 | 按订单项 `(活动 ID, 数量)` 联合条件回补秒杀库存剩余可售量并核减已售数,期望条件命中一次 | 待交叉评审 | -| 发布时原子划拨 | A225 | 商家将活动 Draft 推进为 Published 时,同一数据库事务内条件扣减普通商品可售库存并创建秒杀库存初始事实 | 待交叉评审 | +| 商家创建秒杀活动(含草稿即落计划配额) | A220 | 校验时间 / 价格 / 库存配额并写入草稿,事务内同时在 `seckill_inventory` 写入 `activatedAt=NULL` 的计划配额行 | 待交叉评审 | +| 商家更新秒杀活动(仅 Draft / 尚未开始) | A221 | 仅在 Draft 或尚未开始的 Published 状态允许更新名称 / 价格 / 时间 / 限购;Ongoing / Ended / Cancelled 返回 `SECKILL.INVALID_STATUS` | 待交叉评审 | +| 商家发布秒杀活动(原子划拨与激活) | A222 | 同一事务内按 `activityId` 条件扣减普通商品可售库存并把已存在计划配额的 `activatedAt` 写为 now();不存在 → 回滚并返回 `SECKILL.INVENTORY_NOT_FOUND` | 待交叉评审 | +| 商家取消秒杀活动(保留已分配库存) | A223 | 条件 `status IN ('Draft','Published','Ongoing') → 'Cancelled'`,回滚只下架入口不回收已分配库存 | 待交叉评审 | +| 商家秒杀活动列表 | A224 | 仅返回 `created_by_merchant_user_id = current_user_id` 的活动,支持按状态 / 时间 / 关键词筛选与稳定排序 | 待交叉评审 | +| 商家秒杀活动详情 | A225 | 返回个人活动详情与订单统计;非创建人返回 404 不泄露存在性 | 待交叉评审 | +| 买家秒杀活动列表 | A226 | 仅返回 `status IN ('Published','Ongoing')` 的活动;列表中 `remainingStock` 仅返回 `isSoldOut` 布尔 | 待交叉评审 | +| 买家秒杀活动详情 | A227 | 仅返回已发布 / 进行中活动详情;可附 `currentBuyerOrderCount`、`currentBuyerRemaining` 限购提示 | 待交叉评审 | +| 秒杀下单(条件扣减 + 限购占用) | A228 | 按 `Idempotency-Key` 重放首配,事务内条件扣减 `seckill_inventory` 剩余可售、`(activity_id, buyer_id)` 唯一配额占用并调用 Ordering 公开契约创建共享订单 | 待交叉评审 | +| 秒杀库存回补 | 不分配 Axxx | 由 M04 取消事务或 C03 超时取消触发,按 `(activityId, sku?)` 联合条件回补并释放限购名额,结果一致即可见 | 由 M04 / C03 公共契约承载 | +| 抢购结果查询 | 不分配 Axxx | 走 Ordering 公开契约 A302 / A303,按 `buyer_id` 与订单归属鉴权 | 由 Ordering 公共契约承载 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码与 OpenAPI、ProblemDetails 不得反向写入业务图;接口设计 1.12 通用幂等规则与 4.6 资金类幂等约束同样适用于秒杀订单。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" index e7378ef..eb06d2c 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" @@ -11,7 +11,7 @@ 本模块负责买家在登录态下维护本人购物车,覆盖查看列表、加入商品、修改数量、删除条目、单选 / 全选、选择失效处理、服务端计价和下单前 / 下单事务内的购物车清理。购物车只承担“下单前的暂存区”,不承载营销、优惠、推荐、凑单,也不维护独立状态机;选中状态、价格、库存上限由服务端实时派生,客户端不得越权决定订单金额。 -本文先依据需求确定业务参与者、上游输入、状态派生口径、原子结果和模块出入口,再由这些动作派生接口契约。A2xx(A201 加购、A202 查看、A203 改数量、A204 删除、A205 选中、A206 服务端计价、A207 清空、A208 幂等记录)只用于流程完成后的契约映射与缺口检查,不能反向拼接流程,也不能用现有 Axxx 反向覆盖业务语义。 +本文先依据需求确定业务参与者、上游输入、状态派生口径、原子结果和模块出入口,再由这些动作派生接口契约。A2xx(A201 加购、A202 查看、A203 改数量、A204 删除、A205 批量删除、A206 切换选中、A207 清空、A208 结算预览)只用于流程完成后的契约映射与缺口检查,不能反向拼接流程,也不能用现有 Axxx 反向覆盖业务语义。A208 之前的“幂等记录”草表属于加购主接口内嵌能力,不作为独立 HTTP 契约单独列出。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| @@ -105,11 +105,15 @@ flowchart TD B -- "否" --> X["按不存在 / 无权限处理,不泄露归属"] B -- "是" --> C{"新数量为正整数?"} C -- "否" --> Y["拒绝修改并提示;不允许设为 0 或负数"] - C -- "是" --> D{"可售库存 ≥ 新数量?"} - D -- "否" --> Y["拒绝调大;调小始终允许"] - D -- "是" --> E["落库并返回最新数量、小计与最大可设值"] + C -- "是" --> D{"新数量 ≥ 旧数量(调大请求)?"} + D -- "否" --> E["调小路径:可不校验库存上限,直接落库并返回最新数量、小计"] + D -- "是" --> F{"可售库存 ≥ 新数量?"} + F -- "否" --> Y["拒绝调大;返回当前最大可设值,维持旧数量"] + F -- "是" --> G["落库并返回最新数量、小计与最大可设值"] ``` +> 说明:调小路径不重复校验实时库存上限(库存可能已下降但仍允许把数量往下调),仅校验“数量 > 0”与归属;调大请求才走实时可售库存上限校验,保证前端按最大可设值截断时不出现“提示截断到 N、接口又拒绝”的状态分歧。 + ### 4.3 删除与清空 ```mermaid @@ -210,12 +214,13 @@ flowchart TD |---|---|---|---| | 加入购物车(可选幂等) | A201 | 校验数量 + 库存,同 `(买家+商品)` 累加,幂等不重复累加 | 待交叉评审 | | 查看本人购物车 | A202 | 仅返回本人条目,含可结算 / 失效标记与失效原因 | 待交叉评审 | -| 修改本人条目数量 | A203 | 实时校验库存上限;调小始终允许;返回最新数量与最大可设值 | 待交叉评审 | -| 删除本人条目 / 清空 | A204 | 按 `(买家+条目ID)` 过滤;幂等;多次删除同一 ID 结果一致 | 待交叉评审 | -| 切换单选 / 全选 / 反选 | A205 | 服务端持久化选中;失效条目不允许被选中 | 待交叉评审 | -| 服务端计价(结算预览) | A206 | 实时重读单价并计算总额;前端不能指定金额 | 待交叉评审 | +| 修改本人条目数量 | A203 | 调大按实时库存校验并返回最大可设值;调小不验库存上限;返回最新数量与小计 | 待交叉评审 | +| 删除本人条目(按 ID) | A204 | 按 `(买家+条目ID)` 过滤;幂等;多次删除同一 ID 结果一致 | 待交叉评审 | +| 批量删除本人条目 | A205 | 按 `(买家+条目ID 集合)` 一次性物理删除;非本人条目忽略并计入 skippedCount | 待交叉评审 | +| 切换单选 / 全选 / 反选 | A206 | 服务端持久化选中;失效条目不允许被选中 | 待交叉评审 | | 一键清空购物车 | A207 | 幂等;只影响本人;失效条目一并清理 | 待交叉评审 | -| 加购幂等记录 | A208 | 按 `(买家+幂等键)` 持久化记录;同键同请求重放首次结果 | 待交叉评审 | +| 结算预览(获取总价) | A208 | 实时重读单价并计算总额;前端不能指定金额 | 待交叉评审 | +| 加购幂等(内嵌) | 不单独分配 Axxx | 加购 / 改数量 / 抢锁以 `Idempotency-Key` 形式由调用方携带,按接口设计 1.12 通用幂等规则重放首配结果 | 不作为独立 HTTP 契约 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段、错误码与幂等键传递方式不得反向写入业务图,接口设计 1.12 通用幂等规则统一承载。 -- Gitee From bc472cbfbf1bc1a46fb7ef1156083a65eefc4899 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 17:12:16 +0800 Subject: [PATCH 079/118] =?UTF-8?q?docs(process):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E5=94=90=E5=AE=87=E6=98=8A=E4=B8=8E=E9=A1=BE=E6=AC=A3=E6=9C=88?= =?UTF-8?q?=E8=AF=84=E5=AE=A1=E9=97=AE=E9=A2=98=EF=BC=9B=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E8=B7=A8=E6=A8=A1=E5=9D=97=E5=A5=91=E7=BA=A6=E4=B8=8E=E8=AF=84?= =?UTF-8?q?=E4=BB=B7=E5=9B=BE=E7=89=87=E6=9A=82=E5=AD=98=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interface/interface-tyh.md" | 2 +- ...04\344\273\267\346\265\201\347\250\213.md" | 28 +++++++++---------- ...45\345\217\243\350\256\276\350\256\241.md" | 2 +- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" index f474216..453995c 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" @@ -1057,7 +1057,7 @@ FavoriteListResponse { #### 缓存、事件或外部依赖 -- 不缓存;商品摘要由 Catalog 模块通过共享 OpenAPI 返回,或在接口层做受控 Join。 +- 不缓存;商品摘要由 Engagement 通过 Catalog 公开应用契约批量取得,不直接读取 Catalog 内部表,也不通过跨模块 Join 绕过公开边界。 #### 验证场景 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" index 4640c25..b2c904b 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" @@ -141,15 +141,13 @@ stateDiagram-v2 **暂存图片孤儿清理**: -评价图片采用"上传即暂存"模式(A141 返回对象键),买家在评价事务提交前可能放弃提交、关闭页面、评价事务失败或被防重复规则拦截,导致对象存储里出现未被评价引用的对象键。这些对象不属于业务事实,但持续占用对象存储容量且无任何业务用途,必须按以下策略清理: +评价图片采用"上传即暂存"模式(A141 返回 `imageId` 与对象键)。买家在评价事务提交前可能放弃提交、关闭页面、评价事务失败或被防重复规则拦截,导致对象存储里出现未被评价引用的对象。这些对象不属于已完成评价事实,但持续占用对象存储容量,必须按以下边界清理: -- 每张暂存图片在上传时记录 `uploadedAt`、`uploadedBy`、`status`(暂存/已绑定/已移除),写入图片暂存表(与评价主表分离)。 -- 提交评价事务在绑定图片成功后才将 `status` 由"暂存"切到"已绑定",并把对象键写入评价图片关联表;切换在同一受控事务内完成,整体回滚时 `status` 不变。 -- 评价事务失败、用户主动移除图片或买家放弃提交时,相关图片 `status` 标记为"已移除",但对象键保留到过期清理窗口。 -- 后台清理任务:定时(建议每日凌晨)扫描 `status='已移除'` 且 `uploadedAt` 早于保留窗口(建议 24 小时)的记录,调用对象存储删除接口并物理删除暂存表记录。 -- 暂存保留窗口内,已移除图片对应的对象键可被同一买家在原订单项上重新上传复用,不立即物理删除以避免误删正在重试的对象。 -- 用户主动"移除失败图片"立即标记 `status='已移除'` 并进入清理窗口;用户移除成功图片若已绑定到评价,则按 M07 删除评价图片契约处理,删除关联表记录并删除对象存储对象,不走暂存清理。 -- 清理任务失败的对象键需记录重试次数和最后错误,超过重试上限(建议 5 次)后由可观测性告警并保留记录供人工排查,不允许静默吞掉。 +- A141 为非幂等上传;每次上传或重试都生成新的暂存图片和对象键,不复用先前上传的对象键。 +- A142 只关联当前买家通过 `imageIds` 引用且仍有效的暂存图片;评价与图片关联在同一受控事务内提交,事务失败时不产生已完成评价关联。 +- 用户提交前移除图片、放弃提交、评价事务失败或重复评价被拦截时,未被评价引用的暂存图片保留到清理窗口,由后台清理任务回收对象存储对象及对应元数据。 +- 清理任务只能处理超过保留窗口且仍未被评价引用的图片,不能删除已被评价关联的对象;具体持久化结构、识别字段和索引由数据库设计确认,本流程不预先指定独立暂存表。 +- 保留窗口、清理调度频率、失败重试上限和告警方式仍是待评审项;清理失败必须可追踪,不允许静默吞掉。 ## 六、结果反馈与页面衔接 @@ -185,8 +183,8 @@ flowchart TD | 同一订单项重复评价或重复点击 | 返回已评价结果 | 不新增重复记录 | | 评分、文字或图片不合规 | 字段级错误 | 保留可恢复的表单内容 | | 部分图片上传失败 | 标记失败项 | 允许重试或移除 | -| 评价事务失败、用户放弃提交或防重复拦截 | 暂存图片标记为"已移除" | 保留至暂存保留窗口后由清理任务删除对象存储对象 | -| 暂存保留窗口内同一买家重新上传 | 复用原对象键 | 不重复上传、不重复计费 | +| 评价事务失败、用户放弃提交或防重复拦截 | 暂存图片保持未被评价引用 | 保留至暂存保留窗口后由清理任务删除对象存储对象 | +| 同一买家重新上传或重试 | 生成新的暂存图片与对象键 | 原未引用对象等待清理,不复用旧对象键 | | 清理任务失败 | 记录重试次数与最后错误 | 超过重试上限由可观测性告警并保留记录,不静默吞掉 | | 提交时登录失效 | 引导重新登录 | 保留未提交内容,重新提交时执行完整资格校验 | | 评价事务失败 | 整体回滚 | 提示稍后重试 | @@ -200,8 +198,8 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 上传评价图片(提交前暂存) | A141 评价图片上传 | 校验合规、上传到对象存储并写入暂存表,返回对象键与暂存记录 | 待交叉评审 | -| 提交商品评价(幂等) | A142 评价提交 | 校验身份、订单项归属、完成状态、唯一性、字段与图片并原子写入;图片在同一事务内由暂存切换为已绑定 | 待交叉评审 | +| 上传评价图片(提交前暂存) | A141 评价图片上传 | 校验合规,每次生成新的暂存图片和对象键并返回 `imageId`;具体持久化结构由数据库设计确认 | 待交叉评审 | +| 提交商品评价(幂等) | A142 评价提交 | 校验身份、订单项归属、完成状态、唯一性、字段与图片并原子写入;在同一事务内关联有效的暂存图片 | 待交叉评审 | | 查询订单项评价资格/结果 | A143 评价资格 | 按当前买家返回 Completed 订单项的可评价状态与已提交评价 | 待交叉评审 | | 商品公开评价分页 + 评分汇总 | A140 公开评价 | 分页返回评分、文字、图片、时间和脱敏展示名,并返回总数与平均分汇总 | 待交叉评审 | | 单条公开评价详情查询 | A144 评价详情 | 返回单条公开评价的完整字段,供评价详情或举报链路使用 | 待交叉评审 | @@ -225,8 +223,8 @@ flowchart TD 5. 商品详情读取评价是否要求登录状态、是否区分登录与游客可见范围,需要在接口设计中明确。 6. 评价事务失败的回滚语义需要与图片上传失败处理保持一致,避免“评价已存但图片缺失”的部分结果。 7. DBxxx 评价表字段尚未形成可实施的完整定义,数据库字段、约束和索引必须由数据库设计任务另行确认。 -8. 评价与图片上传的字段命名(`reviews`、`review_images`、`review_image_staging`)已与命名规范统一,需在数据库设计中按词根实现;暂存表与评价图片关联表分离,孤儿清理任务依赖此分离结构。 -9. 暂存图片保留窗口(建议 24 小时)、清理任务调度频率(建议每日凌晨)、清理重试上限(建议 5 次)需要在接口和运维设计中明确,避免对象存储无限增长或被激进清理误删。 +8. 评价与图片元数据的具体持久化结构、未引用图片识别字段和清理索引尚未确认,需在数据库设计中统一定义;本流程不预设独立暂存表。 +9. 暂存图片保留窗口、清理任务调度频率、清理重试上限和告警方式需要在数据库与运维设计中明确,避免对象存储无限增长或被激进清理误删。 ## 十一、验收证据清单 @@ -257,4 +255,4 @@ flowchart TD 升级到”待交叉评审”的条件:自检完成、主流程与异常分支齐全、并发场景验证、订单项越界问题已修正、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 -升级到”已确认”的条件:Ordering 主责确认订单项归属与完成状态口径并认可”已评价由 Review 派生”的设计,Identity 主责确认买家脱敏快照生成时机,根文档 3.6 节中评价接入点对应追踪项成熟度同步更新。 \ No newline at end of file +升级到”已确认”的条件:Ordering 主责确认订单项归属与完成状态口径并认可”已评价由 Review 派生”的设计,Identity 主责确认买家脱敏快照生成时机,根文档 3.6 节中评价接入点对应追踪项成熟度同步更新。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" index 3e52661..0abbbcb 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" @@ -1788,7 +1788,7 @@ FavoriteListResponse { #### 缓存、事件或外部依赖 -- 不缓存;商品摘要由 Catalog 模块通过共享 OpenAPI 返回,或在接口层做受控 Join。 +- 不缓存;商品摘要由 Engagement 通过 Catalog 公开应用契约批量取得,不直接读取 Catalog 内部表,也不通过跨模块 Join 绕过公开边界。 #### 验证场景 -- Gitee From 691d4e23f66b691926f71b69852c92712c2ee7e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=B5=B7=E6=B4=8B?= <2040454617@qq.com> Date: Fri, 24 Jul 2026 17:12:36 +0800 Subject: [PATCH 080/118] =?UTF-8?q?docs(daily):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=BC=A0=E6=B5=B7=E6=B4=8B=202026-07-24=20=E6=97=A5=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 今日完成(zhy 今日 6 个 commit + 1 个 PR + 1 项规则同步): 1. 09:57 (8a5d615) A401-A425 接口契约草稿 2. 10:50 (b4c75bb) A431 边界修复 + A434 退货物流新增 3. 14:32 (71cc5fc) M10 售后 + C08 支付回调流程新建 4. 14:52 (05c8a8f) M10 状态机 + C08 引用收紧 5. 15:01 (90c8686) C08 批次/退款对账术语收紧 6. 16:09 (e7d3d55 → d6a4f27 !58 squash) C08/M10 第一轮 8 个阻断性漏洞修复(PR !58 合入 dev,任务分支已删) 7. .claude/CLAUDE.md + 6 个核心 Skill 增量同步(六句话运行政策 + 阶段自动提交授权) 第二轮回看发现 6 个新漏洞,PR 在 docs/c08-m10-vuln-round2-zhy 分支待评审合并(commit 8f9cce4)。 遇到的问题:第一次误以为 PR 已合 → 核实后未擅自删;Gitee PR 链接默认 master → zhy 在网页改 dev;PowerShell 中文编码错乱 → UTF-8 临时文件解决;M10 第 366 行待评审项矛盾 → 同步修复。 明日计划:等 PR !58 交叉评审;复核 M05;看 gxy/tyh 修复的跨模块影响;整理周报。 --- ...4-\345\274\240\346\265\267\346\264\213.md" | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 "reports/daily/20260724-\345\274\240\346\265\267\346\264\213.md" diff --git "a/reports/daily/20260724-\345\274\240\346\265\267\346\264\213.md" "b/reports/daily/20260724-\345\274\240\346\265\267\346\264\213.md" new file mode 100644 index 0000000..e255828 --- /dev/null +++ "b/reports/daily/20260724-\345\274\240\346\265\267\346\264\213.md" @@ -0,0 +1,45 @@ +# 日报 - 张海洋 - 2026-07-24 + +## 今日完成 + +1. 09:57 在本地分支 `docs/zhy-process-flow-zhy` 提交 `8a5d615`,按 M05 / C08 流程派生的 Payment + AfterSales A401-A425 接口契约草稿(`docs/02-设计文档/接口设计.md`,A401~A408 支付应用契约、A411~A419 售后申请、A421~A425 支付回调与对账)。⚠️ 当时个人 Axxx 草稿已就位,等待统一汇总到主接口文档。 +2. 10:50 在 `docs/zhy-process-flow-zhy` 提交 `b4c75bb`,修复 A431 边界(`IRefundService` 退款入账内部应用契约已并入 A411 流程说明),新增 A434 买家提交退货/寄回信息接口(`docs/02-设计文档/接口设计.md`,+34 / -8)。 +3. 14:32 在 `docs/zhy-process-flow-zhy` 提交 `71cc5fc`,新增 M10-售后流程.md、C08-支付回调与对账流程.md(`docs/02-设计文档/process/zhy/`,新建 2 个文件,参考 `process/README.md` 6.1 / 6.2 模板:状态机含 `待审核 → 已撤销 / 已拒绝 / 退款中 / 待退货 → 待收货 → 退款中 → 已退款 / 退款失败`,回调处理含 `Received → Processing → Processed / Ignored / Difference / Failed`)。 +4. 14:52 在 `docs/zhy-process-flow-zhy` 提交 `05c8a8f`,修 M10 状态机(删除 3 处非法转换:待退货→已撤销、待收货→已拒绝、退款失败→终态,统一英文 PascalCase,新增 X04 状态名对照表)+ 收紧 C08 引用 + 修复 Mermaid 语法(`docs/02-设计文档/process/zhy/M10-售后流程.md` + `C08-支付回调与对账流程.md`)。 +5. 15:01 在 `docs/zhy-process-flow-zhy` 提交 `90c8686`,按 zhy 评审反馈 1-7 项收紧 C08 批次对账范围(明确"只读取 Processed 状态的回调 Inbox")+ 修复退款记录/钱包入账命名(`wallets.transactions` → `wallet_ledgers`)(`docs/02-设计文档/process/zhy/C08-支付回调与对账流程.md`)。 +6. 16:09 在新分支 `docs/c08-m10-vuln-fix-zhy` 提交 `e7d3d55`,按漏洞清单修复 zhy 自己负责的 8 个阻断性流程文档漏洞(`docs/02-设计文档/process/zhy/C08-支付回调与对账流程.md` +20 / -14、`M10-售后流程.md` +39 / -29,共 +59 / -43): + + **C08 修复 3 处:** + - 回调接收事务拆断:把 Inbox 写入、订单状态条件更新、Outbox 事件并入单一 PostgreSQL 事务,通过 Ordering 公开应用契约锁定 orders 行 + - 失败分支错误发布支付成功事实:拆分 PendingPayment 失败、Paid/Cancelled/其他非 PendingPayment 状态的 Inbox 终态路径,仅 PendingPayment + Success 写 Outbox 支付成功事实 + - 对账批次漏掉 Difference:批次读取范围从 Processed 扩展为 Processed 或 Difference,确保"已取消订单收到迟到成功"等差异进入每日对账 + + **M10 修复 5 处:** + - 创建申请并发风险:通过 Ordering 公开应用契约在同一 PostgreSQL 事务中锁定 orders 行,用条件聚合核算"剩余可售后数量 = 订单项数量 − 处理中数量 − 已退款数量" + - RefundOnly 异步分段事务:审核通过时同事务内同步调用 IRefundService,成功 → Refunded,失败 → RefundFailed + - 商家确认收货后异步退款:把库存回补、IRefundService 退款、状态终态合并到单一 PostgreSQL 事务;任一步失败整体回滚 + - 快递单号全局唯一约束:删除全局唯一判断和约束说明,按 A434 总契约允许同订单多笔申请共享同一包裹 + - RefundFailed 不可达:IRefundService 事务失败时不整体回滚 Refunding 申请,用独立失败记录置为 RefundFailed 供 A419 重试 + +7. 完成 `docs/c08-m10-vuln-fix-zhy` 分支的完整 Git 生命周期:建分支(从最新 dev `2e3ce8b` !56)→ 阶段自动 Commit(`e7d3d55`,按"阶段自动提交授权",仓库级 Commit 授权不再逐次询问)→ `git push -u origin docs/c08-m10-vuln-fix-zhy` → 在 Gitee 创建 PR !58 → 由 zhy 合入 dev(squash 成 `d6a4f27`)→ `git switch dev` + `git merge --ff-only origin/dev` 同步到 `4716663` → `git branch -d` + `git push origin --delete` 删除任务分支。 +8. 按 2026-07-23 部署提示词对 `.claude/CLAUDE.md` 和 6 个核心项目 Skill(`eshop-project-workflow` / `eshop-deliver-feature` / `eshop-fix-bug` / `eshop-align-docs` / `eshop-verify-acceptance` / `eshop-manage-git`)做增量同步:平台路径转换(`$eshop-*` → `/eshop-*`、`.agents/skills/` → `.claude/skills/`、`AGENTS.md` → `CLAUDE.md`)+ 政策转换("全面禁止全局 Skill/Agent"旧政策 → "项目优先 / 能力缺口补充 / 禁止全局规则 / 禁止全局 Agent / 允许项目级 Agent / 上传包仅用于部署/同步"六句话)+ 保留新规则(阶段自动提交授权、process 目录集中维护、个人原稿规则、需求→流程→接口/数据库的设计顺序硬约束)。本机副本由 `.git/info/exclude` 隔离,未进入 Git 工作区。 + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式/求助对象 | +|----------|----------|-------------------| +| 第一次"删除 docs/zhy-process-flow-zhy 分支"前,误以为 PR 已合入 dev | 已解决 | 经核实 `git log docs/zhy-process-flow-zhy ^dev`,3 个 docs(process) commit 当时不在 dev 历史(`90c8686` / `05c8a8f` / `71cc5fc` 均在 `docs/zhy-process-flow-zhy` 独占);明确告知 zhy 未擅自删除;后续 zhy 在 Gitee 创建 PR 后由小猪协助删除本地 + 远端分支 | +| Gitee 返回的 PR 创建链接默认目标分支为 `master`,但本项目 Git 协作流程约定为 `dev` | 已解决 | 在 PR 交付报告里明确要求 zhy 在 Gitee 网页把链接里的 `master` 改为 `dev`;Gitee 网页操作由 zhy 完成 | +| PowerShell 5.1 here-string 把中文字面量传给 Python 时编码错乱,导致静态自检脚本误报"入口缺少关键语义: capability gap" | 已解决 | 改用 `[System.IO.File]::WriteAllText($tmp, $content, [System.Text.Encoding]::UTF8)` 写入 Temp 文件再用 `python -X utf8 $tmp` 执行;脚本里 `sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")` 修正输出编码;Temp 文件执行后用 `Remove-Item -Force` 清理 | +| 漏洞清单第 366 行 M10 "待评审项"列表残留"退货快递单号全局唯一"描述,与已修复的第 6 节"不对快递单号施加全局唯一约束"矛盾 | 已解决 | 在 PR 提交前发现并同步修改为"退货快递单号共享:同一快递单号可承载同一订单多笔退货申请" | + +## 明日计划 + +1. **等待 PR !58 交叉评审反馈**:找 wqq(韦乾强,M04 订单协作人)和 lhc(罗皓晨,C10 高可用协作人)评审 `e7d3d55` 的 8 个漏洞修复;如有修订意见,按评审迭代。如评审通过即闭环。 +2. **复核 M05 流程文档**:漏洞清单说"相对完整"未列具体行号。准备按"修改前要求"先读 M05 流程文档 + 接口契约 A401-A408 + 教师基线 F10 同步支付边界,确认是否真的无阻断性漏洞;如果发现具体漏洞,开 `docs/m05-vuln-fix-zhy` 分支处理。 +3. **看 gxy (!60) / tyh (!59) 修复中的跨模块影响**:M05/M10 退款链路(IRefundService)、M04 订单状态机(PendingPayment / Paid / Cancelled)、Payment 公共应用契约(IRefundService)是否被影响;如发现需要协调的接口或状态机不一致,开 issue 或在群内对齐。 +4. **整理本周工作并准备周报**:周四或周五汇总本周(07-21 ~ 07-25)的需求细化、接口契约、流程文档漏洞修复、规则同步、Git 协作演练进展;按"日报周报必须真实"原则只写有 Git 证据或运行证据的工作。 + +## 今日工时 + +约 __ 小时(zhy 补充) -- Gitee From 881eb6e3294b67ca98ce4e3dc5157893d6efb4e7 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 17:22:19 +0800 Subject: [PATCH 081/118] =?UTF-8?q?docs(process):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E5=BC=A0=E6=B5=B7=E6=B4=8B=E5=94=AE=E5=90=8E=E9=80=80=E6=AC=BE?= =?UTF-8?q?=E4=BA=8B=E5=8A=A1=EF=BC=9B=E7=BB=9F=E4=B8=80=E5=9B=9E=E6=BB=9A?= =?UTF-8?q?=E4=B8=8E=E5=A4=B1=E8=B4=A5=E8=AE=B0=E5=BD=95=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...56\345\220\216\346\265\201\347\250\213.md" | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" index f4be56a..a75a402 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" @@ -75,7 +75,7 @@ stateDiagram-v2 - 申请只能由买家提交;只有 `PendingReview` 状态可被买家主动撤销。 - 商家只能在 `PendingReview` 状态下审核;审核拒绝后不可再次审核。 - 退货退款必须经历 `PendingReturn → PendingReceipt → Refunding`;商家确认收货是退款前置条件。 -- `Refunding` 是事务内瞬间中间状态,事务提交后立即转为 `Refunded`(退款成功)或 `RefundFailed`(退款失败);外部观察和监控按终态(`Refunded` / `RefundFailed`)过滤。 +- `Refunding` 是受控退款事务内的瞬间中间状态;退款事务成功时提交为 `Refunded`,失败时先整体回滚,再由独立失败记录事务置为 `RefundFailed`;外部观察和监控按终态(`Refunded` / `RefundFailed`)过滤。 - 退款失败**不是**终止状态,可通过幂等重试回到 `Refunding`。 - 重复退款请求返回首次结果,不重复写入钱包流水。 @@ -153,11 +153,14 @@ flowchart TD J0 --> J1["同事务内调用 IRefundService:原子写入退款记录 + 钱包入账 + 钱包流水"] J1 --> J2{"IRefundService 返回结果?"} J2 -- "退款成功" --> J3["同事务内:状态推进 PendingReview → Refunded"] - J2 -- "退款失败" --> J4["同事务内:用独立失败记录保留状态 RefundFailed
供 A419 失败重试使用"] + J2 -- "退款失败" --> J4["整体回滚退款受控事务:库存、钱包、流水和售后终态均不生效"] + J4 --> J5["另开独立失败记录事务:保留审核结果并将状态置为 RefundFailed
供 A419 失败重试使用"] + J3 --> JMR{"退款受控事务提交成功?"} + JMR -- "否" --> J4 + JMR -- "是" --> N I -- "ReturnAndRefund" --> L["状态条件推进 PendingReview → PendingReturn"] L --> L1["保留审核意见,等待买家提交退货物流"] - J3 --> MR - J4 --> MR + J5 --> N L --> MR G --> MR MR{"事务提交成功?"} @@ -213,13 +216,16 @@ flowchart TD G --> H["同事务内调用 IRefundService:原子写入退款记录 + 钱包入账 + 钱包流水"] H --> H1{"IRefundService 返回结果?"} H1 -- "退款成功" --> H2["状态推进 PendingReceipt → Refunded"] - H1 -- "退款失败" --> H3["用独立失败记录保留状态 RefundFailed
供 A419 失败重试使用"] + H1 -- "退款失败" --> H3["整体回滚退款受控事务:库存、钱包、流水和售后终态均不生效"] + H3 --> H4["另开独立失败记录事务:保留确认收货事实并将状态置为 RefundFailed
供 A419 失败重试使用"] H2 --> I - H3 --> I + H4 --> KF["通知买家退款失败,等待 A419 重试"] I["记录待发布退款事实 + 确认收货审计日志"] I --> J{"事务提交成功?"} - J -- "否" --> JR["整体回滚:库存不回补、状态保持 PendingReceipt,锁随事务结束释放"] - J -- "是" --> K["通知买家退款处理中"] + J -- "否" --> JR["整体回滚:库存、钱包、流水、售后终态和完成事实均不生效"] + JR --> JRF["另开独立失败记录事务:保留确认收货事实并将状态置为 RefundFailed"] + JRF --> KF + J -- "是" --> K["通知买家退款成功"] ``` 退款入账流程(`IRefundService`,内部应用能力,必走): @@ -233,13 +239,12 @@ flowchart TD C -- "同 Key 同金额" --> D["返回首次结果"] C -- "同 Key 不同金额" --> CZ["抛 IdempotencyKeyReusedException"] C -- "否" --> E["开启退款事务"] - E --> F["原子写入:退款记录 + 钱包入账 + 钱包流水"] + E --> F["原子写入:退款记录 + 钱包入账 + 钱包流水
+ 申请状态 Refunding → Refunded + 退款完成事实"] F --> G{"事务提交成功?"} - G -- "是" --> GS["申请状态推进 Refunding → Refunded"] - GS --> H["记录待发布退款完成事实"] - H --> I["通知买家退款成功"] - G -- "否" --> GF["不整体回滚:用独立失败记录保留状态 RefundFailed(钱包入账失败/退款记录失败等中间失败时),由 A419 失败重试入口处理"] - GF --> GFA["独立失败记录由 A419 失败重试入口处理"] + G -- "是" --> GS["受控事务提交:退款结果与完成事实同时生效"] + GS --> I["通知买家退款成功"] + G -- "否" --> GF["整体回滚受控退款事务:退款记录、钱包入账、钱包流水、申请终态和完成事实均不生效"] + GF --> GFA["AfterSales 另开独立事务记录 RefundFailed 与失败原因
由 A419 失败重试入口处理"] GFA --> IFAIL["通知相关方失败原因,等待重试"] ``` @@ -254,7 +259,7 @@ flowchart TD = 同一事务提交成功 ``` -任一步失败时按失败子操作分别处理:钱包入账或退款记录中间失败时**不整体回滚**,用独立失败记录保留 `RefundFailed` 供 A419 重试;其他非法状态变更(订单被覆盖、申请被回退到非终态)才整体回滚。不允许出现"钱包已入账但退款记录缺失"或"申请已退款但订单被覆盖"的部分结果。 +受控退款事务中的任一步失败都必须整体回滚,退款记录、钱包入账、钱包流水、库存回补、售后终态和退款完成事实均不得留下部分结果。退款事务回滚完成后,AfterSales 再通过独立失败记录事务保存失败原因,并将申请置为 `RefundFailed` 供 A419 幂等重试;退款审核或确认收货等已经成立的业务事实必须随失败记录保留,不能伪装成从未处理。 ## 八、买家撤销申请 -- Gitee From 96fd70dbd36d945faffd87969f3c25d004144dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9F=A6=E4=B9=BE=E5=BC=BA?= <3195306445@qq.com> Date: Fri, 24 Jul 2026 17:27:48 +0800 Subject: [PATCH 082/118] =?UTF-8?q?docs(process):=20=E4=BF=AE=E6=AD=A3=20M?= =?UTF-8?q?04/C03=20=E7=9A=84=E6=B8=B2=E6=9F=93=E9=94=99=E8=AF=AF=E4=B8=8E?= =?UTF-8?q?=E5=85=B3=E9=94=AE=E5=A5=91=E7=BA=A6=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - M04-63: 库存扣减条件改为语义描述(商品可售 AND 库存充足),不写 OnSale - M04-176: 修复第七节 Mermaid 围栏转义问题 - M04-259: M05 事件消费路径改为 Messaging 总线路由 - M04-214/258: 确认收货和 M05 的消息通知改为 Messaging 总线触发 M09 - C03-81: 单笔订单重试 3 次后标记永久失败告警,不再无限重试 - C03-98/151: 重试次数需持久化到 DB,约束和异常表语义一致 --- ...266\205\346\227\266\346\265\201\347\250\213.md" | 13 ++++++------- ...256\242\345\215\225\346\265\201\347\250\213.md" | 14 +++++++------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" index 66e591f..fbb5b3a 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" @@ -78,12 +78,11 @@ flowchart TD J3 --> K["写入 Outbox:OrderCancelledIntegrationEvent(cancel_reason = TIMEOUT)"] K --> L["提交事务"] L --> M{"提交成功?"} - M -- "否" --> N["记录错误日志,重试(最多 3 次)"] - N --> O{"超过重试次数?"} - O -- "是" --> P["发送告警,记录失败订单列表"] - O -- "否" --> E + M -- "否" --> N["记录错误日志,重试次数 +1"] + N --> O{"重试次数 < 3?"} + O -- "是" --> E + O -- "否" --> P["标记永久失败,发送告警,人工介入"] M -- "是" --> Q["继续处理下一条"] - P --> Q Q --> R{"批次处理完成?"} R -- "否" --> D R -- "是" --> Z @@ -95,7 +94,7 @@ flowchart TD - 每批次处理上限 100 条,避免长时间锁表。 - 使用条件更新 `WHERE status = 'PendingPayment'` 保证幂等。 - 库存回补与订单状态变更、Outbox 写入处于同一事务;任一失败则整体回滚,不允许出现"订单已取消但库存未回补"的部分成功状态。 -- 失败重试 3 次后告警,订单保留 PendingPayment,重试次数需持久化以支持跨 Worker 重启后继续。 +- 单笔订单最多重试 3 次;3 次均失败后标记永久失败、发送告警,由人工介入,不再自动重试;重试次数持久化到 DB。 ## 五、与支付的状态竞争处理 @@ -148,7 +147,7 @@ flowchart TD | 扫描时订单已被买家取消 | 条件更新影响行数=0,跳过 | 订单保持 Cancelled | | 库存回补时商品已删除或秒杀活动已结束 | 事务整体回滚,记录错误日志,重试(最多 3 次);3 次仍失败则告警,订单保留 PendingPayment | 订单不变更,库存不丢失,等待下次扫描重新处理 | | 多实例 Worker 并发扫描 | 扫描查询使用 SELECT FOR UPDATE SKIP LOCKED,后续条件更新依赖行锁;领取与处理在同一事务内完成 | 同一订单只被一个 Worker 处理 | -| 连续失败超过阈值 | 告警,订单保留 PendingPayment,重试次数持久化;下次扫描时继承次数并继续重试 | 人工可查告警记录,订单最终仍会被处理或人工介入 | +| 单笔订单重试 3 次均失败 | 标记永久失败,发送告警,人工介入 | 订单保留 PendingPayment,不再自动处理 | ## 八、与 M04 订单模块的复用关系 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" index ddde240..15671c6 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" @@ -60,7 +60,7 @@ flowchart TD C -- "否" --> Y["拒绝:地址无效"] C -- "是" --> D["服务端计算订单总额 = Σ(实时单价 × 数量)"] D --> E["开启订单创建事务"] - E --> F["条件扣减库存:WHERE stock >= quantity AND status = 'OnSale'"] + E --> F["条件扣减库存:商品可售 AND 库存充足"] F --> G["创建订单主记录(PendingPayment)+ 订单项快照"] G --> H["解析并保存 assignedMerchantUserId"] H --> I["删除已下单的购物车条目"] @@ -73,7 +73,7 @@ flowchart TD 关键约束: - 同一幂等键 `(buyer_id, idempotency_key)` 只创建一张订单,重复请求返回首次成功结果。 -- 库存扣减使用条件更新 `WHERE stock >= quantity AND status = 'OnSale'`,避免并发超卖和商品下架后仍被下单。 +- 库存扣减使用商品可售且库存充足的条件更新,避免并发超卖和商品下架后仍被下单。 - 订单项保存商品名称、图片、成交单价快照,后续改价不影响已有订单。 - 地址保存快照,后续修改不影响已有订单。 - 订单金额由服务端计算,不接受客户端传入。 @@ -173,7 +173,7 @@ flowchart TD ## 七、与商家发货的协作(M06-02 / F12) -\`\`\`mermaid +```mermaid flowchart TD PAID["Paid 订单"] --> SHIP["商家请求发货:orderId + 物流信息"] SHIP --> A{"订单存在且 assignedMerchantUserId == currentUserId?"} @@ -187,8 +187,8 @@ flowchart TD E --> F{"事务提交成功?"} F -- "否" --> R["返回错误"] F -- "是" --> G["返回发货成功"] - G --> H["M09 消费 OrderShippedIntegrationEvent,发送站内消息"] -\`\`\` + G --> H["Messaging 消费 OrderShippedIntegrationEvent,触发 M09 站内消息"] +``` 关键约束: @@ -211,7 +211,7 @@ flowchart TD D --> E["事务提交成功?"] E -- "否" --> R["返回错误"] E -- "是" --> F["返回确认成功"] - F --> G["M09 发送站内消息给买家"] + F --> G["Messaging 消费 OrderCompletedIntegrationEvent,触发 M09 站内消息"] G --> H["X01 开放评价入口(若已实现)"] ``` @@ -256,7 +256,7 @@ flowchart TD - C03 订单超时自动取消:复用取消事务逻辑,Worker 触发,不走买家主动接口;C03 复用 M04 的库存回补和状态变更逻辑。 - X01 商品评价:Completed 状态后开放评价入口,评价模块消费 OrderCompletedIntegrationEvent。 - M09 站内消息:消费 OrderCreatedIntegrationEvent、OrderCancelledIntegrationEvent、OrderShippedIntegrationEvent、OrderCompletedIntegrationEvent。 -- M05 支付:消费 OrderPaidIntegrationEvent 更新订单状态为 Paid。 +- M05 支付:在支付事务内更新订单状态为 Paid 并发布 OrderPaidIntegrationEvent;Messaging 事件总线消费事件后触发 M09 站内消息。 ## 十二、验收证据清单 -- Gitee From b505df03f37e9ba4d99ecd9a9e0fc746a31c661d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A1=BE=E6=AC=A3=E6=9C=88?= Date: Fri, 24 Jul 2026 17:46:07 +0800 Subject: [PATCH 083/118] docs: add 20260724 daily report (zhh) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增朱惠惠 2026-07-24 日报至 reports/daily/20260724-朱惠惠.md --- ...4-\346\234\261\346\203\240\346\203\240.md" | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 "reports/daily/20260724-\346\234\261\346\203\240\346\203\240.md" diff --git "a/reports/daily/20260724-\346\234\261\346\203\240\346\203\240.md" "b/reports/daily/20260724-\346\234\261\346\203\240\346\203\240.md" new file mode 100644 index 0000000..5ae7121 --- /dev/null +++ "b/reports/daily/20260724-\346\234\261\346\203\240\346\203\240.md" @@ -0,0 +1,26 @@ +# 日报 - 朱惠惠 - 2026-07-24 + +## 今日完成 + +1. **建立并整理个人接口契约**:在 `docs/02-设计文档/interface/interface-zhh.md` 新增并完善 Cart、Seckill 两个模块的个人接口原稿,覆盖购物车 `A201`~`A208` 和秒杀 `A220`~`A228`,明确 `A229/A230` 取消占号,并将秒杀订单列表、详情查询统一复用 Ordering 的 `A302/A303`,避免建立平行订单查询接口。相关提交:`b6e264b`、`212d4c1`。 +2. **新增 M03 与 C01 业务流程文档**:在 `docs/02-设计文档/process/zhh/` 新增 `M03-购物车流程.md` 和 `C01-秒杀流程.md`,补充购物车主流程、数量和选中状态处理、结算预览,以及秒杀活动维护、库存划拨、抢购下单、订单衔接和并发失败分支。相关提交:`5bbdb27`。 +3. **修正流程与接口映射**:将 C01 中的秒杀订单查询改为复用 `A302/A303`,并同步调整 M03/C01 的流程映射。相关提交:`7294109`。 +4. **收紧 M03/C01 业务规则**:M03 将购物车数量调整拆分为“调大”和“调小”两条路径,调大才校验实时库存上限;C01 将接口映射重新对齐到 `A220`~`A228`,明确草稿保存时写入未激活的计划配额,发布时按 `activityId` 在同一事务中条件扣减普通库存并激活配额,同时限制编辑仅适用于 `Draft` 或尚未开始的 `Published` 活动,页面库存只作提示、最终以数据库条件更新为准。相关提交:`92f9366`。 + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式/求助对象 | +|----------|----------|-------------------| +| 初版 M03/C01 流程中的 Axxx 编号与个人接口契约存在错位,秒杀订单查询也与 Ordering 的公共接口重复 | 已解决 | 通过 `7294109`、`92f9366` 重新核对接口清单和流程映射,统一复用 Ordering 的 `A302/A303` | +| 购物车数量调整、秒杀活动编辑与发布库存处理的边界不够清晰,容易造成前端提示与数据库最终结果不一致 | 已解决 | 拆分购物车调大/调小路径,收紧 C01 活动状态条件,并明确发布事务、数据库条件更新和页面库存提示的职责边界 | +| M03/C01 仍需与 Catalog、Ordering 等直接协作模块进行交叉评审,个人接口原稿也需要后续纳入总接口契约 | 未解决 | 明日继续按流程文档和接口设计的评审要求核对状态、权限、事务边界及跨模块公开契约 | + +## 明日计划 + +1. 与 Catalog、Ordering 负责人交叉评审 M03/C01 流程中的商品库存、秒杀库存回补、订单创建和取消衔接,记录并处理边界问题。 +2. 根据评审结果继续核对 `interface-zhh.md` 与总接口设计的 Axxx、鉴权、错误码和跨模块契约,保持流程、接口和数据库事实一致。 +3. 整理 Cart、Seckill 后续实现所需的接口联调清单和并发/幂等验证场景,为进入开发阶段做准备。 + +## 今日工时 + +约 8 小时(按今日 08:50—17:08 的提交时间记录估算) -- Gitee From ef4b1d3574a548d2549d920a6613e9b00e95fa91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A1=BE=E6=AC=A3=E6=9C=88?= Date: Fri, 24 Jul 2026 18:05:55 +0800 Subject: [PATCH 084/118] =?UTF-8?q?docs(report):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E7=AC=AC1=E5=91=A8=E5=91=A8=E6=8A=A5=EF=BC=882026-07-22=20~=20?= =?UTF-8?q?2026-07-24=EF=BC=89=EF=BC=9B=E6=B1=87=E6=80=BB=E9=9C=80?= =?UTF-8?q?=E6=B1=82=E3=80=81=E6=9E=B6=E6=9E=84=E3=80=81=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E3=80=81=E6=8E=A5=E5=8F=A3=E3=80=81=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E3=80=81=E5=9F=BA=E7=A1=80=E5=B7=A5=E7=A8=8B=E4=B8=8E=E5=8E=9F?= =?UTF-8?q?=E5=9E=8B=E8=AE=BE=E8=AE=A1=E4=BA=A7=E5=87=BA=EF=BC=8C=E6=8C=89?= =?UTF-8?q?=20reports/weekly/README.md=20=E6=A8=A1=E6=9D=BF=E5=A1=AB?= =?UTF-8?q?=E5=86=99=EF=BC=9B=E4=B8=8B=E9=98=B6=E6=AE=B5=E8=BF=9B=E5=85=A5?= =?UTF-8?q?=20M01/M02/M03/M04/M05=20=E6=8E=A5=E5=8F=A3=E4=B8=8E=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...1\345\221\250-\345\221\250\346\212\245.md" | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 "reports/weekly/\347\254\2541\345\221\250-\345\221\250\346\212\245.md" diff --git "a/reports/weekly/\347\254\2541\345\221\250-\345\221\250\346\212\245.md" "b/reports/weekly/\347\254\2541\345\221\250-\345\221\250\346\212\245.md" new file mode 100644 index 0000000..e72e15b --- /dev/null +++ "b/reports/weekly/\347\254\2541\345\221\250-\345\221\250\346\212\245.md" @@ -0,0 +1,87 @@ +# 第 1 周周报(2026-07-22 ~ 2026-07-24) + +> 组别:class1 group7 汇总人:顾欣月 + +## 一、本周目标回顾 + +| 计划目标 | 完成情况 | 说明 | +|----------|----------|------| +| 需求规格说明书 v0.1(覆盖 F01~F13、选做、挑战模块) | 完成 | 六人共同完成;唐宇昊定稿 M01/M06-03/M08 四身份差异化;朱惠惠定稿 M03-01 购物车;韦乾强补 M04/C03/M06 详述;顾欣月补 M02/M06-01/M07/C04 详述;张海洋扩写 C08 支付回调幂等与对账 | +| 系统架构设计 v0.1 | 完成 | 确认 Vue 3 + TypeScript 前端、.NET 10 Web API、EF Core 10 + PostgreSQL 后端,以及 Redis、RabbitMQ、Outbox、Worker、Aspire、Docker Compose、Nginx 的职责边界 | +| Git 团队协作流程文档 | 完成 | 明确 master / dev 长期分支、短生命周期任务分支、PR 流程、交叉 Code Review 与合并清理 | +| 验收标准定稿 | 完成 | 4 项选做、7 项挑战模块的具体验收口径 | +| 业务流程文档(process) | 部分完成 | 已落 tyh(F01/F02/F03/F13/X02,共 5 篇)、M03 购物车 + C01 秒杀、M10 售后 + C08 支付回调、商品/订单/评价/搜索、消息/实时推送/缓存/高可用等流程;个别流程存在 Mermaid 规范与 A 编号错位问题,已通过 1b48ae9、10d0175、eef2f43、e7d3d55、92f9366 等 commit 修复 | +| 数据库设计 | 部分完成 | 韦乾强、顾欣月完成个人模块数据库设计;唐宇昊、朱惠惠、罗皓晨正在节奏中 | +| 接口设计 | 部分完成 | 已成型 interface-tyh(A001~A025)、interface-zhh(A201~A208 + A220~A228,A229/A230 取消占号)、interface-gxy、订单模块 A301~A307/A308、支付与售后 A401~A425;尚未在总文档中标记"已确认" | +| 前后端基础工程初始化 | 完成 | 新增 Vue 3 / Vite 前端项目,以及包含 API、Application、Domain、Infrastructure、Worker 和测试项目的 .NET 10 解决方案基础结构(commit `fc9ed60`) | +| 原型设计初版 | 部分完成 | 顾欣月完成整体框架,页面细节与交互流程仍需细化 | +| 项目级 Skill / Agent 同步 | 完成 | 按 2026-07-23 部署提示词对 `.claude/CLAUDE.md` 与 6 个核心项目 Skill(`eshop-project-workflow` / `eshop-deliver-feature` / `eshop-fix-bug` / `eshop-align-docs` / `eshop-verify-acceptance` / `eshop-manage-git`)增量同步(平台路径转换 + 政策转换 + 保留新规则),本机副本由 `.git/info/exclude` 隔离,未进入 Git 工作区 | + +## 二、本周主要产出 + +1. **需求规格说明书 v0.1/v0.2**:`docs/01-需求文档/需求规格说明书.md`(罗皓晨、唐宇昊、朱惠惠、韦乾强、顾欣月、张海洋共同完成)。 +2. **系统架构设计**:`docs/02-设计文档/系统架构设计.md`(罗皓晨主笔)。 +3. **Git 团队协作流程文档**:`docs/02-设计文档/Git团队协作流程.md`(罗皓晨)。 +4. **验收标准**:`docs/00-项目要求/验收标准.md` 4 项选做 + 7 项挑战模块验收口径(罗皓晨)。 +5. **业务流程文档(process)**: + - `docs/02-设计文档/process/tyh/` 新增 F01/F02/F03/F13/X02 共 5 篇(唐宇昊,`e9a095e`/`1b48ae9`/`10d0175`)。 + - `docs/02-设计文档/process/zhh/` 新增 M03 购物车 + C01 秒杀 2 篇(朱惠惠,`5bbdb27`/`7294109`/`92f9366`)。 + - `docs/02-设计文档/process/zhy/` 新增 M10 售后 + C08 支付回调与对账 2 篇,并修复 8 个阻断性流程漏洞(张海洋,`71cc5fc`/`05c8a8f`/`90c8686`/`e7d3d55`)。 + - `docs/02-设计文档/process/gxy/` 新增商品/订单/评价/搜索流程(顾欣月,`b14d9f7`/`35d678c`/`7367922`)。 + - 业务流程总集:`docs/02-设计文档/业务流程设计.md` + `docs/02-设计文档/process/README.md`(罗皓晨,`d6e4255`/`aa2ecda`)。 +6. **接口契约初稿**: + - `docs/02-设计文档/interface/interface-tyh.md` A001~A025(唐宇昊,`2c79bd5`/`eef2f43`)。 + - `docs/02-设计文档/interface/interface-zhh.md` A201~A208 + A220~A228(A229/A230 取消占号),秒杀订单列表/详情复用 Ordering 的 A302/A303(朱惠惠,`b6e264b`/`212d4c1`)。 + - `docs/02-设计文档/interface/interface-gxy.md`(顾欣月,`b14d9f7` 等)。 + - 订单模块 A301~A307 + A308 买家确认收货 + 支付与售后 A401~A425 + A431/A434 边界修复(韦乾强、张海洋,`7236562`/`5035456`/`8a5d615`/`b4c75bb`/`9e728bc`)。 + - 团队接口契约汇总 + 跨模块口径对齐(罗皓晨,`9c25f26`/`94e2528`)。 + - 上述个人接口原稿按 `docs/02-设计文档/接口设计.md` 2.2 节要求,仅作个人贡献与交叉评审追踪。 +7. **数据库设计初稿**:M04 订单 + C03 订单超时自动取消、M02 商品 + M06-01 + M07 + C04 等模块(顾欣月、韦乾强)。 +8. **项目原型设计初版**:完成主要页面与交互的整体框架(顾欣月)。 +9. **项目级 Skill / Agent 配置**:`.claude/skills/` 与项目级 Agent 配置增量同步(顾欣月、罗皓晨、张海洋)。 +10. **项目命名与 Logo 设计**:与小组成员共同确定项目名称,并完成 Logo 初稿设计(顾欣月)。 +11. **7 月 22 日项目复盘会议纪要**:商城规划、Gitee 协作要求与行动项(顾欣月)。 +12. 7月24日会议记录(顾欣月) + +## 三、成员工作量统计 + +| 姓名 | 负责模块 | 本周主要工作 | 日报提交(天) | +|------|----------|-------------|--------------| +| 顾欣月(组长) | M02 商品、M06-01 后台分类与商品管理、M07 商品评价与晒图(X01)、C04 商品搜索进阶 | 7/22 通读项目要求、梳理 4 周节点、参与需求规格说明书初稿、参加小组分工会议;7/23 项目命名与 Logo 初稿,完成 M02-01/M02-02/M06-01/M07/C04 功能详述,完成项目级 Skill 配置与入口 Skill / 1 个专项 Skill 路由;7/24 完成接口设计、数据库设计整版,原型设计初版(页面布局与主要交互流程),更新项目级 Agent 与 Skill 配置,整理本周工作并完成周报编写(`f27c48e`) | 3/3 | +| 罗皓晨 | 项目脚手架、登录鉴权公共组件、集成联调 + 业务流程与项目级规范 | 7/22 完成需求规格说明书 v0.1、系统架构设计、Git 团队协作流程、验收标准(`ace466f`/`c39e0cc`);7/23 梳理补充负责模块的需求与身份处理说明,对齐需求规格、系统架构、接口命名规范,补充 PC Web 与后端 Web API 交付边界,同步完善团队项目级规则和分支命名规范;7/24 初始化前后端基础工程(`fc9ed60`),整理接口与数据库协作规则(`7569005`/`74415cc`/`9c25f26`/`94e2528`),建立业务流程文档路由(`d6e4255`/`aa2ecda`),完成站内消息、实时推送、缓存与高可用流程,完善项目工作流、文档路由与阶段提交边界规则(`b9ca003`/`6200df7`) | 3/3 | +| 唐宇昊 | M01 登录鉴权、M06-03 后台用户管理、M08 收藏与浏览历史 | 7/22 排查 Git 工作目录锁与 Gitee 认证(孤儿锁 + `GIT_TERMINAL_PROMPT=0` + cached credential),生成并提交首份日报;7/23 M01-01/02/03、M06-03、M08 四身份差异化规则定稿(+218/-19),新建短生命周期分支 `docs/daily-reports-tyh` 提交日报;7/24 process/tyh 落地 F01/F02/F03/F13/X02 五条流程文档(`e9a095e`/`1b48ae9`/`10d0175`),interface-tyh.md 新增 A001~A025(`2c79bd5`/`eef2f43`),将 A006/A007 迁回 `/api/users/me` 资源域、收紧 A010~A014 为 `BuyerOnly` | 3/3 | +| 张海洋 | M03 支付、C08 支付回调对账、M10 售后 | 7/22 通读 Git 团队协作流程文档;7/23 在 `dev` 提交《需求规格说明书》支付/售后/对账细化(`db840e4`/`88929d1`),扩写 C08 支付回调幂等与对账章节(15 项 FR + 3 核心流程 + 13 类异常 + 15 步验收脚本,commit `44a08d0`);7/24 在 `docs/zhy-process-flow-zhy` 提交 A401-A425 接口契约 + 修复 A431 边界 + 新增 A434 买家退货寄回信息(`8a5d615`/`b4c75bb`),新增 M10-售后流程与 C08-支付回调与对账流程(`71cc5fc`/`05c8a8f`/`90c8686`),在 `docs/c08-m10-vuln-fix-zhy` 修复 8 个阻断性漏洞(C08 回调事务/失败分支/对账批次 3 处 + M10 售后并发/Refunding 异步/退款合并/快递单号/RefundFailed 不可达 5 处,commit `e7d3d55`),并完成阶段自动 Commit → push → PR !58 → 合入 dev → 删除任务分支的完整 Git 生命周期,同步 `.claude/CLAUDE.md` 与 6 个核心项目 Skill | 3/3 | +| 朱惠惠 | M03 购物车、C01 秒杀 | 7/22 参加项目小组会议、完成新项目分工;7/23 补提 20260722 日报(`18fdce2`),M03-01 购物车管理需求详述(七节结构、15 条 FR、覆盖主流程与异常边界,commit `5740b10`,+211/-8);7/24 在 `interface-zhh.md` 新增并完善 Cart A201~A208 + Seckill A220~A228(A229/A230 取消占号,秒杀订单列表/详情复用 Ordering 的 A302/A303),新增 `process/zhh/` M03-购物车流程与 C01-秒杀流程(`5bbdb27`/`7294109`),收紧 M03 数量调整的"调大/调小"双路径与 C01 活动编辑/发布事务边界(`92f9366`) | 3/3 | +| 韦乾强 | M04 订单、C03 订单超时自动取消、M06 商家运营与后台管理 | 7/22 通读项目要求;参与需求规格说明书初稿;参加小组分工会议;7/23 梳理补充负责模块的需求与身份处理说明,明确各功能模块的负责人与命名空间对应关系,撰写 M04 订单 + C03 订单超时自动取消 + M06 商家运营与后台管理详述;7/24 完成数据库设计与项目接口设计整版,完成 M04-订单和 C03-订单超时自动取消设计文档(commit `96fd70d` 修正 M04/C03 渲染错误与关键契约错误) | 3/3 | + +> 注:本周共 3 个工作日(7/22 周三、7/23 周四、7/24 周五)。提交次数取自 `git log --since="2026-07-22" --until="2026-07-24 23:59" --all` 统计结果(含由其他成员代为合并的 PR 提交)。全员 3 天日报按时提交。 + +## 四、问题与风险 + +| 问题/风险 | 影响 | 应对措施 | 是否需要老师协助 | +|-----------|------|----------|------------------| +| 张海洋 7/23 在 `docs/c08-payment-callback-zhy` 分支完成 C08 章节 commit `44a08d0` 后切走分支未走 PR 合入 dev,commit 当前 unreachable | C08 章节工作存在丢失风险 | 已于 7/24 在 `docs/zhy-process-flow-zhy` 重建并通过 PR !58 合入 dev(`d6a4f27`),C08 工作已闭环;本风险视为已解决 | 否 | +| 个别流程文档首版存在 Mermaid 命名、形状、连线方向与团队规范不一致(tyh 流程 A 编号错位、TOCTOU、退出清理、地址编辑校验;zhy M10 状态机非法转换、C08 范围;zhh M03/C01 流程 A 编号与接口契约错位) | 影响流程可读性与评审通过 | 已通过 `1b48ae9`/`10d0175`/`eef2f43`(tyh)、`05c8a8f`/`90c8686`/`e7d3d55`(zhy)、`7294109`/`92f9366`(zhh)等 commit 修复;后续继续按 Mermaid 规范与接口契约回填 | 否 | +| C08 支付回调/对账、M10 售后、C04 商品搜索进阶等扩展模块的存储与索引方案(SeaweedFS 启用时间窗、分词器选型、倒排索引存储介质)尚未定稿 | 阻塞对应模块接口与数据库设计收尾 | 罗皓晨牵头评估对象存储与 Search 方案;先以"可插拔"形式描述接口边界 | 是 | +| 各模块接口契约目前以个人原稿(interface-tyh / interface-zhh / interface-gxy / interface-wq + A401~A425)形式散落,未在总文档 `docs/02-设计文档/接口设计.md` 中标记"已确认" | 跨模块联调时易出现 A 编号、鉴权、错误码口径不一致 | 由罗皓晨汇总到接口设计总文档并标记待交叉评审项;下周完成 A001~AAll 总文档同步 | 否 | +| 张海洋 7/24 同步的 `.claude/CLAUDE.md` 与 6 个核心项目 Skill 增量同步副本由 `.git/info/exclude` 隔离,未进入 Git 工作区 | 个体本地环境与仓库规则可能存在差异,跨成员协作时需谨慎 | 后续在仓库层统一合并,提交前先确认本地副本与仓库版本一致 | 否 | +| 原型设计仅完成初版大致框架,页面细节与交互流程仍需完善 | 影响 M02/M06-01/M07 前端开发的页面参照 | 顾欣月下周继续细化,并结合组内意见迭代 | 否 | +| 接口设计与数据库设计已完成本轮整理,但尚未进行仓库级交叉评审 | 后续实现可能基于不一致的设计 | 下周组内交叉核对、记录评审意见并修订 | 否 | +| 6 人分工表中的 M01~M17 编号与 Git 分支命名 ``(auth / catalog / order / payment / c08 等)的映射尚未最终对齐 | 后续创建任务分支时存在命名漂移风险 | 由组长与罗皓晨牵头先固化一份映射表,再应用到下一阶段任务分支 | 否 | +| PowerShell 5.1 here-string 把中文字面量传给 Python 时编码错乱,导致静态自检脚本误报"入口缺少关键语义" | 影响本地辅助脚本运行 | 改用 `[System.IO.File]::WriteAllText($tmp, $content, [System.Text.Encoding]::UTF8)` 写入 Temp 文件,再用 `python -X utf8` 执行;脚本里 `sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")` 修正输出编码 | 否 | + +## 五、下周计划 + +| 任务 | +|------| +| 完成需求规格说明书 v0.2 评审闭环,统一各模块接口边界与异常分支 | +| 完成数据库设计定稿(M01/M03/M04/M06/M07/M08 + C01/C03/C04/C06/C07/C08),并提交相应 Migration 初版 | +| 将个人接口契约(interface-tyh / interface-zhh / interface-gxy / interface-wq + A401~A425)汇总到 `docs/02-设计文档/接口设计.md` 总文档,标记"已确认"或"待交叉评审" | +| 启动 M01 登录鉴权前后端实现(注册 / 登录 / 鉴权 / 个人中心 / 默认头像),并可演示 | +| 启动 M02 商品模块前后端实现(分类 / 列表 / 搜索 / 详情 / 后台管理),并可演示 | +| 完成 M03 购物车、M04 订单前后端原型框架,串联"加车 → 下单"链路 | +| 完成 M05 支付 / M10 售后的服务端实现与 C08 支付回调幂等、对账批次的开发 | +| 评估并定稿对象存储与搜索方案(SeaweedFS 启用时间窗、C04 分词器与索引存储介质) | +| 细化原型设计(页面布局、交互流程、关键状态视觉) | +| 评测漏洞清单上的其他模块(M05 等)是否存在阻断性漏洞,按需建分支处理 | +| 每日 18:00 前提交 `reports/daily/YYYYMMDD-姓名.md` 日报;周五 18:00 前汇总提交第 2 周周报 | -- Gitee From b86d36b82a43297ac2c8430be4be937725f169d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A1=BE=E6=AC=A3=E6=9C=88?= Date: Fri, 24 Jul 2026 18:11:18 +0800 Subject: [PATCH 085/118] =?UTF-8?q?docs(docs):=20=E8=A1=A5=E5=85=857?= =?UTF-8?q?=E6=9C=8824=E6=97=A5=E5=91=A8=E5=A4=8D=E7=9B=98=E4=BC=9A?= =?UTF-8?q?=E8=AE=AE=E7=BA=AA=E8=A6=81=EF=BC=9B=E8=AE=B0=E5=BD=95=E6=9C=AC?= =?UTF-8?q?=E5=91=A8=E9=97=AE=E9=A2=98=E4=B8=8E=E4=B8=8B=E5=91=A8=E5=8E=9F?= =?UTF-8?q?=E5=9E=8B=E3=80=81=E6=9E=B6=E6=9E=84=E5=8F=8A=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- ...45\344\275\234\350\256\241\345\210\222.md" | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 "docs/04-\344\274\232\350\256\256\350\256\260\345\275\225/2026-07-24-\346\234\254\345\221\250\351\227\256\351\242\230\345\244\215\347\233\230\344\270\216\344\270\213\345\221\250\345\267\245\344\275\234\350\256\241\345\210\222.md" diff --git "a/docs/04-\344\274\232\350\256\256\350\256\260\345\275\225/2026-07-24-\346\234\254\345\221\250\351\227\256\351\242\230\345\244\215\347\233\230\344\270\216\344\270\213\345\221\250\345\267\245\344\275\234\350\256\241\345\210\222.md" "b/docs/04-\344\274\232\350\256\256\350\256\260\345\275\225/2026-07-24-\346\234\254\345\221\250\351\227\256\351\242\230\345\244\215\347\233\230\344\270\216\344\270\213\345\221\250\345\267\245\344\275\234\350\256\241\345\210\222.md" new file mode 100644 index 0000000..94b8d8d --- /dev/null +++ "b/docs/04-\344\274\232\350\256\256\350\256\260\345\275\225/2026-07-24-\346\234\254\345\221\250\351\227\256\351\242\230\345\244\215\347\233\230\344\270\216\344\270\213\345\221\250\345\267\245\344\275\234\350\256\241\345\210\222.md" @@ -0,0 +1,47 @@ +# 会议纪要:本周问题复盘与下周工作计划 + +| 项目 | 内容 | +|------|------| +| 会议时间 | 2026-07-24 | +| 参会人员 | 顾欣月、罗皓晨、唐宇昊、张海洋、朱惠惠、韦乾强 | +| 缺席人员及原因 | 无 | +| 记录人 | 顾欣月 | + +## 一、会议议题 + +1. 汇总本周需求、设计、协作和项目初始化工作的实际进展。 +2. 复盘本周暴露的文档一致性、Git 协作、方案选型和任务衔接问题。 +3. 安排下周原型设计、后端基础架构完善和基础功能填充工作。 +4. 明确下周阶段成果、负责人和协作要求。 + +## 二、讨论内容与结论 + +| 议题 | 讨论要点 | 结论/决策 | +|------|----------|-----------| +| 本周工作进展 | 1. 六名成员已围绕登录鉴权、商品、购物车、订单、支付、售后、评价、收藏、秒杀与后台管理等负责模块补充需求详述,部分模块已继续形成业务流程、个人接口和数据库设计材料。
2. 唐宇昊完成 F01/F02/F03/F13/X02 流程与 Identity、Engagement 个人接口契约整理;朱惠惠完成 M03 购物车、C01 秒杀流程及 Cart、Seckill 个人接口契约;张海洋完成 M10 售后、C08 支付回调与对账流程和 Payment、AfterSales 接口契约,并修复多项事务与状态机问题。
3. 项目名称与 Logo 已形成初稿,原型设计已完成主要页面和交互的大致框架,但细节尚未定稿。
4. 前端 Vue 3/Vite 工程和包含 API、Application、Domain、Infrastructure、Worker、测试项目的 .NET 解决方案基础结构已经初始化。
5. 团队已逐步完善 Git 协作、文档路由、命名和阶段提交规则。 | 本周整体仍处于“需求与设计逐步收敛、工程基础结构已建立”的阶段。需求详述、流程和个人设计稿不等于功能已实现;下周应从文档设计阶段转入“原型细化 + 基础架构完善 + 核心功能纵向实现”阶段。 | +| 文档与设计一致性问题 | 本周多次出现需求、业务流程、接口编号、数据库设计、鉴权范围和模块命名之间不一致的情况;具体包括流程引用的 Axxx 编号错位、秒杀订单查询与 Ordering 公共接口重复、支付/售后状态机存在非法流转,以及回调、退款和库存处理的事务边界不完整。相关负责人已完成一轮修正,但 M03/C01、M05/M10/C08 等流程仍需与 Catalog、Ordering、Payment 等协作模块交叉评审。商品图片存储启用时间、C04 搜索分词与索引方案、部分基础设施精确版本仍未最终确认。 | 各成员在开始编码前必须按“已确认需求 → 业务流程 → 接口/数据库 → 实现与测试”的顺序复核本人模块。个人接口和数据库原稿只能作为评审材料,评审通过并汇总到主文档后才能作为实现依据;跨模块流程必须由直接协作负责人共同核对状态、权限、事务、并发和公开契约,未确认方案继续标记为待定,不得在代码中自行选型。 | +| Git 与协作问题 | 本周出现过误推 `dev`、在长期汇总分支提交、任务提交未通过 PR/MR 合入、分支切换后提交难以追踪、删除前误判分支已合并,以及先改结论再补文档导致多次往返等情况;同时也暴露了 Gitee 凭据、Git 锁文件、分支命名理解不一致和 Gitee 新建 PR 默认指向 `master` 的问题。 | 继续执行“最新 `dev` → 短生命周期任务分支 → 精确暂存 → 提交与推送 → PR/MR → CI/真实验证 → 交叉 Review → 合入 `dev`”流程。一个分支只承载一个任务,禁止直接在 `master`、`dev` 开发或 Push;创建 PR/MR 时必须手动确认目标分支为 `dev`,删除分支前必须验证成果已经进入 `dev`;每日提交和日报必须与本人真实产出一致。 | +| 后端基础架构计划 | .NET 解决方案基础目录已经建立,但仍需依据真实模块完善组合根、配置、依赖注入、统一错误处理、认证授权、EF Core/PostgreSQL、OpenAPI 和测试入口等公共基础能力。现阶段不能把“工程已初始化”描述为后端业务功能已经完成。 | 在现有解决方案上完善最小可运行的后端基础架构;优先保证 API 能启动、配置可分环境管理、数据库连接与 Migration 路径清晰、统一错误响应可用、鉴权和 OpenAPI 具备后续模块接入条件。公共能力只实现两个以上模块已经确认需要的部分,不提前堆叠复杂基础设施。 | +| 基础功能填充计划 | 按项目四周计划,下周应优先推进用户、登录鉴权和商品等核心功能,并由各模块负责人按数据库、后端接口、前端页面和测试的纵向链路交付。购物车、订单和支付模块先完成与核心功能衔接所需的最小设计和实现准备。 | 第一批实现优先覆盖 F01 用户注册、F02 登录退出、F03 个人信息与地址、F04 商品分类与列表、F05 商品搜索和 F06 商品详情。各功能必须以已确认接口和数据库设计为依据,并包含正常流程、权限、异常处理和针对性测试;完成一条可验证链路后再继续扩展。 | + +## 三、行动项 + +| 事项 | 负责人 | 截止时间 | 状态 | +|------|--------|----------|------| +| 细化 PC Web 原型,补齐核心页面、角色入口、主流程和异常交互,并组织组内评审 | 顾欣月牵头,全体成员参与 | 2026-07-27 | 进行中 | +| 在现有 .NET 解决方案上完善可运行的后端基础架构和公共接入能力 | 罗皓晨牵头,各模块负责人配合 | 2026-07-28 | 进行中 | +| 复核并实现 F01~F03 登录鉴权、个人信息与地址的第一条纵向链路 | 唐宇昊 | 2026-07-31 | 待办 | +| 复核并实现 F04~F06 商品分类、列表、搜索和详情的第一条纵向链路 | 顾欣月 | 2026-07-31 | 待办 | +| 对 M03 购物车、C01 秒杀与 Catalog、Ordering 的库存、订单创建和取消衔接进行交叉评审,整理联调及并发/幂等验证清单 | 顾欣月、朱惠惠、韦乾强 | 2026-07-29 | 待办 | +| 复核 M05 支付、M10 售后、C08 支付回调与对账流程,以及退款、订单状态和跨模块事务契约 | 张海洋、韦乾强、罗皓晨 | 2026-07-29 | 待办 | +| 继续收敛订单、支付、售后和后台管理设计,为后续基础功能实现做准备 | 韦乾强、张海洋 | 2026-07-31 | 待办 | +| 对个人接口、数据库和流程文档进行交叉评审,确认后同步至主文档 | 各模块负责人、罗皓晨 | 2026-07-29 | 进行中 | +| 每个任务使用独立短生命周期分支,通过 PR/MR 和交叉 Review 合入 `dev` | 全体成员 | 持续执行 | 进行中 | +| 按真实提交与验证结果填写个人日报,及时记录阻塞项和次日计划 | 全体成员 | 每个工作日结束前 | 进行中 | + +## 四、遗留问题 + +1. C04 商品搜索的分词器、索引结构和部署方案仍需结合开发与验收环境确定。 +2. Identity、Engagement、Cart、Seckill、Payment、AfterSales 等个人接口原稿及相关流程仍需完成交叉评审并汇总到主文档,未汇总内容暂不能视为正式实现契约。 +3. M03/C01 与 Catalog、Ordering 的库存和订单衔接,以及 M05/M10/C08 涉及的退款、订单状态和跨模块事务边界仍需协作负责人共同确认。 +4. 后端基础工程已经初始化,但业务模块实现、数据库 Migration 和自动化测试仍需按下周计划逐项落地和真实验证。 -- Gitee From 02ce9eacda359e3663919c06a565353057639bce Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 18:14:16 +0800 Subject: [PATCH 086/118] =?UTF-8?q?docs(process):=20=E4=BF=AE=E6=AD=A3=20W?= =?UTF-8?q?QQ=20=E8=B6=85=E6=97=B6=E5=8F=96=E6=B6=88=E4=BA=8B=E5=8A=A1?= =?UTF-8?q?=EF=BC=9B=E5=90=8C=E6=AD=A5=E9=87=8D=E8=AF=95=E4=B8=8E=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=B6=88=E8=B4=B9=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interface/interface-wqq.md" | 7 ++-- ...05\346\227\266\346\265\201\347\250\213.md" | 42 +++++++++++-------- ...42\345\215\225\346\265\201\347\250\213.md" | 10 ++--- ...45\345\217\243\350\256\276\350\256\241.md" | 7 ++-- 4 files changed, 37 insertions(+), 29 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" index 3287ccb..1148d33 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" @@ -723,7 +723,7 @@ ### 业务规则 1. **超时时间配置**:订单超时时间通过配置项 `OrderTimeoutMinutes` 管理,默认30分钟 -2. **扫描策略**:Worker定时扫描 `PendingPayment` 状态且 `created_at + timeout < now()` 的订单 +2. **扫描策略**:创建订单时按当时生效的配置固化 `expiresAt`;Worker 定时扫描 `PendingPayment` 且 `expiresAt <= now()` 的订单,后续配置变化不追溯改变既有订单 3. **取消事务**:复用 A304 的内部取消用例,在同一受控事务内完成 `PendingPayment → Cancelled`、按普通/秒杀原通道回补库存并释放秒杀限购名额、写入 `cancelled_at` 和 `cancel_reason = 'TIMEOUT'` 4. **幂等保证**:使用条件更新 `WHERE status = 'PendingPayment'`,同一订单多次扫描只有一次成功 5. **支付竞争**:与M05支付并发时,条件更新确保只有一个成功 @@ -740,8 +740,9 @@ 1. 扫描间隔建议 ≤ 超时时间/2 2. 每批次处理上限100条,避免长时间锁表 -3. 失败重试3次后告警,订单保留待处理状态 -4. 多实例Worker使用 `SELECT FOR UPDATE SKIP LOCKED` 避免重复处理 +3. 单轮扫描内同一订单最多尝试 3 次;每次失败先回滚订单事务,再独立持久化失败次数、原因和结果 +4. 失败重试锁定并重新校验同一订单 ID;3 次均失败时告警并加入本轮排除集合,订单保持 `PendingPayment`,退避后由后续扫描继续,Worker 重启后也能恢复 +5. 多实例 Worker 在同一单笔订单事务内使用 `SELECT ... FOR UPDATE SKIP LOCKED LIMIT 1` 完成领取和处理,行锁保持到取消事务提交或回滚 ### 验证场景 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" index fbb5b3a..c6c5f3f 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" @@ -37,7 +37,7 @@ flowchart LR - C03 复用 M04 的订单状态变更和库存回补逻辑,不独立发明新事务。 - C03 由 Worker 后台任务触发,不提供买家主动接口。 -- 超时时间从订单 `created_at` 计算,不从其他时间点计算。 +- 创建订单时按当时生效的超时配置计算并固化支付截止时间;Worker 只扫描该截止时间,后续配置变化不追溯改变既有订单。 ## 三、超时时间配置 @@ -47,8 +47,9 @@ flowchart TD B --> C{"配置存在?"} C -- "否" --> D["使用默认值 30 分钟"] C -- "是" --> E["使用配置值"] - D --> F["订单超时时间已确认"] + D --> F["仅用于新订单计算支付截止时间"] E --> F + F --> G["创建订单时固化支付截止时间
既有订单截止时间不随配置变化"] ``` 关键约束: @@ -57,19 +58,20 @@ flowchart TD - 正式环境默认 30 分钟。 - 演示环境可通过配置调整为更短时间(如 5 分钟),但需说明与正式参数的对应关系。 - 配置不得硬编码。 +- 超时配置只在创建订单时用于计算支付截止时间;Worker 不使用当前配置重新计算既有订单是否到期。 ## 四、Worker 超时扫描流程 ```mermaid flowchart TD - A["Worker 定时触发(建议间隔 ≤ 超时时间/2)"] --> B["查询超时订单(FOR UPDATE SKIP LOCKED):status = PendingPayment AND created_at + timeout < NOW()"] - B --> C{"有待处理订单?"} - C -- "否" --> Z["本次扫描结束"] - C -- "是" --> D["按批次处理(建议每批 ≤ 100 条)"] - D --> E["对每条超时订单开启独立事务"] - E --> F["条件更新状态为 Cancelled:WHERE status = PendingPayment"] + A["Worker 定时触发(建议间隔 ≤ 超时时间/2)"] --> B["初始化本轮处理计数和排除集合,最多 100 条"] + B --> C["开启单笔订单事务
首次领取下一条,失败重试则锁定当前订单 ID"] + C --> D["首次领取:FOR UPDATE SKIP LOCKED LIMIT 1,并排除本轮已跳过订单
重试:按当前订单 ID 重新校验 PendingPayment 和 expires_at"] + D --> E{"领取成功?"} + E -- "否" --> Z["提交空事务,本次扫描结束"] + E -- "是" --> F["条件更新状态为 Cancelled:WHERE status = PendingPayment"] F --> G{"影响行数 = 1?"} - G -- "否" --> H["订单已被其他操作处理(如已支付/已取消),跳过"] + G -- "否" --> H["订单已被支付或取消:回滚当前事务并记录最终状态"] G -- "是" --> I{"订单类型?"} I -- "普通订单" --> J1["回补普通库存:stock = stock + quantity"] I -- "秒杀订单" --> J2["回补秒杀活动库存 + 释放买家限购额度"] @@ -78,13 +80,15 @@ flowchart TD J3 --> K["写入 Outbox:OrderCancelledIntegrationEvent(cancel_reason = TIMEOUT)"] K --> L["提交事务"] L --> M{"提交成功?"} - M -- "否" --> N["记录错误日志,重试次数 +1"] + M -- "否" --> N["回滚订单事务;独立持久化失败次数、原因和本次结果"] N --> O{"重试次数 < 3?"} - O -- "是" --> E - O -- "否" --> P["标记永久失败,发送告警,人工介入"] - M -- "是" --> Q["继续处理下一条"] - Q --> R{"批次处理完成?"} - R -- "否" --> D + O -- "是" --> C + O -- "否" --> P["发送告警并加入本轮排除集合
保留 PendingPayment,退避后由后续扫描继续"] + H --> Q["本轮处理计数 +1"] + M -- "是" --> Q + P --> Q + Q --> R{"已处理 100 条?"} + R -- "否" --> C R -- "是" --> Z ``` @@ -92,9 +96,11 @@ flowchart TD - 扫描间隔建议 ≤ 超时时间/2(如超时 30 分钟,扫描间隔 ≤ 15 分钟)。 - 每批次处理上限 100 条,避免长时间锁表。 +- 领取和处理同一订单必须处于同一数据库事务;`FOR UPDATE SKIP LOCKED` 的行锁保持到该笔取消事务提交或回滚,不能先批量查询后再另开事务处理。 +- 失败后的当前轮重试必须重新锁定并校验同一订单 ID;达到本轮尝试上限后把该订单加入本轮排除集合,避免下一次领取立即命中。排除集合只控制本轮扫描,不保存业务事实。 - 使用条件更新 `WHERE status = 'PendingPayment'` 保证幂等。 - 库存回补与订单状态变更、Outbox 写入处于同一事务;任一失败则整体回滚,不允许出现"订单已取消但库存未回补"的部分成功状态。 -- 单笔订单最多重试 3 次;3 次均失败后标记永久失败、发送告警,由人工介入,不再自动重试;重试次数持久化到 DB。 +- 单轮扫描内同一订单最多尝试 3 次;每次失败都在订单事务回滚后独立持久化失败次数、原因和结果。3 次均失败时发送告警并结束该订单的本轮处理,不新增永久失败业务状态;订单保持 `PendingPayment`,按退避策略由后续扫描继续,Worker 重启后也能恢复。 ## 五、与支付的状态竞争处理 @@ -145,9 +151,9 @@ flowchart TD | 数据库连接短暂中断 | 记录错误日志,下次扫描重试 | 最多延迟一个扫描周期 | | 扫描时订单已被支付 | 条件更新影响行数=0,跳过 | 订单保持 Paid | | 扫描时订单已被买家取消 | 条件更新影响行数=0,跳过 | 订单保持 Cancelled | -| 库存回补时商品已删除或秒杀活动已结束 | 事务整体回滚,记录错误日志,重试(最多 3 次);3 次仍失败则告警,订单保留 PendingPayment | 订单不变更,库存不丢失,等待下次扫描重新处理 | +| 库存回补时商品已删除或秒杀活动已结束 | 事务整体回滚并独立记录失败结果;本轮最多尝试 3 次,仍失败则告警并退避 | 订单保留 PendingPayment,库存不丢失,后续扫描继续处理或由人工排查根因 | | 多实例 Worker 并发扫描 | 扫描查询使用 SELECT FOR UPDATE SKIP LOCKED,后续条件更新依赖行锁;领取与处理在同一事务内完成 | 同一订单只被一个 Worker 处理 | -| 单笔订单重试 3 次均失败 | 标记永久失败,发送告警,人工介入 | 订单保留 PendingPayment,不再自动处理 | +| 单笔订单本轮尝试 3 次均失败 | 发送告警,本轮跳过该订单并继续处理批次中的其他订单 | 订单保留 PendingPayment,退避后由后续扫描继续,不遗漏到期订单 | ## 八、与 M04 订单模块的复用关系 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" index 15671c6..4793ec2 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" @@ -187,14 +187,14 @@ flowchart TD E --> F{"事务提交成功?"} F -- "否" --> R["返回错误"] F -- "是" --> G["返回发货成功"] - G --> H["Messaging 消费 OrderShippedIntegrationEvent,触发 M09 站内消息"] + G --> H["M09(Messaging 模块)幂等消费 OrderShippedIntegrationEvent,生成站内消息"] ``` 关键约束: -- 商家只能操作 \`assignedMerchantUserId == currentUserId\` 的订单(单店 B2C 严格校验)。 +- 商家只能操作 `assignedMerchantUserId == currentUserId` 的订单(单店 B2C 严格校验)。 - 发货前须检查是否存在未完结的售后申请(AfterSales 锁定同一订单行)。 -- 发货使用条件更新 \`WHERE status = 'Paid'\` 保证幂等。 +- 发货使用条件更新 `WHERE status = 'Paid'` 保证幂等。 - 重复发货返回成功,不重复变更状态。 ## 八、买家确认收货 @@ -211,7 +211,7 @@ flowchart TD D --> E["事务提交成功?"] E -- "否" --> R["返回错误"] E -- "是" --> F["返回确认成功"] - F --> G["Messaging 消费 OrderCompletedIntegrationEvent,触发 M09 站内消息"] + F --> G["M09(Messaging 模块)幂等消费 OrderCompletedIntegrationEvent,生成站内消息"] G --> H["X01 开放评价入口(若已实现)"] ``` @@ -256,7 +256,7 @@ flowchart TD - C03 订单超时自动取消:复用取消事务逻辑,Worker 触发,不走买家主动接口;C03 复用 M04 的库存回补和状态变更逻辑。 - X01 商品评价:Completed 状态后开放评价入口,评价模块消费 OrderCompletedIntegrationEvent。 - M09 站内消息:消费 OrderCreatedIntegrationEvent、OrderCancelledIntegrationEvent、OrderShippedIntegrationEvent、OrderCompletedIntegrationEvent。 -- M05 支付:在支付事务内更新订单状态为 Paid 并发布 OrderPaidIntegrationEvent;Messaging 事件总线消费事件后触发 M09 站内消息。 +- M05 支付:在支付事务内更新订单状态为 Paid 并发布 OrderPaidIntegrationEvent;M09(Messaging 模块)幂等消费事件并生成站内消息。 ## 十二、验收证据清单 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" index 0abbbcb..b1e8c83 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" @@ -8011,7 +8011,7 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 ##### 业务规则 1. **超时时间配置**:订单超时时间通过配置项 `OrderTimeoutMinutes` 管理,默认30分钟 -2. **扫描策略**:Worker定时扫描 `PendingPayment` 状态且 `created_at + timeout < now()` 的订单 +2. **扫描策略**:创建订单时按当时生效的配置固化 `expiresAt`;Worker 定时扫描 `PendingPayment` 且 `expiresAt <= now()` 的订单,后续配置变化不追溯改变既有订单 3. **取消事务**:复用 A304 的内部取消用例,在同一受控事务内完成 `PendingPayment → Cancelled`、按普通/秒杀原通道回补库存并释放秒杀限购名额、写入 `cancelled_at` 和 `cancel_reason = 'TIMEOUT'` 4. **幂等保证**:使用条件更新 `WHERE status = 'PendingPayment'`,同一订单多次扫描只有一次成功 5. **支付竞争**:与M05支付并发时,条件更新确保只有一个成功 @@ -8028,8 +8028,9 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 1. 扫描间隔建议 ≤ 超时时间/2 2. 每批次处理上限100条,避免长时间锁表 -3. 失败重试3次后告警,订单保留待处理状态 -4. 多实例Worker使用 `SELECT FOR UPDATE SKIP LOCKED` 避免重复处理 +3. 单轮扫描内同一订单最多尝试 3 次;每次失败先回滚订单事务,再独立持久化失败次数、原因和结果 +4. 失败重试锁定并重新校验同一订单 ID;3 次均失败时告警并加入本轮排除集合,订单保持 `PendingPayment`,退避后由后续扫描继续,Worker 重启后也能恢复 +5. 多实例 Worker 在同一单笔订单事务内使用 `SELECT ... FOR UPDATE SKIP LOCKED LIMIT 1` 完成领取和处理,行锁保持到取消事务提交或回滚 ##### 验证场景 -- Gitee From 1fb3f4cd5e204e6b30ff162802f14385c4a94231 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 18:16:49 +0800 Subject: [PATCH 087/118] =?UTF-8?q?docs(process):=20=E6=81=A2=E5=A4=8D=20C?= =?UTF-8?q?03=20=E4=B8=9A=E5=8A=A1=E8=AF=AD=E4=B9=89=E8=BE=B9=E7=95=8C?= =?UTF-8?q?=EF=BC=9B=E4=B8=8B=E6=B2=89=E6=8A=80=E6=9C=AF=E9=94=81=E5=AE=9A?= =?UTF-8?q?=E7=BB=86=E8=8A=82=E5=88=B0=E6=8E=A5=E5=8F=A3=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...05\346\227\266\346\265\201\347\250\213.md" | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" index c6c5f3f..ad44c77 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" @@ -65,22 +65,22 @@ flowchart TD ```mermaid flowchart TD A["Worker 定时触发(建议间隔 ≤ 超时时间/2)"] --> B["初始化本轮处理计数和排除集合,最多 100 条"] - B --> C["开启单笔订单事务
首次领取下一条,失败重试则锁定当前订单 ID"] - C --> D["首次领取:FOR UPDATE SKIP LOCKED LIMIT 1,并排除本轮已跳过订单
重试:按当前订单 ID 重新校验 PendingPayment 和 expires_at"] + B --> C["开始处理一笔到期订单
首次领取下一条,失败时重试当前订单"] + C --> D["确认订单已到支付截止时间且仍为 PendingPayment
多 Worker 竞争时只允许一个进入处理"] D --> E{"领取成功?"} - E -- "否" --> Z["提交空事务,本次扫描结束"] - E -- "是" --> F["条件更新状态为 Cancelled:WHERE status = PendingPayment"] - F --> G{"影响行数 = 1?"} - G -- "否" --> H["订单已被支付或取消:回滚当前事务并记录最终状态"] + E -- "否" --> Z["本次扫描结束"] + E -- "是" --> F["条件推进 PendingPayment → Cancelled"] + F --> G{"状态推进成功?"} + G -- "否" --> H["订单已被支付或取消:放弃本次取消并记录最终状态"] G -- "是" --> I{"订单类型?"} - I -- "普通订单" --> J1["回补普通库存:stock = stock + quantity"] + I -- "普通订单" --> J1["按订单项数量恢复普通商品库存"] I -- "秒杀订单" --> J2["回补秒杀活动库存 + 释放买家限购额度"] - J1 --> J3["记录 cancelled_at = NOW() 和 cancel_reason = TIMEOUT"] + J1 --> J3["记录超时取消时间和原因"] J2 --> J3 - J3 --> K["写入 Outbox:OrderCancelledIntegrationEvent(cancel_reason = TIMEOUT)"] - K --> L["提交事务"] + J3 --> K["登记可靠的订单取消事实,供买家通知使用"] + K --> L["原子提交订单状态、库存回补和取消事实"] L --> M{"提交成功?"} - M -- "否" --> N["回滚订单事务;独立持久化失败次数、原因和本次结果"] + M -- "否" --> N["整体回滚本笔取消;独立记录失败次数、原因和本次结果"] N --> O{"重试次数 < 3?"} O -- "是" --> C O -- "否" --> P["发送告警并加入本轮排除集合
保留 PendingPayment,退避后由后续扫描继续"] @@ -96,10 +96,10 @@ flowchart TD - 扫描间隔建议 ≤ 超时时间/2(如超时 30 分钟,扫描间隔 ≤ 15 分钟)。 - 每批次处理上限 100 条,避免长时间锁表。 -- 领取和处理同一订单必须处于同一数据库事务;`FOR UPDATE SKIP LOCKED` 的行锁保持到该笔取消事务提交或回滚,不能先批量查询后再另开事务处理。 -- 失败后的当前轮重试必须重新锁定并校验同一订单 ID;达到本轮尝试上限后把该订单加入本轮排除集合,避免下一次领取立即命中。排除集合只控制本轮扫描,不保存业务事实。 -- 使用条件更新 `WHERE status = 'PendingPayment'` 保证幂等。 -- 库存回补与订单状态变更、Outbox 写入处于同一事务;任一失败则整体回滚,不允许出现"订单已取消但库存未回补"的部分成功状态。 +- 领取和处理同一订单必须处于同一受控事务,多 Worker 竞争时只有一个能够推进订单状态;具体锁定方式由下游接口和实现契约承接。 +- 失败后的当前轮重试必须重新确认同一订单及其状态;达到本轮尝试上限后把该订单加入本轮排除集合,避免下一次领取立即命中。排除集合只控制本轮扫描,不保存业务事实。 +- 只有 `PendingPayment` 可以推进为 `Cancelled`,保证重复扫描和支付竞争不会产生第二个业务结果。 +- 库存回补、订单状态变更和可靠取消事实必须原子提交;任一失败则整体回滚,不允许出现"订单已取消但库存未回补"的部分成功状态。 - 单轮扫描内同一订单最多尝试 3 次;每次失败都在订单事务回滚后独立持久化失败次数、原因和结果。3 次均失败时发送告警并结束该订单的本轮处理,不新增永久失败业务状态;订单保持 `PendingPayment`,按退避策略由后续扫描继续,Worker 重启后也能恢复。 ## 五、与支付的状态竞争处理 @@ -152,7 +152,7 @@ flowchart TD | 扫描时订单已被支付 | 条件更新影响行数=0,跳过 | 订单保持 Paid | | 扫描时订单已被买家取消 | 条件更新影响行数=0,跳过 | 订单保持 Cancelled | | 库存回补时商品已删除或秒杀活动已结束 | 事务整体回滚并独立记录失败结果;本轮最多尝试 3 次,仍失败则告警并退避 | 订单保留 PendingPayment,库存不丢失,后续扫描继续处理或由人工排查根因 | -| 多实例 Worker 并发扫描 | 扫描查询使用 SELECT FOR UPDATE SKIP LOCKED,后续条件更新依赖行锁;领取与处理在同一事务内完成 | 同一订单只被一个 Worker 处理 | +| 多实例 Worker 并发扫描 | 领取与处理在同一受控事务内完成,竞争失败方跳过当前订单 | 同一订单只被一个 Worker 推进状态 | | 单笔订单本轮尝试 3 次均失败 | 发送告警,本轮跳过该订单并继续处理批次中的其他订单 | 订单保留 PendingPayment,退避后由后续扫描继续,不遗漏到期订单 | ## 八、与 M04 订单模块的复用关系 -- Gitee From efb99ad13b6ae5620c03067be63e338405593395 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 19:19:19 +0800 Subject: [PATCH 088/118] =?UTF-8?q?docs(process):=20=E6=A0=A1=E5=87=86=20Z?= =?UTF-8?q?HH=20=E8=B4=AD=E7=89=A9=E8=BD=A6=E4=B8=8E=E7=A7=92=E6=9D=80?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=EF=BC=9B=E7=BB=9F=E4=B8=80=E5=B9=82=E7=AD=89?= =?UTF-8?q?=E5=92=8C=E5=BA=93=E5=AD=98=E5=B1=95=E7=A4=BA=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 7 +- ...22\346\235\200\346\265\201\347\250\213.md" | 435 +++++++++--------- ...51\350\275\246\346\265\201\347\250\213.md" | 127 ++--- 3 files changed, 306 insertions(+), 263 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index ed610fd..fbf19cb 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -1,6 +1,6 @@ # 电子商城需求规格说明书 -> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.1 +> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.2 > > 文档状态:内容和格式已完成内部统一,待全组与指导教师评审后冻结 > 截止:第 1 周周三提交初稿 @@ -10,6 +10,7 @@ | 版本 | 日期 | 修改人 | 修改说明 | |---|---|---|---| | v0.1 | 2026-07-24 | 全体成员(罗皓晨统稿) | 形成并统一需求规格,明确四类角色、必做功能、4 项选做、7 项挑战、单店 B2C 边界、核心状态与六人模块职责 | +| v0.2 | 2026-07-24 | 罗皓晨 | 消解 C01 库存展示绝对一致与“不预占库存”的冲突,统一为数据库权威快照、旧结果防覆盖及并发失败后同一交互刷新 | ## 业务流程设计入口 @@ -1767,7 +1768,7 @@ C01 要求在面向买家的高并发秒杀场景下保证库存“只减不超 | C01-FR07 | 时间窗口校验 | 提交秒杀订单时服务端再次校验当前 UTC 时间落在活动开始和结束之间;活动开始前和结束后均不进入扣减;状态字段也参与校验,避免服务器之间时钟轻微漂移造成提早或延后成功。 | | C01-FR08 | 接口幂等 | 秒杀下单接口接受稳定的请求幂等标识,同一买家、活动和标识在约定窗口内重复提交只生成一笔订单并返回同一结果;重复请求不得重复扣减库存或创建订单。 | | C01-FR09 | 失败回退与取消 | 秒杀订单与普通订单取消(买家主动或 C03 超时取消)后的库存回补必须回补到同一活动的秒杀可售库存,不污染普通商品库存;同一笔秒杀订单不得重复回补,无论取消接口被调用多少次都只能回补一次。 | -| C01-FR10 | 状态展示与售罄 | 活动剩余库存为 0 时立即展示“已售罄”并禁用抢购入口;前端轮询或服务端推送给出的剩余库存必须与数据库实际剩余库存一致,不允许出现“前端还有库存但下单失败”或“前端售罄但实际还能抢”的体验偏差。 | +| C01-FR10 | 状态展示与售罄 | 活动剩余库存为 0 时立即展示“已售罄”并禁用抢购入口。前端轮询或服务端推送每次都必须返回读取时点的数据库权威库存快照,旧结果不得覆盖新结果;本期不做库存预占,展示快照与并发提交之间可能发生竞争,若最后库存被其他请求先扣减,失败响应必须带回提交后的权威库存并在同一交互中立即刷新。已确认售罄后不得继续显示可抢,数据库仍有库存时不得错误展示售罄。 | | C01-FR11 | 流量削峰与体验保护 | 在 Nginx、API、应用层或数据库连接池执行有上限的限流,超过上限的请求被快速拒绝并返回 `429/409`;普通商品入口和秒杀入口流量隔离,秒杀高并发不得拖垮普通商品查询。 | | C01-FR12 | 日志与追踪 | 所有秒杀提交请求记录买家 ID、活动 ID、商品 ID、请求数量、抢购买结果(成功/已售罄/超限/重复/未开始/已结束)、受影响行数、订单号(成功时)和 traceId;不记录 Token、密码或支付卡号。 | | C01-FR13 | 与订单、支付、消息衔接 | 秒杀成功订单沿用 M04 提交订单后状态机、支付(M05)、商家发货(M06-02)、消息(M09)和售后(M10)流程;不需要为秒杀订单开辟独立支付通道或独立通知渠道,但需要在订单上保留“秒杀活动 ID / 秒杀价快照”便于追溯。 | @@ -1804,7 +1805,7 @@ C01 要求在面向买家的高并发秒杀场景下保证库存“只减不超 #### 6. 异常与边界场景 - 活动未开始 / 已结束:客户端入口禁用且按钮置灰;服务端仍以数据库条件更新状态为准,未到时间或已结束的请求一律不扣减库存;活动结束后退化为普通商品详情。 -- 库存售罄:剩余库存 = 0 时服务端仍要接受请求并以“已售罄”返回,受影响行数 = 0;前端轮询或订阅缓存剩余库存需与数据库 `remaining` 一致;不允许出现“前端可见但实际已无库存”或“前端售罄但接口还能成功”的相反情景。 +- 库存售罄:剩余库存 = 0 时服务端仍要接受已到达请求并以“已售罄”返回,受影响行数 = 0;前端轮询或推送每次返回读取时点的数据库权威库存快照,旧结果不得覆盖新结果。本期不预占库存,展示与提交间发生并发竞争时,失败响应必须带回提交后的权威库存并在同一交互中刷新;已确认售罄后不得继续显示可抢,数据库仍有库存时不得错误展示售罄。 - 高并发竞争:100 并发抢 10 件库存时,数据库条件更新天然串行化保证超卖不发生;未成功的 90 个请求应尽快以明确状态返回,不应在应用层堆积;响应延迟受数据库行锁和连接池限制,压测报告记录 P50/P95/P99。 - 重复提交与连点:买家在同一秒内多次点击“立即抢购”,按钮立即置灰、幂等键去重、条件更新受影响行数 = 0 三道闸门共同保证只生成一笔订单;移动端断网重发也按幂等键处理。 - 单用户超限:同一买家试图抢多件超过单用户限购时,第 N+1 次请求以“超过单用户限购”拒绝,剩余可购数量随剩余库存动态变化;不允许通过更换账号或绕过前台校验绕过此限制。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" index b00dd67..01c5952 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" @@ -4,268 +4,287 @@ > 覆盖:C01-01、M03-01 与 M04-01 的秒杀衔接、X04 不参与秒杀取消回补 > 基础核心流程:F11(商品上下架)、F04/F06(活动浏览)、F08(下单)、F10(支付)、F09/F12(取消 / 发货) > 直接协作:顾欣月(M02 Catalog 与 M06-01 活动维护)、韦乾强(M04 Ordering 与 C03 超时取消)、张海洋(M05 Payment 与 C08 回调) -> 文档状态:初稿,待朱惠惠自审及 Catalog/Ordering 交叉评审;C08 异步回调替换边界按根文档要求保持“待决” +> 文档状态:已按需求校准,可作为接口与数据库设计输入;待 Catalog/Ordering 交叉评审 > 需求事实源:[需求规格说明书 C01](../../../01-需求文档/需求规格说明书.md) 的“C01 秒杀与防超卖”完整七节 ## 一、范围与事实来源 -本扩展在面向买家的高并发秒杀场景下保证库存“只减不超、不少不丢”:每一份秒杀库存只能被一名买家以一份订单成功购买,重复请求不能产生重复扣减或重复订单,所有失败请求不得留下半扣减、未提交订单或孤立记录。本期秒杀以“限时一口价活动”为模型,关联一个已上架的普通商品和一份独立维护的秒杀库存。活动期内买家点击“立即抢购”直接提交秒杀订单,跳过普通加车流程;活动结束或库存耗尽后入口立刻失效,进入商品详情时只能看到普通购买。 +本扩展面向“一个已上架商品、一个限时一口价活动、一份独立秒杀库存”的高并发抢购场景。每一份库存最多形成一份成功订单;重复请求不得重复扣减或重复下单;失败请求不得留下半扣减、孤立订单或支付前置记录。买家从秒杀入口直接提交订单,不经过 M03 购物车;成功后沿用 M04 订单、M05 支付、M06-02 履约、M09 消息与 M10 售后流程。 -数据库事务是秒杀正确性的唯一事实来源。Redis、消息队列、Nginx 限流和前端防抖只承担性能与体验,不得用作并发正确性边界。本文按“先确认业务参与者和原子结果,再确定流程步骤和不可变核心事实”的顺序编写;A22x(A220 创建活动、A221 更新活动、A222 发布活动、A223 取消活动、A224 商家活动列表、A225 商家活动详情、A226 买家活动列表、A227 买家活动详情、A228 秒杀下单;项目约定的 C01 接口范围为 A220~A228)只用于流程完成后的契约映射与缺口检查,不能反向拼接流程。库存回补、抢购结果查询与库存划拨不占用 A22x;其中库存回补走 M04 取消事务通过 Ordering 公开应用契约回写到 `seckill_inventory`,抢购结果由 Ordering 的 A302/A303 承担,发布划拨走 A222 在同一事务内落 `seckill_inventory.activated_at`。 +数据库事务与条件更新是秒杀正确性的唯一事实来源。缓存、队列、限流、前端防抖和按钮置灰只承担性能与体验保护,不得单独判定抢购成功。本文先确认活动生命周期、库存归属、原子成功结果、失败回退与模块出入口;接口编号只在第九章作为下游映射,表、字段、索引和约束在流程确认后由数据库设计统一派生。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| -| C01 需求 | 完整定义 | 作为业务语义事实源 | -| F04/F06/F08/F10/F09/F12 核心流程 | 已校准基线 | 作为秒杀接入点与回归目标 | -| C03 超时取消 | 部分定义(韦乾强负责) | 仅引用其回补通道 | -| C08 异步回调 | 待细化(张海洋负责) | 仅引用其对秒杀订单的幂等规则 | -| C10 高可用 | 待细化(罗皓晨负责) | 仅引用其实例分发的最终一致性 | -| C07 缓存 | 待细化(罗皓晨、顾欣月负责) | 仅引用其对活动列表的失效策略 | -| A22x 接口 | 部分定义、未冻结 | 由流程派生并做映射 | -| 秒杀相关表(活动、库存、限购配额、幂等记录) | 模板/占位 | 本文不发明表名、字段、约束和索引 | -| X04 售后退款 | 独立扩展 | 仅登记边界,不卷入秒杀取消回补 | - -## 二、扩展直接出入口 +| C01 需求与教师 C01 验收项 | 完整定义 | 作为业务语义和硬指标事实源 | +| 本文业务流程 | 已校准、待交叉评审 | 明确状态、动作、原子结果、异常与模块边界 | +| M04/M05/M06-02/M09/M10 核心流程 | 部分已定义 | 复用其公开业务出入口,不建立第二套订单链路 | +| C03/C07/C08/C10 挑战流程 | 部分定义 | 仅登记与秒杀直接相交的责任 | +| A220~A228 接口 | 部分定义、未冻结 | 待按本文第九章重新派生和补齐 | +| 秒杀相关数据设计 | 模板/占位 | 待全部流程完成后从零统一设计 | +| X04 售后退款 | 独立扩展 | 不并入“待支付订单取消回补”流程 | + +## 二、参与者与模块直接出入口 ```mermaid flowchart LR - ID["M01 Identity
已认证买家、角色、账号状态"] -->|"BuyerOnly 通过"| SEC["C01 Seckill
活动、秒杀库存、个人限购"] - MERCHANT["M06-01 商家运营"] -->|"创建 / 发布 / 取消 / 状态推进"| SEC - PUBLIC["买家秒杀入口 / 活动列表 / 商品详情"] -->|"活动浏览、倒计时、立即抢购"| SEC - SEC -->|"活动基础信息、主图、秒杀价、原价、倒计时、剩余库存和已售数量"| PUBLIC - - SEC -->|"通过 Ordering 公开应用契约
写入共享 orders / order_items
带 orderType=Seckill、activityId、秒杀价快照、限购配额占用"| ORD["M04 Ordering"] - ORD -->|"PendingPayment 订单"| PAY["M05 Payment"] - ORD -->|"买家主动或 C03 超时取消 → 回补秒杀库存并释放限购名额"| SEC - SEC -. "事务提交后" .-> MSG["M09 消息持久化 / 通知"] - - ID -->|"游客、商家或账号禁用"| X["拒绝抢购,不创建独立秒杀订单"] - ORD -->|"支付回调回写、售后退款"| Y["按 M04/M05/M10 公开契约处理,不允许直接改写秒杀库存"] - C08["C08 异步回调"] -. "作用于秒杀订单的幂等回写" .-> ORD - - RATE["入口侧限流"] -->|"超过承载阈值时快速失败"| SEC - CA["C07 缓存"] -->|"活动列表、商品基础信息读取"| PUBLIC - SEC -. "库存事实" .-> CA + MERCHANT["已认证且账号正常的商家"] -->|"维护本人有权管理商品的秒杀活动"| SEC["C01 秒杀
活动生命周期、独立库存、个人限购"] + PUBLIC["游客 / 买家"] -->|"浏览即将开始或进行中的活动"| SEC + BUYER["已认证且账号正常的买家"] -->|"立即抢购:活动、数量、收货地址、稳定请求标识"| SEC + CAT["M02 Catalog
商品归属、销售状态、普通可售库存与当前价格"] -->|"创建、发布前校验与库存划拨输入"| SEC + + SEC -->|"权威活动状态、倒计时、剩余库存、已售数量"| PUBLIC + SEC -->|"原子成功:秒杀库存扣减、限购占用、共享订单与价格快照"| ORD["M04 Ordering"] + ORD -->|"待支付订单"| PAY["M05 Payment"] + ORD -->|"买家取消或 C03 超时取消"| BACK["按原秒杀通道回补并释放限购"] + BACK --> SEC + + ORD -->|"支付后订单"| FUL["M06-02 履约"] + ORD -->|"可靠订单事实"| MSG["M09 消息"] + ORD -->|"已支付后的退款 / 退货"| AFTER["M10 售后"] + + SEC -->|"身份、归属、活动、时间、库存或限购不满足"| REJECT["拒绝且不产生任何业务副作用"] ``` 边界约束: -- C01 不与 M03 购物车模块直连;秒杀订单通过 M04 Ordering 公开应用契约写入共享 `orders / order_items`,不建立独立的秒杀订单状态机。 -- 秒杀库存只走数据库条件更新;Redis、队列、客户端状态都不能单独承担正确性。 -- 秒杀活动取消或结束后,已存在订单按 M04/M05/M09 流程继续走完;取消与回补必须落到秒杀原通道,不污染普通库存。 -- M03 购物车不参与秒杀:秒杀成功不写购物车,秒杀回补也不联动购物车;普通加购不读秒杀库存。 +- 游客可以浏览公开活动,但抢购必须使用已认证且状态正常的买家身份;商家和管理员不能以其当前角色参与抢购。 +- 商家只能维护本人有权管理商品的活动,也只能查看本人活动的经营结果;越权时按不存在 / 无权限拒绝,不泄露他人活动。 +- 秒杀订单复用 M04 的共享订单状态机,不建立独立订单表或独立支付、发货、消息通道。 +- 秒杀库存从普通可售库存中一次性划拨后成为独立通道;普通下单不能消耗它,取消回补也不能写回普通库存。 +- C01 不读写购物车;秒杀成功、取消或回补均不改变 M03 条目。 + +## 三、活动维护、发布与生命周期 -## 三、活动维护、状态推进与倒计时 +### 3.1 创建、编辑与发布 ```mermaid flowchart TD - A["商家在 M06-01 进入秒杀活动维护"] --> B{"新建还是编辑?"} - B -- "新建" --> C["绑定已上架商品、设定开始时间、结束时间、秒杀价、库存总量和单用户限购"] - C --> D{"开始 ≥ 当前 UTC、结束 > 开始、库存 ≤ 商品当前可售库存且为正整数?"} - D -- "否" --> X["拒绝保存并指出缺失项"] - D -- "是" --> E["保存为草稿,状态 Draft,并在 seckill_inventory 写入计划配额行:remainingStock=totalStock、soldCount=0、frozenCount=0、activatedAt=NULL"] - B -- "编辑" --> EE{"活动状态?"} - EE -- "Draft / 尚未开始的 Published" --> FF["允许修改名称 / 价格 / 时间 / 限购等可调字段;totalStock 仅在 Draft 可调"] - EE -- "Ongoing / Ended / Cancelled" --> X["拒绝编辑(已锁定)"] - E --> F{"手动发布?"} - F -- "否" --> EE1["保持 Draft"] - F -- "是" --> G1["开启发布事务,先按 totalStock 条件扣减普通商品可售库存"] - G1 --> G2{"商品当前可售库存 ≥ 秒杀库存总量?"} - G2 -- "否" --> X2["拒绝发布,回滚事务,并指出普通库存不足"] - G2 -- "是" --> G3["按 activityId 条件激活已存在的 seckill_inventory 计划配额行:activatedAt=now(),并按数据库 now() 把活动从 Draft 条件推进为 Published 或 Ongoing"] - G3 --> G4{"发布事务整体提交?"} - G4 -- "否" --> X3["整体回滚:普通库存未被扣减、秒杀计划配额保持未激活、活动保持 Draft"] - G4 -- "是" --> G["状态写为 Published 或 Ongoing,并由数据库 UTC now() 后续推进到 Ended"] - G --> H["活动详情可在卖家与买家入口查询"] - H --> I["剩余库存售罄时立即标记 Sold Out 并禁用抢购入口"] + A["商家进入本人秒杀活动管理"] --> B{"身份正常且有权管理目标商品?"} + B -- "否" --> X["拒绝,不创建或修改活动"] + B -- "是" --> C{"新建还是编辑草稿?"} + C -- "新建" --> D["选择已上架商品,填写开始/结束时间、秒杀价、计划秒杀量和单用户限购"] + C -- "编辑" --> E{"活动仍为 Draft?"} + E -- "否" --> Y["拒绝编辑;发布后只允许取消"] + E -- "是" --> D + D --> F{"开始时间不早于权威当前时间、结束晚于开始、价格为正、计划量与限购为正整数,且计划量不超过当前普通可售库存?"} + F -- "否" --> Z["指出具体问题,保持原状态"] + F -- "是" --> G["保存 Draft 与计划秒杀量;此时不扣普通库存、不产生可抢库存"] + G --> H{"商家确认发布?"} + H -- "否" --> I["保持 Draft,可继续编辑或取消"] + H -- "是" --> J["重新读取商品归属、销售状态、当前普通可售库存和权威时间"] + J --> K{"仍属于本人、仍可售、尚未到开始时间且普通库存足够?"} + K -- "否" --> L["拒绝发布,草稿与普通库存均保持原状"] + K -- "是" --> M["在一个原子结果中把计划量从普通库存划入独立秒杀库存,并推进为 Published"] + M --> N{"原子结果整体成功?"} + N -- "否" --> L + N -- "是" --> O["活动公开为即将开始;重复同一发布请求不重复划拨"] ``` -状态机: +业务规则: + +- 草稿只记录“计划秒杀量”,不代表库存已分配,也不能向买家暴露为可抢库存。 +- 保存草稿时即按权威当前时间和普通可售库存检查输入,但这只是“计划量可行性”校验;发布时必须再次读取并校验,不能用草稿保存时的旧库存代替发布判断。 +- 发布是库存归属切换点:必须重新校验商品仍归该商家管理、仍可售、开始时间尚未来到且普通库存足够;划拨与状态推进必须同时成功或同时失败。 +- 发布后不再允许修改商品、价格、时间、秒杀量或限购,避免已公开规则与库存事实发生漂移;商家需要变更时应取消原活动并新建草稿。 +- 同一活动只能成功划拨一次;网络重试或重复点击发布不得再次减少普通库存。 +- 秒杀价必须为正数;计划秒杀量和单用户限购必须为正整数,且单用户限购不得超过计划秒杀量。 + +### 3.2 状态推进、取消与剩余库存归宿 ```mermaid stateDiagram-v2 - [*] --> Draft: 商家保存 - Draft --> Published: 商家主动发布 + [*] --> Draft: 商家保存草稿 + Draft --> Published: 商家发布且库存划拨成功 Draft --> Cancelled: 商家取消 - Published --> Running: 数据库 UTC now() ≥ 开始时间且 < 结束时间 + Published --> Ongoing: 权威 UTC 时间到达开始时间 Published --> Cancelled: 商家取消 - Running --> Ended: 数据库 UTC now() ≥ 结束时间 - Running --> Cancelled: 商家取消 + Ongoing --> Ended: 权威 UTC 时间到达结束时间 + Ongoing --> Cancelled: 商家取消 Ended --> [*] Cancelled --> [*] - Published --> Published: 重复发布幂等返回 ``` -关键约束: +- 业务状态统一为 `Draft`(草稿)、`Published`(已发布)、`Ongoing`(进行中)、`Ended`(已结束)、`Cancelled`(已取消);“已售罄”仅由剩余库存为 0 派生,不是第六种活动状态。 +- 状态推进以数据库权威 UTC 时间为准。`Published` 在开始时间到达后转为 `Ongoing`,`Ongoing` 在结束时间到达后转为 `Ended`;抢购时仍必须再次同时校验状态与时间窗口。 +- `Draft`、`Published`、`Ongoing` 可以取消;`Ended` 或 `Cancelled` 再次取消必须拒绝。取消后不再接受新抢购,已有订单继续走订单状态机。 +- 进行中取消与并发抢购通过同一权威活动状态竞争:取消先提交时后续抢购条件不再命中;抢购先整体提交时该订单属于“已有订单”,随后取消活动不撤销它。 +- 草稿取消时没有库存划拨。已发布或进行中的活动取消后,未售库存仍隔离保留在原活动,不回到普通库存。 +- 活动自然结束后剩余库存,以及结束后因待支付订单取消而回补的库存,仍保留在原活动并停止销售,不自动回到普通库存;本期不增加二次处置流程。 -- **草稿即落计划配额**:商家保存草稿时,必须在同一数据库事务内按 `seckill_inventory (activity_id)` 唯一行写入计划配额 `remainingStock=totalStock`、`soldCount=0`、`frozenCount=0`、`activatedAt=NULL`,为后续 A222 发布提供唯一的事实锚点;不允许草稿与发布两个阶段重复创建秒杀库存行。 -- **发布即原子激活与划拨**:商家将活动由 Draft 推进为 Published(或在开始时间 ≤ 数据库 `now()` 时直接为 Ongoing)时,必须在同一数据库事务内按 `activityId` 把等量普通商品可售库存条件扣减,再按同一 `activityId` 把计划配额的 `activatedAt` 写为数据库 `now()`;不允许在发布阶段重新插入秒杀库存行。事务失败整笔回滚,普通库存和秒杀计划配额均不留半改;只有事务整体提交后才能把活动状态推进为 Published 或 Ongoing。 -- **库存通道互不混淆**:普通下单只能扣减普通库存,秒杀下单只能扣减秒杀库存;发布时一次性划拨后,活动期间普通下单不会消耗已被划走的那部分库存。 -- 活动状态字段由数据库维护并参与所有业务校验;商家只能在 Draft 或尚未开始的 Published 时执行编辑,运行中 / 结束 / 已取消一律拒绝;取消动作只对 Draft / Published / Ongoing 生效,Ended / Cancelled 拒绝重复状态变更。 -- 状态推进在数据库侧以 UTC `now()` 为权威,避免应用实例时钟漂移造成提早或延后成功。 -- 活动取消后已存在订单继续走完;未提交请求直接拒绝;本期不回收已分配秒杀库存,避免被普通订单夹带走量。 - -## 四、活动浏览与库存倒计时展示 +## 四、公开浏览、倒计时与售罄展示 ```mermaid flowchart TD - A["买家进入秒杀入口 / 商品详情秒杀 Banner"] --> B["服务端读取活动当前状态、剩余库存和已售数量"] - B --> C{"活动状态?"} - C -- "Draft" --> X1["入口隐藏,普通商品详情展示"] - C -- "Published" --> X2["展示开始倒计时,按钮置灰"] - C -- "Cancelled" --> X3["入口隐藏,普通商品详情展示"] - C -- "Ended" --> X4["入口隐藏,普通商品详情展示"] - C -- "Running" --> D{"剩余可售库存?"} - D -- "= 0" --> X5["入口标记已售罄,按钮置灰"] - D -- "> 0" --> E["倒计时至结束时间,按钮可点击"] - E --> F["页面仅展示当前轮询/缓存视图"] - F --> G["提交时以数据库条件更新原子扣减为准:竞争失败按已售罄或状态冲突返回"] + A["游客或买家进入秒杀入口 / 商品详情"] --> B["读取公开活动、商品基础信息与权威库存事实"] + B --> C{"活动当前状态?"} + C -- "Draft / Cancelled / Ended" --> X["不出现在可抢入口,回到普通商品浏览"] + C -- "Published" --> D["展示开始倒计时;抢购按钮禁用"] + C -- "Ongoing" --> E{"权威剩余库存是否为 0?"} + E -- "是" --> F["立即展示已售罄并禁用抢购入口"] + E -- "否" --> G["展示结束倒计时、剩余库存、已售数量;允许买家发起抢购"] + D --> H["按权威时间和最新活动结果刷新"] + F --> H + G --> H ``` -关键约束: +展示一致性规则: -- 倒计时统一以数据库 UTC 时间计算;同 / 跨实例用户看到一致的剩余库存和倒计时。 -- 页面库存仅作提示,不作为最终抢锁事实:高并发秒杀下页面视图天然可能短暂落后;最终结果必须以提交时数据库条件更新为准。 -- 库存与限购的正确性边界只由数据库条件更新承担,不依赖页面显示与 Redis。竞争失败按 `SOLD_OUT` / `PER_BUYER_LIMIT_EXCEEDED` / 状态冲突返回,不允许产生超卖或重复订单。 -- 活动列表与商品基础信息读取可经 C07 缓存,但秒杀库存本身不进入缓存;C07 失效时降级为实时读 + 短 TTL,售罄状态以数据库为准重新加载活动详情。 -- 活动详情暴露的剩余库存只能来源于发布时落地的事务事实;发布事务未提交的草稿不在抢购入口暴露可售数。 +- 公开列表和详情必须返回活动、商品、主图、秒杀价、原价、开始 / 结束时间、权威剩余库存与已售数量;不能只返回“是否售罄”替代数量。 +- 倒计时使用服务端给出的权威 UTC 时间口径。客户端本地计时只负责平滑展示,每次刷新都以服务端结果校正。 +- 商品名称、主图等静态信息可以缓存;剩余库存、已售数量和“已售罄”派生结果不得使用脱离数据库事实的缓存值。 +- 轮询或推送每次只能发布刚从权威库存事实得到的结果;前端按结果先后顺序应用更新,旧结果不得覆盖新结果。 +- 每次抢购完成或失败后,响应都要带回本次处理后的权威活动结果,页面在同一交互中更新。并发请求抢走最后库存时,未抢到的买家应立即看到剩余为 0 和“已售罄”,不能继续显示可抢。 +- 无法取得权威库存时,页面展示“正在刷新”并暂时禁用提交,不得猜测为“有库存”或“已售罄”。 -## 五、立即抢购主流程 +## 五、立即抢购与原子成功结果 ```mermaid flowchart TD - A["买家点击立即抢购,携带商品 ID、活动 ID、数量、幂等键"] --> B["服务端解析登录身份(JWT + role=buyer)"] - B --> C{"身份合法?"} - C -- "否" --> X["登录失效,引导登录"] - C -- "是" --> D{"活动存在且状态为 Running?"} - D -- "否" --> Y["按不存在 / 已结束 / 未开始返回明确原因"] - D -- "是" --> E{"当前 UTC 时间落在开始和结束之间?"} - E -- "否" --> Y - E -- "是" --> F{"数量为正整数?"} - F -- "否" --> Y["数量非法"] - F -- "是" --> G{"同 (买家+活动+幂等键) 已有处理结果?"} - G -- "同键同请求" --> P0["返回首次确定结果,不重复扣减库存"] - G -- "同键不同请求" --> Z["拒绝标识被不同请求复用"] - G -- "否" --> H{"活动剩余可售库存 ≥ 请求数量?"} - H -- "否" --> Z1["已售罄"] - H -- "是" --> I{"单用户当前限购内?"} - I -- "否" --> Z2["超过单用户限购"] - I -- "是" --> J["开启秒杀事务"] - J --> K["条件扣减秒杀库存,联合校验活动状态、窗口、活动 ID 与剩余可售量,期望条件命中一次"] - K --> K1{"条件是否命中?"} - K1 -- "否" --> ZR["并发竞争失败:按已售罄或状态竞争失败返回"] - K1 -- "是" --> L["原子占用 (activity_id, buyer_id) 限购配额行"] - L --> M["通过 Ordering 公开应用契约写入共享 orders / order_items,带 orderType=Seckill、activityId、秒杀价快照"] - M --> N["写入待发布订单创建事实(OrderCreatedIntegrationEvent)"] - N --> O{"秒杀事务整体提交?"} - O -- "否" --> ZR["整体回滚:库存未扣减、限购未占用、订单未生成、待发布事实未写入"] - O -- "是" --> P["返回订单号、活动 ID、剩余库存与购买结果"] - P --> Q["跳转 M04 后续支付;M05/C03/M06-02/M09/M10 按既有流程继续"] + A["买家提交活动、正整数数量、本人有效收货地址和稳定请求标识"] --> B{"身份与固定输入有效?"} + B -- "否" --> X["拒绝,不进入库存与订单处理"] + B -- "是" --> C{"该买家、活动与稳定标识是否已有结果?"} + C -- "同标识同请求" --> R["重放首次确定结果,不重复扣减或下单"] + C -- "同标识不同请求" --> Y["拒绝复用标识,不产生副作用"] + C -- "全新请求" --> D{"当前流量是否在可承载上限内?"} + D -- "否" --> Z["形成过载的确定业务结果;普通商品入口继续可用"] + D -- "是" --> E["读取活动、商品、买家当前限购占用和地址归属"] + E --> F{"活动为 Ongoing、权威时间在窗口内、商品匹配、地址归本人且请求未超限?"} + F -- "否" --> G["形成未开始 / 已结束 / 已取消 / 超限 / 地址无效等确定业务结果"] + F -- "是" --> H["开启短事务"] + H --> I["以活动、状态、时间窗口和剩余量为条件原子扣减独立秒杀库存"] + I --> J{"扣减是否成功?"} + J -- "否" --> K["回滚主事务,形成售罄 / 状态竞争等确定业务结果"] + J -- "是" --> L["原子增加当前买家的活动限购占用,且不得超过上限"] + L --> M{"限购占用是否成功?"} + M -- "否" --> K + M -- "是" --> N["创建共享待支付订单与订单项快照,记录秒杀来源、活动和成交价"] + N --> O["可靠记录订单已创建事实,并绑定本次稳定请求结果"] + O --> P{"库存、限购、订单、快照、可靠事实与请求结果是否整体提交?"} + P -- "否" --> T["整体回滚;属于未形成确定结果的瞬态失败,原标识可重试"] + P -- "是" --> Q["返回同一订单号、购买结果和提交后的权威剩余库存,进入 M05 支付"] + Z --> W["先把确定业务结果与稳定请求标识持久绑定"] + G --> W + K --> W + W --> U{"结果绑定是否成功?"} + U -- "是" --> V["返回已绑定的首次结果;以后同标识直接重放"] + U -- "否" --> T ``` 不可变核心事实: -- 抢购条件扣减使用一条带状态/窗口/活动 ID/剩余可售量联合判定的数据库更新;只有当前请求数量、买家限购与状态版本完全命中可售条件时才算扣减成功,未命中条件时立即拒绝并按既定失败分支返回。 -- 事务短小:秒杀事务只覆盖秒杀库存行、限购配额占用、订单写入和必要快照;事务内禁止远程调用、等待用户输入或长计算。 -- 锁粒度按活动 ID 单行:抢购事务只对单个活动的库存和订单写入加锁,不全表扫描。 +- 秒杀库存扣减必须使用数据库条件更新,一次同时约束目标活动、`Ongoing` 状态、权威时间窗口与剩余量;未命中就失败,禁止在应用层“先读取、后递减”。 +- 秒杀库存扣减、买家限购占用、共享订单、订单项快照、可靠订单事实和幂等结果属于同一个原子成功结果;任一步失败全部回滚,不产生支付前置记录。 +- 完成身份与固定输入校验后,必须先查询稳定请求结果,再进入限流、时间、库存和限购判断。同标识同请求重放首次确定结果,不得因当前活动、库存、限购或流量变化重新裁决。 +- 成功、未开始、已结束、已取消、售罄、超限、地址无效和已正式返回的过载结果都属于可重放的确定业务结果,必须先与稳定请求标识持久绑定再返回。数据库连接中断、事务提交失败等无法确认结果的瞬态故障不得伪装成确定业务失败,也不得固化;客户端使用原标识重试。 +- 买家限购以“同一活动下当前有效占用量”作为唯一并发事实;待支付与已支付订单都占用名额,只有取消成功才释放。 +- 事务保持短小,只处理单个活动和本次订单;事务内不调用外部 HTTP、不等待用户输入、不发送即时消息、不做长计算或全表扫描。 +- 秒杀价、商品归属、订单金额和快照都由服务端重读并计算;客户端价格只能用于展示,不能决定成交金额。 +- 请求充足且没有身份、时间、限购等业务失败时,系统必须持续接受可处理请求直至库存售罄;限流配置不得导致库存仍有剩余却提前停止销售。 -## 六、取消、回补与限购名额释放 +## 六、待支付订单取消、回补与限购释放 ```mermaid flowchart TD - A["买家主动取消或 C03 系统定时任务触发取消"] --> B{"订单 orderType = Seckill 且存在 activityId?"} - B -- "否" --> X["走 M04 普通取消通道,不进入秒杀回补"] - B -- "是" --> C["开启取消事务"] - C --> D["条件推进 PendingPayment → Cancelled,影响订单状态"] - D --> E{"订单状态条件更新成功?"} - E -- "否" --> Y["返回失败,已支付或状态竞争"] - E -- "是" --> F["按订单项 (活动 ID, 数量) 联合条件回补秒杀库存剩余可售量并核减已售数,期望条件命中一次"] - F --> G["释放 (activity_id, buyer_id) 限购配额:已用数量 -= qty,影响行数 ≥ 1"] - G --> H["记录取消时间和待发布订单取消事实"] - H --> I{"取消事务整体提交?"} - I -- "否" --> Z["整体回滚,允许定时任务或买家请求安全重试"] - I -- "是" --> J["C01 直接输出:秒杀库存回补、限购释放、订单 Cancelled"] - J --> K["M04 通知买家与商家;消息消费由 M09 按既有流程完成"] -``` - -关键约束: - -- 秒杀取消回补必须落到原活动的秒杀可售库存;不得回补到普通商品库存。 -- 同一笔订单的重复取消请求只能回补一次:以订单状态条件推进为唯一幂等保障(同一订单的多次取消只能产生一次回补)。 -- 买家主动取消、C03 超时取消的库存回补在同一事务内完成;任何一步失败整体回滚,不产生“库存已回补但订单仍为 PendingPayment”的部分结果。 -- 已 `Paid` 订单不走取消回补;后续退款 / 退货按 M10 售后流程处理。 -- 秒杀库存回补以联合条件更新回写到剩余可售量并核减已售数;活动结束后回补仍允许,只是不再允许新抢购。 - -## 七、与其他挑战模块的衔接 - -```mermaid -flowchart LR - A["C01 秒杀事务提交"] -->|"OrderCreatedIntegrationEvent"| B["M09 Outbox / 站内消息"] - A -->|"PendingPayment 订单"| C["C03 30 分钟超时检查"] - C -->|"超时取消"| D["M04 取消事务 → C01 回补通道"] - A -->|"支付成功后回调"| E["C08 异步回调幂等"] - A -->|"任一 API 实例受理"| F["C10 双实例分发"] - A -. "活动列表 / 商品基础信息" .-> G["C07 Cache-Aside"] + A["买家主动取消或 C03 触发超时取消"] --> B["M04 读取订单归属、当前状态和原库存来源"] + B --> C{"属于当前买家或合法系统任务,且订单为待支付秒杀订单?"} + C -- "否" --> X["按普通订单 / 已支付 / 越权等对应分支处理,不进入秒杀回补"] + C -- "是" --> D["开启取消事务"] + D --> E["把订单从待支付原子推进为已取消"] + E --> F{"本次是否首次成功推进?"} + F -- "否" --> Y["不回补、不重复释放;返回已确定状态"] + F -- "是" --> G["按订单原活动与数量回补独立秒杀库存并减少已售数量"] + G --> H["按同一数量释放该买家的活动限购占用"] + H --> I["可靠记录订单已取消事实"] + I --> J{"状态、库存、限购与可靠事实是否整体提交?"} + J -- "否" --> K["全部回滚,可由原请求安全重试"] + J -- "是" --> L["返回订单已取消及回补完成结果"] ``` -- C08:支付回调在 M05 上做幂等,作用覆盖秒杀订单;C01 不暴露独立的支付通道。 -- C10:秒杀入口由任一实例受理,最终一致性必须由数据库条件更新承担;不能由实例本地状态决定成败。 -- C07:活动列表、商品基础信息可缓存,必须按已定义的失效策略更新;秒杀库存与状态字段不进入缓存。 - -## 八、由流程派生的接口契约映射 +- 同一订单只有第一次从待支付推进为已取消的请求可以回补库存;重复调用、并发调用或定时任务重试都不得再次回补。 +- 回补必须回到原活动的独立秒杀库存,绝不能增加普通商品可售库存。 +- 订单状态变更、秒杀库存回补、已售数量调整、限购释放和可靠取消事实必须同一事务提交。 +- 活动已经 `Ended` 或 `Cancelled` 时仍允许为合法的待支付订单执行取消回补,但回补后的库存继续留在原活动且不可再次购买。 +- 已支付订单不进入本流程;退款 / 退货由 M10 处理,是否产生其他库存动作由售后流程决定,不能复用待支付取消回补。 -本节是第三至第七章业务流程的下游映射,不是流程输入。先确认“业务动作、当前状态、成功或失败后得到什么结果”,再决定由哪个接口承载。若现有 Axxx 与流程冲突,先记录接口缺口并修改接口设计;只有业务需求本身变化时,才回到前文重新评审流程。 +## 七、失败、流量保护与可观察性 -| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +| 场景 | 处理结果 | 不得发生 | +|---|---|---| +| 游客、商家、管理员提交抢购 | 拒绝并引导使用合法买家身份 | 创建订单或占用库存 | +| 活动未开始、已结束或已取消 | 返回明确状态,权威库存不变 | 仅靠前端按钮拦截 | +| 库存竞争失败 | 返回本次权威剩余库存;为 0 时立即售罄 | 负库存、孤立订单 | +| 单用户超限 | 原子拒绝并返回当前可购结果 | 先创建订单后补查限购 | +| 同标识同请求重试 | 重放首次结果 | 再次扣减或创建订单 | +| 同标识不同请求 | 拒绝标识复用 | 覆盖首次结果 | +| 数据库或连接池不可用 | 快速失败并允许携带原标识重试 | 长时间占用普通接口资源 | +| 事务任一步失败 | 整体回滚 | 半扣库存、半占限购或半订单 | +| 重复取消 | 返回既有订单状态 | 二次回补或二次释放 | + +流量与日志规则: + +- 秒杀入口与普通商品查询使用可独立配置的承载上限;超过上限的秒杀请求快速失败,不能拖垮普通列表与详情。 +- 队列可以削峰或传递通知,但“进入队列”不等于抢购成功;缓存可以服务静态活动信息,但不能决定库存与限购。 +- 每个秒杀提交都记录买家、活动、商品、请求数量、结果类别、数据库影响结果、成功订单号和链路标识;不得记录 Token、密码或支付卡号。 +- 对成功、售罄、超限、重复、未开始、已结束、已取消、过载和系统失败分别统计,压测报告记录吞吐与 P50/P95/P99。 + +## 八、跨模块衔接 + +- **M02 Catalog / M06-01 商家运营**:提供商品归属、销售状态、普通库存和当前价格;发布时完成普通库存到秒杀库存的原子划拨,之后两个通道互不混用。 +- **M03 Cart**:秒杀立即抢购绕过购物车,成功、失败、取消和回补均不读写购物车条目。 +- **M04 Ordering**:接收 C01 的原子下单要求并生成共享订单与快照;订单必须保留秒杀来源、活动和成交价,供查询、取消与追溯。 +- **M05 Payment / C08 回调**:秒杀订单沿用统一支付和幂等回写;支付成功与取消竞争由订单状态条件推进裁决。 +- **C03 超时取消**:只触发 M04 公开取消入口,由订单来源决定回补普通库存还是原秒杀库存,不得直接改写 C01 数据。 +- **M06-02 履约、M09 消息、M10 售后**:分别沿用发货、可靠消息和售后流程,不为秒杀建立第二套通道。 +- **C07 缓存**:可缓存商品与活动静态信息;权威剩余库存、已售数量和售罄结果不从缓存判定。 +- **C10 多实例**:任一实例都可受理秒杀请求,最终结果只由共享数据库事务决定,不能依赖进程内状态。 + +## 九、由流程派生的接口契约映射 + +本节是第三至第八章的下游映射,不是流程输入。接口设计应逐项承接已确认的业务动作、状态前置条件、原子成功结果和失败结果;若现有 Axxx 与本文冲突,修改接口,不回头按旧接口改流程。 + +| 已确认流程能力 | 派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 商家创建秒杀活动(含草稿即落计划配额) | A220 | 校验时间 / 价格 / 库存配额并写入草稿,事务内同时在 `seckill_inventory` 写入 `activatedAt=NULL` 的计划配额行 | 待交叉评审 | -| 商家更新秒杀活动(仅 Draft / 尚未开始) | A221 | 仅在 Draft 或尚未开始的 Published 状态允许更新名称 / 价格 / 时间 / 限购;Ongoing / Ended / Cancelled 返回 `SECKILL.INVALID_STATUS` | 待交叉评审 | -| 商家发布秒杀活动(原子划拨与激活) | A222 | 同一事务内按 `activityId` 条件扣减普通商品可售库存并把已存在计划配额的 `activatedAt` 写为 now();不存在 → 回滚并返回 `SECKILL.INVENTORY_NOT_FOUND` | 待交叉评审 | -| 商家取消秒杀活动(保留已分配库存) | A223 | 条件 `status IN ('Draft','Published','Ongoing') → 'Cancelled'`,回滚只下架入口不回收已分配库存 | 待交叉评审 | -| 商家秒杀活动列表 | A224 | 仅返回 `created_by_merchant_user_id = current_user_id` 的活动,支持按状态 / 时间 / 关键词筛选与稳定排序 | 待交叉评审 | -| 商家秒杀活动详情 | A225 | 返回个人活动详情与订单统计;非创建人返回 404 不泄露存在性 | 待交叉评审 | -| 买家秒杀活动列表 | A226 | 仅返回 `status IN ('Published','Ongoing')` 的活动;列表中 `remainingStock` 仅返回 `isSoldOut` 布尔 | 待交叉评审 | -| 买家秒杀活动详情 | A227 | 仅返回已发布 / 进行中活动详情;可附 `currentBuyerOrderCount`、`currentBuyerRemaining` 限购提示 | 待交叉评审 | -| 秒杀下单(条件扣减 + 限购占用) | A228 | 按 `Idempotency-Key` 重放首配,事务内条件扣减 `seckill_inventory` 剩余可售、`(activity_id, buyer_id)` 唯一配额占用并调用 Ordering 公开契约创建共享订单 | 待交叉评审 | -| 秒杀库存回补 | 不分配 Axxx | 由 M04 取消事务或 C03 超时取消触发,按 `(activityId, sku?)` 联合条件回补并释放限购名额,结果一致即可见 | 由 M04 / C03 公共契约承载 | -| 抢购结果查询 | 不分配 Axxx | 走 Ordering 公开契约 A302 / A303,按 `buyer_id` 与订单归属鉴权 | 由 Ordering 公共契约承载 | - -接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码与 OpenAPI、ProblemDetails 不得反向写入业务图;接口设计 1.12 通用幂等规则与 4.6 资金类幂等约束同样适用于秒杀订单。 - -## 九、扩展接入边界 - -- 不修改 F01~F13 核心订单状态机;PendingPayment → Paid / Cancelled 的两路竞争通过数据库条件更新自然处理,秒杀不引入额外状态。 -- 不预留额外支付通道:秒杀订单沿用 M05 模拟支付;不引入邀请码 / 概率中奖 / 限购用户分组 / 多 SKU 组合 / 跨活动互斥等本期不实现的扩展。 -- 不替代 C07 缓存策略:秒杀列表和商品基础信息走 C07 缓存;秒杀库存与状态字段不进入缓存。 -- 不改变 M03 购物车:M03 仅承载普通加购;C01 立即抢购不写购物车,取消也不联动购物车。 -- 与 X04 售后退款的衔接:已支付秒杀订单的售后按 M10 处理;本文仅记录 C01 不参与退款库存通道,防止污染普通库存。 -- 双实例 / 负载均衡:C10 接管的秒杀入口由任一实例受理;条件更新和限购配额占用的数据库事实保证最终一致性。 - -## 十、由流程反查出的接口与数据待评审项 - -1. 抢购接口 A222 的最终扣减库存与价格必须走服务端重读:客户端不得指定秒杀价或库存;当前 A222 草案若允许 `expectedAmount / expectedQty` 参与业务判定,需要明确“仅作为客户端旧值冲突保护”,不得成为扣减事实。 -2. 单用户限购以 `(activity_id, buyer_id)` 唯一配额事实为准;A222 必须先占用配额再进入主流程;同 Key 同请求重放首配结果,不得再次增加限额。 -3. 库存语义:`remaining + sold + frozen = 初始总量`;本期不启用 `frozen`,所有提交要么直接成功,要么立即失败;若后续启用 `frozen`,A222 需要回看本文第五节并保留 6.6 异常分支。 -4. C08 异步回调:若 C08 替换 F10 的部分支付确认步骤,必须先在根文档决定其接入 F10 的哪个同步步骤;不能让同步支付和异步回调同时成为最终支付事实。 -5. C03 超时取消:必须在 M04 完成条件推进 `PendingPayment → Cancelled` 后进入秒杀回补通道;不能绕过 M04 直接改写秒杀库存。 -6. 活动取消运营动作:A224 的“取消”动作只对 Draft / Published / Running 生效;取消后已存在订单继续按既有流程走完,本期不回收已分配库存,避免与普通订单混淆。 -7. 限流分级:A222 与 A221 / M03 的 A2xx 应分桶限流,秒杀高并发不得拖垮普通商品查询;入口侧任意一层(网关、API、应用、连接池)触发后都能快速失败。 -8. 时间口径:服务端使用 UTC 写入;状态推进与活动判断以数据库 `now()` 为权威;应用节点间的时钟轻微漂移不影响业务结果。 -9. 数据隔离:A221~A226 全部按 `(买家 ID, 活动 ID)` 或 `(订单 ID, 活动 ID)` 双重过滤;越权返回“不存在 / 无权限”统一错误,不暴露记录是否存在。 -10. 秒杀活动表、库存表、限购配额表、幂等记录表的字段、约束和索引必须由数据库设计任务另行确认;本文不发明表名或字段。 +| 商家创建草稿 | A220 | 校验商品管理权与活动规则,只保存草稿和计划量,不划拨库存 | 待重建详细契约 | +| 商家更新草稿 | A221 | 仅 `Draft` 可修改;发布后拒绝编辑 | 待重建详细契约 | +| 商家发布活动 | A222 | 重新校验并原子划拨普通库存,成功后进入 `Published`;重复请求不重复划拨 | 待重建详细契约 | +| 商家取消活动 | A223 | 仅 `Draft` / `Published` / `Ongoing` 可取消;不回收已划拨库存 | 待重建详细契约 | +| 商家活动列表 | A224 | 仅本人有权管理的活动,支持状态、时间和关键词筛选 | 待重建详细契约 | +| 商家活动详情 | A225 | 返回本人活动、库存与订单汇总;越权不泄露存在性 | 待重建详细契约 | +| 买家公开活动列表 | A226 | 返回 `Published` / `Ongoing` 活动及需求规定的完整展示信息 | 待重建详细契约 | +| 买家公开活动详情 | A227 | 返回权威倒计时、剩余库存、已售数量及当前买家限购提示 | 待重建详细契约 | +| 买家秒杀下单 | A228 | 稳定请求重放、过载保护、条件扣减、限购占用、共享订单与快照原子提交 | 待重建详细契约 | +| 秒杀订单查询 | 复用 A302 / A303 | 按买家归属查询共享订单和秒杀追溯信息 | 由 M04 契约承载 | +| 取消与秒杀回补 | 复用 M04 公开取消契约 | 首次成功取消时按原通道回补并释放限购 | 由 M04 / C03 契约承载 | + +HTTP 路径、方法、请求响应字段、状态码、错误码、幂等标识传递方式和 OpenAPI Schema 均在下一阶段由本表派生;不得把旧草案中的字段或错误码反向写回业务流程。 + +## 十、接口与数据库后续设计必须承接的事实 + +1. 接口必须区分草稿保存、发布划拨、状态取消、公开浏览和立即抢购,不能把多个原子边界拼成一个含糊动作。 +2. 发布契约必须返回“划拨成功且状态已推进”或“全部未发生”中的一种结果;数据库据此保证同一活动最多成功划拨一次。 +3. 抢购契约必须携带活动、正整数数量、本人收货地址和稳定请求标识;成交价、商品归属、限购和库存全部由服务端确定。 +4. 数据库必须表达活动状态、计划量、已划拨总量、剩余量、已售量、每名买家当前占用量、共享订单追溯信息、稳定请求结果和取消是否已回补;具体表名与字段在统一数据库设计中确定。 +5. 活动期间必须满足“剩余量 + 已售量 = 已划拨总量”;本期不引入冻结量。取消成功时剩余量增加、已售量减少,二者仍保持恒等。 +6. 同一活动的买家当前占用量不得超过单用户限购;同一订单最多释放一次,同一稳定请求最多形成一个确定订单结果。 +7. 共享订单必须能区分普通购买与秒杀购买,并能追溯原活动、成交价和原库存通道;取消时不得依赖客户端告诉系统回补到哪里。 +8. 活动结束或取消后的剩余量继续归属原活动且不可售,不自动并回普通库存;数据库设计不能把这部分库存丢失或误计为普通可售。 +9. 公开库存展示必须从权威库存事实派生;接口需提供足够的结果顺序或版本信息,保证旧刷新结果不能覆盖新结果。 +10. 接口与数据库完成后,必须回到本文逐项验证动作、状态、异常和原子结果;若实现成本暴露设计缺口,记录缺口并修正下游设计,不能擅自改变已确认业务语义。 ## 十一、验收证据清单 -- [ ] 发布时原子划拨:商家将秒杀活动 Draft → Published 时,数据库事务内同一次提交完成“普通商品可售库存 -= 秒杀量、秒杀库存初始事实写入”;普通库存不足时整笔拒绝,普通库存和秒杀库存均无半改;活动状态推进到 Published 的唯一条件是事务整体提交。 -- [ ] 100 并发请求抢 10 件库存:成功订单数 = 10,剩余可售 = 0,已售 = 10;其余 90 个请求以已售罄或限流快速失败;不存在长期堆积的服务异常。 -- [ ] 校验数据库一致性:秒杀库存剩余可售 + 已售 = 初始总量;成功订单一一对应一次库存扣减;失败请求无扣减记录、无订单。 -- [ ] 单用户限购:同一买家连续两次抢购仅一笔成功;第二次返回“超过单用户限购”或“已售罄”;数据库同一 `(activity_id, buyer_id)` 仅一笔秒杀订单。 -- [ ] 时间窗口:把开始时间改为未来 1 分钟后立刻抢购 → 全部返回“活动未开始”,库存不变;到达开始时间后可正常抢购;把结束时间改为过去 1 分钟后立刻抢购 → 全部返回“活动已结束”,库存不变。 -- [ ] 幂等:在约定窗口内用同一标识连续提交两次 → 仅生成一笔订单;剩余库存只扣减一次;两次响应携带同一订单号。 -- [ ] 取消与回补:抢购成功后主动取消或触发 C03 超时取消 → 秒杀可售库存回补 +1,已售 -1;同笔订单重复取消请求只回补一次。 -- [ ] 流量隔离:秒杀压测同时反复访问普通商品列表与详情 → 普通接口响应未因秒杀压测显著恶化;秒杀入口 P50 / P95 / P99 记录在压测报告中。 -- [ ] 双实例:在两实例 API 环境下重复硬指标验收 → 成功订单数 = 10 不变;Nginx / API 实例标识日志显示请求被分发到至少两个实例。 -- [ ] 活动取消:草稿 / 已发布 / 进行中活动可取消;已结束或已取消活动拒绝重复状态变更;取消后已分配库存不回收。 -- [ ] 缓存一致性:秒杀库存不进入缓存;活动列表与商品基础信息走缓存时,按失效策略更新;售罄状态变化必须以数据库为准重新加载活动详情。 -- [ ] 越权访问:用买家 B 身份请求买家 A 的秒杀订单或抢购资格 → 拒绝并返回“不存在 / 无权限”,不暴露记录是否存在。 -- [ ] 接口可观察:所有秒杀请求日志包含买家 ID、活动 ID、商品 ID、请求数量、抢购结果、受影响行数、订单号(成功时)和 traceId;不记录 Token、密码或支付卡号。 +- [ ] 生命周期:`Draft → Published → Ongoing → Ended` 按权威 UTC 时间推进;`Draft` / `Published` / `Ongoing` 可取消,`Ended` / `Cancelled` 拒绝重复状态变更。 +- [ ] 发布原子性:普通库存足够时一次性划拨并进入 `Published`;任一步失败时库存和活动状态均不变化;重复发布不重复划拨。 +- [ ] 公开展示:列表和详情包含剩余库存与已售数量;最后一份库存被抢走后,相关响应使页面同一交互内变为“已售罄”并禁用入口。 +- [ ] 100 并发抢 10 份库存:成功订单数恰好为 10、剩余为 0、已售为 10、无负库存、无孤立订单或孤立订单项;其余请求有明确失败原因。 +- [ ] 不少卖:请求充足且不存在身份、时间、限购等业务失败时,10 份库存全部形成 10 笔成功订单,限流配置不提前截断全部有效请求。 +- [ ] 单用户限购:同一买家的当前有效秒杀数量不超过上限;并发请求不能绕过;取消成功后按数量释放。 +- [ ] 幂等:同一买家、活动和稳定标识重复提交只形成一笔订单并返回同一结果;同标识不同请求被拒绝。 +- [ ] 事务回滚:库存、限购、订单、订单项快照、可靠订单事实或请求结果任一步失败时,全部恢复原状。 +- [ ] 时间窗口:开始前、结束后、取消后的请求均不扣库存;应用实例时钟偏差不改变结果。 +- [ ] 取消回补:买家主动取消或 C03 超时取消只回补原活动一次;不增加普通库存;活动结束 / 取消后回补量仍不可售。 +- [ ] 流量隔离:秒杀压测时普通商品列表与详情仍可用;过载请求快速失败,秒杀 P50/P95/P99 与普通接口影响记录在报告。 +- [ ] 多实例:两个 API 实例共同受理时仍保持成功数、库存、限购和订单一致。 +- [ ] 权限:商家不能维护他人商品活动,买家不能查看他人订单或限购明细,游客不能提交抢购。 +- [ ] 可观察性:成功、售罄、超限、重复、时间冲突、取消、过载和系统失败均有可追踪且脱敏的日志。 +- [ ] 演示资产:压测脚本、10 份基线数据、100 并发参数、数据库前后快照和一键恢复基线脚本均可重复执行。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" index eb06d2c..6f997ed 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" @@ -4,7 +4,7 @@ > 覆盖:M03-01、F07 > 基础核心流程:F01、F02、M02 公开浏览、F08 下单、M09 消息 > 直接协作:韦乾强(M04 Ordering)、顾欣月(M02 Catalog)、张海洋(秒杀边界 C01) -> 文档状态:初稿,待朱惠惠自审及 Catalog/Ordering 交叉评审 +> 文档状态:已按需求校准,可作为接口与数据库设计输入;待 Catalog/Ordering 交叉评审 > 需求事实源:[需求规格说明书 M03-01](../../../01-需求文档/需求规格说明书.md) 的“M03-01 购物车管理(F07)”完整七节 ## 一、范围与事实来源 @@ -26,7 +26,7 @@ ```mermaid flowchart LR - ID["M01 Identity
已认证买家、角色、账号状态"] -->|"BuyerOnly 通过"| CART["M03 Cart
条目、选中状态、服务端金额"] + ID["M01 Identity
已认证且账号状态正常的买家"] -->|"身份校验通过"| CART["M03 Cart
条目、选中状态、服务端金额"] CAT["M02 Catalog
销售状态、实时价格、实时可售库存"] -->|"加购 / 改数量 / 结算校验输入"| CART BUYER["买家购物车页、加购入口、收银台"] -->|"维护 / 选择 / 去结算动作"| CART CART -->|"本人选中条目、数量、选中状态、服务端金额"| ORD["M04 Ordering
服务端重读、计价、原子扣减"] @@ -54,32 +54,36 @@ flowchart LR ```mermaid flowchart TD - ID["M01:已认证且状态正常的买家"] --> A["买家在商品列表 / 详情提交加购:商品 ID + 数量 + 可选幂等键"] - CAT["M02 直接输入:商品销售状态、实时价格、实时可售库存"] --> B - A --> B{"商品已上架且数量合法并不超过实时可售库存?"} - B -- "否" --> X["拒绝加购并返回当前最大可购值与失效原因"] + ID["M01:已认证且状态正常的买家"] --> A["买家在商品列表 / 详情提交加购:商品 ID + 数量 + 可选稳定请求标识"] + A --> V{"商品标识和数量等固定输入有效?"} + V -- "否" --> V1["拒绝字段错误,不进入业务处理"] + V -- "是" --> G{"携带稳定请求标识?"} + G -- "是" --> G1{"该买家与标识是否已有确定结果?"} + G1 -- "同标识同请求" --> H2["直接重放首次确定结果,不读取当前商品或库存"] + G1 -- "同标识不同请求" --> Y["拒绝标识被不同请求复用"] + G1 -- "没有结果" --> B + G -- "否" --> B + CAT["M02 直接输入:商品销售状态、实时价格、实时可售库存"] --> B{"商品已上架且请求数量不超过实时可售库存?"} + B -- "否" --> X["形成当前最大可购值与失效原因的确定业务结果"] B -- "是" --> C{"同一买家+商品已存在条目?"} - C -- "否" --> D["新增条目,写入当前数量、小计和最新修改时间"] + C -- "否" --> D["新增条目并记录当前数量"] C -- "是" --> E{"新累加数量是否仍不超过实时可售库存?"} - E -- "否" --> X["拒绝累加,返回当前最大可设值"] - E -- "是" --> F["条目数量=旧数量+新数量,重算小计"] - D --> G{"携带幂等键?"} - F --> G - G -- "否" --> H["直接落库"] - G -- "是" --> G1{"该 (买家+幂等键) 已有处理结果?"} - G1 -- "同键同请求" --> H2["返回首次确定结果,不重复累加"] - G1 -- "同键不同请求" --> Y["拒绝标识被不同请求复用"] - G1 -- "否" --> H - H --> I["提交并返回最新条目、最大可购值、当前小计和生效时间"] + E -- "否" --> X["形成拒绝累加与当前最大可设值的确定业务结果"] + E -- "是" --> F["把条目数量更新为旧数量与本次数量之和"] + X --> XR["携带稳定标识时先保存该确定失败结果,再返回"] + D --> H["形成条目变更及本次成功结果"] + F --> H + H --> I["条目变更与稳定请求结果原子提交;按实时价格返回小计、最大可购值和生效时间"] ``` 关键约束: -- 主键为 `(buyer_id, product_id)`,同一组合在同一购物车中只允许一条;重复加购按数量累加,禁止多行并存。 +- 同一买家的同一商品在购物车中只允许一个条目;重复加购按数量累加,禁止生成多个并行条目。对应唯一性约束由后续数据库设计派生。 - 数量上下限 `1 ≤ 数量 ≤ 商品当前实时可售库存`,调小 / 删除不受上限约束,但不允许设为 0 或负数。 - 加购、改数量、累加均按实时库存拒绝越界请求,并返回当前最大可设值;前端据此截断,不依赖前端控制。 -- 携带稳定幂等键时,同一买家、同一幂等键在窗口内重复提交视为同一请求,不重复累加。 -- 所有动作必须按 `(buyer_id, product_id)` 归属过滤;条目 ID 不允许跨用户访问。 +- 完成身份与固定字段校验后,必须先查稳定请求标识,再读取当前商品与库存;同标识同请求直接重放首次确定结果,不得因之后的下架、改价或库存变化改变结果。 +- 成功结果与条目变更原子提交;已形成的商品不可售、库存不足等确定业务失败也要先保存再返回。数据库或依赖服务故障等未形成确定结果的瞬态失败不固化,允许携带原标识重试。 +- 所有动作必须同时校验当前买家与目标商品 / 条目的归属;条目标识不允许跨用户访问。 ## 四、查看 / 修改数量 / 删除 / 清空 @@ -87,14 +91,14 @@ flowchart TD ```mermaid flowchart TD - A["M01:买家进入购物车页"] --> B["按当前买家 ID 拉取本人全部条目"] + A["M01:买家进入购物车页"] --> B["按当前买家与分页条件查询本人条目"] B --> C["服务端读取每个商品的实时销售状态与可售库存"] - C --> D{"商品仍可售且库存 > 0?"} + C --> D{"商品仍可售、库存 > 0 且条目数量不超过实时库存?"} D -- "是" --> E["标记为可结算,返回实时单价、当前数量、小计、是否选中"] - D -- "否" --> F["标记失效并写明失效原因(下架 / 售罄 / 禁用)"] + D -- "否" --> F["标记失效并写明失效原因(下架 / 售罄 / 禁用 / 数量超过实时库存)"] E --> G["购物车页统一渲染:选中、未选中、失效三类状态"] F --> G - G --> H["返回最大可设库存和失效原因给前端,按需本地分页"] + G --> H["返回最大可设库存和失效原因,由服务端按请求分页"] ``` ### 4.2 修改数量 @@ -105,32 +109,50 @@ flowchart TD B -- "否" --> X["按不存在 / 无权限处理,不泄露归属"] B -- "是" --> C{"新数量为正整数?"} C -- "否" --> Y["拒绝修改并提示;不允许设为 0 或负数"] - C -- "是" --> D{"新数量 ≥ 旧数量(调大请求)?"} - D -- "否" --> E["调小路径:可不校验库存上限,直接落库并返回最新数量、小计"] - D -- "是" --> F{"可售库存 ≥ 新数量?"} - F -- "否" --> Y["拒绝调大;返回当前最大可设值,维持旧数量"] - F -- "是" --> G["落库并返回最新数量、小计与最大可设值"] + C -- "是" --> D{"新数量与旧数量相比?"} + D -- "相等" --> E["不改变数量,按当前销售状态、价格和库存重新派生展示结果"] + D -- "更小" --> F["允许调小并保存新数量"] + F --> F1["按商品当前销售状态与库存重新派生是否可结算"] + E --> G["返回最新数量、小计、最大可设值和可结算状态"] + F1 --> G + D -- "更大" --> H{"商品仍可售且实时可售库存 ≥ 新数量?"} + H -- "否" --> Y["拒绝调大;返回当前最大可设值,维持旧数量"] + H -- "是" --> I["保存新数量"] + I --> F1 ``` -> 说明:调小路径不重复校验实时库存上限(库存可能已下降但仍允许把数量往下调),仅校验“数量 > 0”与归属;调大请求才走实时可售库存上限校验,保证前端按最大可设值截断时不出现“提示截断到 N、接口又拒绝”的状态分歧。 +> 说明:调小路径不以实时库存上限作为拒绝条件(库存可能已下降但仍允许把数量往下调),仅校验“数量 > 0”与归属;保存后再按商品是否上架、是否启用以及新数量是否不超过实时库存派生可结算状态。只有商品仍可售且新数量已落入库存上限时,条目才恢复可结算。调大请求必须同时校验销售状态和实时库存上限;数量相等是无副作用操作,不得误判为调大。 ### 4.3 删除与清空 ```mermaid flowchart TD - A["买家提交删除 / 清空"] --> B{"按 (买家+条目ID) 或 (买家) 过滤?"} - B -- "否" --> X["拒绝访问,不暴露归属"] - B -- "是" --> C{"是否幂等删除?"} - C -- "否" --> Y["删除条目并返回最新列表"] - C -- "是" --> Z["重复请求按首次结果幂等返回"] - Z -. "条目不存在" .-> Z1["按不存在 / 无权限处理"] - Y --> W["含失效条目一并清除"] - W --> V["返回最新购物车或空状态"] + A["买家提交单条删除 / 批量删除 / 清空及稳定请求标识"] --> R{"该买家与标识是否已有结果?"} + R -- "同标识同请求" --> S["重放首次确定结果,不重新执行"] + R -- "同标识不同请求" --> T["拒绝标识复用,不产生副作用"] + R -- "没有结果" --> B{"动作类型?"} + B -- "单条删除" --> C{"该条目存在且属于当前买家?"} + C -- "否" --> X["形成不存在 / 无权限的确定结果,不泄露归属"] + C -- "是" --> D["删除该条目"] + B -- "批量删除" --> E["一次读取并校验全部目标条目"] + E --> F{"每个目标都存在且属于当前买家?"} + F -- "否" --> Y["形成整批拒绝结果,任何条目都不删除"] + F -- "是" --> G["在一个原子操作中删除全部目标条目"] + B -- "清空" --> H["删除当前买家的全部条目,含失效条目"] + D --> I["形成最新购物车或空状态结果"] + G --> I + H --> I + X --> J["副作用与确定结果原子提交;无副作用的确定失败先保存再返回"] + Y --> J + I --> J ``` 关键说明: -- 删除 / 清空全部幂等执行,重复删除同一 ID 结果一致;条目不存在或归属错误返回统一响应。 +- 单条删除、批量删除与清空均按当前买家过滤并携带稳定请求标识;同标识同请求重放首次结果,同标识不同请求拒绝。若使用新的请求标识删除已不存在条目,仍按不存在 / 无权限形成新的确定失败。 +- 删除副作用与成功结果必须原子提交;不存在、越权、整批校验失败等无副作用的确定失败也要先保存结果再返回。瞬态基础设施失败不保存确定结果,允许原标识重试。 +- 批量删除采用“全部校验、全部删除”的原子语义:只要任一目标不存在或不属于当前买家,整批拒绝且一个也不删除,不能静默忽略越权条目。 +- 清空只影响当前买家的全部条目;购物车本来为空时仍按成功的空结果返回。 - 查看、修改、删除、清空都不返回他人条目;前端不缓存购物车金额或选中状态作为最终结果。 ## 五、选中、失效与服务端计价 @@ -155,7 +177,7 @@ flowchart TD - 选中状态保存在服务端;刷新和重新登录后仍按服务端记录渲染;失效条目禁止被选中。 - 选中总额一律由服务端按实时单价计算,前端展示金额仅供参考;客户端不得指定最终金额。 -- 失效原因必须来自 `下架 / 库存归零 / 禁用` 三类客观状态,不允许写入主观提示。 +- 失效原因必须来自 `下架 / 库存归零 / 禁用 / 当前数量超过实时库存` 四类客观状态,不允许写入主观提示。 ## 六、与订单模块的协作:结算下单与清理 @@ -172,7 +194,7 @@ flowchart TD E --> F["条件扣减普通库存(M04 与 M02 内部完成)"] F --> G["M04 创建订单与订单项快照、保存服务端总额、订单状态 PendingPayment"] G --> H["M04 在同一事务内删除本次已结算的购物车条目"] - H --> I["待发布订单创建事实(OrderCreatedIntegrationEvent)已写入"] + H --> I["可靠记录订单已创建事实,供后续消息流程消费"] I --> J{"订单事务整体提交?"} J -- "否" --> R["整体回滚:库存、订单、待发布事实、购物车均恢复原状"] J -- "是" --> K["M03 直接输出:已下单条目被清理;其余购物车条目继续保留"] @@ -181,7 +203,7 @@ flowchart TD 不变量: -- 库存扣减、订单与快照写入、待发布订单创建事实、购物车清理属于同一下单事务,任一失败整体回滚。 +- 库存扣减、订单与快照写入、可靠记录订单已创建事实、购物车清理属于同一下单事务,任一失败整体回滚。 - 订单回滚时购物车条目原状保留,禁止“订单失败但条目丢失”。 - 买家主动取消订单或 C03 超时取消后,本期不自动恢复购物车条目;用户希望重新购买需手动再次加车。 - C01 秒杀绕过购物车:秒杀成功订单与取消后库存回补均不读写本文购物车条目。 @@ -215,30 +237,30 @@ flowchart TD | 加入购物车(可选幂等) | A201 | 校验数量 + 库存,同 `(买家+商品)` 累加,幂等不重复累加 | 待交叉评审 | | 查看本人购物车 | A202 | 仅返回本人条目,含可结算 / 失效标记与失效原因 | 待交叉评审 | | 修改本人条目数量 | A203 | 调大按实时库存校验并返回最大可设值;调小不验库存上限;返回最新数量与小计 | 待交叉评审 | -| 删除本人条目(按 ID) | A204 | 按 `(买家+条目ID)` 过滤;幂等;多次删除同一 ID 结果一致 | 待交叉评审 | -| 批量删除本人条目 | A205 | 按 `(买家+条目ID 集合)` 一次性物理删除;非本人条目忽略并计入 skippedCount | 待交叉评审 | +| 删除本人条目(按 ID) | A204 | 按当前买家与条目标识过滤;同一稳定请求重放首次结果,新请求删除已不存在条目仍被拒绝 | 待交叉评审 | +| 批量删除本人条目 | A205 | 先校验全部目标均存在且归属本人,再原子删除;任一目标无效或越权时整批拒绝且不删除;同一稳定请求可重放 | 待交叉评审 | | 切换单选 / 全选 / 反选 | A206 | 服务端持久化选中;失效条目不允许被选中 | 待交叉评审 | | 一键清空购物车 | A207 | 幂等;只影响本人;失效条目一并清理 | 待交叉评审 | | 结算预览(获取总价) | A208 | 实时重读单价并计算总额;前端不能指定金额 | 待交叉评审 | -| 加购幂等(内嵌) | 不单独分配 Axxx | 加购 / 改数量 / 抢锁以 `Idempotency-Key` 形式由调用方携带,按接口设计 1.12 通用幂等规则重放首配结果 | 不作为独立 HTTP 契约 | +| 加购幂等(内嵌) | 不单独分配 Axxx | 调用方携带稳定请求标识,同一标识与同一请求重放首次确定结果;具体传递方式由接口通用约定派生 | 不作为独立 HTTP 契约 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段、错误码与幂等键传递方式不得反向写入业务图,接口设计 1.12 通用幂等规则统一承载。 ## 九、扩展接入边界 -- C01 秒杀:立即抢购绕过购物车,Seckill 与 Ordering 自行完成资格、限购、活动库存和订单校验;本文购物车不参与秒杀扣减 / 回补,也不被秒杀回补触发的库存变化影响。M04 必须按 `orderType` 与 `seckillActivityId` 区分库存回补通道,确保秒杀回补不误增普通库存。 +- C01 秒杀:立即抢购绕过购物车,Seckill 与 Ordering 自行完成资格、限购、活动库存和订单校验;本文购物车不参与秒杀扣减 / 回补,也不被秒杀回补触发的库存变化影响。M04 必须按订单来源识别原库存通道,确保秒杀回补不误增普通库存。 - X02 收藏 / 浏览历史:仅引用商品 ID 维度的公开数据,不读写购物车条目;用户在收藏页点击“加入购物车”时调用本文 A201,遵循同一所有权与库存上限校验。 -- M09 站内消息:仅消费 M04 下单事务提交后发布的 `OrderCreatedIntegrationEvent`;M03 不主动发布消息,也不依赖消息反馈修改条目。 +- M09 站内消息:仅消费 M04 下单事务提交后形成的“订单已创建”可靠事实;M03 不主动发送消息,也不依赖消息反馈修改条目。 - M06-01 后台商品上下架:通过商品销售状态变更触发购物车失效标记,不直接修改他人购物车条目。 ## 十、由流程反查出的接口与数据待评审项 -1. 本文要求购物车主键为 `(buyer_id, product_id)`,单一组合唯一;若现有 A2xx 在多次加购时按 “条目 ID 自增” 创建多条记录,接口语义必须改为“按 `(买家, 商品)` 唯一累加”。 +1. 本文要求同一买家的同一商品只有一个购物车条目;若现有 A2xx 在多次加购时创建多条记录,接口语义必须改为“按买家与商品唯一累加”,数据库再据此派生唯一约束。 2. 数量上下限必须在服务端实时校验并返回最大可设值;A203 必须区分“调大拒绝”和“调小允许”,不能统一返回字段错误导致前端无法截断。 -3. 加购幂等键的窗口期需要与库存 / 上限校验配合:同一幂等键只能重放首次成功结果,不同请求体携带同键视为标识复用并被拒绝。 +3. 加购幂等键的窗口期需要与库存 / 上限校验配合:同一幂等键只能重放首次确定结果,不同请求体携带同键视为标识复用并被拒绝;瞬态失败不固化,可用原标识重试。 4. 下单成功后,订单模块在事务内清理购物车条目;若现有 A2xx 与订单模块的下单接口跨事务异步清理,必须先改为同事务清理,保证事务回滚不丢条目。 -5. 结算预览返回的服务端金额是“可选预览”,下单时订单模块必须再重读一次商品与库存,不能直接复用 A206 的金额作为最终扣款事实。 -6. 失效条目清理:本流程要求“仅随用户主动删除 / 清空”,A204 必须不复用物理删除批量逻辑;后台清理需另起保留期规则,不在本文范围。 +5. 结算预览返回的服务端金额是“可选预览”,下单时订单模块必须再重读一次商品与库存,不能直接复用 A208 的金额作为最终扣款事实。 +6. A204 单条删除、A205 批量删除和 A207 清空必须分别承接本流程语义;其中批量删除须先校验全部归属再原子执行,不得静默忽略无效或越权条目。后台清理需另起保留期规则,不在本文范围。 7. 购物车数据归属全部按 `(买家 ID, 商品 ID)` 或 `(买家 ID, 条目 ID)` 双重过滤;A2xx 不能仅按条目 ID 给出可访问性。 8. 价格变动:商品改价后购物车再次展示用实时单价;现有接口若缓存条目的小计或反推金额,需在列表时重算并返回最新单价。 9. 购物车表、幂等记录表的字段、约束和索引必须由数据库设计任务另行确认,本文档不发明。 @@ -250,12 +272,13 @@ flowchart TD - [ ] 修改数量超过商品实时可售库存时拒绝并返回最大可设值;改为 0、负数或非整数被拒绝并提示原因。 - [ ] 选中条目总额由服务端按实时单价计算,前端篡改金额或数量再提交被服务端拒绝,订单总额与数据库一致。 - [ ] 商品下架后,已加入条目在购物车页标记“不可结算”,不可调大、不可累加、不能勾选进入结算;库存为 0 或被禁用同样标记。 -- [ ] 失效条目可下调数量、可删除;下调到合法值后恢复可结算状态。 +- [ ] 失效条目可下调数量、可删除;仅当商品仍处于上架且启用状态、下调后的数量不超过实时可售库存时,条目恢复可结算状态。 - [ ] 越权:用买家 B 身份请求买家 A 的条目被拒绝,响应不暴露该条目是否存在及归属信息。 - [ ] 下单成功后,对应购物车条目在同一事务内被清除;订单事务回滚时购物车条目原状保留,未出现“订单失败但条目丢失”。 - [ ] 买家主动取消订单或 C03 超时取消后,对应购物车条目本期不自动恢复。 - [ ] 并发:同一条目同时被改数量和删除,最终只出现删除结果或最新数量,两者不会同时生效导致数据错乱。 - [ ] 幂等:相同请求幂等标识在约定窗口内重复提交不重复累加数量,返回结果一致。 +- [ ] 删除:单条、批量和清空只影响本人;批量目标中任一条目不存在或越权时整批拒绝且零删除,同一稳定删除请求重试返回首次结果。 - [ ] C01 衔接:秒杀路径独立执行活动库存与个人限购校验,超卖拒绝且不影响普通购物车条目。 - [ ] 库存与下单协作:购物车不预留库存,订单提交事务内完成扣减;C03 超时取消正确回补库存,购物车无需联动处理。 - [ ] 性能与可用性:购物车页 30~100 条目在常规环境下加载时间低于 2 秒;大量条目启用分页,禁止无上限返回。 -- Gitee From ee550b956bf4ebb862d726275d95461cbcc764c7 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 19:36:10 +0800 Subject: [PATCH 089/118] =?UTF-8?q?docs(process):=20=E4=BF=AE=E6=AD=A3=20M?= =?UTF-8?q?05=20=E6=94=AF=E4=BB=98=E6=88=AA=E6=AD=A2=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E7=AB=9E=E4=BA=89=EF=BC=9B=E9=98=BB=E6=AD=A2=E8=BF=87=E6=9C=9F?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E4=BB=98=E6=AC=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...57\344\273\230\346\265\201\347\250\213.md" | 96 +++++++++++-------- 1 file changed, 58 insertions(+), 38 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" index 3f90118..b377b7e 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" @@ -4,7 +4,7 @@ > 覆盖:M05-01、F10 > 基础核心流程:F08、F09 > 直接协作:韦乾强(M04 Ordering)、罗皓晨(M09 消息) -> 文档状态:初稿,待张海洋自审及 Ordering 交叉评审 +> 文档状态:F10 同步核心已按需求校准,可作为核心接口设计输入;待 Ordering/C03 交叉评审,C08 异步替代边界在 C08 流程任务中冻结 > 需求事实源:[需求规格说明书 M05-01](../../../01-需求文档/需求规格说明书.md) 的“M05-01 模拟支付(F10)”完整七节 ## 一、范围与事实来源 @@ -25,7 +25,7 @@ ```mermaid flowchart LR - ID["M01 Identity
已认证买家、角色和账号状态"] -->|"BuyerOnly 通过"| PAY["M05 Payment
钱包、充值、支付记录"] + ID["M01 Identity
已认证且账号状态正常的买家"] -->|"身份校验通过"| PAY["M05 Payment
钱包、充值、支付记录"] ORD["M04 Ordering
订单号、买家归属、持久化应付金额、当前状态和支付截止时间"] -->|"本人订单事实"| PAY PAY -->|"充值成功:确定余额和充值流水"| BUYER["买家钱包/充值记录页面"] PAY -->|"支付成功:确定支付记录
原子推进 PendingPayment → Paid"| ORD @@ -35,6 +35,7 @@ flowchart LR ID -->|"游客、非买家、账号禁用或令牌失效"| X["拒绝访问,不返回钱包与订单数据"] ORD -->|"非本人或不存在"| Y["按不存在/无权限处理,不泄露归属"] + ORD -->|"已到支付截止时间"| T["拒绝扣款并进入 M04/C03 过期取消通道"] PAY -->|"状态竞争或事务失败"| Z["返回当前最终状态
钱包不产生部分扣款"] ``` @@ -43,6 +44,7 @@ flowchart LR - M05 只接受订单标识,不接受客户端指定最终扣款金额;实际金额来自 M04 已持久化订单事实。 - M04、M05 位于同一模块化单体事务边界时,通过公开应用能力协调原子结果,不跨模块访问内部仓储。 - 支付成功事实只能在支付事务整体成功后交给 M09;具体事件名、Outbox 和 Worker 重试方式由系统架构设计承接,不进入业务流程图。 +- 订单支付截止时间属于可支付性的强约束,不依赖 C03 是否已经扫描到该订单:权威时间达到截止点后,M05 必须拒绝扣款,并把订单交给 M04/C03 的过期取消能力处理。 ## 三、小金库充值流程 @@ -75,29 +77,42 @@ flowchart TD flowchart TD ID["M01 直接输入:已认证买家"] --> A["从下单成功页、订单列表或详情进入统一收银台"] ORD["M04 直接输入:订单号、归属、持久化应付金额、当前状态和支付截止时间"] --> A - A --> B["服务端读取本人订单事实与本人钱包余额"] + A --> B["服务端读取本人订单事实、本人钱包余额与权威当前时间"] B --> C{"订单当前状态?"} C -- "Paid" --> P0["返回已有确定支付结果,不重复扣款"] C -- "Cancelled/其他不可支付状态" --> X["拒绝支付并展示当前最终状态"] - C -- "PendingPayment" --> D{"余额充足?"} - D -- "否" --> E["说明差额并进入第三章充值流程"] - E --> B - D -- "是" --> F["买家确认支付并携带防重复标识"] - F --> G{"该防重复标识已有处理结果?"} + C -- "PendingPayment" --> D{"权威当前时间早于支付截止时间?"} + D -- "否" --> EX["收银台标记已过期并禁用支付;请求 M04/C03 条件取消和原库存回补"] + D -- "是" --> E{"页面所示余额是否充足?"} + E -- "否" --> E1["说明差额并进入第三章充值流程"] + E1 --> B + E -- "是" --> SUB["买家确认支付:订单标识 + 稳定请求标识"] + RETRY["支付提交 / 原标识重试入口"] --> SUB + SUB --> V{"身份与固定输入有效?"} + V -- "否" --> V1["拒绝字段错误,不进入支付处理"] + V -- "是" --> G{"该买家与稳定标识是否已有确定结果?"} G -- "同标识同请求" --> P0 - G -- "同标识不同订单/请求" --> Y["拒绝标识被不同请求复用"] - G -- "否" --> H["开启支付事务并重新读取订单、金额和余额"] - H --> I["对本人钱包执行余额充足条件扣减"] - I --> J{"余额扣减条件命中?"} - J -- "否" --> R["整体回滚并返回余额不足或当前状态"] - J -- "是" --> K["条件推进本人订单 PendingPayment → Paid"] - K --> L{"订单状态条件更新成功?"} - L -- "否" --> R - L -- "是" --> M["记录钱包流水、确定支付记录、首次结果和待发布支付成功事实"] - M --> N{"支付事务提交成功?"} - N -- "否" --> R - N -- "是" --> O["M05 直接输出:支付记录已落库,订单为 Paid"] - O --> P["M04 展示最新详情;M06-02 可查询待发货订单"] + G -- "同标识不同请求" --> Y["拒绝标识被不同请求复用"] + G -- "没有结果" --> H["开启支付事务并重新读取订单归属、金额、状态、截止时间和余额"] + H --> I{"仍属于本人、仍为 PendingPayment 且权威时间仍早于截止时间?"} + I -- "否" --> RSTATE["整体回滚,形成已支付 / 已取消 / 已过期等确定业务结果"] + I -- "是" --> J["对本人钱包执行余额充足条件扣减"] + J --> K{"余额扣减条件命中?"} + K -- "否" --> RBAL["整体回滚,形成余额不足的确定业务结果"] + K -- "是" --> L["条件推进本人订单 PendingPayment → Paid"] + L --> M{"订单状态条件更新成功?"} + M -- "否" --> RSTATE + M -- "是" --> N["记录钱包流水、支付记录、稳定请求结果和可靠支付成功事实"] + N --> O{"支付事务整体提交?"} + O -- "否" --> TR["确认回滚或结果未知:不固化伪结果,使用原标识查询 / 重试"] + O -- "是" --> P["M05 直接输出:支付记录已确认,订单为 Paid"] + P --> Q["M04 展示最新详情;M06-02 可查询待发货订单"] + RSTATE --> RR["先把无副作用的确定结果与稳定请求标识持久绑定,再返回"] + RBAL --> RR + RR --> RS{"结果绑定成功?"} + RS -- "否" --> TR + RS -- "是" --> RT["返回首次确定结果;以后同标识直接重放"] + RSTATE -. "若结果为已过期" .-> EX ``` 主流程不规定数据库锁的具体取得顺序,但必须满足以下原子结果: @@ -107,12 +122,12 @@ flowchart TD + 支付钱包流水已写入 + 支付记录已写入 + 首次处理结果已写入 -+ 订单 PendingPayment → Paid ++ 订单在截止时间前由 PendingPayment → Paid + 待发布的支付成功事实已写入 = 同一事务提交成功 ``` -任一步失败时整体回滚,不允许出现“余额已扣但订单未支付”或“订单已支付但没有支付记录”的部分结果。 +任一步失败时整体回滚,不允许出现“余额已扣但订单未支付”或“订单已支付但没有支付记录”的部分结果。完成身份与固定字段校验后,稳定请求结果检查必须早于实时状态、余额和截止时间判断:同标识同请求重放首次确定结果;余额不足、已取消、已过期等已返回业务结果先持久绑定再返回;事务或基础设施故障未形成确定结果时不固化,允许原标识重试。 ## 五、核心状态与并发竞争 @@ -121,8 +136,8 @@ F10 不增加“支付中”等订单状态。同步模拟支付提交前,订 ```mermaid stateDiagram-v2 [*] --> PendingPayment: F08 下单事务成功 - PendingPayment --> Paid: F10 支付事务成功 - PendingPayment --> Cancelled: F09 主动取消或 C03 超时取消成功 + PendingPayment --> Paid: 截止时间前 F10 支付事务成功 + PendingPayment --> Cancelled: F09 主动取消,或截止时间到达后 M04/C03 取消成功 Paid --> Paid: 重复支付返回已有结果 Cancelled --> Cancelled: 支付重试被拒绝 ``` @@ -131,13 +146,16 @@ stateDiagram-v2 ```mermaid flowchart LR - A["订单 PendingPayment"] --> B["F10 支付事务
条件推进为 Paid"] - A --> C["F09/C03 取消事务
条件推进为 Cancelled"] - B --> D{"谁先成功更新状态?"} - C --> D - D -->|"支付胜出"| E["Paid;扣款、记录和支付成功事实同时提交"] - D -->|"取消胜出"| F["Cancelled;支付整体回滚且钱包不扣款"] - D -->|"本方未胜出"| G["重新查询并返回订单当前最终状态"] + A["订单 PendingPayment"] --> T{"权威时间是否早于支付截止时间?"} + T -- "是" --> B["F10 支付事务
条件推进为 Paid"] + T -- "否" --> C["M04/C03 过期取消事务
条件推进为 Cancelled"] + A --> ACTIVE["F09 买家主动取消事务
条件推进为 Cancelled"] + B --> DECIDE{"谁先成功更新状态?"} + C --> DECIDE + ACTIVE --> DECIDE + DECIDE -->|"支付胜出"| E["Paid;扣款、记录和支付成功事实同时提交"] + DECIDE -->|"主动或过期取消胜出"| F["Cancelled;支付整体回滚且钱包不扣款"] + DECIDE -->|"本方未胜出"| G["重新查询并返回订单当前最终状态"] ``` ## 六、结果查询与页面反馈 @@ -147,7 +165,7 @@ flowchart TD A["已认证买家进入钱包、收银台或支付记录页"] --> B{"查询类型"} B -- "钱包余额" --> C["返回本人实时余额"] B -- "充值记录" --> D["分页返回本人充值记录"] - B -- "收银台" --> E["返回本人订单金额、状态、余额和可支付性"] + B -- "收银台" --> E["返回本人订单金额、状态、余额、截止时间和按权威时间派生的可支付性"] B -- "订单支付结果" --> F["返回已确定支付结果
无记录时的响应语义待评审"] B -- "支付记录/详情" --> G["仅返回本人支付记录"] C --> H["页面展示确定状态和下一步"] @@ -170,9 +188,10 @@ flowchart TD | 余额不足 | 不开启或回滚支付事务 | 订单保持 `PendingPayment` | | 订单已支付 | 返回已有确定结果 | 订单保持 `Paid`,不重复扣款 | | 订单已取消 | 拒绝支付 | 订单保持 `Cancelled` | +| 订单仍为待支付但权威时间已到截止点 | 拒绝扣款并触发过期取消通道 | 钱包不扣款;M04/C03 条件推进订单并回补原库存 | | 同一防重复标识、同一请求重试 | 返回首次确定结果 | 不重复产生副作用 | | 同一防重复标识、不同请求 | 返回标识复用冲突 | 不执行新副作用 | -| 支付与取消并发 | 状态条件唯一胜出 | `Paid` 与 `Cancelled` 只能成立一个 | +| 支付与取消并发 | 截止时间与状态条件共同裁决 | 截止时间前支付可与主动取消竞争;截止时间到达后支付不得胜出 | | 钱包、订单、记录或支付成功事实任一步失败 | 整个支付事务回滚 | 不产生部分扣款或部分状态 | | 事务后消息发布失败 | 保留已提交支付事实 | M09 按架构确定的可靠机制重试 | @@ -185,8 +204,8 @@ flowchart TD | 查询本人钱包余额 | A401 | 仅返回当前买家的确定余额 | 待交叉评审 | | 对本人钱包模拟充值 | A402 | 校验金额并幂等、原子地产生余额和充值流水结果 | 待交叉评审 | | 查询本人充值记录 | A403 | 按当前买家隔离并分页返回充值记录 | 待交叉评审 | -| 打开本人订单收银台 | A404 | 基于 M04 持久化订单事实返回金额、状态、余额和可支付性 | 待交叉评审 | -| 确认模拟支付 | A405 | 幂等提交原子支付事务,并处理与取消的状态竞争 | 待交叉评审 | +| 打开本人订单收银台 | A404 | 基于 M04 持久化订单事实和权威时间返回金额、状态、余额、截止时间与可支付性 | 待交叉评审 | +| 确认模拟支付 | A405 | 先重放确定结果;新请求仅在截止时间前幂等提交原子支付事务,并处理与取消的状态竞争 | 待交叉评审 | | 查询订单支付结果 | A406 | 返回当前买家该订单的确定支付结果 | 待交叉评审 | | 查询本人支付记录 | A407 | 按当前买家隔离并分页返回支付记录 | 待交叉评审 | | 查询本人支付详情 | A408 | 仅返回当前买家可访问的单笔支付详情 | 待交叉评审 | @@ -208,7 +227,8 @@ flowchart TD 5. F10 同步核心流程不包含迟到回调;A406 的“取消订单收到迟到成功支付”属于 C08,`PAYMENT.NOT_FOUND` 也不能只根据订单是否为 `PendingPayment/Paid` 推断。 6. 流程只要求每个买家拥有独立钱包且无钱包记录时余额语义确定;A401 尚未确认钱包是在注册时创建还是首次查询时按需初始化。 7. 需求尚未定义充值记录和支付记录的 `Pending/Failed/Succeeded` 状态机;A403/A407 草案中的这些状态不能反向写进流程,需先完成业务确认。 -8. DB081~DB100 尚未形成可实施的完整表定义,数据库字段、约束和索引必须由数据库设计任务另行确认。 +8. A404/A405 必须把支付截止时间作为服务端可支付条件;不能只看 `PendingPayment` 状态,否则 C03 扫描延迟时会放过过期支付。到期结果由 M04/C03 取消能力完成状态推进和原库存回补。 +9. DB081~DB100 尚未形成可实施的完整表定义,数据库字段、约束和索引必须由数据库设计任务另行确认。 ## 十一、验收证据清单 @@ -218,7 +238,7 @@ flowchart TD - [ ] 余额不足不扣款,订单保持 `PendingPayment`。 - [ ] 已支付订单重复提交不重复扣款,返回既有结果。 - [ ] 已取消订单不能通过重试进入 `Paid`。 -- [ ] 支付与主动/超时取消并发时只有一个最终状态。 +- [ ] 截止时间前支付与主动取消并发时只有一个最终状态;截止时间达到后即使 C03 尚未扫描,支付也被拒绝且钱包不扣款。 - [ ] 模拟任一步失败时事务整体回滚。 - [ ] 网络结果未知时使用原防重复标识或结果查询动作得到确定结果。 - [ ] 保留页面、接口响应、数据库事务结果和可靠消息重试证据。 -- Gitee From 983936a1be788d6ae5f83ba3f85b0e0668524531 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 19:47:35 +0800 Subject: [PATCH 090/118] =?UTF-8?q?docs(process):=20=E9=87=8D=E6=9E=84=20C?= =?UTF-8?q?08=20=E5=9B=9E=E8=B0=83=E4=B8=8E=E5=AF=B9=E8=B4=A6=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=EF=BC=9B=E5=86=BB=E7=BB=93=E5=90=8C=E6=AD=A5=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E7=AB=9E=E4=BA=89=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 7 +- ...71\350\264\246\346\265\201\347\250\213.md" | 477 +++++++++--------- ...57\344\273\230\346\265\201\347\250\213.md" | 4 +- 3 files changed, 233 insertions(+), 255 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index fbf19cb..6854709 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -1,6 +1,6 @@ # 电子商城需求规格说明书 -> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.2 +> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.3 > > 文档状态:内容和格式已完成内部统一,待全组与指导教师评审后冻结 > 截止:第 1 周周三提交初稿 @@ -11,6 +11,7 @@ |---|---|---|---| | v0.1 | 2026-07-24 | 全体成员(罗皓晨统稿) | 形成并统一需求规格,明确四类角色、必做功能、4 项选做、7 项挑战、单店 B2C 边界、核心状态与六人模块职责 | | v0.2 | 2026-07-24 | 罗皓晨 | 消解 C01 库存展示绝对一致与“不预占库存”的冲突,统一为数据库权威快照、旧结果防覆盖及并发失败后同一交互刷新 | +| v0.3 | 2026-07-24 | 罗皓晨 | 冻结 F10 同步钱包与 C08 受控模拟回调的互斥入账边界,并明确回调同样受订单状态和支付截止时间约束 | ## 业务流程设计入口 @@ -1145,7 +1146,7 @@ M06-02 提供发货结果,M04 维护订单状态,M09 向买家发送完成 | 商家 | 不操作买家钱包,只依据可靠的已支付订单进行发货 | | 管理员 | 本期不介入买家充值和支付 | -M04 提供订单金额与状态,M09接收支付结果通知,C03处理支付与超时取消竞争,C08处理回调幂等和对账;支付模块不直接修改这些模块的内部数据。 +M04 提供订单金额、状态和支付截止时间,M09 接收支付结果通知,C03 处理支付与超时取消边界,C08 处理受控模拟通道的回调幂等和对账;支付模块不直接修改这些模块的内部数据。F10 默认使用小金库同步支付;C08 不扣小金库,只能凭受控模拟通道的合法回调与同步支付、主动取消共同竞争同一订单的待支付状态,并且在支付截止时间达到后不得把订单推进为已支付。 #### 3. 功能需求 @@ -2167,6 +2168,8 @@ C07 使用 Redis 优化首页商品摘要和商品详情两个高频只读场景 模拟支付回调可能重复、乱序或延迟到达时,系统仍保持支付记录和订单状态正确,并通过每日对账发现“支付成功但订单未更新”等差异。本挑战不接入真实支付机构,也不自动执行未经确认的资金修复。 +F10 的默认买家路径仍为小金库同步支付。C08 仅提供受控的挑战模拟通道:合法成功回调不扣买家小金库,而是形成模拟通道支付事实,并与 F10 同步支付、F09 主动取消和 C03 超时取消共同竞争订单的待支付状态。只有订单仍为待支付且权威时间早于支付截止时间时,成功回调才可推进订单;任一其他路径先形成结果后,回调不得重复入账或覆盖订单终态。买家和商家只看到稳定的订单与支付结果,不看到回调内部处理状态。 + #### 2. 身份处理与协作边界 | 身份 | 处理方式 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" index 9b14db0..98998b6 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" @@ -1,345 +1,320 @@ # C08 支付回调与对账流程 > 负责人:张海洋 -> 覆盖:C08、X04 对账 -> 基础核心流程:F10、F09 -> 直接协作:韦乾强(M04 Ordering)、罗皓晨(C10 多实例 + M00 后台基础设施 + M09 消息) -> 文档状态:初稿,待张海洋自审及 Ordering/集成事件 交叉评审 -> 需求事实源:[需求规格说明书 C08](../../../01-需求文档/需求规格说明书.md) 的"C08 支付回调幂等与对账"完整七节 -> 当前范围说明:dev 上的 C08 章节为 v0.1 FR01~FR10,本流程按此基线编写;优先级管理同步 F10 同步支付与 C08 异步回调的替换边界尚未冻结,本文档登记待确认项并以此为评审入口。 +> 覆盖:C08、X04 退款对账 +> 基础核心流程:F10、F09、M10 +> 直接协作:韦乾强(M04 Ordering)、罗皓晨(M09 消息、C10 多实例与 Worker 基础设施) +> 文档状态:已按需求校准,可作为接口设计输入;待 Payment/Ordering 交叉评审 +> 需求事实源:[需求规格说明书 C08](../../../01-需求文档/需求规格说明书.md) 的“C08 支付回调幂等与对账”完整七节 +> 当前范围说明:F10 默认使用小金库同步支付;C08 是不扣小金库的受控挑战模拟通道,两者与取消共同竞争订单状态和支付截止时间,最多一方形成最终结果 ## 一、范围与事实来源 -本模块负责模拟支付通道回调的接收、鉴别、幂等、乱序处理、事务一致性、每日对账批次生成、差异识别与闭环,以及售后退款对账。本挑战不接入真实支付机构、不自动执行未经确认的资金修复、不在为买家或商家展示的页面中暴露回调内部过程。 +C08 负责受控模拟支付通道的回调接收、来源鉴别、幂等、乱序处理、订单状态竞争、每日对账和差异闭环。回调可能重复、乱序、延迟或在客户端未知结果后重放;系统必须保证同一通道支付最多确认一次,迟到回调不能覆盖已取消或已支付订单,失败重试不能留下半处理状态。 -本文先依据需求确定业务参与者、状态、判断分支、事务结果和模块出入口,再由这些流程步骤派生接口能力。A421~A425 只用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 +F10 的默认买家路径仍为 M05 小金库同步支付。C08 不扣买家小金库,也不新增第二套买家支付页面;它只接收受控挑战脚本或模拟通道产生的合法回调。同步钱包支付、模拟通道成功回调、买家主动取消和 C03 超时取消共同竞争订单的 `PendingPayment` 状态。模拟成功回调只有在订单仍为待支付且权威时间早于支付截止时间时才能推进为 `Paid`;其他路径先成功后,回调只能重放、忽略或登记差异。 + +本文先确认回调结果、订单竞争、事务结果、对账范围和差异闭环,再由这些业务动作派生 A421~A425。具体签名格式、HTTP 字段、表名、索引、Inbox/Outbox 结构和 Worker 参数属于接口、数据库或架构下游设计,不能反向塑造业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| -| C08 需求 v0.1 FR01~FR10 | 完整定义 | 作为业务语义事实源 | -| C08 扩展 FR11~FR15 | 仅 zhy 本地草稿(commit 44a08d0,unreachable) | 待 PR 合入 dev 后再并入流程 | -| 本文业务流程 | 初稿 | 先确认角色、状态、分支、事务边界和模块出入口 | -| A421~A425 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | -| DB081~DB100 数据库设计 | 模板/占位 | 本文不发明表字段、枚举编码和索引 | -| 与 F10 同步支付的边界 | 待决 | 业务流程设计 §3.5 明确"暂不让同步和异步同时成为最终支付事实" | -| 与 X04 售后退款对账 | 全量接入 | 退款流水必须纳入 C08 每日对账 | +| C08 需求与教师挑战目标 | 完整定义 | 作为回调、对账和差异闭环事实源 | +| F10/F09/M10 流程 | 已定义或校准中 | 作为支付、取消和退款协作边界 | +| 本文业务流程 | 已校准、待交叉评审 | 冻结回调终态、原子结果、截止时间与对账闭环 | +| A421~A425 接口 | 部分定义、未冻结 | 待按第十一章重新派生 | +| 回调、支付、对账数据设计 | 模板/占位 | 流程完成后统一派生,不在业务图中预设字段 | -## 二、模块直接出入口 +## 二、参与者与模块直接出入口 ```mermaid flowchart LR - ORD["M04 Ordering
订单号、归属、当前状态、支付截止时间"] --> CB["C08 Payment Callback
回调接收、幂等、乱序处理"] - CH["模拟支付通道
唯一回调 ID + 支付流水 + 订单 + 结果 + 时间"] -->|"已签名回调载荷"| CB - CB -->|"唯一约束 + 签名校验"| CB - CB -->|"事务提交:支付记录 + 订单状态 + Inbox/处理记录 + Outbox"| ORD - CB -->|"支付成功事实供 X03 消费"| MSG["M09 消息持久化/通知"] - W1["Worker Service 调度"] -->|"每日定时"| BAT["对账批次生成"] - BAT -->|"匹配 / 差异"| DIFF["差异识别与闭环"] - DIFF -->|"管理员处理"| ADM["AdminOnly:差异处理"] - CB -. "回调历史" .-> BAT - AS["M10 售后退款成功"] -. "退款流水" .-> BAT - RECON["退款入账记录"] -. "钱包入账" .-> BAT - - CH -->|"签名错误、伪造或未知回调 ID"| X["拒绝,不写入数据库"] - ORD -->|"订单不存在或非本人"| Y["拒绝,不泄露归属"] - CB -->|"重复回调"| Z["返回首次结果,不重复记账或通知"] - BAT -->|"同一日期重复执行"| W["不重复生成矛盾批次"] + CHANNEL["受控模拟支付通道"] -->|"合法来源、唯一回调、支付流水、订单、金额、结果和发生时间"| C08["C08 回调处理"] + ORD["M04 Ordering
订单归属、应付金额、当前状态、支付截止时间"] -->|"权威订单事实"| C08 + M05["M05 小金库同步支付"] -->|"与回调竞争同一待支付状态"| ORD + CANCEL["F09 / C03 取消"] -->|"与回调竞争待支付状态和截止时间"| ORD + + C08 -->|"成功回调原子结果:通道支付确认 + 订单 Paid + 可靠支付事实"| ORD + C08 -->|"失败 / 忽略 / 差异的确定处理结果"| CHANNEL + ORD -->|"已提交支付成功事实"| MSG["M09 消息"] + + WORKER["Worker 每日调度"] --> RECON["支付与退款对账批次"] + RECON -->|"全部匹配 / 存在差异"| ADMIN["管理员对账页面"] + ADMIN -->|"领取、说明、引用受控处理结果并闭环"| RECON + M10["M10 已退款事实"] --> RECON + WALLET["M05 退款操作与小金库入账事实"] --> RECON + + CHANNEL -->|"来源无效或固定字段非法"| REJECT["拒绝,不改变业务事实"] ``` 边界约束: -- C08 回调通道身份使用服务端签名的模拟通道凭证;接入端不通过回调核实买家身份,回调只携带业务标识。 -- C08 回调处理与订单状态推进在同一事务内完成;事务失败整体回滚,事务中断可安全重试。 -- C08 对账只生成可追踪的差异记录,不直接修改业务数据;修复必须由管理员在受控流程内完成。 -- C08 暂不替代 F10 同步支付;触发路径上"同步支付走 M05,异步通道走 C08",任一成为最终支付事实需在订单提交时决定。 +- 回调调用方使用模拟通道身份,不使用买家登录身份;游客、买家和商家没有回调或对账管理入口。 +- 管理员只查看和闭环对账差异,不代替买家付款、不直接修改订单、支付、退款或钱包事实。 +- C08 成功回调形成“模拟通道支付”事实,不扣小金库;M05 同步支付形成“小金库支付”事实。订单只能接受其中一个成功来源。 +- 买家和商家只看到已提交的订单与支付结果,不看到回调接收、重试或差异处理内部状态。 +- M09 只消费已提交的支付成功事实;消息投递失败不能回滚支付或改变回调结果。 -## 三、核心状态流转 +## 三、回调处理结果与对账状态 -### 3.1 回调处理状态 +### 3.1 回调处理终态 ```mermaid stateDiagram-v2 - [*] --> Received: 通道发送回调 - Received --> Processing: 签名校验通过 - Processing --> Processed: 事务提交成功 - Processing --> Ignored: 订单状态拒绝目标结果 - Processing --> Difference: 已取消订单收到迟到成功 - Processing --> Failed: 事务中断或字段错误 - Failed --> Processing: 安全重试 - Processed --> [*] + [*] --> ProcessedSuccess: 合法成功回调原子确认支付 + [*] --> ProcessedFailure: 合法失败回调已记录 + [*] --> Ignored: 合法但不应改变现有终态 + [*] --> Difference: 合法回调与订单、金额或时间事实冲突 + ProcessedSuccess --> [*] + ProcessedFailure --> [*] Ignored --> [*] Difference --> [*] ``` -合法转换回执: - -- `Processed` 表示回调已被纳入账务并完成订单状态推进。 -- `Ignored` 表示回调信号被业务规则主动忽略(如订单已 `Paid` 又收到重复成功)。 -- `Difference` 表示已登记差异,等待对账阶段暴露并由管理员处理。 -- 任一终态都保留 Inbox 记录,可重复分析但不可再修改业务状态。 -- 签名错误或伪造回调**不写入 Inbox**(由 §四 流程图 X 节点处理),不进入 Inbox 状态机的任何状态;如需审计追踪,由网关层或安全日志保留。 +- `ProcessedSuccess`:模拟通道支付、订单 `Paid`、回调结果和可靠支付事实已原子提交。 +- `ProcessedFailure`:通道明确返回失败,本次回调已处理,订单未被推进为 `Paid`;支付截止时间前可由新的支付尝试继续处理。 +- `Ignored`:回调合法但属于不改变订单的旧信号或无副作用结果,仍保留首次确定回执。 +- `Difference`:已取消 / 已过期订单收到成功回调、已由其他来源支付后又收到新成功回调、金额不符或订单缺失等,需要管理员对账闭环。 +- 本期不持久化含义含混的 `Processing` / `Failed` 回调状态。事务失败时所有业务写入回滚,不存在已提交但永久卡住的处理中记录;通道重试后仍按首次请求处理。 ### 3.2 对账批次状态 ```mermaid stateDiagram-v2 - [*] --> Matched: Worker 同事务内生成,全部匹配无差异 - [*] --> HasDifferences: Worker 同事务内生成,存在差异 - HasDifferences --> Resolved: 管理员闭环所有差异 + [*] --> Matched: 批次生成且全部匹配 + [*] --> HasDifferences: 批次生成且存在差异 + HasDifferences --> Resolved: 所有差异均已闭环 Matched --> [*] Resolved --> [*] ``` -合法转换回执: - -- 同一日期同一范围不重复生成矛盾批次;Worker 重复执行以 `date + range` 唯一约束去重。 -- 批次状态由 Worker 在同事务内根据差异结果直接定为 `Matched`(全部匹配)或 `HasDifferences`(存在差异),不经过 `Pending` 中间态;这与 §七 对账批次生成流程图 I 节点"同事务内生成批次记录"一致。 -- `HasDifferences` 必须保留所有差异条目和处理状态,差异未闭环时批次仍处于 `HasDifferences`。 +- 批次在生成事务提交时直接得到 `Matched` 或 `HasDifferences`,不增加没有业务用途的 `Pending` 批次状态。 +- `HasDifferences` 只有在该批次所有差异都进入 `Resolved` 后才能变为 `Resolved`。 +- 同一对账日期和范围最多形成一个有效批次;重复调度返回已有批次,不生成矛盾统计。 ### 3.3 差异处理状态 ```mermaid stateDiagram-v2 - [*] --> Pending: 批次生成时登记 - Pending --> InProgress: 管理员开始处理 - InProgress --> Resolved: 管理员闭环处理(必须先填写处理说明) + [*] --> Pending: 批次登记差异 + Pending --> InProgress: 管理员领取 + InProgress --> Resolved: 处理完成并填写说明与证据引用 Resolved --> [*] ``` -合法转换回执: - -- 状态条件 `WHERE status = ?` 唯一推进,避免并发处理同一差异。 -- `Resolved` 必须保留处理说明和处理人,不得靠直接改库绕开记录。 -- `Pending` 和 `InProgress` 都是未完成状态,不允许直接终止;只有填完处理说明并提交事务后才能进入 `Resolved` 终止。 +- `Pending`、`InProgress`、`Resolved` 是差异状态,不得与批次状态或回调结果混用。 +- 管理员不能从 `Pending` 直接无说明关闭;同一差异并发领取或关闭时只有一次状态推进成功。 +- “已解决”只表示差异已按受控流程核实和记录,不表示 C08 自动修改了资金或订单事实。 -## 四、回调接收与鉴别 +## 四、回调接收、鉴别与幂等 ```mermaid flowchart TD - A["模拟支付通道发送回调"] --> B["接收 HTTP 回调
含 callbackId + paymentSerial + orderId + result + occurredAt + signature"] - B --> C{"签名校验通过?"} - C -- "否" --> X["拒绝:不写入数据库,不返回敏感信息"] - C -- "是" --> D{"callbackId 唯一?"} - D -- "否" --> E["命中 Inbox:返回首次处理结果"] - D -- "是" --> F{"字段合法?"} - F -- "否" --> Y["拒绝:字段错误或缺失"] - F -- "是" --> G["开启单一 PostgreSQL 事务"] - G --> H["通过 Ordering 公开应用契约锁定 orders 行 + 写入 Inbox 记录(状态 Processing)"] - H --> I["查询订单当前状态 + 判定下一动作(见第五章 幂等与乱序处理)"] - I --> J["同事务内:支付记录 + 订单状态条件更新 + Inbox 终态 + Outbox 事件"] - J --> K{"事务提交成功?"} - K -- "否" --> KR["整体回滚,Inbox 保持 Processing,等待安全重试"] - K -- "是" --> Z["事务一致性已完成:Outbox 由 M09 可靠推送"] + A["模拟通道发送回调"] --> B{"来源凭证和签名有效?"} + B -- "否" --> X["拒绝并记录安全日志,不创建业务处理记录"] + B -- "是" --> C{"唯一回调、支付流水、订单、金额、结果、发生时间等固定输入有效?"} + C -- "否" --> Y["拒绝字段错误,不进入订单处理"] + C -- "是" --> D{"回调标识或支付流水是否已有确定结果?"} + D -- "同标识同内容" --> E["重放首次确定结果,不重复记账或通知"] + D -- "同标识不同内容" --> Z["拒绝标识复用并记录冲突,不改变业务事实"] + D -- "没有结果" --> F["开启短事务并取得本次唯一处理资格"] + F --> G["读取权威订单、应付金额、状态、支付截止时间和已有支付来源"] + G --> H["按第五章结果矩阵确定 ProcessedSuccess / ProcessedFailure / Ignored / Difference"] + H --> I["按结果形成对应原子业务写入"] + I --> J{"事务整体提交?"} + J -- "否" --> R["全部回滚,不留下已提交处理中状态;通道可安全重试"] + J -- "是" --> K["返回首次确定结果;后续重复回调直接重放"] ``` -签名与字段校验: +鉴别与幂等规则: -- 签名按模拟支付通道约定计算 HMAC;签名不通过不得进入 Inbox。 -- 必填字段:`callbackId`、`paymentSerialNumber`、`orderId`、`result`、`occurredAt`、`amount`、`currency`。 -- `callbackId` 与 `paymentSerialNumber` 均建唯一约束;任一重复返回首次结果。 +- 来源校验通过只说明回调来自受控模拟通道,不代表业务必然成功;订单、金额、状态和截止时间仍由服务端重读。 +- 回调标识与支付流水都承担去重责任。任一标识命中同一内容时重放首次结果;命中不同内容时拒绝,不允许覆盖已有结果。 +- 并发到达的相同回调只有一个取得处理资格;其他请求读取并返回已提交结果。 +- 签名错误、固定字段非法和基础设施故障不伪装成支付失败终态;只有业务事务成功提交后才形成四种回调终态之一。 +- 具体签名算法、密钥轮换、请求字段和响应码在接口与架构阶段派生;流程只固定“来源可信、内容完整、不可重放篡改”的结果。 -## 五、幂等处理与乱序 +## 五、乱序、截止时间与订单竞争 -```mermaid -flowchart TD - A["已确认接收:Inbox Processing"] --> B{"该 callbackId 已存在结果?"} - B -- "是" --> BX["返回首次结果,不重复入库"] - B -- "否" --> C{"订单当前状态?"} - C -- "PendingPayment" --> D{"回调结果?"} - D -- "Success" --> E["条件推进 PendingPayment → Paid"] - D -- "Failed" --> E2["写入失败记录,订单保持 PendingPayment
不写 Outbox 支付成功事实"] - E2 --> IE2["同事务内:推进 Inbox 至 Failed(保留失败结果,不停留在 Processing)"] - C -- "Paid" --> F{"回调结果?"} - F -- "Success" --> FX["已支付成功回调,标记 Ignored,不重复写支付记录"] - F -- "Failed" --> FY["登记为差异:支付记录重复但订单已支付"] - C -- "Cancelled" --> G{"回调结果?"} - G -- "Success" --> GX["重要:已取消订单收到迟到成功 → 标记 Difference,不改为 Paid"] - G -- "Failed" --> GY["失败回调到达已取消订单,标记 Ignored"] - C -- "其他不可支付状态" --> H["标记 Ignored"] - E --> I["同事务内:写入支付记录、订单状态 Paid、Outbox 支付成功事实"] - FX --> IFX["同事务内:仅推进 Inbox 至 Ignored,不写支付记录、不发 Outbox"] - FY --> IFY["同事务内:推进 Inbox 至 Difference,登记差异条目"] - GX --> IGX["同事务内:推进 Inbox 至 Difference,登记差异条目,不改为 Paid"] - GY --> IGY["同事务内:推进 Inbox 至 Ignored"] - H --> IH["同事务内:推进 Inbox 至 Ignored"] - I --> J{"事务提交成功?"} - IE2 --> J - IFX --> J - IFY --> J - IGX --> J - IGY --> J - IH --> J - J -- "否" --> JR["整体回滚,Inbox 保持 Processing,等待安全重试"] - J -- "是" --> K["提交事务:Outbox 支付成功事实由 M09 在事务外可靠推送"] -``` +| 权威订单 / 时间事实 | 回调结果 | C08 确定结果 | 订单与资金结果 | +|---|---|---|---| +| `PendingPayment` 且早于支付截止时间,金额与订单一致 | 成功 | `ProcessedSuccess` | 原子形成模拟通道支付并推进 `Paid`;不扣小金库 | +| `PendingPayment` 且早于支付截止时间 | 失败 | `ProcessedFailure` | 订单保持待支付;本次通道尝试失败,不发支付成功事实 | +| `PendingPayment` 但已到支付截止时间 | 成功 | `Difference` | 不推进 `Paid`;登记“过期后通道成功”差异并触发 M04/C03 过期取消 | +| `PendingPayment` 但已到支付截止时间 | 失败 | `ProcessedFailure` | 不推进 `Paid`;触发 M04/C03 过期取消 | +| 已由 M05 或其他模拟通道支付 | 新的成功 | `Difference` | 不重复入账、不改变订单;登记潜在重复支付差异 | +| 已支付 | 新的失败 | `Ignored` | 保留合法 `Paid` 终态,不反向回退 | +| 已取消 | 成功 | `Difference` | 不改为 `Paid`;登记迟到成功差异 | +| 已取消或其他不可支付终态 | 失败 | `Ignored` | 不改变订单 | +| 订单不存在、金额 / 币种 / 订单关联不一致 | 任意 | `Difference` | 不创建成功支付、不改变订单,进入对账 | -乱序关键规则: +关键规则: -- `Cancelled` 订单收到迟到成功回调是核心规则,不得改为 `Paid`;必须标记 `Difference` 并由对账系统暴露。 -- 同一订单收到多次成功回调时,状态条件只允许一次从 `PendingPayment → Paid`;其余标记 `Ignored`。 -- 失败回调到达已支付订单,仍能作为支付记录的辅助记录,但不得重复创建支付记录或改变订单状态。 -- 回调事务必须在同一事务内完成:支付记录 + 订单状态 + Inbox 记录 + Outbox 事件。 +- 成功回调推进订单时,条件必须同时包含订单仍为 `PendingPayment`、权威时间早于支付截止时间、金额和订单一致。任一条件未命中都不得创建成功支付事实。 +- M05 同步钱包支付先提交时,C08 新成功回调登记差异;C08 先提交时,M05 读取 `Paid` 并返回已有状态,不扣钱包。 +- 买家主动取消可在截止时间前与支付竞争;达到截止时间后,M05 与 C08 均不得再支付,M04/C03 的过期取消成为唯一合法推进方向。 +- 已处理成功与失败乱序时,回调标识 / 支付流水去重优先;不同支付流水造成的冲突按矩阵登记差异,不覆盖已提交终态。 +- 回调发生时间用于追踪和对账,是否仍可支付以服务端处理时的权威时间和订单截止时间为准,不能信任调用方时间决定状态。 -## 六、事务一致性 +## 六、回调事务原子结果 ```mermaid flowchart TD - A["开启事务"] --> B["支付记录写入或命中"] - B --> C["订单状态条件更新"] - C --> D["Inbox/处理记录状态推进"] - D --> E["Outbox 支付成功事实写入"] - E --> F{"全部成功?"} - F -- "否" --> R["整体回滚,Inbox 保持 Processing"] - F -- "是" --> G["提交事务"] - G --> H["通知买家与商家"] + A["已通过来源、固定输入和唯一处理资格校验"] --> B{"业务结果?"} + B -- "ProcessedSuccess" --> S["模拟通道支付 + 订单 Paid + 回调终态 + 可靠支付事实"] + B -- "ProcessedFailure" --> F["失败通道尝试 + 回调终态;订单保持原状态"] + B -- "Ignored" --> I["仅记录无副作用终态和首次回执"] + B -- "Difference" --> D["回调终态 + 待处理差异;不写成功支付、不改订单终态"] + S --> C{"对应结果是否整体提交?"} + F --> C + I --> C + D --> C + C -- "否" --> R["整体回滚,视为尚无确定结果"] + C -- "是" --> O["结果可重放;仅成功结果交给 M09"] ``` -事务原子结果: - -```text -支付记录已写入(首次) -+ 订单状态条件更新 -+ Inbox/处理记录状态推进 -+ Outbox 支付成功事实写入 -= 同一事务提交成功 -``` +原子性要求: -任一步失败时整体回滚,不允许出现"支付记录已写但订单未更新"或"订单已支付但无支付记录"的部分结果。 +- `ProcessedSuccess` 的模拟通道支付、订单状态、回调终态和可靠支付事实必须同时成功或同时失败。 +- `ProcessedFailure`、`Ignored`、`Difference` 也必须先保存完整确定结果再返回;`Difference` 的回调事实与差异条目不能一有一无。 +- 任一事务回滚后,不得声称某个处理中状态仍已提交;通道使用相同标识重试时重新取得处理资格。 +- 通知发生在业务事务提交之后。通知失败只重试投递,不回滚支付、订单、回调或差异事实。 -## 七、对账批次生成 +## 七、每日对账批次 ```mermaid flowchart TD - W["Worker 定时触发"] --> A["确定对账日期与范围"] - A --> B{"该日期+范围已存在批次?"} - B -- "是" --> BX["跳过:不重复生成"] - B -- "否" --> C["开启单一 PostgreSQL 事务"] - C --> D["读取支付记录、订单状态、终态为 Processed 或 Difference 的回调 Inbox"] - D --> E["读取 M10 退款记录、wallet_ledgers 中退款入账记录"] - E --> F["比对支付记录 vs 订单状态"] - F --> G["比对退款成功 vs 退款流水 vs 钱包入账"] - G --> H["发现差异则同事务内逐条登记为待处理差异条目"] - H --> I["同事务内生成批次记录,状态按差异结果为 Matched 或 HasDifferences"] - I --> J{"事务提交成功?"} - J -- "否" --> JR["整体回滚:批次和差异明细均未生成,避免'有差异统计但没有差异明细'"] - J -- "是" --> L["通知管理员有批次生成"] + W["Worker 每日触发"] --> A["确定上一完整 UTC 接收日的固定范围"] + A --> B{"该日期和范围是否已有有效批次?"} + B -- "是" --> X["返回已有批次,不重复统计"] + B -- "否" --> C["读取范围内已提交的同步钱包支付、模拟通道支付、订单终态和回调终态"] + C --> D["读取范围内 M10 已退款事实、退款操作结果和小金库退款入账"] + D --> E["逐项比对支付、订单、回调与退款三方事实"] + E --> F["汇总总数、匹配数与差异数;逐条形成待处理差异"] + F --> G["在一个原子结果中生成批次与全部差异条目"] + G --> H{"生成是否整体成功?"} + H -- "否" --> R["全部回滚,稍后按相同范围重试"] + H -- "是且无差异" --> M["批次 Matched"] + H -- "是且有差异" --> N["批次 HasDifferences,管理员页面可查询"] ``` -关键规则: +对账口径: -- 同一日期同一范围不重复生成矛盾批次;以 `(date, range)` 唯一约束去重。 -- 批次范围只包含已提交事务的支付记录(对应 `payment.status = 'Succeeded'`)、M10 已退款的记录和终态为 `Processed` 或 `Difference` 的回调 Inbox,不包含 `Processing`、`Failed` 等中间态;`Difference` 状态承担"已取消订单收到迟到成功"等需管理员闭环的差异暴露,由 A424/A425 处理。 -- 资金类比对必须用 PostgreSQL 条件查询与聚合;不在应用层先读后算。 +- 每日范围按回调 / 支付 / 退款结果的服务端接收或提交时间归入上一完整 UTC 自然日;迟到回调在实际接收日进入后续批次,不因其通道发生时间较早而遗漏。 +- 支付至少核对:成功支付是否有对应 `Paid` 订单、`Paid` 订单是否有且只有一个成功支付来源、回调成功终态是否与支付和订单一致。 +- 回调中的 `Difference` 直接进入差异清单;`ProcessedFailure` 不应被误认作成功支付缺失。 +- 退款至少核对:M10 `Refunded` 终态、成功退款操作和本人小金库入账三方是否一一对应且金额一致。 +- Worker 重复执行同一范围只返回已有批次;任务中断时批次和差异必须同时不存在或同时完整。 +- 本期不自动修复资金和订单,也不通过对账任务发送管理员站内消息;管理员在对账页面通过查询或轮询看到新批次。 -## 八、差异识别与闭环 +## 八、差异领取与闭环 ```mermaid flowchart TD - A["管理员登录后台查看批次"] --> B["选择 HasDifferences 批次"] - B --> C["查看差异列表"] - C --> D{"选择单条差异"} - D -- "查看详情" --> E["展示订单号、支付/退款记录、状态时间线、Inbox 历史"] - D -- "开始处理" --> F["状态条件推进 Pending → InProgress"] - F --> H["填写处理说明(必填)"] - H --> I["开启单一 PostgreSQL 事务"] - I --> J["同事务内:写入处理说明 + 处理人 + 处理时间"] - J --> K["同事务内:推进差异状态 InProgress → Resolved"] - K --> L{"事务提交成功?"} - L -- "否" --> LR["整体回滚,处理说明和 Resolved 状态均未生效;状态保持 InProgress"] - L -- "是" --> M["同事务内:记录处理审计日志"] - M --> N{"批次所有差异都已 Resolved?"} - N -- "是" --> O["批次状态推进 HasDifferences → Resolved"] - N -- "否" --> P["保持 HasDifferences"] + A["管理员打开 HasDifferences 批次"] --> B["查看差异类型、关联业务事实和时间线"] + B --> C{"领取 Pending 差异?"} + C -- "状态已变化" --> X["返回当前处理人和状态,不重复领取"] + C -- "成功" --> D["差异进入 InProgress"] + D --> E["管理员通过所属模块核实;必要纠正必须使用该模块受控业务动作"] + E --> F["填写处理说明、结果和业务动作证据引用"] + F --> G["原子校验差异仍为 InProgress,写入处理结果并推进为 Resolved"] + G --> H["同一原子结果内重新判断该批次是否仍有未解决差异"] + H --> I{"是否全部解决?"} + I -- "否" --> J["批次保持 HasDifferences"] + I -- "是" --> K["批次推进为 Resolved"] + G -. "任一步失败" .-> R["整体回滚,差异保持 InProgress,允许补充后重试"] ``` -关键规则: - -- 差异修复必须可追踪,不能通过直接改库隐藏原因。 -- 状态条件 `WHERE status = 'Pending'` 或 `WHERE status = 'InProgress'` 唯一推进。 -- 售后显示退款成功但钱包未入账、钱包重复入账或退款金额不一致均必须进入对账差异。 +- C08 差异处理本身不直接改订单、支付、退款或钱包;如确需纠正,管理员先通过事实所属模块的受控动作完成,再在差异中引用结果。 +- 处理说明、处理人、处理时间、证据引用、差异终态和最后一个差异触发的批次终态必须形成可恢复的一致结果。 +- 不建设需求外的通用后台审计框架;差异记录自身的状态时间线就是本流程所需追踪证据。 +- 管理员并发领取或关闭同一差异时只有一人成功,其他请求返回最新状态。 ## 九、退款对账 ```mermaid flowchart TD - A["对账批次生成时"] --> B["读取 M10 退款成功记录"] - B --> C["读取 wallet_ledgers 中退款入账记录"] - C --> D["读取 refunds 表中对应记录"] - D --> E{"三方一致?"} - E -- "是" --> F["标记为匹配"] - E -- "否" --> G["登记差异条目"] - G --> H["管理员进入差异处理流程"] + A["每日批次读取 M10 最终结果"] --> B{"售后是否为 Refunded?"} + B -- "否" --> X["不按成功退款参与三方匹配;RefundFailed 保留给 M10 重试"] + B -- "是" --> C["读取对应成功退款操作"] + C --> D["读取本人小金库退款入账"] + D --> E{"售后终态、退款操作、钱包入账是否一一对应且金额一致?"} + E -- "是" --> F["计为匹配"] + E -- "否" --> G["登记退款差异:缺失、重复或金额不一致"] ``` -退款对账匹配规则: - -- `refunds.success` 与 `wallet_ledgers` 中退款入账记录按 `(refund_id)` 一一对应。 -- 金额不一致、缺失或重复入账均登记为差异。 -- 售后退款成功但钱包未入账的情况必须被发现。 +- 只有 M10 已明确进入 `Refunded` 的申请才要求存在成功退款操作和钱包入账。 +- `RefundFailed` 表示售后尚未完成退款,不应伪造成功退款记录,也不应被对账误判为“成功记录缺失”;重试后进入 `Refunded` 再参与成功三方匹配。 +- 钱包重复入账、退款操作重复、任一事实缺失或金额不一致都必须登记差异。 +- 对账只发现和追踪问题,不代替 M10 的退款重试,也不直接增加或扣减钱包余额。 ## 十、异常、回滚与责任 -| 场景 | C08 处理 | 最终状态/责任 | +| 场景 | C08 处理 | 最终状态 / 责任 | |---|---|---| -| 同一回调重复到达 | 返回首次结果 | 不重复记账或通知 | -| 成功与失败回调乱序 | 按状态机保留合法终态 | 冲突记录可追踪结果 | -| 已取消订单收到迟到成功 | 不改为 `Paid`,标记 Difference | 由对账系统暴露后管理员闭环 | -| 事务处理中断 | 整体回滚,安全重试 | 仍只处理一次 | -| Worker 重复执行对账 | 同一日期范围不重复生成 | 批次唯一约束 | -| 差异处理并发 | 状态条件唯一胜出 | 仅一次有效处理 | -| 签名错误或伪造 | 拒绝 | 不写入数据库 | -| 字段缺失或非法 | 拒绝 | 不写入数据库 | -| 通知暂时失败 | 回调事实保留 | M09 按可靠机制重试 | -| 管理员误操作 | 差异状态可恢复 | 不允许直接改库,必须通过差异处理流程 | +| 来源签名无效或固定字段非法 | 拒绝 | 不改变订单、支付或对账事实 | +| 同标识同内容重复到达 | 重放首次确定结果 | 不重复入账、推进状态或通知 | +| 同标识不同内容 | 拒绝复用并记录冲突 | 不覆盖首次结果 | +| 并发重复回调 | 唯一处理资格只允许一次成功 | 其他请求读取已提交结果 | +| 成功与失败乱序 | 按唯一标识、支付流水和订单终态裁决 | 不让 `Paid` 反向跳变 | +| 过期或已取消订单收到成功回调 | 登记 `Difference` | 不推进 `Paid` | +| 已由 M05 支付后收到新成功回调 | 登记 `Difference` | 不重复支付,不扣钱包 | +| 回调事务失败 | 整体回滚 | 无已提交处理中记录,可安全重试 | +| Worker 重复或中断 | 返回已有批次或整体重试 | 不生成重复 / 半批次 | +| 差异并发领取或解决 | 一次状态推进成功 | 其他请求返回当前状态 | +| 管理员直接改库企图隐藏差异 | 不属于合法流程 | 必须使用所属模块受控动作并留证 | +| M09 通知暂时失败 | 保留支付成功事实 | 可靠投递重试,不修改回调结果 | ## 十一、由流程派生的接口契约映射 -本节是第二至十章业务流程的下游映射,不是流程输入。先确认"要完成什么业务动作、处于什么状态、成功或失败后得到什么结果",再决定由哪个接口承载。若现有 Axxx 与流程冲突,先记录接口缺口并修改接口设计;只有业务需求本身变化时,才回到前文重新评审流程。 +本节是第二至第十章业务流程的下游映射,不是流程输入。现有接口若与回调终态、截止时间、原子结果或对账状态冲突,应修改接口,而不是按旧请求字段或状态码修改前文。 -| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +| 已确认流程能力 | 派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 接收模拟支付通道回调 | A421 | 签名校验、字段校验、Inbox 唯一约束 + 乱序处理 | 待交叉评审 | -| 对账批次列表 | A422 | 按日期分页查询批次及状态 | 待交叉评审 | -| 对账批次详情 | A423 | 展示批次范围、总数、匹配/差异数和按类型汇总 | 待交叉评审 | -| 差异列表 | A424 | 按批次分页查询差异条目 | 待交叉评审 | -| 差异处理 | A425 | 管理员推进差异状态并记录处理说明 | 待交叉评审 | - -接口详细定义与实现必须承接上述流程结果。回调接口必须支持 `Idempotency-Key`(与 `callbackId` 同值);后台对账接口必须使用 `AdminOnly` 策略;签名校验和唯一约束必须在数据库侧强制。 - -## 十二、扩展接入边界 - -- F10 同步支付:当前 F10 走 M05 钱包同步支付;C08 走模拟异步通道回调。任一成为最终支付事实需在订单提交时确定;订单进入 `PendingPayment` 之后,只能由一方推进,避免双结果。 -- C10 多实例:两个 API 实例必须能同时处理回调并保持一致性;唯一约束 + 事务边界保证不重复;Nginx 负载均衡转发回调保持 `X-Forwarded-For` 与 traceId。 -- X04 售后退款:所有退款成功记录在批次生成阶段纳入三方对账;退款失败但订单已推进的状态由 M10 维护,不进入 C08 业务差异。 -- M09 消息:回调事务成功后 Outbox 支付成功事实;M09 通知失败按可靠机制重试,不反向修改回调事实。 -- C06 实时推送:差异闭环结果可推送给管理员;推送失败不影响差异处理事务。 - -## 十三、由流程反查出的接口与数据待评审项 - -1. C08 与 F10 同步支付的边界:流程要求二者任一成为最终支付事实;当前 M05 同步支付未明确"是否同时走 C08 通道",需在订单提交时确定走哪条路径。 -2. 回调签名密钥:流程要求按模拟支付通道约定验证字段和来源;A421 签名密钥管理与轮换由 M00 公共能力承接,待评审。 -3. 失败回调对账:流程要求按需识别"失败"语义;当前批次生成只覆盖成功支付的对账,失败回调的对账口径待评审。 -4. 回调幂等窗口:流程要求幂等记录可恢复;A421 Inbox 记录保留期与归档策略(数据库 vs 缓存)需在数据库设计中确认。 -5. 迟到回调时间窗口:流程未明确"多迟算迟到";迟到窗口与对账批次范围(如:批次范围只覆盖昨天完成的支付)需在 Worker 调度中定义。 -6. 差异状态字段:流程要求差异有受控状态;DBxxx 差异表字段(type、status、resolutionNote、resolvedBy、resolvedAt)需在数据库评审中确认。 -7. 退款对账范围:流程要求每日核对退款成功 + 退款流水 + 钱包入账;M10 退款是否纳入 C08 每日批次,还是按售后单独批次,待评审。 -8. 批次切分粒度:流程要求按日生成批次;当日订单量较大时是否按小时或范围切分,影响 Worker 性能与差异范围精度。 -9. 管理员差异化处理权限:流程要求 AdminOnly 推进差异;A425 是否允许区分"查看差异"与"处理差异"两个权限粒度,避免误操作。 -10. DB081~DB100 尚未形成可实施的完整表定义,数据库字段、约束和索引必须由数据库设计任务另行确认。 +| 接收受控模拟支付回调 | A421 | 来源鉴别、固定输入校验、双重业务标识幂等、截止时间与状态竞争、四种确定终态 | 待重建详细契约 | +| 对账批次列表 | A422 | 管理员按日期与状态分页查看 `Matched` / `HasDifferences` / `Resolved` 批次 | 待重建详细契约 | +| 对账批次详情 | A423 | 返回范围、总数、匹配数、差异数、类型汇总和当前闭环状态 | 待重建详细契约 | +| 差异列表 / 详情 | A424 | 管理员按批次、类型和状态查询差异事实与时间线 | 待重建详细契约 | +| 差异领取与解决 | A425 | 条件领取、必填处理说明和证据引用、原子关闭差异并按需关闭批次 | 待重建详细契约 | + +A421 的业务幂等由回调标识、支付流水和请求指纹承载,不要求客户端另造一个独立幂等语义;A422~A425 只向管理员开放。具体 HTTP 方法、路径、签名头、请求响应、状态码和错误码在接口阶段由本表派生。 + +## 十二、跨模块边界 + +- **M05/F10**:小金库同步支付是默认买家路径;C08 不扣小金库。两者只通过 M04 订单状态竞争,不能同时形成成功支付。 +- **M04/F09/C03**:订单状态和支付截止时间是权威裁决。到期后即使 C03 尚未扫描,C08 也不能确认支付;过期回调触发同一内部取消能力。 +- **M09**:只通知已提交的支付成功结果。失败、忽略和差异不作为买家 / 商家支付成功通知;管理员通过对账页面查看差异。 +- **M10**:只把 `Refunded` 终态、成功退款操作和钱包入账纳入成功三方对账;`RefundFailed` 仍由 M10 重试。 +- **C10**:多个 API 实例可并发接收回调,业务唯一处理资格与共享事务确保只处理一次;不能依赖进程内记忆去重。 +- **C06**:不向管理员建立需求外实时推送;对账页面使用查询或轮询即可。 + +## 十三、接口与后续数据设计必须承接的事实 + +1. 回调终态只有 `ProcessedSuccess`、`ProcessedFailure`、`Ignored`、`Difference`;基础设施处理失败不占用这四个业务终态。 +2. 同一回调标识和同一支付流水都只能绑定一个请求内容和一个首次确定结果;并发重复不能形成两笔成功支付。 +3. 成功回调必须同时满足待支付状态、截止时间、金额、币种和订单关联,且模拟通道支付、订单 `Paid`、回调终态与可靠支付事实原子提交。 +4. 回调失败事务整体回滚后不存在已提交 `Processing` 记录;不得设计“同事务回滚但 Processing 仍保留”的不可能状态。 +5. 对账批次直接生成 `Matched` 或 `HasDifferences`,不增加 `Pending`;差异自身使用 `Pending` / `InProgress` / `Resolved`。 +6. 最后一个差异关闭时,差异处理结果和批次 `Resolved` 必须在一致边界内完成,不能先返回成功再异步补批次状态。 +7. C08 的 `Difference`、每日比对发现的支付差异和退款差异都进入统一差异闭环,但必须保留类型和来源。 +8. 支付成功来源必须区分小金库与受控模拟通道;同一订单最多一个成功来源,回调不得生成钱包流水。 +9. 管理员差异处理不直接更新业务表;需要纠正时必须引用所属模块的受控操作结果。 +10. 接口完成后从本文逐项复核鉴别、幂等、截止时间、乱序、事务、批次和差异状态,不能用现有 A421~A425 草案反向修改流程。 ## 十四、验收证据清单 -- [ ] 重复回调不重复记账、不重复通知,返回首次结果。 -- [ ] 成功与失败回调乱序到达时,订单状态按状态机保留合法终态。 -- [ ] 已取消订单收到迟到成功回调不改为 `Paid`,登记为差异。 -- [ ] 事务中断可安全重试,且不会重复处理。 -- [ ] Worker 重复执行对账不重复生成矛盾批次。 -- [ ] 差异处理并发仅一次有效推进。 -- [ ] 每日对账可列出匹配与差异,差异状态及处理记录可追踪。 -- [ ] 退款对账能够发现售后退款成功、退款流水和钱包入账之间的缺失、重复及金额不一致。 -- [ ] 买家和商家只看到稳定业务状态,不展示回调内部过程。 -- [ ] 管理员能够查看并闭环差异,差异修复可追踪。 -- [ ] 签名错误或伪造回调不被写入数据库。 -- [ ] 字段缺失或非法的回调不被写入数据库。 -- [ ] 保留回调重放脚本、乱序脚本、原始结果和对账清单。 -- [ ] 解释唯一约束、事务边界、Inbox/Outbox 和修复流程的工作原理。 +- [ ] 同一回调标识或支付流水重复提交只返回首次结果,不重复支付、更新订单或通知。 +- [ ] 同一标识不同内容被拒绝,首次结果不被覆盖。 +- [ ] 合法成功回调仅在订单仍为 `PendingPayment` 且早于截止时间时推进 `Paid`,并且不扣小金库。 +- [ ] M05 同步支付与 C08 成功回调并发时最多一个成功来源;另一方不重复入账。 +- [ ] 订单已取消、已过期或已支付后收到新的成功回调,订单终态不变并登记差异。 +- [ ] 成功与失败回调乱序不会让 `Paid` 反向跳变。 +- [ ] 回调事务任一步失败时无支付、订单、回调或可靠事实半提交,原回调可安全重试。 +- [ ] 回调业务终态中不存在含义冲突的 `Processing` / `Failed`;失败通道结果与处理器故障可以明确区分。 +- [ ] Worker 重复执行同一 UTC 日期与范围只得到一个有效批次;中断不留下半批次或孤立差异。 +- [ ] 批次只使用 `Matched` / `HasDifferences` / `Resolved`,差异只使用 `Pending` / `InProgress` / `Resolved`。 +- [ ] 管理员并发领取或关闭同一差异仅一次成功,处理说明、证据和批次收敛一致。 +- [ ] 对账能发现支付成功但订单未更新、订单已支付但无成功支付、重复成功来源和回调差异。 +- [ ] 退款对账能发现 M10 `Refunded`、成功退款操作与小金库入账之间的缺失、重复和金额不一致。 +- [ ] `RefundFailed` 不伪造成功退款记录,M10 重试成功后才进入成功三方匹配。 +- [ ] 买家和商家只看到稳定业务状态;管理员通过对账页面查询差异,不依赖 M09/C06 推送。 +- [ ] 保留回调重放、乱序、迟到、同步支付竞争、事务回滚、每日对账和差异闭环的真实证据。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" index b377b7e..a06149b 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" @@ -4,7 +4,7 @@ > 覆盖:M05-01、F10 > 基础核心流程:F08、F09 > 直接协作:韦乾强(M04 Ordering)、罗皓晨(M09 消息) -> 文档状态:F10 同步核心已按需求校准,可作为核心接口设计输入;待 Ordering/C03 交叉评审,C08 异步替代边界在 C08 流程任务中冻结 +> 文档状态:F10 同步核心已按需求校准,可作为核心接口设计输入;待 Ordering/C03 交叉评审,C08 受控模拟回调边界已冻结 > 需求事实源:[需求规格说明书 M05-01](../../../01-需求文档/需求规格说明书.md) 的“M05-01 模拟支付(F10)”完整七节 ## 一、范围与事实来源 @@ -215,7 +215,7 @@ flowchart TD ## 九、扩展接入边界 - X04 售后退款通过 Payment 的公开应用能力幂等退回小金库,不直接修改钱包内部数据;不属于本文 F10 主流程。 -- C08 从“支付确认阶段”接入重复/乱序回调与每日对账;在明确替换 F10 的哪个同步步骤之前,不得让同步结果和异步回调同时成为最终支付事实。 +- C08 是不扣小金库的受控挑战模拟通道,不替换 F10 默认同步钱包路径。C08 成功回调与 F10、F09、C03 共同以订单状态和支付截止时间竞争:最多一方把待支付订单推进为已支付或已取消;回调先成功时后续钱包支付返回已有已支付结果且不扣款,钱包支付先成功时新的成功回调登记差异且不得重复入账。 - X03/M09 只消费支付事务提交后的支付成功事实;消息失败不能反向修改支付或订单状态。具体事件名和可靠投递机制由系统架构设计派生。 ## 十、由流程反查出的接口与数据待评审项 -- Gitee From 48204d9ad16dcef21f48da78d906eb4e05855795 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 19:52:01 +0800 Subject: [PATCH 091/118] =?UTF-8?q?docs(process):=20=E8=A1=A5=E4=B8=A5=20C?= =?UTF-8?q?08=20=E4=B9=B1=E5=BA=8F=E8=81=9A=E5=90=88=E4=B8=8E=E5=B7=AE?= =?UTF-8?q?=E5=BC=82=E9=97=AD=E7=8E=AF=EF=BC=9B=E6=B6=88=E9=99=A4=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E5=92=8C=E6=BC=8F=E8=B4=A6=E9=A3=8E=E9=99=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...71\350\264\246\346\265\201\347\250\213.md" | 92 ++++++++++++------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" index 98998b6..d0d9268 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" @@ -113,11 +113,11 @@ flowchart TD B -- "否" --> X["拒绝并记录安全日志,不创建业务处理记录"] B -- "是" --> C{"唯一回调、支付流水、订单、金额、结果、发生时间等固定输入有效?"} C -- "否" --> Y["拒绝字段错误,不进入订单处理"] - C -- "是" --> D{"回调标识或支付流水是否已有确定结果?"} - D -- "同标识同内容" --> E["重放首次确定结果,不重复记账或通知"] - D -- "同标识不同内容" --> Z["拒绝标识复用并记录冲突,不改变业务事实"] - D -- "没有结果" --> F["开启短事务并取得本次唯一处理资格"] - F --> G["读取权威订单、应付金额、状态、支付截止时间和已有支付来源"] + C -- "是" --> D{"回调标识是否已有确定结果?"} + D -- "同标识同内容" --> E["重放该回调的首次确定结果,不重复记账或通知"] + D -- "同标识不同内容" --> Z["拒绝回调标识复用并记录安全冲突,不改变业务事实"] + D -- "新回调标识" --> F["开启短事务并取得该回调的唯一处理资格"] + F --> G["读取支付流水聚合、权威订单、应付金额、状态、支付截止时间和已有支付来源"] G --> H["按第五章结果矩阵确定 ProcessedSuccess / ProcessedFailure / Ignored / Difference"] H --> I["按结果形成对应原子业务写入"] I --> J{"事务整体提交?"} @@ -128,7 +128,9 @@ flowchart TD 鉴别与幂等规则: - 来源校验通过只说明回调来自受控模拟通道,不代表业务必然成功;订单、金额、状态和截止时间仍由服务端重读。 -- 回调标识与支付流水都承担去重责任。任一标识命中同一内容时重放首次结果;命中不同内容时拒绝,不允许覆盖已有结果。 +- 回调标识唯一标记一次回调投递:同一回调标识、同一内容只重放该回调的首次确定结果;同一回调标识换内容属于篡改或标识复用,直接拒绝。 +- 支付流水标识一次模拟通道支付尝试,而不是第二个回调幂等键。同一支付流水可以按时间收到多个不同回调标识,C08 必须聚合这些信号并处理先失败后成功、先成功后失败等乱序。 +- 支付流水第一次出现时绑定订单、金额和币种;后续回调若改变这些不可变事实,形成 `Difference` 来源并拒绝业务推进,但不能在幂等门口静默丢弃。 - 并发到达的相同回调只有一个取得处理资格;其他请求读取并返回已提交结果。 - 签名错误、固定字段非法和基础设施故障不伪装成支付失败终态;只有业务事务成功提交后才形成四种回调终态之一。 - 具体签名算法、密钥轮换、请求字段和响应码在接口与架构阶段派生;流程只固定“来源可信、内容完整、不可重放篡改”的结果。 @@ -152,7 +154,9 @@ flowchart TD - 成功回调推进订单时,条件必须同时包含订单仍为 `PendingPayment`、权威时间早于支付截止时间、金额和订单一致。任一条件未命中都不得创建成功支付事实。 - M05 同步钱包支付先提交时,C08 新成功回调登记差异;C08 先提交时,M05 读取 `Paid` 并返回已有状态,不扣钱包。 - 买家主动取消可在截止时间前与支付竞争;达到截止时间后,M05 与 C08 均不得再支付,M04/C03 的过期取消成为唯一合法推进方向。 -- 已处理成功与失败乱序时,回调标识 / 支付流水去重优先;不同支付流水造成的冲突按矩阵登记差异,不覆盖已提交终态。 +- 同一支付流水先失败后成功时,新的成功回调仍要重新执行本章矩阵:订单仍可支付则形成 `ProcessedSuccess`,订单已过期或已有其他终态则形成 `Difference`。 +- 同一支付流水先成功后失败时,新的失败回调形成 `Ignored`,不得把支付或订单回退;同一支付流水重复成功且不可变事实一致时也形成无副作用的 `Ignored`。 +- 不同支付流水对同一订单宣称成功时,第一笔合法成功来源保留,后续成功回调形成 `Difference`,不得覆盖已提交终态。 - 回调发生时间用于追踪和对账,是否仍可支付以服务端处理时的权威时间和订单截止时间为准,不能信任调用方时间决定状态。 ## 六、回调事务原子结果 @@ -163,7 +167,7 @@ flowchart TD B -- "ProcessedSuccess" --> S["模拟通道支付 + 订单 Paid + 回调终态 + 可靠支付事实"] B -- "ProcessedFailure" --> F["失败通道尝试 + 回调终态;订单保持原状态"] B -- "Ignored" --> I["仅记录无副作用终态和首次回执"] - B -- "Difference" --> D["回调终态 + 待处理差异;不写成功支付、不改订单终态"] + B -- "Difference" --> D["回调终态及完整差异来源;不提前创建批次差异,不写成功支付、不改订单终态"] S --> C{"对应结果是否整体提交?"} F --> C I --> C @@ -175,22 +179,24 @@ flowchart TD 原子性要求: - `ProcessedSuccess` 的模拟通道支付、订单状态、回调终态和可靠支付事实必须同时成功或同时失败。 -- `ProcessedFailure`、`Ignored`、`Difference` 也必须先保存完整确定结果再返回;`Difference` 的回调事实与差异条目不能一有一无。 +- `ProcessedFailure`、`Ignored`、`Difference` 也必须先保存完整确定结果再返回;`Difference` 回调结果本身保留类型、来源和关联事实,供每日批次读取。 +- 回调事务不直接创建管理员待处理差异条目。每日对账只把一个 `Difference` 来源归入一个有效批次和一个差异条目,避免回调阶段与批次阶段重复建单。 - 任一事务回滚后,不得声称某个处理中状态仍已提交;通道使用相同标识重试时重新取得处理资格。 -- 通知发生在业务事务提交之后。通知失败只重试投递,不回滚支付、订单、回调或差异事实。 +- 通知发生在业务事务提交之后。通知失败只重试投递,不回滚支付、订单或回调事实。 ## 七、每日对账批次 ```mermaid flowchart TD - W["Worker 每日触发"] --> A["确定上一完整 UTC 接收日的固定范围"] + W["Worker 每日触发"] --> A["确定上一完整 UTC 业务提交日的固定范围"] A --> B{"该日期和范围是否已有有效批次?"} B -- "是" --> X["返回已有批次,不重复统计"] B -- "否" --> C["读取范围内已提交的同步钱包支付、模拟通道支付、订单终态和回调终态"] C --> D["读取范围内 M10 已退款事实、退款操作结果和小金库退款入账"] D --> E["逐项比对支付、订单、回调与退款三方事实"] - E --> F["汇总总数、匹配数与差异数;逐条形成待处理差异"] - F --> G["在一个原子结果中生成批次与全部差异条目"] + E --> F["合并指向同一业务差异的来源与比较规则;汇总总数、匹配数与差异数"] + F --> F2["未归属来源逐条形成待处理差异;已有来源复用唯一差异条目"] + F2 --> G["在一个原子结果中生成批次与全部差异条目"] G --> H{"生成是否整体成功?"} H -- "否" --> R["全部回滚,稍后按相同范围重试"] H -- "是且无差异" --> M["批次 Matched"] @@ -199,9 +205,11 @@ flowchart TD 对账口径: -- 每日范围按回调 / 支付 / 退款结果的服务端接收或提交时间归入上一完整 UTC 自然日;迟到回调在实际接收日进入后续批次,不因其通道发生时间较早而遗漏。 +- 每日范围只按业务结果成功提交时的服务端时间归入 UTC 自然日,不使用回调接收时间、通道发生时间或客户端时间。跨 UTC 零点收到但在零点后提交的结果归入新的一日。 +- 批次使用固定的日界线和一致读取水位,只纳入在范围结束前已经成功提交并可见的事实;未在该水位前提交的事实进入其实际提交日的后续批次,不能回填已冻结旧批次。 - 支付至少核对:成功支付是否有对应 `Paid` 订单、`Paid` 订单是否有且只有一个成功支付来源、回调成功终态是否与支付和订单一致。 -- 回调中的 `Difference` 直接进入差异清单;`ProcessedFailure` 不应被误认作成功支付缺失。 +- 回调中的 `Difference` 是差异来源,不是预先创建的差异条目。批次按“差异业务对象及关联标识 + 比较规则”建立唯一归属;来源类型只作为证据。同一问题同时被回调结果和横向比对发现时合并为一个条目并保留全部证据引用。 +- `ProcessedFailure` 不应被误认作成功支付缺失;同一支付流水后续成功时,以后续成功回调的提交日和最终聚合结果参加对账。 - 退款至少核对:M10 `Refunded` 终态、成功退款操作和本人小金库入账三方是否一一对应且金额一致。 - Worker 重复执行同一范围只返回已有批次;任务中断时批次和差异必须同时不存在或同时完整。 - 本期不自动修复资金和订单,也不通过对账任务发送管理员站内消息;管理员在对账页面通过查询或轮询看到新批次。 @@ -213,18 +221,29 @@ flowchart TD A["管理员打开 HasDifferences 批次"] --> B["查看差异类型、关联业务事实和时间线"] B --> C{"领取 Pending 差异?"} C -- "状态已变化" --> X["返回当前处理人和状态,不重复领取"] - C -- "成功" --> D["差异进入 InProgress"] - D --> E["管理员通过所属模块核实;必要纠正必须使用该模块受控业务动作"] - E --> F["填写处理说明、结果和业务动作证据引用"] - F --> G["原子校验差异仍为 InProgress,写入处理结果并推进为 Resolved"] - G --> H["同一原子结果内重新判断该批次是否仍有未解决差异"] - H --> I{"是否全部解决?"} - I -- "否" --> J["批次保持 HasDifferences"] - I -- "是" --> K["批次推进为 Resolved"] - G -. "任一步失败" .-> R["整体回滚,差异保持 InProgress,允许补充后重试"] + C -- "成功" --> D["差异进入 InProgress,并记录当前领取人"] + D --> T{"当前领取人仍有效且持有领取权?"} + T -- "否" --> U["其他管理员显式接管或原领取人释放;保留领取与转交历史"] + T -- "是" --> E["通过所属模块核实;必要纠正必须使用该模块受控业务动作"] + U --> E + E --> F{"选择受控处置结果"} + F -- "已纠正" --> F1["引用所属模块已完成的纠正动作"] + F -- "确认无业务影响" --> F2["引用可验证的重复、迟到或无资金变更规则"] + F1 --> G["系统重新读取权威事实并重跑原比较规则"] + F2 --> G + G --> V{"差异已消除,或规则能证明无未决资金 / 订单影响?"} + V -- "否" --> N["拒绝关闭,保持 InProgress 并展示仍不一致的事实"] + V -- "是" --> H["原子记录处置类型、说明、证据和复核结果并推进为 Resolved"] + H --> I{"该批次是否仍有未解决差异?"} + I -- "是" --> J["批次保持 HasDifferences"] + I -- "否" --> K["同一原子结果内将批次推进为 Resolved"] + H -. "任一步失败" .-> R["整体回滚,差异保持 InProgress,允许补充后重试"] ``` - C08 差异处理本身不直接改订单、支付、退款或钱包;如确需纠正,管理员先通过事实所属模块的受控动作完成,再在差异中引用结果。 +- 差异只有两类可关闭结果:“所属模块已纠正且复核一致”或“按固定规则确认没有未决资金与订单影响”。仅填写文字说明、承诺稍后处理或接受仍存在的不一致,都不能进入 `Resolved`。 +- 关闭时必须重新读取权威业务事实并执行产生该差异的同一比较规则;复核仍不一致时保持 `InProgress`。 +- 当前领取人被禁用、主动释放或领取权失效时,其他管理员可显式接管;接管不清空原处理记录。领取有效期和续期方式由接口与配置派生,但任何时刻只有当前有效领取人可以提交关闭。 - 处理说明、处理人、处理时间、证据引用、差异终态和最后一个差异触发的批次终态必须形成可恢复的一致结果。 - 不建设需求外的通用后台审计框架;差异记录自身的状态时间线就是本流程所需追踪证据。 - 管理员并发领取或关闭同一差异时只有一人成功,其他请求返回最新状态。 @@ -270,13 +289,13 @@ flowchart TD | 已确认流程能力 | 派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 接收受控模拟支付回调 | A421 | 来源鉴别、固定输入校验、双重业务标识幂等、截止时间与状态竞争、四种确定终态 | 待重建详细契约 | +| 接收受控模拟支付回调 | A421 | 来源鉴别、回调标识幂等、支付流水聚合与乱序、截止时间和状态竞争、四种确定终态 | 待重建详细契约 | | 对账批次列表 | A422 | 管理员按日期与状态分页查看 `Matched` / `HasDifferences` / `Resolved` 批次 | 待重建详细契约 | | 对账批次详情 | A423 | 返回范围、总数、匹配数、差异数、类型汇总和当前闭环状态 | 待重建详细契约 | | 差异列表 / 详情 | A424 | 管理员按批次、类型和状态查询差异事实与时间线 | 待重建详细契约 | -| 差异领取与解决 | A425 | 条件领取、必填处理说明和证据引用、原子关闭差异并按需关闭批次 | 待重建详细契约 | +| 差异领取、转交与解决 | A425 | 条件领取 / 接管、受控处置类型、权威事实复核、原子关闭差异并按需关闭批次 | 待重建详细契约 | -A421 的业务幂等由回调标识、支付流水和请求指纹承载,不要求客户端另造一个独立幂等语义;A422~A425 只向管理员开放。具体 HTTP 方法、路径、签名头、请求响应、状态码和错误码在接口阶段由本表派生。 +A421 以回调标识和请求指纹保证同一次投递幂等,以支付流水聚合一次支付尝试的多个时序信号,不要求客户端另造独立幂等语义;A422~A425 只向管理员开放。具体 HTTP 方法、路径、签名头、请求响应、状态码和错误码在接口阶段由本表派生。 ## 十二、跨模块边界 @@ -290,29 +309,32 @@ A421 的业务幂等由回调标识、支付流水和请求指纹承载,不要 ## 十三、接口与后续数据设计必须承接的事实 1. 回调终态只有 `ProcessedSuccess`、`ProcessedFailure`、`Ignored`、`Difference`;基础设施处理失败不占用这四个业务终态。 -2. 同一回调标识和同一支付流水都只能绑定一个请求内容和一个首次确定结果;并发重复不能形成两笔成功支付。 +2. 同一回调标识只能绑定一个请求内容和一个首次确定结果;同一支付流水绑定稳定的订单、金额和币种,但允许多个回调标识形成有序聚合,必须支持失败后成功和成功后失败。 3. 成功回调必须同时满足待支付状态、截止时间、金额、币种和订单关联,且模拟通道支付、订单 `Paid`、回调终态与可靠支付事实原子提交。 4. 回调失败事务整体回滚后不存在已提交 `Processing` 记录;不得设计“同事务回滚但 Processing 仍保留”的不可能状态。 5. 对账批次直接生成 `Matched` 或 `HasDifferences`,不增加 `Pending`;差异自身使用 `Pending` / `InProgress` / `Resolved`。 6. 最后一个差异关闭时,差异处理结果和批次 `Resolved` 必须在一致边界内完成,不能先返回成功再异步补批次状态。 -7. C08 的 `Difference`、每日比对发现的支付差异和退款差异都进入统一差异闭环,但必须保留类型和来源。 +7. C08 的 `Difference`、每日比对发现的支付差异和退款差异都进入统一差异闭环,但同一业务差异与比较规则只能归属一个有效差异条目,并保留所有发现来源和证据。 8. 支付成功来源必须区分小金库与受控模拟通道;同一订单最多一个成功来源,回调不得生成钱包流水。 -9. 管理员差异处理不直接更新业务表;需要纠正时必须引用所属模块的受控操作结果。 -10. 接口完成后从本文逐项复核鉴别、幂等、截止时间、乱序、事务、批次和差异状态,不能用现有 A421~A425 草案反向修改流程。 +9. 管理员差异处理不直接更新业务表;需要纠正时必须引用所属模块的受控操作结果,并由系统重跑原比较规则确认已经一致。 +10. 对账归属只使用成功提交时间和固定 UTC 水位;领取转交、处置类型、复核失败和当前领取人权限必须由接口承载。 +11. 接口完成后从本文逐项复核鉴别、幂等、截止时间、乱序、事务、批次和差异状态,不能用现有 A421~A425 草案反向修改流程。 ## 十四、验收证据清单 -- [ ] 同一回调标识或支付流水重复提交只返回首次结果,不重复支付、更新订单或通知。 -- [ ] 同一标识不同内容被拒绝,首次结果不被覆盖。 +- [ ] 同一回调标识重复提交只返回该回调首次结果;同标识换内容被拒绝且首次结果不被覆盖。 +- [ ] 同一支付流水的失败后成功、成功后失败和重复成功均按聚合规则处理,既不被幂等门口误拒绝,也不重复支付。 - [ ] 合法成功回调仅在订单仍为 `PendingPayment` 且早于截止时间时推进 `Paid`,并且不扣小金库。 - [ ] M05 同步支付与 C08 成功回调并发时最多一个成功来源;另一方不重复入账。 - [ ] 订单已取消、已过期或已支付后收到新的成功回调,订单终态不变并登记差异。 - [ ] 成功与失败回调乱序不会让 `Paid` 反向跳变。 - [ ] 回调事务任一步失败时无支付、订单、回调或可靠事实半提交,原回调可安全重试。 - [ ] 回调业务终态中不存在含义冲突的 `Processing` / `Failed`;失败通道结果与处理器故障可以明确区分。 -- [ ] Worker 重复执行同一 UTC 日期与范围只得到一个有效批次;中断不留下半批次或孤立差异。 +- [ ] Worker 只按成功提交时间和固定 UTC 水位归属事实;跨零点与迟到提交不遗漏,重复执行只得到一个有效批次。 +- [ ] 回调 `Difference` 与横向比对发现的同一问题只形成一个有效差异条目,不重复计数或处理。 - [ ] 批次只使用 `Matched` / `HasDifferences` / `Resolved`,差异只使用 `Pending` / `InProgress` / `Resolved`。 -- [ ] 管理员并发领取或关闭同一差异仅一次成功,处理说明、证据和批次收敛一致。 +- [ ] 管理员并发领取、接管或关闭同一差异仅一次成功;领取人失效后可追踪转交。 +- [ ] 仍存在资金或订单不一致时不能仅凭文字说明关闭;处置类型、权威事实复核、证据和批次收敛一致。 - [ ] 对账能发现支付成功但订单未更新、订单已支付但无成功支付、重复成功来源和回调差异。 - [ ] 退款对账能发现 M10 `Refunded`、成功退款操作与小金库入账之间的缺失、重复和金额不一致。 - [ ] `RefundFailed` 不伪造成功退款记录,M10 重试成功后才进入成功三方匹配。 -- Gitee From d7bbfea8951eb33bbf39327d39c2787eb66b7074 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 20:00:23 +0800 Subject: [PATCH 092/118] =?UTF-8?q?docs(process):=20=E9=87=8D=E6=9E=84=20M?= =?UTF-8?q?10=20=E5=94=AE=E5=90=8E=E6=B5=81=E7=A8=8B=EF=BC=9B=E9=97=AD?= =?UTF-8?q?=E5=90=88=E9=80=80=E6=AC=BE=E5=BA=93=E5=AD=98=E4=B8=8E=E5=B1=A5?= =?UTF-8?q?=E7=BA=A6=E7=AB=9E=E4=BA=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...56\345\220\216\346\265\201\347\250\213.md" | 658 ++++++++++-------- 1 file changed, 359 insertions(+), 299 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" index a75a402..590b926 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" @@ -2,386 +2,446 @@ > 负责人:张海洋 > 覆盖:M10、X04 -> 基础核心流程:F08、F09、F10、F12 -> 直接协作:韦乾强(M04 Ordering)、罗皓晨(M09 消息 + M00 后台基础设施) -> 文档状态:初稿,待张海洋自审及 Ordering/C08 交叉评审 -> 需求事实源:[需求规格说明书 M10](../../../01-需求文档/需求规格说明书.md) 的"M10 售后流程(X04)"完整七节 +> 基础核心流程:F08、F10、F11、F12 +> 直接协作:韦乾强(M04 Ordering、M06-02 履约)、顾欣月(M02 Catalog)、朱惠惠(C01 Seckill)、罗皓晨(M09 Messaging) +> 文档状态:已按需求重构,可作为接口设计输入;待 Ordering、Payment、Catalog、Messaging 交叉评审 +> 需求事实源:[需求规格说明书 M10](../../../01-需求文档/需求规格说明书.md) 的“M10 售后流程(X04)”完整七节 -## 一、范围与事实来源 +## 一、范围与设计顺序 -本模块负责买家针对本人已支付、已发货或完成后 7 天内的订单项发起退款或退货申请,商家在后台审核并推进售后状态,退款采用模拟处理并退回买家小金库。它不接入真实退款渠道、不参与 C03 待支付订单超时扫描、不主动修改订单的核心履约状态。 +M10 负责订单项售后资格、申请数量占用、商家审核、退货说明、确认收货、退款推进、退款失败重试、状态时间线和查询权限。它不接入真实退款渠道,不参加待支付订单超时取消,也不以售后状态覆盖订单的核心履约状态。 -本文先依据需求确定业务参与者、状态、判断分支、事务结果和模块出入口,再由这些流程步骤派生接口能力。A411~A419 与 A432、A434 只用于流程完成后的契约映射和缺口检查(A431 已取消并入 `IRefundService` 内部应用能力),不能反向决定或拼接业务流程。 +本流程先依据需求冻结角色、资格、状态、数量、退款和库存规则,再由这些业务动作派生接口。现有接口编号只在第十二章做下游映射;若旧接口与流程冲突,应修改接口,不得反向改变本流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| -| M10/X04 需求 | 完整定义 | 作为业务语义事实源 | -| 本文业务流程 | 初稿 | 先确认角色、状态、分支、事务边界和模块出入口 | -| A411~A419、A432、A434 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | -| IRefundService 内部应用能力(A431 已取消并入) | 内部契约 | 作为退款入账的内部服务,不占 Axxx HTTP 编号 | -| DB081~DB100 数据库设计 | 模板/占位 | 本文不发明表字段、枚举编码和索引 | -| C08 退款对账 | 独立扩展 | 只登记接入点,不混入 F10 核心状态机 | -| C03 超时取消 | 独立流程 | 售后申请与退款处理不参与 C03 扫描 | +| M10 / X04 需求 | 完整定义 | 作为业务语义事实源 | +| M04 / M06-02 订单与履约边界 | 已定义、待统一整合 | 冻结订单状态、指定商家、可履约数量和发货竞争 | +| M05 退款能力 | 已定义、待由本流程补齐退款契约 | 只承接幂等退款,不决定售后资格与库存 | +| M02 / C01 库存通道 | 已定义、待统一整合 | 售后成功时按订单项原库存来源回补 | +| M09 通知 | 已定义、待按本流程校准接收人 | 只消费已经提交的售后事实 | +| 本文业务流程 | 已重构、待交叉评审 | 作为接口与后续数据设计输入 | -## 二、模块直接出入口 +## 二、参与者、事实归属与直接出入口 ```mermaid flowchart LR - ID["M01 Identity
已认证买家、角色和账号状态"] -->|"BuyerOnly 通过"| AS["M10 AfterSales
申请、审核、状态时间线"] - ID -->|"MerchantOnly 通过"| AS - ORD["M04 Ordering
订单项、归属、实付快照、当前状态"] -->|"本人订单项事实"| AS - AS -->|"仅退款审核通过或退货确认收货后"| PAY["M05 Payment
IRefundService(内部应用能力)"] - PAY -->|"退款成功:钱包入账 + 退款流水"| AS - AS -->|"申请提交、审核结果、退款结果"| MSG["M09 消息持久化/通知"] - AS -->|"退货数量回补"| INV["M02 Catalog
库存按订单项归属通道回补"] - RECON["C08 对账 Worker"] -. "每日对账" .-> AS - - ID -->|"游客、非授权角色、账号禁用、令牌失效"| X["拒绝访问,不返回售后数据"] - ORD -->|"订单项不属于买家或不在售后期限/状态"| Y["拒绝申请,不泄露他人订单内容"] - AS -->|"状态竞争或事务失败"| Z["返回当前最终状态,不产生部分退款或部分状态"] + BUYER["买家"] -->|"本人订单项:申请、撤销、退货说明、查询"| AS["M10 AfterSales"] + MERCHANT["订单指定商家"] -->|"授权申请:审核、确认收货、退款失败重试、查询"| AS + ORDER["M04 Ordering
订单归属、指定商家、状态、订单项实付与数量快照"] --> AS + FULFILL["M06-02 履约
发货前售后快照、已发货事实"] <--> AS + AS -->|"唯一退款操作及确定金额"| PAYMENT["M05 Payment
幂等退回本人小金库"] + AS -->|"符合回补条件的已退款数量"| STOCK["M02 Catalog / C01 Seckill
原库存来源通道"] + AS -->|"已提交申请、审核、寄回和退款事实"| MESSAGE["M09 Messaging"] + AS -->|"已退款事实"| RECON["C08 每日退款对账"] + + GUEST["游客、越权角色、禁用账号"] -->|"主动操作被拒绝"| DENY["不返回他人售后内容"] + ADMIN["管理员"] -->|"本期无售后审批入口"| DENY ``` -边界约束: +事实归属: -- M10 只通过 M04 的订单项标识索取事实,不复制订单金额、地址或商品快照;金额由 M04 已持久化实付单价 × 申请数量计算,客户端不得指定最终金额。 -- M10 调用 M05 的 `IRefundService.CreateRefundAsync` 内部应用能力退款,不直接修改钱包数据;M05 不得重复入账或改变订单 `Paid` 状态。 -- M10 拒绝在订单行上重复审核;同一订单项的处理中申请阻断发货,已退款数量从可履约数量中扣除。 -- M10 通知失败不能反向修改售后事实;具体事件名、Outbox 和 Worker 重试方式由系统架构设计承接,不进入业务流程图。 +- M04 拥有订单、订单项、实付单价、购买数量、核心状态、支付与完成时间、指定处理商家和原库存来源快照;M10 只读取这些权威事实,不接受客户端自报金额、归属或订单状态。 +- M10 拥有售后申请、申请类型、数量占用、审核意见、退货说明、退款推进状态和状态时间线。 +- M05 拥有小金库退款操作和入账事实。M10 只能通过公开应用能力发起同一退款操作,不直接修改钱包。 +- M02 与 C01 分别拥有普通商品库存和秒杀独立库存。M10 只提交“哪个原通道、哪笔订单项、多少数量已经满足回补条件”的业务动作。 +- M09 只通知明确接收人。通知失败不得回滚申请、审核、退款或库存结果。 +- 管理员本期不代替商家审批、不替买家申请,也不通过直接改数据完成退款。 -## 三、核心状态流转 +## 三、状态机与数量占用 -X04 售后有 8 个独立状态,不能与订单核心履约状态(`PendingPayment` / `Paid` / `Shipped` / `Completed` / `Cancelled`)合并或覆盖。 +### 3.1 售后申请状态 ```mermaid stateDiagram-v2 [*] --> PendingReview: 买家提交申请 - PendingReview --> Cancelled: 买家在审核前主动撤销 - PendingReview --> Rejected: 商家审核拒绝 + PendingReview --> Cancelled: 买家在审核前撤销 + PendingReview --> Rejected: 指定商家拒绝 PendingReview --> Refunding: 仅退款审核通过 PendingReview --> PendingReturn: 退货退款审核通过 - PendingReturn --> PendingReceipt: 买家提交退货物流 - PendingReceipt --> Refunding: 商家确认收到退货 - Refunding --> Refunded: 钱包退款成功 - Refunding --> RefundFailed: 钱包退款失败 - RefundFailed --> Refunding: 幂等重试 - PendingReview --> [*] + PendingReturn --> PendingReceipt: 买家提交退货说明 + PendingReceipt --> Refunding: 指定商家确认收货 + Refunding --> Refunded: 退款原子结果成功 + Refunding --> RefundFailed: 退款得到确定失败结果 + RefundFailed --> Refunding: 对同一退款操作安全重试 Cancelled --> [*] Rejected --> [*] Refunded --> [*] ``` -合法转换回执: +| 中文显示 | 状态代码 | 含义与后续动作 | +|---|---|---| +| 待审核 | `PendingReview` | 已占用申请数量,等待指定商家审核;买家仍可撤销 | +| 待退货 | `PendingReturn` | 退货退款已通过,等待买家提交必要退货说明 | +| 待收货 | `PendingReceipt` | 买家已提交退货说明,等待指定商家确认收到该申请全部数量 | +| 退款中 | `Refunding` | 审核或收货事实已提交,唯一退款操作正在执行或核实 | +| 已退款 | `Refunded` | 钱包、退款、必要库存回补和售后终态已形成完整成功结果 | +| 退款失败 | `RefundFailed` | 得到确定失败结果,未增加余额、未回补库存,可安全重试 | +| 已拒绝 | `Rejected` | 商家拒绝并释放申请数量,终止状态 | +| 已撤销 | `Cancelled` | 买家在审核前撤销并释放申请数量,终止状态 | -- 申请只能由买家提交;只有 `PendingReview` 状态可被买家主动撤销。 -- 商家只能在 `PendingReview` 状态下审核;审核拒绝后不可再次审核。 -- 退货退款必须经历 `PendingReturn → PendingReceipt → Refunding`;商家确认收货是退款前置条件。 -- `Refunding` 是受控退款事务内的瞬间中间状态;退款事务成功时提交为 `Refunded`,失败时先整体回滚,再由独立失败记录事务置为 `RefundFailed`;外部观察和监控按终态(`Refunded` / `RefundFailed`)过滤。 -- 退款失败**不是**终止状态,可通过幂等重试回到 `Refunding`。 -- 重复退款请求返回首次结果,不重复写入钱包流水。 +状态规则: -### 3.1 X04 状态名对照 +- 只有表中箭头是合法转换;接口或实现不得自行增加跳转、回退或“已退款订单”核心状态。 +- `PendingReview`、`PendingReturn`、`PendingReceipt`、`Refunding`、`RefundFailed` 都是非终态并继续占用申请数量。 +- `Refunding` 是可查询、可恢复的真实状态,不是假设必定瞬时完成的代码步骤。 +- 退款结果未知时保持 `Refunding`,先核实同一退款操作;只有得到确定失败结果时才进入 `RefundFailed`。 +- `RefundFailed` 不是终止状态。重试只能回到 `Refunding`,并继续使用同一退款操作身份。 +- `Cancelled`、`Rejected`、`Refunded` 是终止状态,不能再次审核、退货、退款或撤销。 -| 中文显示 | 英文代码(PascalCase) | 状态描述 | -|---|---|---| -| 待审核 | `PendingReview` | 买家提交申请后等待商家审核 | -| 待退货 | `PendingReturn` | 退货退款审核通过后等待买家提交退货物流 | -| 待收货 | `PendingReceipt` | 买家提交退货物流后等待商家确认收货 | -| 退款中 | `Refunding` | 退款执行中(仅退款审核通过 / 退货确认收货后) | -| 已退款 | `Refunded` | 钱包退款成功,终止状态 | -| 退款失败 | `RefundFailed` | 钱包退款失败,可重试 | -| 已拒绝 | `Rejected` | 商家审核拒绝,终止状态 | -| 已撤销 | `Cancelled` | 买家主动撤销(仅 `PendingReview` 时),终止状态 | +### 3.2 可申请数量 + +同一订单项允许分次申请,但每个购买单位在任一时刻只能属于“仍可申请”“处理中”或“已退款”之一: + +```text +剩余可申请数量 += 订单项购买数量 +- 全部非终态申请占用数量 +- 已退款数量 +``` + +- `PendingReview`、`PendingReturn`、`PendingReceipt`、`Refunding`、`RefundFailed` 的数量都属于“处理中”。 +- `Cancelled`、`Rejected` 不再占用数量;`Refunded` 数量永久计入已退款数量。 +- 同一请求重复提交不能再次占用;不同请求并发提交时,最终占用总量不得超过剩余可申请数量。 +- 金额始终由订单项实付单价乘以本次申请数量计算;单次和累计退款不得超过该订单项尚未退款的实付金额。 + +## 四、资格预检与提交申请 -## 四、可申请判断与提交申请 +### 4.1 状态、时限与类型矩阵 + +| 订单核心状态 | 时间条件 | 可选申请类型 | 说明 | +|---|---|---|---| +| `Paid` | 订单尚未发货 | `RefundOnly` | 未发货订单只允许仅退款 | +| `Shipped` | 无额外完成期限 | `RefundOnly`、`ReturnAndRefund` | 已发货可仅退款,也可退货退款 | +| `Completed` | 权威时间早于完成时间加 7 天 | `RefundOnly`、`ReturnAndRefund` | 达到截止时间后不再新建申请 | +| `PendingPayment` | 任意 | 无 | 应走主动取消或超时取消,不进入售后 | +| `Cancelled` | 任意 | 无 | 未发生有效支付,不进入售后 | +| `Completed` | 已达到完成时间加 7 天 | 无 | 明确提示售后期限已过 | + +完成后 7 天使用权威 UTC 时间计算,申请提交成功必须发生在截止时间之前。页面资格预检只用于展示入口;真正提交时必须重新读取订单状态、完成时间、发货事实和剩余数量。 + +### 4.2 提交主流程 ```mermaid flowchart TD - ID["M01:已认证且状态正常的买家"] --> A["从订单详情选择符合条件的订单项"] - ORD["M04:订单项、归属、实付快照、当前状态和已售后数量"] --> A - A --> B{"订单项归属本人?"} - B -- "否" --> X["拒绝访问,不泄露他人订单"] - B -- "是" --> C{"订单状态可申请?"} - C -- "PendingPayment" --> CY["拒绝:未支付订单不进入售后"] - C -- "Paid" --> CT1["仅退款:未发货订单只允许仅退款"] - C -- "Shipped" --> CT2["退款/退货均可"] - C -- "Completed 7 天内" --> CT2 - C -- "Cancelled 或超期" --> CY2["拒绝:已取消或超期不可申请"] - CT1 --> D - CT2 --> D - D{"申请数量 ≤ 剩余可售后数量?"} - D -- "否" --> DZ["提示可申请范围,不创建申请"] - D -- "是" --> E{"类型与状态匹配?"} - E -- "仅退款 + Paid" --> F["确定类型为 RefundOnly"] - E -- "退款/退货 + Shipped/Completed" --> G["确定类型为 ReturnAndRefund"] - F --> H["通过 Ordering 公开应用契约在同一 PostgreSQL 事务中锁定 orders 行"] - G --> H - H --> I["在锁定行内用 PostgreSQL 条件聚合核算:
剩余可售后数量 = 订单项数量 − 处理中数量 − 已退款数量"] - I --> J{"申请数量 ≤ 剩余可售后数量(同事务内复算)?"} - J -- "否" --> JZ["拒绝:剩余可售后数量不足"] - J -- "是" --> K["同事务内:写入申请 + 申请单状态 PendingReview + audit_log + Outbox 申请提交事实"] - K --> L{"事务提交成功?"} - L -- "否" --> LR["整体回滚:申请、Outbox 事实均未写入;锁随事务结束释放"] - L -- "是" --> N["通知商家审核(Outbox 由 M09 在事务外可靠推送)"] - N --> O["买家可查看本人申请详情"] + A["状态正常的买家从本人订单详情选择订单项"] --> B["选择申请类型、数量并填写原因"] + B --> C["服务端重新读取订单归属、核心状态、完成时间、指定商家、实付单价和原库存来源"] + C --> D{"订单项属于当前买家?"} + D -- "否" --> X["拒绝且不泄露他人订单内容"] + D -- "是" --> E{"状态、时限与申请类型符合 4.1?"} + E -- "否" --> Y["返回当前状态和允许的后续入口,不创建申请"] + E -- "是" --> F["重新核算处理中、已退款和剩余可申请数量"] + F --> G{"数量为正且不超过剩余数量?"} + G -- "否" --> Z["提示最新可申请数量,不占用额度"] + G -- "是" --> H["与同订单发货动作串行复核最新履约事实"] + H --> I{"复核后仍满足资格?"} + I -- "否" --> R["按最新已发货或售后事实重新返回可选类型与数量"] + I -- "是" --> J["形成原子申请结果:PendingReview、数量占用、服务端金额、状态时间线、指定商家通知事实"] + J --> K{"结果整体提交?"} + K -- "否" --> T["全部不生效,保留页面输入并允许安全重试"] + K -- "是" --> U["买家看到待审核;指定商家收到待处理通知"] ``` -关键规则: +提交规则: -- 实付单价与可用数量由 M04 已持久化订单事实决定;客户端不得传入任意金额。 -- 同一买家、同一订单项已有 `PendingReview` 申请时拒绝重复申请,避免占用未售后数量。 -- `PendingPayment` 订单不进入售后;请走 F09 主动取消或等待 C03 自动取消。 -- 申请提交后写入 `audit_log`,明确记录申请人与提交时间。 +- 买家只能为本人订单项提交,商家、管理员和游客不能代为创建。 +- 买家提交申请类型、原因和正整数数量,不提交最终退款金额。 +- 每次用户动作具有稳定请求身份。同一身份、同一内容重放原申请;同一身份换内容拒绝;新的合法部分申请使用新的请求身份。 +- 已有一笔非终态申请不禁止同一订单项继续申请,但新的申请只能使用尚未被占用或退款的数量。 +- 创建申请与商家发货必须对同一订单履约事实串行复核: + - 申请先提交时,非终态售后阻断后续发货; + - 发货先提交时,申请按最新 `Shipped` 事实重新判断类型和库存规则; + - 不能把同一数量同时当作“未发货退款数量”和“本次待发货数量”。 +- 申请成功后只通知订单指定商家,不向全部 Merchant 角色广播。 +- 禁用买家不能新建申请;已存在申请和已提交事实不会因账号禁用而删除。 -## 五、商家审核与状态推进 +## 五、列表、详情与操作入口 ```mermaid flowchart TD - ID["M01:已认证且状态正常的商家"] --> A["分页查询待审核申请"] - AUTH["M04:订单项、归属、当前状态"] --> A - A --> B{"申请归属当前商家授权范围?"} - B -- "否" --> X["拒绝访问,不泄露申请内容"] - B -- "是" --> C{"申请状态为 PendingReview?"} - C -- "否" --> CY["拒绝:状态非法或不修改已审核申请"] - C -- "是" --> D{"决策?"} - D -- "Reject" --> E["记录审核意见"] - E --> F["状态条件推进 PendingReview → Rejected"] - F --> G["记录待发布审核拒绝事实"] - D -- "Approve" --> H["记录审核意见"] - H --> I{"申请类型?"} - I -- "RefundOnly" --> J["开启审核事务"] - J --> J0["同事务内:按普通/秒杀原通道回补未发货库存(保留库存可售数量)"] - J0 --> J1["同事务内调用 IRefundService:原子写入退款记录 + 钱包入账 + 钱包流水"] - J1 --> J2{"IRefundService 返回结果?"} - J2 -- "退款成功" --> J3["同事务内:状态推进 PendingReview → Refunded"] - J2 -- "退款失败" --> J4["整体回滚退款受控事务:库存、钱包、流水和售后终态均不生效"] - J4 --> J5["另开独立失败记录事务:保留审核结果并将状态置为 RefundFailed
供 A419 失败重试使用"] - J3 --> JMR{"退款受控事务提交成功?"} - JMR -- "否" --> J4 - JMR -- "是" --> N - I -- "ReturnAndRefund" --> L["状态条件推进 PendingReview → PendingReturn"] - L --> L1["保留审核意见,等待买家提交退货物流"] - J5 --> N - L --> MR - G --> MR - MR{"事务提交成功?"} - MR -- "否" --> MRR["整体回滚,状态保持 PendingReview"] - MR -- "是" --> N["通知买家审核结果"] - N --> O["商家可查看审核结果与状态时间线"] + A["已认证用户进入售后页面"] --> B{"当前角色?"} + B -- "买家" --> C["只按当前买家查询本人申请"] + B -- "商家" --> D["只按订单 assignedMerchantUserId 查询授权申请"] + B -- "其他角色" --> X["拒绝访问"] + C --> E["按状态和时间分页展示申请"] + D --> E + E --> F["打开详情:订单项快照、实付金额、申请数量与原因、审核意见、退货说明、退款结果、状态时间线"] + F --> G["按当前角色和最新状态派生可用动作"] ``` -审核并发规则: +- 买家列表和详情不得暴露其他买家申请;商家只能看到明确分配给当前账号的订单申请。 +- 商家看到的买家信息只限履行审核和退货确认所需的最小摘要,不返回无关隐私。 +- 页面按钮不是权限事实。撤销、审核、退货说明、确认收货和重试时都必须在服务端重新校验身份、归属、账号状态和申请状态。 +- 买家账号禁用后不能主动撤销或提交退货说明;相关事实保留,恢复后可继续符合状态的动作。无需买家参与的已在途退款和系统恢复仍可继续。 +- 商家超时未审核不自动同意或拒绝,申请持续为 `PendingReview` 并显示待处理。 -- 两名商家对同一 `PendingReview` 申请同时审核时,状态条件只允许一次成功;败方收到 `409 + AFTER_SALES.INVALID_STATUS`。 -- 商家不能修改买家原始申请内容,只能在审核意见中表达观点。 -- 商家超时未处理不自动同意或拒绝;只持续显示待处理并提醒。 +## 六、买家撤销与商家审核 -## 六、买家提交退货物流 +### 6.1 买家撤销 ```mermaid flowchart TD - ID["M01:已认证且状态正常的买家"] --> A["按本人申请选择 ReturnAndRefund 待退货申请"] - A --> B{"申请归属本人且状态为 PendingReturn?"} - B -- "否" --> X["拒绝访问"] - B -- "是" --> C["输入快递公司、快递单号、寄出时间与备注"] - C --> D{"字段合法?"} - D -- "否" --> DZ["保留输入并提示字段错误"] - D -- "是" --> H["开启单一 PostgreSQL 事务"] - H --> H0["通过 Ordering 公开应用契约在同一 PostgreSQL 事务中锁定 orders 行"] - H0 --> I["状态条件推进 PendingReturn → PendingReceipt"] - I --> J["同事务内:写入退货物流信息 + audit_log + Outbox 退货物流提交事实"] - J --> K{"事务提交成功?"} - K -- "否" --> KR["整体回滚:状态、物流信息、Outbox 事实均未生效;锁随事务结束释放"] - K -- "是" --> M["通知商家待收货(Outbox 由 M09 在事务外可靠推送)"] + A["状态正常的买家选择本人申请"] --> B{"申请仍为 PendingReview?"} + B -- "否" --> X["拒绝审核后撤销并返回当前状态"] + B -- "是" --> C["与商家审核竞争同一待审核状态"] + C --> D{"撤销是否唯一胜出?"} + D -- "否" --> Y["返回已提交的审核结果,不回退"] + D -- "是" --> E["原子推进为 Cancelled、释放申请数量并写入时间线"] + E --> F["买家看到已撤销;商家列表刷新后不再显示为待审核"] ``` -退货物规则: - -- 状态条件 `WHERE status = 'PendingReturn' AND buyer_id = currentBuyerId` 唯一推进。 -- 不对快递单号施加全局唯一约束:同一包裹可承载同一订单的多笔退货申请(A434 总契约);重复提交由申请状态、`Idempotency-Key` 和单号在 `audit_log` 中的处理说明控制。 -- 仅退款(`RefundOnly`)申请不需要走本流程。 +- 重复撤销已为 `Cancelled` 的本人申请返回已撤销结果,不重复释放数量。 +- `Rejected`、`PendingReturn`、`PendingReceipt`、`Refunding`、`RefundFailed`、`Refunded` 均不得撤销。 +- 撤销与审核并发时只有一个结果:撤销先提交则审核失败;审核先提交则撤销失败。 -## 七、商家确认收货与退款入账 +### 6.2 商家审核 ```mermaid flowchart TD - ID["M01:已认证且状态正常的商家"] --> A["按授权范围查询 PendingReceipt 申请"] - A --> B{"申请归属当前商家且状态为 PendingReceipt?"} - B -- "否" --> X["拒绝访问"] - B -- "是" --> C["输入收到数量与备注"] - C --> D{"收到数量合法?"} - D -- "否" --> DZ["保留输入并提示错误"] - D -- "是" --> E["开启单一 PostgreSQL 事务"] - E --> F["状态条件推进 PendingReceipt"] - F --> G["按退货数量回补订单项原库存通道"] - G --> H["同事务内调用 IRefundService:原子写入退款记录 + 钱包入账 + 钱包流水"] - H --> H1{"IRefundService 返回结果?"} - H1 -- "退款成功" --> H2["状态推进 PendingReceipt → Refunded"] - H1 -- "退款失败" --> H3["整体回滚退款受控事务:库存、钱包、流水和售后终态均不生效"] - H3 --> H4["另开独立失败记录事务:保留确认收货事实并将状态置为 RefundFailed
供 A419 失败重试使用"] - H2 --> I - H4 --> KF["通知买家退款失败,等待 A419 重试"] - I["记录待发布退款事实 + 确认收货审计日志"] - I --> J{"事务提交成功?"} - J -- "否" --> JR["整体回滚:库存、钱包、流水、售后终态和完成事实均不生效"] - JR --> JRF["另开独立失败记录事务:保留确认收货事实并将状态置为 RefundFailed"] - JRF --> KF - J -- "是" --> K["通知买家退款成功"] + A["状态正常的指定商家打开 PendingReview 申请"] --> B["重新读取申请、订单归属、数量占用和当前状态"] + B --> C{"仍由当前商家处理且状态为 PendingReview?"} + C -- "否" --> X["拒绝越权或重复审核并返回当前状态"] + C -- "是" --> D["填写审核意见并选择同意或拒绝"] + D --> E{"审核决定?"} + E -- "拒绝" --> F["原子推进 Rejected、释放申请数量、记录意见与买家通知事实"] + E -- "同意仅退款" --> G["原子记录同意意见、唯一退款操作和 PendingReview → Refunding"] + E -- "同意退货退款" --> H["原子记录同意意见并推进 PendingReview → PendingReturn"] + G --> I["进入第八章退款处理"] + H --> J["通知买家提交退货说明"] + F --> K["通知买家审核拒绝"] ``` -退款入账流程(`IRefundService`,内部应用能力,必走): +审核规则: + +- 只有订单指定商家可审核,不能按 Merchant 角色全局操作。 +- 商家只能填写意见和决定,不得修改买家申请类型、原因、数量、订单快照或退款金额。 +- `RefundOnly` 同意后先可靠形成“已同意 + `Refunding` + 唯一退款操作”,再执行退款。即使后续退款失败,审核事实也不能丢失或回到待审核。 +- `ReturnAndRefund` 同意后只进入 `PendingReturn`,不能提前退款或回补库存。 +- 两个审核请求并发时只有一个决定生效;相同请求重放当前结果,相反决定不得覆盖首次结果。 +- 审核通过、拒绝和待退货结果通知买家。退款最终结果另按第九章通知。 + +## 七、退货说明与确认收货 + +### 7.1 买家提交退货说明 ```mermaid flowchart TD - CLI["调用方:A416 审核 + A417 确认收货 + A419 失败重试"] --> A["计算目标金额 = 实付单价 × 申请数量"] - A --> B{"金额与申请计算金额一致?"} - B -- "否" --> BZ["失败回滚"] - B -- "是" --> C{"幂等键已存在?"} - C -- "同 Key 同金额" --> D["返回首次结果"] - C -- "同 Key 不同金额" --> CZ["抛 IdempotencyKeyReusedException"] - C -- "否" --> E["开启退款事务"] - E --> F["原子写入:退款记录 + 钱包入账 + 钱包流水
+ 申请状态 Refunding → Refunded + 退款完成事实"] - F --> G{"事务提交成功?"} - G -- "是" --> GS["受控事务提交:退款结果与完成事实同时生效"] - GS --> I["通知买家退款成功"] - G -- "否" --> GF["整体回滚受控退款事务:退款记录、钱包入账、钱包流水、申请终态和完成事实均不生效"] - GF --> GFA["AfterSales 另开独立事务记录 RefundFailed 与失败原因
由 A419 失败重试入口处理"] - GFA --> IFAIL["通知相关方失败原因,等待重试"] + A["状态正常的买家打开本人 PendingReturn 申请"] --> B["填写必要承运方、运单号、寄出时间和备注"] + B --> C{"信息完整且申请仍为 PendingReturn?"} + C -- "否" --> X["保留输入并提示错误或当前状态"] + C -- "是" --> D["原子保存退货说明、推进 PendingReceipt 并记录状态时间线"] + D --> E["通知订单指定商家待确认收货"] ``` -退款入账原子结果: +- 本期不接真实物流平台,不查询轨迹,只保存审核和收货所需说明。 +- 一个包裹可以承载同一订单的多笔申请,运单号不作为跨申请的业务唯一身份。 +- 同一申请重复提交完全相同的退货说明时返回当前结果;进入 `PendingReceipt` 后不得用新内容静默覆盖已提交说明。 +- `RefundOnly` 不进入退货流程。 -```text -钱包入账成功 -+ 退款记录已写入 -+ 钱包流水已写入 -+ 申请状态 Refunding → Refunded -+ 待发布的退款完成事实已写入 -= 同一事务提交成功 +### 7.2 商家确认收货 + +```mermaid +flowchart TD + A["状态正常的指定商家打开 PendingReceipt 申请"] --> B["核对申请全部数量与退货说明"] + B --> C{"是否确认收到该申请全部数量?"} + C -- "否" --> X["保持 PendingReceipt,填写沟通备注但不退款"] + C -- "是" --> D["重新校验归属、状态和退款金额"] + D --> E["原子记录收货事实、唯一退款操作并推进 PendingReceipt → Refunding"] + E --> F["进入第八章退款处理"] ``` -受控退款事务中的任一步失败都必须整体回滚,退款记录、钱包入账、钱包流水、库存回补、售后终态和退款完成事实均不得留下部分结果。退款事务回滚完成后,AfterSales 再通过独立失败记录事务保存失败原因,并将申请置为 `RefundFailed` 供 A419 幂等重试;退款审核或确认收货等已经成立的业务事实必须随失败记录保留,不能伪装成从未处理。 +- 本期一笔申请按申请数量整体确认,不引入部分收货、拆分退款或新的子状态。数量有争议时保持 `PendingReceipt`,不得先退部分金额。 +- 只有订单指定商家可确认;买家、管理员和其他商家不能代替确认。 +- 收货确认后不得回到 `PendingReturn`,退款失败也保留已经确认收货的事实。 +- 库存不会在点击确认时单独回补;符合条件的回补与退款成功在第八章形成一个完整结果,避免退款失败却先增加库存。 + +## 八、退款执行、未知结果与安全重试 + +### 8.1 唯一退款操作 + +每笔售后申请最多对应一个退款操作身份,金额、收款买家、订单项和申请数量一经确定不得改变: + +- 首次进入 `Refunding` 时建立该退款操作身份。 +- 同一退款操作可以有多次受控尝试,但任何时刻最多一个尝试执行。 +- 已成功时,任何审核重放、确认收货重放、人工重试或系统恢复都返回首次成功结果,不再次增加余额或库存。 +- 已得到确定失败时,申请进入 `RefundFailed`;新尝试仍关联原退款操作,不创建第二笔业务退款。 +- 结果未知时保持 `Refunding`,先查询或恢复原尝试;不得把“超时未收到响应”直接当失败,也不得立即发起无法去重的新退款。 -## 八、买家撤销申请 +### 8.2 退款主流程 ```mermaid flowchart TD - ID["M01:已认证且状态正常的买家"] --> A["按本人申请选择 PendingReview 申请"] - A --> B{"申请归属本人?"} - B -- "否" --> X["拒绝访问"] - B -- "是" --> C{"状态为 PendingReview?"} - C -- "否" --> CY["拒绝:审核后不允许撤销"] - C -- "是" --> D["开启单一 PostgreSQL 事务"] - D --> E["释放订单项已占用的未售后数量"] - E --> F["状态条件推进 PendingReview → Cancelled"] - F --> G["同事务内:写入审计日志 + Outbox 申请撤销事实"] - G --> H{"事务提交成功?"} - H -- "否" --> HR["整体回滚:状态变更、审计日志、Outbox 事实均未生效"] - H -- "是" --> J["通知商家申请已撤销(Outbox 由 M09 在事务外可靠推送)"] + A["申请已可靠进入 Refunding"] --> B["读取本人、服务端退款金额、订单原库存来源和本次库存规则"] + B --> C{"同一退款操作是否已有成功结果?"} + C -- "是" --> R["重放 Refunded,不重复入账或回补"] + C -- "否" --> D{"是否有结果未知的在途尝试?"} + D -- "是" --> E["核实原尝试;保持 Refunding,不开启第二笔退款"] + D -- "否" --> F["M05 对同一退款操作执行小金库退款"] + F --> G{"退款得到什么结果?"} + G -- "确定成功" --> H["形成完整原子结果:退款操作成功、本人余额增加、必要库存回补、申请 Refunded、时间线和买家通知事实"] + G -- "确定失败" --> I["确认未入账且未回补后,记录失败原因并推进 RefundFailed"] + G -- "未知" --> E + H --> J["余额立即可用;C08 后续核对三方一致"] + I --> K["通知买家退款失败;允许指定商家或系统安全重试"] ``` -撤销售后规则: +成功原子结果必须满足: -- 审核通过的申请不允许撤销或回退;买家只能等待自然终态。 -- 部分退款后订单仍可发起退货,但"已退款数量"从可履约数量中扣除。 +```text +同一退款操作已成功 ++ 本人小金库只增加一次正确金额 ++ 必要时按原库存来源只回补一次正确数量 ++ 售后申请进入 Refunded ++ 状态时间线和退款成功通知事实已形成 += 一个完整成功结果 +``` -## 九、消息通知与页面反馈 +任一步失败时,不能留下“余额增加但售后仍失败”“库存回补但余额未增加”或“售后显示成功但没有退款操作”的部分结果。 + +### 8.3 失败重试 ```mermaid flowchart TD - A["已登录买家或商家进入售后页面"] --> B{"查询类型"} - B -- "申请列表" --> C["按本人或授权范围分页查询"] - B -- "申请详情" --> D["展示订单项快照、实付金额、申请内容、审核意见、状态时间线"] - B -- "审核日志" --> E["按 audit_log 时序展示所有状态变更"] - B -- "退款详情" --> F["展示退款金额、到账状态、流水编号"] - C --> G["页面展示确定状态和操作入口"] - D --> G - E --> G - F --> G - A -->|"资源非本人"| X["404/403,不泄露他人售后内容"] + A["指定商家或系统选择 RefundFailed 申请"] --> B["重新读取申请状态和原退款操作"] + B --> C{"是否仍为 RefundFailed 且没有成功 / 未知尝试?"} + C -- "否" --> X["返回当前 Refunding 或 Refunded 结果,不开启新尝试"] + C -- "是" --> D["唯一推进 RefundFailed → Refunding,并为同一退款操作开启受控重试"] + D --> E["复用 8.2 退款主流程"] ``` -通知触发点: +- 买家不能主动执行退款重试;指定商家可手动重试,系统恢复任务也可重试确定失败的申请。 +- 商家重试与系统重试并发时只有一个进入执行,其他调用读取当前结果。 +- 重试不重新审核、不要求买家再次申请,也不改变原金额、数量、库存通道或收款人。 +- 系统无法确认旧尝试结果时继续保持 `Refunding` 并进入核实,不得伪造 `RefundFailed`。 + +## 九、库存、履约和订单状态协作 -- 申请提交 → 通知商家"待审核"。 -- 商家审核拒绝 → 通知买家"审核拒绝"。 -- 商家审核通过(仅退款)→ 通知买家"退款中"。 -- 商家审核通过(退货退款)→ 通知买家"待退货"。 -- 买家提交退货物流 → 通知商家"待收货"。 -- 商家确认收货 → 通知买家"退款中"。 -- 退款成功 → 通知买家"退款成功"。 -- 退款失败 → 通知买家"退款失败",商家可重试。 +### 9.1 库存回补矩阵 -## 十、异常、回滚与责任 +| 申请时订单事实 | 申请类型 | 商家收货要求 | 退款成功时库存结果 | +|---|---|---|---| +| `Paid`、未发货 | `RefundOnly` | 不需要 | 按申请数量回补订单项原库存来源 | +| `Shipped` | `RefundOnly` | 不需要 | 不回补库存 | +| `Completed` 且在期限内 | `RefundOnly` | 不需要 | 不回补库存 | +| `Shipped` | `ReturnAndRefund` | 必须先确认收货 | 退款成功时按申请数量回补原库存来源 | +| `Completed` 且在期限内 | `ReturnAndRefund` | 必须先确认收货 | 退款成功时按申请数量回补原库存来源 | + +- 普通订单回补 Catalog 库存;秒杀订单回补原 Seckill 活动的独立库存和售后数量事实。 +- 秒杀活动已经结束或取消时,回补数量仍留在原活动通道用于正确核算,不重新开放抢购,也不转成普通商品库存。 +- 普通商品已下架时可以恢复库存事实,但不会因此自动上架。 +- 退款确定失败或结果未知时不回补;重试成功后与退款一起回补一次。 + +### 9.2 发货协作 + +- 任一 `PendingReview`、`PendingReturn`、`PendingReceipt`、`Refunding` 或 `RefundFailed` 申请都阻断该订单发货。 +- `Rejected`、`Cancelled` 不阻断发货;`Refunded` 数量从可履约数量中永久扣除。 +- 部分退款后只发剩余可履约数量;全部数量已退款时不得发货。 +- 售后申请与发货对同一订单串行复核。发货先胜出时,新申请按 `Shipped` 规则处理;申请先胜出时,发货等待该申请进入终态。 +- 商家确认发货后,后续售后不把订单退回 `Paid`;售后通过独立状态和数量表达。 + +### 9.3 订单核心状态 -| 场景 | M10 处理 | 最终状态/责任 | +退款成功不把订单改成统一“已退款”状态: + +- `Paid`、`Shipped`、`Completed` 保持原核心履约状态; +- 订单详情按订单项展示处理中数量、已退款数量和剩余可履约数量; +- 同一订单不同订单项、同一订单项分次退款都能独立表达; +- 售后申请和退款不参与 C03 的 `PendingPayment` 超时扫描。 + +## 十、通知与页面反馈 + +| 已提交事实 | 接收人 | 页面入口 | +|---|---|---| +| 申请创建为 `PendingReview` | 订单指定商家 | 商家售后详情 | +| 审核拒绝 | 申请买家 | 买家售后详情 | +| 仅退款审核通过并进入 `Refunding` | 申请买家 | 买家售后详情 | +| 退货退款审核通过并进入 `PendingReturn` | 申请买家 | 买家售后详情 | +| 买家提交退货说明并进入 `PendingReceipt` | 订单指定商家 | 商家售后详情 | +| 退款成功 | 申请买家 | 买家售后详情 / 小金库记录 | +| 退款确定失败 | 申请买家 | 买家售后详情 | + +- 退款成功和失败只通知申请买家,不向无关商家或管理员广播。 +- 消息只描述已经提交的状态和必要业务标识;打开详情时由 M10 重新校验归属和最新状态。 +- 通知失败不改变售后结果,由 M09 可靠重试;页面查询始终以 M10 当前事实为准。 +- 页面明确展示加载、空状态、处理中、失败、冲突和最终结果;不能在退款结果未知时显示成功或失败。 + +## 十一、异常、竞争与责任 + +| 场景 | M10 处理 | 保证 | |---|---|---| -| 订单项不属于买家或不在售后期限/状态 | 拒绝申请 | 不创建申请,不泄露归属 | -| 申请数量超剩余可售后数量 | 拒绝申请 | 提示可申请范围 | -| 同一买家同一订单项重复申请 | 拒绝 | 不重复占用未售后数量 | -| 商家超时未审核 | 不自动同意/拒绝 | 持续显示待审核 | -| 商家越权审核他人申请 | 拒绝 | 403 | -| 两名商家并发审核 | 状态条件唯一胜出 | 败方收到 409 | -| 重复退款请求 | 返回首次结果 | 不重复入账 | -| 退款执行失败 | 状态保持 `退款失败` | 允许幂等重试,不伪装成功 | -| 通知暂时失败 | 售后事实保留 | M09 按可靠机制重试 | -| 账号禁用 | 不允许新建申请 | 已有申请继续由商家和系统处理 | -| 售后模块不直接修改钱包 | 通过 `IRefundService` 内部应用能力 | AfterSales 不持有钱包表权限 | -| 退款后订单核心履约状态 | 保持原状态 | 不新增"已退款"等破坏核心订单状态 | - -## 十一、由流程派生的接口契约映射 - -本节是第二至十章业务流程的下游映射,不是流程输入。先确认"要完成什么业务动作、处于什么状态、成功或失败后得到什么结果",再决定由哪个接口承载。若现有 Axxx 与流程冲突,先记录接口缺口并修改接口设计;只有业务需求本身变化时,才回到前文重新评审流程。 - -| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +| 订单项不属于当前买家 | 拒绝且不泄露详情 | 不创建申请 | +| 状态、类型或完成后 7 天期限不符 | 返回当前资格 | 不占用数量 | +| 数量超过最新剩余可申请数量 | 返回最新可用数量 | 不超额占用或退款 | +| 同一提交动作重试 | 重放首次确定结果 | 不重复创建或占用 | +| 不同申请并发占用同一剩余数量 | 只有可满足总量的请求成功 | 总占用不超过购买数量 | +| 申请与发货并发 | 按最新提交顺序重新判断 | 同一数量不同时作为未发货退款和待发货数量 | +| 撤销与审核并发 | 只有一个状态转换成功 | 审核结果不被撤销覆盖 | +| 两个商家审核或相反决定并发 | 指定商家的首次合法决定胜出 | 不重复审核 | +| 退货说明重复或试图覆盖 | 同内容重放,已提交后拒绝换内容 | 时间线稳定 | +| 重复确认收货 | 返回当前退款状态 | 不重复退款或回补 | +| 退款确定失败 | 进入 `RefundFailed` | 余额和库存均未增加 | +| 退款结果未知 | 保持 `Refunding` 并核实原尝试 | 不并发创建第二笔退款 | +| 手动与系统退款重试并发 | 一次执行,其他读取当前结果 | 同一操作最多退款一次 | +| 通知失败 | 保留已提交业务事实 | M09 重试,不反向修改 | +| 买家账号禁用 | 拒绝新的主动操作 | 已有事实和系统退款恢复保留 | +| 商家超时未处理 | 保持当前待处理状态 | 不自动同意、拒绝或收货 | + +## 十二、由流程派生的接口契约映射 + +本节只把前十一章已经确认的业务动作映射到接口编号。请求字段、响应、鉴权、错误码和 HTTP 状态必须承接流程,而不是让旧接口草案反向删减状态、资格或竞争分支。 + +| 已确认业务能力 | 派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 售后资格预检 | A411 | 按订单项、状态、时限和剩余可售后数量判断资格 | 待交叉评审 | -| 提交售后申请 | A412 | 校验归属、类型、金额和防重复,并写入 PendingReview | 待交叉评审 | -| 申请列表 | A413 | 按本人或授权范围分页返回申请 | 待交叉评审 | -| 申请详情(含状态时间线 + 审核日志) | A414 | 展示订单项快照、实付金额、申请内容、审核意见、状态时间线、audit_log 时序 | 待交叉评审 | -| 撤销申请 | A415 | 仅 PendingReview 可撤销,状态条件推进 | 待交叉评审 | -| 商家审核 | A416 | 通过业务能力推进状态并发退款或转待退货 | 待交叉评审 | -| 提交退货物流 | A434 | 写入快递公司与运单,状态推进 PendingReturn → PendingReceipt | 待交叉评审 | -| 商家确认收货 | A417 | 状态条件推进 PendingReceipt → Refunding + 库存回补 | 待交叉评审 | -| 退款失败重试 | A419 | 状态条件推进 RefundFailed → Refunding | 待交叉评审 | -| 退款入账 | `IRefundService` 内部应用能力 | 不占 Axxx HTTP 编号(A431 已取消并入),进程内调用 `IRefundService.CreateRefundAsync` | 待交叉评审 | -| 退款详情 | A432 | 展示单笔退款金额、状态、流水编号 | 待交叉评审 | -| 退款列表 | A433 | 按本人或授权范围分页返回退款记录 | 待交叉评审 | - -接口详细定义与实现必须承接上述流程结果。当前接口设计拟使用 `Idempotency-Key` 承载"防重复标识",并需满足接口设计 1.12 的幂等与并发规则以及 4.6 的资金类持久化幂等约束;HTTP 状态码、请求字段和错误码不得反向写入业务图。 - -## 十二、扩展接入边界 - -- C08 收款对账:每日对账范围涵盖 M10 退款成功记录、退款流水和小金库入账,不一致项进入差异并由管理员闭环。 -- C03 超时取消:售后申请与退款处理不参与 C03 待支付订单超时扫描;只强制 `PendingPayment` 订单。 -- M09 消息:售后提交、审核、待退货、退款中、退款成功和退款失败均通过 M09 通知;通知失败按可靠机制重试,不能反向修改售后事实。 -- M04 订单:售后申请阻断发货;部分退款数量从可履约数量中扣除;全部退款后订单项不可发货。 -- M02 库存:未发货仅退款 → 回补库存;已发货仅退款 → 不回补库存;退货退款 → 商家确认收货后按退货数量回补。 - -## 十三、由流程反查出的接口与数据待评审项 - -1. 退款金额不一致:流程要求金额由服务端按实付单价 × 申请数量计算;原 A431 内部应用能力(已并入 `IRefundService`)的 `expectedAmount` 只能承担客户端旧值冲突保护,不能成为扣款事实。 -2. 部分退款后订单状态:流程要求订单保持原核心履约状态;A407 支付记录列表与 A432 退款详情必须分别提供"未退款金额"和"已退款金额",不能合并为"已退款"状态。 -3. 卖家超时未处理:流程不自动同意或拒绝;A416 审核接口必须保留超时仍未处理的 PendingReview 状态,不能引入"超时自动拒绝"。 -4. 退货拦截发货:流程要求处理中申请阻断发货;M04 的发货接口必须能识别订单项未售后数量,禁止对未售后数量不足的订单项发货。 -5. 账号禁用售后:流程要求禁用账号不能新建或主动操作售后;A412 / A415 必须在账号禁用时拒绝;已有申请可被商家和系统继续处理。 -6. 库存回补口径:流程要求按订单项原库存来源通道回补;具体库存通道归属由 M02 协作时确认;M10 不替代 M02 决定库存通道。 -7. 状态机不可逆:流程要求"已退款 / 已拒绝 / 已撤销"为终止状态;A419 退款失败重试只能从 `退款失败` 推进,不能从"已退款"或"已拒绝"推进。 -8. 退货快递单号共享:流程要求同一快递单号可承载同一订单多笔退货申请(A434 总契约);不建快递单号全局唯一约束;重复提交由申请状态和 `Idempotency-Key` 控制。 -9. 商家并发审核:流程要求状态条件唯一胜出;A416 审核必须检查 `WHERE status = 'PendingReview'` 条件更新,失败方收到 `409 + AFTER_SALES.INVALID_STATUS`。 -10. 库存回补时机:流程要求未发货仅退款 + 退货退款确认收货都回补库存;`IRefundService` 内部应用能力(A431 已取消并入,仅承担退款入账)不负责库存回补,由 M04 / M02 在确认收货时完成。 -11. DB081~DB100 尚未形成可实施的完整表定义,数据库字段、约束和索引必须由数据库设计任务另行确认。 +| 售后资格预检 | A411 | 当前可选类型、截止时间、剩余可申请数量;明确预检不是提交承诺 | 待重建详细契约 | +| 提交售后申请 | A412 | 本人归属、最新订单状态、类型、数量、服务端金额、发货竞争和请求幂等 | 待重建详细契约 | +| 买家 / 商家申请列表 | A413 | 本人或 `assignedMerchantUserId` 授权范围、状态筛选和分页 | 待重建详细契约 | +| 申请详情 | A414 | 快照、金额、申请内容、审核意见、退货说明、退款结果和状态时间线 | 待重建详细契约 | +| 买家撤销 | A415 | 仅本人 `PendingReview` 可撤销;与审核竞争并释放数量 | 待重建详细契约 | +| 商家审核 | A416 | 指定商家、同意 / 拒绝、意见、状态竞争;仅退款同意后进入 `Refunding` | 待重建详细契约 | +| 商家确认收货 | A417 | 指定商家确认整笔申请数量,`PendingReceipt → Refunding` | 待重建详细契约 | +| 退款失败重试 | A419 | 仅 `RefundFailed`,复用同一退款操作,返回当前确定或在途状态 | 待重建详细契约 | +| 买家提交退货说明 | A434 | 仅本人 `PendingReturn`,同内容重放,提交后不得静默覆盖 | 待重建详细契约 | +| 小金库退款 | Payment 内部应用契约 | 一个售后申请一个退款操作;成功重放、未知核实、确定失败可安全重试 | 待按本流程重建 | +| 履约快照 | AfterSales 内部应用契约 | 非终态阻断、已退款数量、剩余可履约数量和同订单串行复核 | 待按本流程补齐 | + +接口阶段必须特别修正: + +1. 售后撤销状态使用 AfterSales 作用域内的 `Cancelled`,不得与订单 `Cancelled` 混成同一个状态机。 +2. A416 和 A417 的响应必须返回最新已提交状态;退款可继续处于 `Refunding`,不能为了同步响应伪造 `Refunded`。 +3. A419 不创建新业务退款,只重试原退款操作;若原尝试结果未知,返回 `Refunding` 并先核实。 +4. A412 不接受最终退款金额或商家归属;A411 的可申请数量不能替代 A412 提交时复核。 +5. A413、A414 的商家范围统一使用订单 `assignedMerchantUserId`,不允许全局 Merchant 查询。 +6. A417 不接收任意“收到数量”改变申请金额;本期只确认整笔申请数量。 +7. A434 不把运单号设为全局业务唯一键。 +8. 所有改变状态或资金的动作都要有稳定请求身份;相同请求重放首次结果,换内容不得复用。 +9. 当前流程不派生独立的售后退款详情或退款列表入口:申请详情已经承载退款结果,钱包记录由 M05 承载。旧 A432、A433 应在接口阶段并入这些事实源或登记为取消历史,不得为了保留编号反向增加页面和流程。 + +## 十三、跨模块整合必须承接的事实 + +- **M04 / M06-02**:订单提供状态、指定商家、实付与来源快照;履约在发货前读取非终态售后和已退款数量。售后与发货必须串行复核。 +- **M05**:对一个售后申请形成一个退款操作;钱包入账、退款成功和必要库存回补必须是完整结果。结果未知时先核实,不盲目重试。 +- **M02 / C01**:只接收已经满足回补条件的原通道数量;秒杀活动结束或取消后回补数量仍留在原活动,不恢复抢购。 +- **M09**:申请提交通知指定商家;审核、待退货、退款成功和失败通知申请买家;退货说明通知指定商家。退款失败不额外通知商家。 +- **C08**:只把 M10 `Refunded`、成功退款操作和本人钱包入账纳入成功三方对账;`RefundFailed` 和结果未知的 `Refunding` 不伪造成功记录。 +- **M06-03**:存在任一非终态售后申请时不得禁用负责处理的商家;禁用买家不能发起主动动作,但系统退款恢复可继续。 ## 十四、验收证据清单 -- [ ] 合法买家可对符合条件的订单项提交退款或退货申请。 -- [ ] 越权、超额、超时和重复申请被明确拦截,状态时间线一致。 -- [ ] 商家审核通过后仅退款申请立即进入退款;退货退款申请等待买家提交物流和商家确认收货。 -- [ ] 退款成功金额按订单项实付单价 × 申请数量正确返回本人小金库且立即可用。 -- [ ] 重复退款请求不重复增加钱包余额。 -- [ ] 退款失败状态保留并允许幂等重试,不伪装成功。 -- [ ] 通知失败时售后事实保留,由 M09 可靠重试。 -- [ ] 未发货订单仅退款 → 库存回补;已发货订单仅退款 → 不回补库存;退货退款确认收货后按退货数量回补。 -- [ ] 订单核心履约状态不被部分退款覆盖;可履约数量随已退款数量减少。 -- [ ] 状态机不被误用:审核后不允许撤销;已退款/已拒绝/已撤销为终止状态。 -- [ ] 卖家超时未处理不自动同意或拒绝。 -- [ ] 保存申请、审核、撤销、退款、重复处理、并发审核和越权场景证据。 +- [ ] `Paid` 只允许仅退款;`Shipped` 和期限内 `Completed` 可选择仅退款或退货退款;超期和未支付订单被拒绝。 +- [ ] 资格预检后状态变化时,提交仍按最新状态、时限、数量和发货事实重新判断。 +- [ ] 同一订单项可按剩余数量分次申请;非终态占用与已退款数量不会重复使用或超出购买数量。 +- [ ] 同一提交动作重试不重复创建;并发申请总占用不超额。 +- [ ] 申请与发货并发时结果可解释,同一数量不同时被当作未发货退款和待发货数量。 +- [ ] 买家撤销与商家审核竞争只有一个结果;审核后不能撤销或回退。 +- [ ] 指定商家才能查询、审核、确认收货和人工重试;其他商家看不到申请内容。 +- [ ] 仅退款审核通过后保留审核事实并进入 `Refunding`;退货退款严格经过 `PendingReturn → PendingReceipt → Refunding`。 +- [ ] 买家退货说明可安全重放,进入待收货后不能换内容覆盖;商家只确认整笔申请数量。 +- [ ] 退款成功按服务端金额只增加一次本人余额;同一退款操作重复执行不重复入账。 +- [ ] 退款确定失败时余额和库存均未增加并进入 `RefundFailed`;重试继续使用原退款操作。 +- [ ] 退款结果未知时保持 `Refunding` 并核实原尝试,不误报失败或开启第二笔退款。 +- [ ] 未发货仅退款、已发货仅退款、退货退款分别按第九章矩阵处理库存,且只回补一次原库存通道。 +- [ ] 秒杀售后回补不转入普通库存,活动结束或取消后不重新开放抢购。 +- [ ] 部分退款不改变订单核心状态;已退款数量从可履约数量中扣除,全部退款后不能发货。 +- [ ] 通知接收人正确:申请和退货说明到指定商家,审核和退款结果到申请买家;通知失败不修改业务事实。 +- [ ] C08 能核对 `Refunded`、成功退款操作和本人小金库入账;`RefundFailed` 不伪装成功。 +- [ ] 保存申请、撤销、审核、退货、确认收货、退款成功、确定失败、结果未知、重试、越权和并发场景的真实证据。 -- Gitee From 29282dd5df247b5bba86d0aa87274e472decb7a0 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 20:12:13 +0800 Subject: [PATCH 093/118] =?UTF-8?q?docs(process):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E4=B8=8E=E8=B6=85=E6=97=B6=E5=8F=96=E6=B6=88?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=EF=BC=9B=E7=BB=9F=E4=B8=80=E7=A7=92=E6=9D=80?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E5=88=9B=E5=BB=BA=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...05\346\227\266\346\265\201\347\250\213.md" | 303 +++++----- ...42\345\215\225\346\265\201\347\250\213.md" | 519 +++++++++++------- ...22\346\235\200\346\265\201\347\250\213.md" | 16 +- 3 files changed, 480 insertions(+), 358 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" index ad44c77..ca964b6 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" @@ -1,188 +1,203 @@ # C03 订单超时自动取消流程 > 负责人:韦乾强 -> 覆盖:C03 订单超时自动取消 -> 基础核心流程:M04 订单状态机(F08/F09)、M05 支付(F10) -> 直接协作:罗皓晨(M00 Worker 基础设施)、张海洋(M05 支付)、朱惠惠(M03 购物车) -> 文档状态:初稿 -> 需求事实源:[需求规格说明书 C03 订单超时自动取消](../../../01-需求文档/需求规格说明书.md) 的"C03 订单超时自动取消"完整章节 +> 覆盖:C03 +> 基础核心流程:F08、F09、F10 +> 直接协作:张海洋(M05 Payment)、朱惠惠(C01 Seckill)、罗皓晨(M09 Messaging、Worker 与 C10 多实例基础设施) +> 文档状态:已按需求和 M04 / M05 截止时间边界重构,可作为接口与 Worker 设计输入 +> 需求事实源:[需求规格说明书 C03](../../../01-需求文档/需求规格说明书.md) 的“C03 订单超时自动取消”完整七节 -## 一、范围与事实来源 +## 一、范围与核心结论 -本挑战模块负责在买家未按时支付时,由系统自动取消订单并回补库存。核心目标是"超时即取消、取消即回补、并发安全"。Worker 定时扫描 PendingPayment 订单,对超时订单执行与买家主动取消相同的状态变更和库存回补逻辑。 +C03 发现已经达到支付截止时间、但仍处于 `PendingPayment` 的订单,并调用 M04 的统一过期取消能力。C03 不建立第二套订单状态机、不处理已支付订单退款、不向买家开放手动 Worker 入口,也不根据当前配置重新计算历史订单是否到期。 -本文先确认超时触发条件、超时时间配置、扫描策略、与支付模块的状态竞争处理,再登记对 M04 订单状态机的复用边界和对 M09 站内消息的输出接口。 +必须先冻结的时间边界: + +```text +权威时间 < paymentDeadline +=> 支付仍可与主动取消竞争 + +权威时间 >= paymentDeadline +=> 所有支付通道必须拒绝 +=> 过期取消可以执行或重试 +``` + +Worker 是否已经扫描不影响支付资格。订单一旦达到截止时间,即使仍暂时显示 `PendingPayment`,也已经不可支付。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| -| C03 需求 | 完整定义 | 作为超时取消业务语义事实源 | -| 超时时间配置 | 正式环境 30 分钟,演示环境可配置 | 由部署配置注入,不硬编码 | -| 超时扫描与取消事务 | 初稿 | 确认 Worker 调度、批次上限、重试策略 | -| 与支付的状态竞争 | 完整定义 | 条件更新保证幂等 | -| 站内消息通知 | M09 承载 | 登记输出事件类型和消息内容要求 | +| C03 需求 | 完整定义 | 作为超时取消业务事实源 | +| M04 统一取消能力 | 已重构 | C03 只传递系统触发和过期原因 | +| M05 / C08 支付截止边界 | 已校准 | 到期后无条件拒绝支付 | +| 普通 / 秒杀回补 | 已由 M04 / C01 定义 | 必须按订单项原通道完整回补 | +| Worker 实现参数 | 待后续架构与配置设计 | 流程只要求有界、可恢复和多实例不重复 | -## 二、基础核心流程依赖 +## 二、参与者与直接出入口 ```mermaid flowchart LR - ORDER["M04 Ordering
PendingPayment 订单创建"] --> TIMEOUT["C03 超时 Worker
定时扫描 PendingPayment 订单"] - ORDER --> PAY["M05 Payment
买家主动支付"] - TIMEOUT -->|"超时取消事务|回补库存"| STOCK["M02 Catalog
库存回补"] - TIMEOUT -->|"OrderCancelledIntegrationEvent
超时取消通知"| MSG["M09 站内消息"] - PAY -->|"OrderPaidIntegrationEvent
支付成功通知"| MSG - MSG -->|"超时取消通知
买家站内消息"| BUYER["买家消息中心"] + ORDER["M04 Ordering
PendingPayment、固定支付截止时间、原库存来源"] --> WORKER["C03 Worker
发现到期订单"] + WORKER -->|"系统内部过期取消"| CANCEL["M04 统一取消能力"] + PAYMENT["M05 / C08
支付请求发现已到期"] -->|"立即触发同一过期取消"| CANCEL + CANCEL -->|"唯一结果:Cancelled + 原路回补 + 可靠取消事实"| ORDER + CANCEL -->|"已提交取消事实,只通知当前买家"| MESSAGE["M09 Messaging"] + + BUYER["买家"] -->|"查看倒计时、不可支付和最终取消结果"| ORDER + MERCHANT["商家"] -->|"只查看可靠最终状态"| ORDER + ADMIN["管理员"] -->|"本期不触发单笔取消"| DENY["无 C03 业务入口"] ``` -关键约束: +边界规则: + +- Worker 和 M05 的过期触发使用系统内部权限,不要求订单买家处于登录状态,也不伪装成买家主动取消。 +- 买家主动取消由 M04 接收;如果调用时已经达到截止时间,M04 同样按 `PaymentExpired` 处理。 +- 商家和管理员不能人工调用 C03 取消某个订单。 +- C03 不读取或修改钱包、支付流水、购物车、售后或消息内部数据。 +- M09 只消费已经成功提交的取消事实;推送或消息投递失败不影响取消结果。 -- C03 复用 M04 的订单状态变更和库存回补逻辑,不独立发明新事务。 -- C03 由 Worker 后台任务触发,不提供买家主动接口。 -- 创建订单时按当时生效的超时配置计算并固化支付截止时间;Worker 只扫描该截止时间,后续配置变化不追溯改变既有订单。 +## 三、超时时间与历史订单 -## 三、超时时间配置 +- 正式规则是订单创建后 30 分钟到期。 +- 演示环境可以使用更短参数,但必须在页面、测试证据和答辩中明确它只是演示参数。 +- M04 在创建每张订单时按当时生效的可追踪配置计算并固定 `paymentDeadline`。 +- C03 只比较订单自身的 `paymentDeadline`,后续配置变化不追溯修改历史订单。 +- 时间比较使用服务端权威 UTC 时间;客户端倒计时、回调发生时间和浏览器本地时间不能决定是否到期。 +- 在截止时间前一瞬间提交的支付只有在其业务结果也于截止时间前合法竞争成功才成立;达到截止时间后才处理的请求必须拒绝。 + +## 四、Worker 扫描与统一取消 ```mermaid flowchart TD - A["Worker 启动"] --> B["读取配置 OrderTimeoutMinutes"] - B --> C{"配置存在?"} - C -- "否" --> D["使用默认值 30 分钟"] - C -- "是" --> E["使用配置值"] - D --> F["仅用于新订单计算支付截止时间"] - E --> F - F --> G["创建订单时固化支付截止时间
既有订单截止时间不随配置变化"] + A["Worker 周期触发"] --> B["读取权威时间并查找 paymentDeadline 已到、状态仍为 PendingPayment 的订单"] + B --> C{"本轮是否有候选?"} + C -- "否" --> Z["结束本轮,等待下次调度"] + C -- "是" --> D["按稳定次序领取有界批次,多实例只允许一次有效处理资格"] + D --> E["逐笔调用 M04 统一过期取消,系统原因固定为 PaymentExpired"] + E --> F{"M04 返回什么结果?"} + F -- "首次取消成功" --> G["记录本轮成功;订单、回补和买家通知事实已完整提交"] + F -- "已取消" --> H["记录幂等完成,不重复回补或通知"] + F -- "已支付 / 已发货 / 已完成" --> I["记录状态竞争已由其他动作胜出,不改变订单"] + F -- "暂时失败" --> J["记录本轮失败并按退避策略重试;不标记取消成功"] + G --> K{"本批次还有候选?"} + H --> K + I --> K + J --> K + K -- "是" --> E + K -- "否" --> L["完成本轮;后续继续发现新到期或待重试订单"] ``` -关键约束: +扫描规则: -- 超时时间通过配置项 `OrderTimeoutMinutes` 管理。 -- 正式环境默认 30 分钟。 -- 演示环境可通过配置调整为更短时间(如 5 分钟),但需说明与正式参数的对应关系。 -- 配置不得硬编码。 -- 超时配置只在创建订单时用于计算支付截止时间;Worker 不使用当前配置重新计算既有订单是否到期。 +- 只选择 `paymentDeadline <= 权威时间` 且仍为 `PendingPayment` 的订单。 +- 扫描批次必须有界并使用稳定顺序,避免一次任务长期占用资源;具体间隔、批量大小和单轮次数在 Worker 配置阶段确定,不能写死在业务流程。 +- 多个 Worker 实例并发时可以同时发现同一候选,但最终只有一个 M04 取消动作提交;进程内集合或单机锁不能作为唯一正确性保障。 +- 同一订单在一个处理尝试中只调用一次统一取消。暂时失败后按退避策略重新进入后续尝试,不能无间隔反复占用任务。 +- Worker 重启后只需重新扫描共享的订单事实;不能依赖未持久化队列或内存标记保存唯一到期责任。 +- 任务记录用于运维追踪,不创建“超时失败”订单状态,也不改变业务终态。 -## 四、Worker 超时扫描流程 +## 五、支付、主动取消与 Worker 的竞争 ```mermaid flowchart TD - A["Worker 定时触发(建议间隔 ≤ 超时时间/2)"] --> B["初始化本轮处理计数和排除集合,最多 100 条"] - B --> C["开始处理一笔到期订单
首次领取下一条,失败时重试当前订单"] - C --> D["确认订单已到支付截止时间且仍为 PendingPayment
多 Worker 竞争时只允许一个进入处理"] - D --> E{"领取成功?"} - E -- "否" --> Z["本次扫描结束"] - E -- "是" --> F["条件推进 PendingPayment → Cancelled"] - F --> G{"状态推进成功?"} - G -- "否" --> H["订单已被支付或取消:放弃本次取消并记录最终状态"] - G -- "是" --> I{"订单类型?"} - I -- "普通订单" --> J1["按订单项数量恢复普通商品库存"] - I -- "秒杀订单" --> J2["回补秒杀活动库存 + 释放买家限购额度"] - J1 --> J3["记录超时取消时间和原因"] - J2 --> J3 - J3 --> K["登记可靠的订单取消事实,供买家通知使用"] - K --> L["原子提交订单状态、库存回补和取消事实"] - L --> M{"提交成功?"} - M -- "否" --> N["整体回滚本笔取消;独立记录失败次数、原因和本次结果"] - N --> O{"重试次数 < 3?"} - O -- "是" --> C - O -- "否" --> P["发送告警并加入本轮排除集合
保留 PendingPayment,退避后由后续扫描继续"] - H --> Q["本轮处理计数 +1"] - M -- "是" --> Q - P --> Q - Q --> R{"已处理 100 条?"} - R -- "否" --> C - R -- "是" --> Z + A["订单仍显示 PendingPayment"] --> B{"权威时间是否早于 paymentDeadline?"} + B -- "是" --> C["M05 / C08 支付可与买家主动取消竞争"] + B -- "否" --> D["支付无条件拒绝;M05 或 Worker 可触发过期取消"] + C --> E{"哪个合法动作先提交?"} + E -- "支付" --> P["Paid;后续取消读取最终状态并退出"] + E -- "主动取消" --> Q["Cancelled;后续支付被拒绝"] + D --> F{"过期取消是否已提交?"} + F -- "是" --> R["Cancelled;后续触发幂等重放"] + F -- "否,暂时失败" --> S["保持过期 PendingPayment;仍不可支付,等待取消重试"] ``` -关键约束: +竞争规则: -- 扫描间隔建议 ≤ 超时时间/2(如超时 30 分钟,扫描间隔 ≤ 15 分钟)。 -- 每批次处理上限 100 条,避免长时间锁表。 -- 领取和处理同一订单必须处于同一受控事务,多 Worker 竞争时只有一个能够推进订单状态;具体锁定方式由下游接口和实现契约承接。 -- 失败后的当前轮重试必须重新确认同一订单及其状态;达到本轮尝试上限后把该订单加入本轮排除集合,避免下一次领取立即命中。排除集合只控制本轮扫描,不保存业务事实。 -- 只有 `PendingPayment` 可以推进为 `Cancelled`,保证重复扫描和支付竞争不会产生第二个业务结果。 -- 库存回补、订单状态变更和可靠取消事实必须原子提交;任一失败则整体回滚,不允许出现"订单已取消但库存未回补"的部分成功状态。 -- 单轮扫描内同一订单最多尝试 3 次;每次失败都在订单事务回滚后独立持久化失败次数、原因和结果。3 次均失败时发送告警并结束该订单的本轮处理,不新增永久失败业务状态;订单保持 `PendingPayment`,按退避策略由后续扫描继续,Worker 重启后也能恢复。 +- 截止时间前,支付与买家主动取消竞争同一 `PendingPayment` 状态,只有一个结果。 +- 达到截止时间后,支付不再是合法竞争者;只剩过期取消的首次执行或重试。 +- 到期瞬间若支付已经合法提交为 `Paid`,Worker 读取最终状态后跳过;仅仅“请求已到达”但尚未在截止时间前形成合法结果不算支付成功。 +- 取消先提交后到达的同步支付或回调不得把订单改回 `Paid`;C08 的迟到成功进入差异处理。 +- 买家请求、M05 过期触发和 Worker 同时取消时,只有一个取消事务回补,其他入口重放首次结果。 -## 五、与支付的状态竞争处理 +## 六、普通与秒杀库存回补 ```mermaid flowchart TD - subgraph 并发竞争 - A["C03 Worker:超时扫描并尝试取消"] --> B["条件更新状态为 Cancelled"] - C["买家:主动点击支付"] --> D["M05 支付事务:条件更新状态为 Paid"] - end - - B --> E{"乐观锁结果"} - D --> F{"乐观锁结果"} - - E -- "取消成功,行数=1" --> G["库存已回补,订单已取消"] - F -- "支付成功,行数=1" --> H["余额已扣减,订单已支付"] - E -- "行数=0:已被支付" --> I["取消跳过,返回幂等成功"] - F -- "行数=0:已被取消" --> J["支付跳过,返回余额未扣减"] + A["M04 统一过期取消读取订单项原库存来源"] --> B{"来源类型?"} + B -- "普通 Catalog" --> C["按订单项数量恢复普通商品库存"] + B -- "Seckill" --> D["按订单项数量恢复原活动独立库存,并释放该买家限购数量"] + C --> E["与订单 Cancelled、取消时间 / 原因和可靠事实组成完整结果"] + D --> E + E --> F{"完整结果是否提交?"} + F -- "是" --> G["取消成功,只产生一次回补"] + F -- "否" --> H["全部不生效;订单保持过期 PendingPayment,后续重试"] ``` -关键约束: +- 秒杀活动已结束或取消仍接受其历史待支付订单回补;回补数量留在原活动且不重新开放抢购。 +- 普通商品下架仍可恢复库存事实;下架不会自动重新公开商品。 +- 商品或活动的原库存来源事实必须保留到所有待支付订单的取消责任结束,不能因后台删除导致历史订单无法回补。 +- 一个订单包含多项时,任一项回补失败都使整笔过期取消不成立,不能出现部分库存已回补。 +- 回补成功后再次扫描、Worker 重启或消息重投都不重复增加库存或释放限购数量。 -- 取消与支付使用相同的条件更新 `WHERE status = 'PendingPayment'`。 -- 最终只有一个操作成功,避免"又支付又取消"的矛盾状态。 -- 两者竞争时数据库事务隔离保证最终一致性。 +## 七、失败恢复与可观察结果 -## 六、事件输出 +| 场景 | C03 处理 | 订单与库存结果 | +|---|---|---| +| Worker 暂停后恢复 | 重新扫描所有到期待支付订单 | 不遗漏,不依赖旧进程内状态 | +| 多实例重复发现候选 | 均可尝试,M04 只允许一次提交 | 只取消和回补一次 | +| 到期时支付已成功 | 返回 `Paid` 并结束该候选 | 不取消、不回补 | +| 买家已主动取消 | 重放 `Cancelled` | 不重复回补或通知 | +| 原库存通道暂时不可用 | 整个取消失败并退避重试 | 订单保持过期 `PendingPayment`,仍不可支付 | +| 秒杀活动结束或取消 | 正常回补原活动封闭库存 | 不增加普通库存、不重开活动 | +| 取消事实提交成功但消息未送达 | 保留取消结果 | M09 重试通知 | +| 单轮多次暂时失败 | 结束本轮该候选并延后处理 | 不创建永久失败业务状态 | +| Worker 配置发生变化 | 只影响后续调度 | 历史订单截止时间不变 | -```mermaid -flowchart TD - A["超时取消事务提交成功"] --> B["发布 OrderCancelledIntegrationEvent(cancel_reason = TIMEOUT)"] - B --> C["Outbox 投递到 MQ"] - C --> D["M09 消费事件"] - D --> E["生成站内消息:订单超时取消通知"] - E --> F["买家查看消息中心"] -``` +用户可观察结果: -事件内容要求: +- 截止时间前显示剩余支付时间; +- 达到截止时间后立即隐藏支付入口,即使取消尚在重试; +- 取消成功后展示 `Cancelled`、取消时间和“支付超时”原因; +- 暂时无法取消时展示“订单已过期,系统正在取消”,不重新开放支付; +- 买家最终收到一条可查询的超时取消消息。 -- 消息标题:订单超时取消 -- 消息内容:您的订单 {订单号} 因超时未支付已自动取消,库存已回补,如有需要可重新下单。 -- cancel_reason = 'TIMEOUT' 用于 M09 生成差异化文案。 +## 八、由流程派生的内部契约与实现约束 -## 七、异常与边界场景 +C03 不派生新的公开 HTTP 接口。它依赖 M04 的 Ordering 内部应用契约: -| 场景 | C03 处理 | 最终状态/责任 | +| 内部业务动作 | 调用方 | 契约必须承载的结果 | |---|---|---| -| Worker 停止 | 重启后继续扫描 | 不漏扫,不重复取消 | -| 数据库连接短暂中断 | 记录错误日志,下次扫描重试 | 最多延迟一个扫描周期 | -| 扫描时订单已被支付 | 条件更新影响行数=0,跳过 | 订单保持 Paid | -| 扫描时订单已被买家取消 | 条件更新影响行数=0,跳过 | 订单保持 Cancelled | -| 库存回补时商品已删除或秒杀活动已结束 | 事务整体回滚并独立记录失败结果;本轮最多尝试 3 次,仍失败则告警并退避 | 订单保留 PendingPayment,库存不丢失,后续扫描继续处理或由人工排查根因 | -| 多实例 Worker 并发扫描 | 领取与处理在同一受控事务内完成,竞争失败方跳过当前订单 | 同一订单只被一个 Worker 推进状态 | -| 单笔订单本轮尝试 3 次均失败 | 发送告警,本轮跳过该订单并继续处理批次中的其他订单 | 订单保留 PendingPayment,退避后由后续扫描继续,不遗漏到期订单 | - -## 八、与 M04 订单模块的复用关系 - -C03 复用的 M04 逻辑: - -| M04 逻辑 | C03 复用方式 | -|---|---| -| 取消条件校验 | 直接复用订单归属和状态判断 | -| 库存回补事务 | 直接复用库存回补 SQL | -| 订单状态变更 | 直接复用条件更新 SQL | -| cancelled_at 和 cancel_reason | 直接复用字段写入 | -| OrderCancelledIntegrationEvent | 复用事件结构,cancel_reason = TIMEOUT | - -C03 不改变的 M04 逻辑: - -- 订单创建逻辑 -- 订单查询逻辑 -- 买家主动取消的具体接口契约 - -## 九、验收证据清单 - -- [ ] 超时订单被自动取消,状态变为 Cancelled -- [ ] 库存正确回补,回补量 = 订单项数量 -- [ ] cancelled_at 和 cancel_reason = TIMEOUT 已写入 -- [ ] OrderCancelledIntegrationEvent(TIMEOUT)已发布到 Outbox -- [ ] 买家收到站内消息通知 -- [ ] 买家在超时前支付成功,取消被跳过 -- [ ] 并发取消与支付只有一个成功,不出现矛盾状态 -- [ ] 重复取消返回幂等成功,库存只回补一次 -- [ ] Worker 重启后继续扫描,不漏扫 -- [ ] 多实例 Worker 不重复处理同一订单 -- [ ] 演示环境超时时间可配置(如 30 秒、5 分钟) +| 查询到期候选 | C03 Worker | 固定截止时间、待支付状态、稳定顺序和有界分页 | +| 过期取消订单 | C03 Worker、M05 过期路径 | 系统身份、到期复核、统一 `PaymentExpired` 原因、首次结果或幂等重放 | +| 查询取消最终结果 | Worker 恢复 / 重试 | `Cancelled`、其他终态、过期待重试,不泄露无关数据 | + +后续接口、架构和数据设计必须承接: + +1. 到期条件来自每张订单固定的支付截止时间,不重新用“创建时间 + 当前配置”推导。 +2. 系统内部取消与买家所有权鉴权分离,但仍只允许受信任 Worker / Payment 调用。 +3. 取消操作必须复用 M04 的状态、原库存通道、限购释放和可靠事实完整结果。 +4. 多实例正确性来自共享订单状态与唯一业务推进,不来自单机内存。 +5. 任务执行记录与订单业务状态分离;失败次数、退避和告警不能变成新订单状态。 +6. 批量大小、扫描间隔、单轮重试次数属于可观测配置,应按环境验证后确定,不由流程预设固定数字。 + +## 九、跨模块边界 + +- **M04**:拥有固定支付截止时间、订单状态和统一取消;C03 不复制取消逻辑。 +- **M05 / C08**:任何支付通道在到期后都拒绝。M05 可立即触发同一过期取消,C08 迟到成功不得覆盖取消终态。 +- **C01 / M02**:分别接受秒杀和普通库存原路回补;活动结束、取消或商品下架不消灭历史回补责任。 +- **M09**:只在取消完整结果提交后通知当前买家,通知失败自行重试。 +- **C10**:多 API / Worker 实例共享同一订单事实,重复扫描不会形成重复业务结果。 + +## 十、验收证据清单 + +- [ ] 正式订单在创建后 30 分钟到期;演示参数明确标注且不修改历史订单截止时间。 +- [ ] 权威时间早于截止时间才允许支付;达到截止时间后即使 Worker 未扫描也拒绝所有支付通道。 +- [ ] Worker 只处理已到期且仍为 `PendingPayment` 的订单,使用有界批次和稳定顺序。 +- [ ] Worker、M05 过期路径和买家到期后取消复用 M04 同一过期取消能力。 +- [ ] 支付与截止前主动取消只有一个结果;截止后支付不再参与竞争。 +- [ ] 过期取消暂时失败时订单仍不可支付,恢复后继续取消,不伪装成功。 +- [ ] 普通订单恢复 Catalog 库存;秒杀订单恢复原活动独立库存并释放限购数量。 +- [ ] 秒杀活动结束或取消后仍能回补,但不增加普通库存或重新开放抢购。 +- [ ] 多实例、重复扫描、Worker 重启和消息重投均不重复取消、回补或通知。 +- [ ] 到期时已经合法支付的订单保持 `Paid`,Worker 不取消、不回补。 +- [ ] 买家最终看到唯一取消时间和原因;取消重试期间看到“已过期、正在取消”而非支付入口。 +- [ ] 保存正式 / 演示参数、截止前后支付、主动 / 自动取消竞争、普通 / 秒杀回补、暂时失败、Worker 重启和多实例重复扫描的真实证据。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" index 4793ec2..ee9136a 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" @@ -1,276 +1,381 @@ # M04 订单流程 > 负责人:韦乾强 -> 覆盖:M04-01 提交订单(F08)、M04-02 订单列表与详情(F09)、M04-03 取消订单(F09) -> 基础核心流程:M01 身份与鉴权、M02 分类与商品、M03 购物车、M05 支付、M06-02 后台订单管理 -> 直接协作:朱惠惠(M03 购物车)、张海洋(M05 支付)、顾欣月(M02 商品)、罗皓晨(M09 站内消息) -> 文档状态:初稿 -> 需求事实源:[需求规格说明书 M04 订单模块](../../../01-需求文档/需求规格说明书.md) 的"M04-01、M04-02、M04-03"完整七节 +> 覆盖:M04-01、M04-02、M04-03、M04-04、F08、F09 +> 基础核心流程:F03、F04、F05、F06、F07、F10、F11、F12 +> 直接协作:唐宇昊(M01 Identity)、顾欣月(M02 Catalog)、朱惠惠(M03 Cart、C01 Seckill)、张海洋(M05 Payment、M10 AfterSales)、罗皓晨(M09 Messaging、Worker 基础设施) +> 文档状态:已按需求重构,可作为接口设计输入;待 Cart、Payment、AfterSales、Messaging 交叉评审 +> 需求事实源:[需求规格说明书 M04](../../../01-需求文档/需求规格说明书.md) 的 M04-01~M04-04 完整七节 -## 一、范围与事实来源 +## 一、范围与设计顺序 -本模块负责买家在已登录态下完成订单创建、订单查询、订单取消,以及商家在后台完成订单发货。订单模块承担"交易确认与履约的核心状态机",连接买家购物车结算、支付模拟、商家履约和管理员监督。 +M04 拥有订单创建、买家订单查询、待支付订单取消、订单核心状态和订单完成。商家订单查询与发货由 M06-02 承担,支付资金由 M05 承担,售后由 M10 承担;这些模块通过 M04 的权威订单事实协作,不直接改写彼此内部状态。 -本文先依据需求确定业务参与者、上游输入、状态派生口径、原子结果和模块出入口,再由这些动作派生接口契约。A301~A308 只用于流程完成后的契约映射与缺口检查,不能反向拼接流程,也不能用现有 Axxx 反向覆盖业务语义。 +本文先冻结订单状态、业务动作、时间边界、原子结果和模块交接,再在第十二章映射接口。旧接口、字段或实现只能用于检查可落地性,不能决定业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| -| M04-01/F08、M04-02/F09、M04-03/F09 需求 | 完整定义 | 作为订单业务语义事实源 | -| 本文业务流程 | 初稿 | 先确认参与者、上游输入、状态派生、原子结果和模块出入口 | -| A301~A308 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | -| DBxxx(orders、order_items) | 模板/占位 | 本文不发明字段、约束和索引 | -| C03 订单超时取消 | 独立扩展 | 登记边界,不混入 F08/F09 主流程 | -| M06-02 后台订单管理 | F12 基础履约 | 仅登记与 M04 共享的订单状态机边界 | +| M04-01~M04-04 需求 | 完整定义 | 作为订单语义事实源 | +| M03 购物车与 M01 地址 | 已校准或待整合 | 提供本人购物车条目和地址事实 | +| M02 / C01 库存来源 | 已定义或已校准 | 下单扣减、取消与售后按原通道回补 | +| M05 / C08 支付 | 已校准 | 只在截止时间前竞争待支付状态 | +| M06-02 商家履约 | 缺失,待本轮补齐 | 使用指定商家与订单状态公开能力 | +| M10 售后 | 已重构 | 提供发货阻断和已退款数量 | +| 本文业务流程 | 已重构、待交叉评审 | 作为接口与后续数据设计输入 | -## 二、模块直接出入口 +## 二、参与者、事实归属与模块出入口 ```mermaid flowchart LR - ID["M01 Identity
已认证买家、角色、账号状态"] -->|"BuyerOnly 通过"| ORD["M04 Ordering
订单状态机:PendingPayment → Paid → Shipped → Completed"] - CART["M03 Cart
已选中条目、服务端金额"] -->|"提交结算:商品+数量+地址+幂等键"| ORD - CAT["M02 Catalog
实时价格、实时库存、上架状态"] -->|"价格快照、库存扣减/回补"| ORD - PAY["M05 Payment
模拟支付状态、余额"] -->|"支付成功/失败、余额变化"| ORD - ORD -->|"订单列表、详情、取消/确认动作"| BUYER["买家订单页"] - ORD -->|"发货操作、物流信息"| MERCHANT["商家后台订单页"] - ORD -->|"站内消息通知"| MSG["M09 站内消息"] - MSG -->|"订单状态变更通知"| BUYER - MSG -->|"待发货通知"| MERCHANT - - ID -. "买家取消 / 超时取消".-> ORD - CAT -. "商品下架/改价".-> ORD + BUYER["买家"] -->|"选择购物车条目、地址、提交身份"| ORDER["M04 Ordering"] + CART["M03 Cart
本人条目、数量、当前可结算状态"] --> ORDER + ADDRESS["M01 Identity
本人有效地址与地址快照"] --> ORDER + CATALOG["M02 Catalog
商品公开状态、价格、普通库存"] <--> ORDER + SECKILL["C01 Seckill
秒杀价、独立库存、限购与活动来源"] <--> ORDER + MERCHANT["M01 Identity
唯一启用的默认商家"] --> ORDER + + ORDER -->|"订单号、应付金额、状态、支付截止时间"| PAYMENT["M05 / C08 Payment"] + ORDER <-->|"指定商家、Paid / Shipped 状态交接"| FULFILL["M06-02 商家履约"] + ORDER <-->|"订单项、实付、状态、可履约快照"| AFTER["M10 AfterSales"] + ORDER -->|"已提交创建、取消、完成事实与明确接收人"| MESSAGE["M09 Messaging"] + WORKER["C03 超时取消 / M04 自动完成 Worker"] -->|"系统内部动作"| ORDER + + GUEST["游客或越权角色"] -->|"拒绝买家写操作与私人查询"| DENY["不泄露订单、地址或购物车内容"] ``` -边界约束: +事实归属: -- M04 不接受前端传入最终金额,订单总额由服务端按快照计算。 -- M04 库存扣减/回补在事务内完成,使用条件更新防止超卖。 -- M04 不承担购物车、支付、站内消息的内部状态,只消费外部输入并输出确定性结果。 -- C03 超时取消复用 M04 的状态机与库存回补逻辑,但由 Worker 触发,不走买家主动接口。 +- M04 拥有订单号、买家、指定商家、订单核心状态、总额、地址快照、订单项快照、支付截止时间、发货 / 完成 / 取消时间和完成方式。 +- M03 拥有购物车。M04 只按买家明确选择的购物车条目标识读取当前数量,不接受客户端另传一套最终商品、数量或价格。 +- M01 拥有地址和账号。M04 校验地址归属与有效性,并只在订单成功时保存当时快照。 +- M02 和 C01 拥有各自库存。订单项保存原库存来源,使取消和售后永远回到原通道。 +- M05 / C08 决定资金或模拟通道支付是否成功;M04 只提供应付事实,并接受一次合法的 `Paid` 推进。 +- M06-02 通过 M04 公开动作把 `Paid` 推进为 `Shipped`;M10 提供发货前售后快照。 +- M09 只消费已提交事实。通知失败不能改变订单状态。 -## 三、提交订单(M04-01 / F08) +## 三、订单核心状态机 ```mermaid -flowchart TD - ID["M01:已认证且状态正常的买家"] --> A["买家从购物车提交订单:商品 ID 列表 + 地址 ID + 幂等键"] - CART["M03 Cart:读取选中条目"] --> B - CAT["M02 Catalog:读取实时价格、实时库存、上架状态"] --> B - A --> B{"全部商品可售且库存充足?"} - B -- "否" --> X["整单拒绝,返回问题商品和当前可购数量"] - B -- "是" --> C{"地址归属当前买家?"} - C -- "否" --> Y["拒绝:地址无效"] - C -- "是" --> D["服务端计算订单总额 = Σ(实时单价 × 数量)"] - D --> E["开启订单创建事务"] - E --> F["条件扣减库存:商品可售 AND 库存充足"] - F --> G["创建订单主记录(PendingPayment)+ 订单项快照"] - G --> H["解析并保存 assignedMerchantUserId"] - H --> I["删除已下单的购物车条目"] - I --> J["写入 Outbox:OrderCreatedIntegrationEvent"] - J --> K{"事务提交成功?"} - K -- "否" --> R["库存回滚、订单不创建"] - K -- "是" --> L["返回订单号、应付金额、PendingPayment 状态"] +stateDiagram-v2 + [*] --> PendingPayment: 订单原子创建成功 + PendingPayment --> Paid: 截止时间前合法支付唯一胜出 + PendingPayment --> Cancelled: 买家主动取消唯一胜出 + PendingPayment --> Cancelled: 到达支付截止时间后过期取消唯一胜出 + Paid --> Shipped: 指定商家合法发货 + Shipped --> Completed: 买家主动确认收货 + Shipped --> Completed: 发货满 7 天自动完成 + Completed --> [*] + Cancelled --> [*] ``` -关键约束: +| 状态 | 允许的下一核心状态 | 买家主要入口 | 商家主要入口 | +|---|---|---|---| +| `PendingPayment` | `Paid` 或 `Cancelled` | 截止时间前支付;随时取消;查看倒计时 | 只查看,不发货 | +| `Paid` | `Shipped` | 查看、按 M10 申请售后 | 指定商家发货 | +| `Shipped` | `Completed` | 确认收货、按 M10 申请售后 | 查看履约结果 | +| `Completed` | 无 | 在评价与售后规则允许时进入后续流程 | 查看最终结果 | +| `Cancelled` | 无 | 查看取消结果 | 查看最终结果 | + +状态规则: + +- `PendingPayment` 只能由一次支付或一次取消形成终态竞争结果,不能同时 `Paid` 和 `Cancelled`。 +- 支付是否允许同时受状态与支付截止时间约束;Worker 尚未扫描不代表过期订单仍可支付。 +- `Paid → Shipped → Completed` 不允许跳级或回退。 +- `Cancelled` 与 `Completed` 是订单核心终态。 +- 售后状态独立存在,退款不新增订单“已退款”状态,也不覆盖 `Paid`、`Shipped`、`Completed`。 +- 所有重复、并发和恢复动作都返回已提交的当前事实,不再制造第二次状态时间或通知。 + +## 四、购物车提交订单 + +### 4.1 买家输入与服务端重读 -- 同一幂等键 `(buyer_id, idempotency_key)` 只创建一张订单,重复请求返回首次成功结果。 -- 库存扣减使用商品可售且库存充足的条件更新,避免并发超卖和商品下架后仍被下单。 -- 订单项保存商品名称、图片、成交单价快照,后续改价不影响已有订单。 -- 地址保存快照,后续修改不影响已有订单。 -- 订单金额由服务端计算,不接受客户端传入。 -- 购物车清理在同事务内完成;清理失败时事务整体回滚,库存不扣减,订单不创建。 +买家提交的业务输入只有: -## 四、订单列表与详情(M04-02 / F09) +- 本次选中的本人购物车条目标识; +- 本人收货地址标识; +- 当前提交动作的必填唯一幂等标识。 -### 4.1 订单列表 +客户端不得提交最终订单金额、订单项成交价、处理商家、支付截止时间、库存来源或可直接创建订单的商品清单。 + +### 4.2 主流程 ```mermaid flowchart TD - ID["M01:已认证买家"] --> A["买家请求订单列表:分页 + 可选状态筛选"] - A --> B["按 buyer_id = 当前用户过滤"] - B --> C["按状态筛选(PendingPayment/Paid/Shipped/Completed/Cancelled)"] - C --> D["按创建时间倒序返回列表"] - D --> E["返回订单号、状态、总额、创建时间、商品摘要"] + A["状态正常的买家从购物车选择条目和收货地址"] --> B["生成本次提交的唯一幂等标识"] + B --> C{"同一买家、同一幂等标识是否已有确定结果?"} + C -- "同内容已成功" --> R["重放首次订单号、金额、状态和支付截止时间"] + C -- "换内容复用" --> X["拒绝标识复用,不创建新订单"] + C -- "没有结果" --> D["读取本人选中条目、当前数量、商品状态、实时价格和库存来源"] + D --> E["读取并校验本人有效地址,解析唯一启用的默认商家"] + E --> F{"所有条目、地址和商家事实均可用?"} + F -- "否" --> Y["整单拒绝;购物车、库存和订单均不变化"] + F -- "是" --> G["按权威商品顺序重新校验可售、数量、库存与金额"] + G --> H{"每项都可原子扣减?"} + H -- "否" --> Z["整单失败;已发生的临时变更全部不生效"] + H -- "是" --> I["形成订单号、默认商家、PendingPayment、固定支付截止时间、地址与订单项快照"] + I --> J["形成完整原子结果:各原通道库存扣减、订单与订单项、选中购物车清理、订单创建通知事实"] + J --> K{"完整结果提交?"} + K -- "否" --> T["全部不生效;购物车和库存保持提交前状态,可安全重试"] + K -- "是" --> U["返回订单号、服务端总额、PendingPayment 和支付截止时间"] ``` -关键约束: +### 4.3 下单规则 + +- 订单总额等于服务端实时成交单价乘数量后求和;结果必须为正,客户端金额只可用于显示,不能参与裁决。 +- 订单项保存商品名称、主图、成交单价、数量和原库存来源快照;商品后续改名、改价、上下架不改变历史订单。 +- 地址快照在订单成功时形成;地址后续编辑或删除不改变历史订单。 +- 处理商家固定为提交时唯一启用的默认商家,保存为 `assignedMerchantUserId`。不存在、重复或不可用时整单失败,不能创建无人处理订单。 +- 默认商家禁用与下单必须形成确定顺序:下单先被接受时禁用复核应发现新责任;禁用先生效时下单不能再分配给该账号。 +- 普通购物车订单由 M04 协调 Catalog 库存扣减;秒杀入口由 C01 协调独立活动库存与限购,并在同一原子边界调用 M04 的统一订单创建能力。两条入口都由 M04 生成共享订单、指定商家、快照和固定支付截止时间,订单项库存来源不得混用。 +- 任一条目不可售、数量非法、库存不足、地址无效、默认商家不可用、购物车清理失败或可靠创建事实失败时,整单不成立。 +- 同一买家、同一幂等标识、同一内容只形成一张订单。第一次结果未知时先查询原结果,不能用相同动作再扣一次库存。 +- 支付截止时间在订单创建时按当时可追踪配置固定;正式口径为创建后 30 分钟,演示参数只能缩短演示等待,不改变正式规则。 +- 订单创建成功只通知当前买家;商家待处理提醒在支付成功后产生,避免未付款订单干扰履约。 -- 买家只能查看本人订单,按 buyer_id 过滤。 -- 订单号可脱敏展示,商品摘要最多 3 个。 -- 分页参数 page ≥ 1,pageSize 默认 10,上限 50。 +## 五、买家订单列表、详情与操作入口 -### 4.2 订单详情 +### 5.1 列表与详情 ```mermaid flowchart TD - ID["M01:已认证买家"] --> A["买家请求订单详情:orderId"] - A --> B{"订单存在且归属当前买家?"} - B -- "否" --> X["返回 404 或 403,不泄露归属"] - B -- "是" --> C["返回完整订单信息"] - C --> D["地址快照:收件人、手机号(脱敏)、省市区、详细地址"] - D --> E["订单项快照:商品名称/图片/单价/数量/小计"] - E --> F["状态时间线:创建/支付/发货/完成/取消时间"] - F --> G["可用操作入口:根据状态展示 cancel/confirm/pay"] + A["已认证买家进入订单页面"] --> B{"查询列表或详情?"} + B -- "列表" --> C["只按当前买家分页查询;可按五种核心状态筛选;创建时间倒序"] + B -- "详情" --> D["按订单号与当前买家共同校验归属"] + C --> E["展示订单号、状态、总额、创建时间和最多三个商品摘要"] + D --> F["展示地址快照、全部订单项快照、金额、时间线、支付与售后摘要"] + E --> G["按最新状态派生操作入口"] + F --> G + D -. "订单不存在或不属于本人" .-> X["统一拒绝,不泄露他人订单是否存在"] ``` -关键约束: +详情时间线至少按已发生事实展示: + +- 创建时间与支付截止时间; +- 支付时间和实际支付来源; +- 发货时间; +- 完成时间与完成方式; +- 取消时间与取消原因。 + +列表与详情规则: + +- 买家只能查看本人订单;商家订单查询由 M06-02 提供,不能复用买家接口扩大权限。 +- 列表状态筛选只接受 `PendingPayment`、`Paid`、`Shipped`、`Completed`、`Cancelled` 或全部。 +- 列表每页默认 10 条、最多 50 条,按创建时间倒序并使用稳定次序;超出总页数返回正常空页。 +- 详情使用订单创建时的地址、商品、成交价和数量快照;图片失效时展示占位,不修改快照事实。 +- 地址联系电话按展示场景脱敏;订单归属、金额、状态和时间不能由前端覆盖。 +- 已支付订单展示真实成功支付来源:默认小金库支付或 C08 受控模拟通道;未支付订单不伪造支付记录。 + +### 5.2 操作入口矩阵 -- 订单项为快照,不读取商品实时价格。 -- 地址为快照,不读取地址实时状态。 -- 状态时间线展示所有状态变更节点。 +| 当前事实 | 买家入口 | +|---|---| +| `PendingPayment` 且权威时间早于支付截止时间 | 去支付、取消订单 | +| `PendingPayment` 且已到支付截止时间 | 不展示可支付入口;展示过期取消处理中或最新取消结果 | +| `Paid` | 查看支付结果;按 M10 规则展示未发货仅退款入口 | +| `Shipped` | 确认收货;按 M10 规则展示售后入口 | +| `Completed` | 未评价订单项展示评价入口;完成后 7 天内按 M10 展示售后入口 | +| `Cancelled` | 无支付、发货、完成或售后入口 | -## 五、取消订单(M04-03 / F09) +页面入口只是提示。实际动作仍必须重新校验最新状态、截止时间、归属和模块规则。 + +## 六、统一取消能力 + +### 6.1 取消入口与原因 + +M04 只维护一套取消业务动作: + +- 买家在截止时间前主动取消,原因是 `BuyerRequested`; +- 买家在截止时间达到后发起取消、M05 / C08 发现过期、或 C03 Worker 扫描到期时,原因统一为 `PaymentExpired`; +- C03 和 M05 使用系统内部身份,不伪装成买家,也不经过买家所有权授权; +- 所有入口最终竞争同一 `PendingPayment` 状态并执行同一原库存回补。 + +### 6.2 主流程 ```mermaid flowchart TD - ID["M01:已认证买家"] --> A["买家请求取消订单:orderId"] - A --> B{"订单存在且归属当前买家?"} - B -- "否" --> X["返回 404 或 403"] - B -- "是" --> C{"订单状态为 PendingPayment?"} - C -- "否" --> Y["返回 409:状态不允许取消"] - C -- "是" --> D["开启取消事务"] - D --> E["条件更新状态为 Cancelled:WHERE status = PendingPayment"] - E --> F{"订单类型?"} - F -- "普通订单" --> G1["回补普通库存:stock = stock + quantity"] - F -- "秒杀订单" --> G2["回补秒杀活动库存 + 释放买家限购额度"] - G1 --> H["记录 cancelled_at 和 cancel_reason = BUYER_CANCELLED"] - G2 --> H - H --> I["写入 Outbox:OrderCancelledIntegrationEvent"] - I --> J{"事务提交成功?"} - J -- "否" --> R["返回错误,不回补库存"] - J -- "是" --> K["返回取消成功"] + A["买家主动请求,或 M05 / C08 / C03 触发过期取消"] --> B["读取订单状态、买家归属、支付截止时间和订单项原库存来源"] + B --> C{"调用方是否有权触发?"} + C -- "买家但非本人 / 越权角色" --> X["拒绝且不泄露订单"] + C -- "合法买家或系统内部触发" --> D{"当前订单状态?"} + D -- "Cancelled" --> R["重放首次取消时间、原因和结果"] + D -- "Paid / Shipped / Completed" --> Y["拒绝取消并返回当前最终状态"] + D -- "PendingPayment" --> E["按权威时间确定 BuyerRequested 或 PaymentExpired"] + E --> F["与支付竞争唯一状态结果"] + F --> G{"取消是否唯一胜出?"} + G -- "否" --> H["读取并返回最新 Paid 或 Cancelled 结果"] + G -- "是" --> I["按每个订单项原通道恢复库存;秒杀同时释放对应限购数量"] + I --> J["形成完整原子结果:Cancelled、取消时间与原因、全部原路回补、买家取消通知事实"] + J --> K{"完整结果提交?"} + K -- "否" --> T["全部不生效;订单保持 PendingPayment,若已过期则仍不可支付并等待重试"] + K -- "是" --> U["返回唯一取消结果;后续重试直接重放"] ``` -关键约束: +### 6.3 取消与回补规则 -- 只有 PendingPayment 状态可取消。 -- 使用条件更新 `WHERE status = 'PendingPayment'` 保证幂等。 -- 重复取消返回成功,不重复回补库存。 -- 取消事务回滚时库存不变化。 +- 只有 `PendingPayment` 可以首次取消。`Cancelled` 重放幂等成功;`Paid`、`Shipped`、`Completed` 明确拒绝。 +- 状态、取消时间与原因、普通 / 秒杀库存、秒杀限购数量和可靠通知事实必须同时成功或同时失败。 +- 普通订单回补 Catalog;秒杀订单回补原活动独立库存并释放该买家本次订单占用的限购数量,不得增加普通库存。 +- 秒杀活动已经结束或取消时仍必须接受合法历史订单的取消回补;回补数量留在原活动且不重新开放购买。 +- 商品下架不阻止回补;商品或活动的来源事实不能在所有待支付取消与售后责任结束前被破坏性删除。 +- 取消失败时订单可能仍是已经过期的 `PendingPayment`,但 M05 / C08 仍必须按截止时间拒绝支付;C03 后续继续取消,不得因为 Worker 延迟重开支付窗口。 +- 主动取消和过期取消都只通知当前买家,不向全部商家广播。 +- 待支付订单取消不触发退款;已支付订单只能进入 M10 售后。 -## 六、与支付模块的协作(M05) +## 七、支付交接与截止时间竞争 ```mermaid flowchart TD - PENDING["PendingPayment 订单"] --> PAY["M05 Payment:买家确认支付"] - PAY --> A{"余额充足?"} - A -- "否" --> X["提示余额不足,引导充值"] - A -- "是" --> B["开启支付事务"] - B --> C["扣减钱包余额"] - C --> D["写入 payment 记录"] - D --> E["更新订单状态为 Paid"] - E --> F["写入 Outbox:OrderPaidIntegrationEvent"] - F --> G{"事务提交成功?"} - G -- "否" --> R["余额回滚,订单保持 PendingPayment"] - G -- "是" --> H["返回支付成功"] - H --> I["M09 消费 OrderPaidIntegrationEvent,发送站内消息"] + A["M05 或 C08 请求确认支付"] --> B["M04 提供应付金额、当前状态和固定支付截止时间"] + B --> C{"仍为 PendingPayment 且权威时间早于截止时间?"} + C -- "否且已到期" --> X["拒绝支付并触发统一过期取消"] + C -- "否且已有终态" --> Y["返回当前 Paid 或 Cancelled 结果"] + C -- "是" --> D["支付与主动取消竞争待支付状态"] + D --> E{"支付是否唯一胜出?"} + E -- "否" --> F["返回最新状态,不确认第二次支付"] + E -- "是" --> G["接受已确认的支付来源并原子推进 Paid、支付时间和可靠支付事实"] + G --> H["通知买家支付成功,并通知订单指定商家出现待履约订单"] ``` -关键约束: +边界规则: + +- M04 不检查钱包余额、不扣款、不创建支付流水,也不决定回调签名;这些属于 M05 / C08。 +- M04 只提供订单号、买家、应付金额、当前状态和支付截止时间,并接受一个已由 Payment 确认的成功来源。 +- 权威时间 `< paymentDeadline` 时支付可竞争;`>= paymentDeadline` 时任何支付通道都必须拒绝。 +- 支付成功、订单 `Paid`、支付时间、支付来源和可靠支付事实必须形成一致结果。失败时不得留下订单已支付但没有成功支付来源。 +- 同步钱包与受控模拟回调最多一个成为成功来源;另一方读取 `Paid` 后不得重复扣款或入账。 -- 支付使用条件更新 `WHERE status = 'PendingPayment'` 保证幂等。 -- M05 与 M04 的状态更新共享同一 PendingPayment 条件,避免"又支付又取消"。 -- 支付成功后订单状态流转为 Paid,进入待发货阶段。 +## 八、商家履约交接 -## 七、与商家发货的协作(M06-02 / F12) +M06-02 负责商家页面与发货动作,M04 只冻结共享状态边界: ```mermaid flowchart TD - PAID["Paid 订单"] --> SHIP["商家请求发货:orderId + 物流信息"] - SHIP --> A{"订单存在且 assignedMerchantUserId == currentUserId?"} - A -- "否" --> X["返回 404 或 403"] - A -- "是" --> B{"存在未完结售后申请锁定该订单行?"} - B -- "是" --> Y["返回 409:存在进行中售后,暂不允许发货"] - B -- "否" --> C{"订单状态为 Paid?"} - C -- "否" --> Z["返回 409:状态不允许发货"] - C -- "是" --> D["更新状态为 Shipped,记录 shipped_at、物流信息"] - D --> E["写入 Outbox:OrderShippedIntegrationEvent"] - E --> F{"事务提交成功?"} - F -- "否" --> R["返回错误"] - F -- "是" --> G["返回发货成功"] - G --> H["M09(Messaging 模块)幂等消费 OrderShippedIntegrationEvent,生成站内消息"] + A["订单指定商家请求发货"] --> B["M04 校验 assignedMerchantUserId 和 Paid 状态"] + B --> C["M10 提供非终态售后与已退款数量快照"] + C --> D{"仍可发货且至少有一个可履约数量?"} + D -- "否" --> X["阻断并返回当前订单 / 售后事实"] + D -- "是" --> E["发货与售后申请串行复核"] + E --> F["唯一推进 Paid → Shipped,保存发货时间、剩余履约数量和必要说明"] + F --> G["形成买家发货通知事实"] ``` -关键约束: +- 商家只能操作 `assignedMerchantUserId` 为当前账号的订单,不存在“所有 Merchant 共享全部订单”的范围。 +- 只有 `Paid` 可以发货;`PendingPayment`、`Cancelled`、`Shipped`、`Completed` 不得首次发货。 +- M10 非终态申请阻断发货;已退款数量从可履约数量扣除,部分退款只发剩余数量,全部退款不得发货。 +- 发货与售后申请并发时按同一订单履约事实串行复核,不能让同一数量既按未发货退款又被发出。 +- 重复同一发货动作返回已有 `Shipped` 结果,不重复生成发货时间或通知;具体查询、敏感信息和异常由 M06-02 文档补齐。 -- 商家只能操作 `assignedMerchantUserId == currentUserId` 的订单(单店 B2C 严格校验)。 -- 发货前须检查是否存在未完结的售后申请(AfterSales 锁定同一订单行)。 -- 发货使用条件更新 `WHERE status = 'Paid'` 保证幂等。 -- 重复发货返回成功,不重复变更状态。 +## 九、确认收货与自动完成 -## 八、买家确认收货 +### 9.1 主动确认 ```mermaid flowchart TD - SHIPPED["Shipped 订单"] --> CONFIRM["买家请求确认收货:orderId"] - CONFIRM --> A{"订单存在且归属当前买家?"} - A -- "否" --> X["返回 404 或 403"] - A -- "是" --> B{"订单状态为 Shipped?"} - B -- "否" --> Y["返回 409:状态不允许确认"] - B -- "是" --> C["更新状态为 Completed,记录 completed_at 和 completed_by = BUYER_CONFIRMED"] - C --> D["写入 Outbox:OrderCompletedIntegrationEvent"] - D --> E["事务提交成功?"] - E -- "否" --> R["返回错误"] - E -- "是" --> F["返回确认成功"] - F --> G["M09(Messaging 模块)幂等消费 OrderCompletedIntegrationEvent,生成站内消息"] - G --> H["X01 开放评价入口(若已实现)"] + A["买家在本人 Shipped 订单点击确认收货"] --> B["页面二次提示完成后不能走待支付取消"] + B --> C["服务端重新校验买家归属和 Shipped 状态"] + C --> D["与自动完成竞争唯一完成结果"] + D --> E{"主动确认是否唯一胜出?"} + E -- "否" --> R["返回已提交 Completed、完成时间和方式"] + E -- "是" --> F["原子推进 Completed、记录完成时间和 BuyerConfirmed、形成买家完成通知事实"] + F --> G["刷新评价与售后入口"] ``` -关键约束: +### 9.2 自动完成 + +```mermaid +flowchart TD + A["Worker 查找发货满正式 7 天的 Shipped 订单"] --> B["重新校验订单仍为 Shipped 且已到自动完成时间"] + B --> C["与买家主动确认竞争唯一完成结果"] + C --> D{"自动完成是否唯一胜出?"} + D -- "否" --> R["返回已有 Completed 结果"] + D -- "是" --> E["原子推进 Completed、记录完成时间和 AutoCompleted、形成买家完成通知事实"] + E --> F["后续评价与售后按各自规则开放"] +``` + +完成规则: + +- 正式自动完成时间为发货后 7 天;演示环境可配置更短等待,但验收材料必须标明演示参数。 +- 主动确认和自动完成同时发生时只能记录一个完成时间、一个完成方式和一条完成通知事实。 +- 重复点击、多设备确认、Worker 重扫或 Worker 重启都返回已有完成结果。 +- 自动完成暂时失败时订单保持 `Shipped`,恢复后继续处理,不提前显示完成。 +- 完成不扣减或回补库存,不修改金额、支付、地址或订单项快照。 +- M10 售后状态独立于订单完成;已有售后继续处理,新售后是否可申请由“完成后 7 天”规则决定。 +- X01 评价只从已完成订单详情或订单项入口进入,并在提交时重新校验资格。 -- 只有 Shipped 状态可确认收货。 -- 确认收货后买家可对订单项进行评价(X01)。 -- 使用条件更新 `WHERE status = 'Shipped'` 保证幂等。 +## 十、统一操作矩阵与异常 -## 九、异常、状态竞争与责任 +| 当前状态 / 条件 | 支付 | 取消 | 发货 | 完成 | +|---|---|---|---|---| +| `PendingPayment` 且截止时间前 | 可与取消竞争 | 本人可主动取消 | 不可 | 不可 | +| `PendingPayment` 且已到截止时间 | 不可 | 过期取消可重试 | 不可 | 不可 | +| `Paid` | 重放已有结果 | 不可 | 指定商家按售后快照发货 | 不可 | +| `Shipped` | 不可 | 不可 | 重放已有发货结果 | 本人确认或到期自动完成 | +| `Completed` | 不可 | 不可 | 不可 | 重放已有完成结果 | +| `Cancelled` | 不可 | 重放已有取消结果 | 不可 | 不可 | -| 场景 | M04 处理 | 最终状态/责任 | +| 异常场景 | 处理 | 不变量 | |---|---|---| -| 游客、商家、管理员访问买家订单接口 | 拒绝 | 401/403,不返回数据 | -| 订单不存在或不属于买家 | 404/403 | 不泄露归属 | -| 库存不足 | 整单拒绝 | 事务回滚,库存不扣减 | -| 商品下架 | 整单拒绝 | 事务回滚 | -| 地址无效或不归属 | 整单拒绝 | 事务回滚 | -| 幂等键重复提交 | 返回首次成功结果 | 不重复扣库存,不重复创建订单 | -| 支付时余额不足 | 支付失败 | 订单保持 PendingPayment | -| 取消时订单已为 Cancelled | 条件更新影响行数=0,返回幂等成功 | 不重复取消,不重复回补库存 | -| 取消时订单为 Paid/Shipped/Completed | 条件更新影响行数=0,返回 409 状态冲突 | 订单保持不变 | -| 并发取消与支付 | 条件更新竞争,最终只有一个成功 | 不会出现"又支付又取消" | -| 商家发货时状态已变更 | 条件更新影响行数=0,返回幂等成功 | 不重复变更 | -| C03 超时取消与支付竞争 | 条件更新竞争,最终只有一个成功 | 不会出现矛盾状态 | - -## 十、由流程派生的接口映射 - -| 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | +| 购物车条目、地址或订单不属于当前买家 | 拒绝且不泄露资源 | 不创建 / 不修改订单 | +| 商品下架、数量非法或库存竞争失败 | 整单拒绝 | 不部分扣库存、不清购物车 | +| 默认商家缺失、重复、禁用或并发被禁用 | 整单拒绝或按先后唯一裁决 | 不创建无人处理订单 | +| 同幂等标识同内容重试 | 重放首次订单 | 不重复扣库存 | +| 同幂等标识换内容 | 拒绝 | 不覆盖首次结果 | +| 支付与取消并发 | 只有一个状态推进成功 | 不出现既支付又取消 | +| 过期取消暂时失败 | 保持过期 `PendingPayment` 并重试 | 仍不可支付,库存不丢失 | +| 普通 / 秒杀回补任一步失败 | 整个取消不成立 | 不出现取消但未完整回补 | +| 发货与售后申请并发 | 最新履约事实唯一裁决 | 同一数量不被重复处理 | +| 主动确认与自动完成并发 | 只有一个完成结果 | 时间、方式、通知不重复 | +| 消息暂时不可达 | 保留已提交订单事实 | M09 重试,不回滚订单 | + +## 十一、跨模块必须承接的事实 + +- **M01**:地址归属与默认商家是下单权威输入;默认商家禁用和新订单分配不能同时成功。 +- **M02 / M03**:M04 读取选中购物车条目和实时商品事实;订单成功才清理选中条目,失败保持购物车。普通库存只由 Catalog 扣减和回补。 +- **C01**:秒杀绕过购物车,但不建立第二套订单创建器。C01 负责活动、独立库存和限购,并在同一原子边界调用 M04 生成共享订单、指定商家、快照和固定支付截止时间;取消只回补活动独立库存。 +- **M05 / C08 / C03**:三者共同遵守支付截止时间。到期后支付无条件拒绝,统一过期取消可由支付请求或 Worker 触发。 +- **M06-02 / M10**:订单指定商家是唯一履约范围;发货前读取售后阻断与已退款数量并串行复核。 +- **M09**:订单创建、取消、支付、发货、完成只通知流程明确的买家或指定商家;消息不作为订单状态来源。 +- **X01**:评价入口只在完成订单项出现;Review 提交时重新校验订单项属于当前买家、已完成且未评价。 + +## 十二、由流程派生的接口契约映射 + +本节是业务流程的下游映射。现有接口若缺少截止时间、指定商家、幂等、来源库存、完成方式或状态竞争,应重建契约,不得删除前文分支迁就旧请求体。 + +| 已确认业务能力 | 派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 提交订单(可选幂等) | A301 | 校验库存/地址/商品状态,原子扣减,创建订单快照,返回订单号和 PendingPayment | 待评审 | -| 查询订单列表(分页+筛选) | A302 | 仅返回当前买家订单,按创建时间倒序 | 待评审 | -| 查询订单详情 | A303 | 返回地址快照、订单项快照、状态时间线 | 待评审 | -| 买家取消订单 | A304 | 条件更新状态为 Cancelled,回补库存,写入 cancelled_at | 待评审 | -| 商家查询订单列表 | A305 | 仅返回与商家商品相关的订单 | 待评审 | -| 商家查询订单详情 | A306 | 返回商家可见的订单信息 | 待评审 | -| 商家发货 | A307 | 条件更新状态为 Shipped,记录物流信息 | 待评审 | -| 买家确认收货 | A308 | 条件更新状态为 Completed,记录 completed_at | 待评审 | - -## 十一、扩展接入边界 - -- C03 订单超时自动取消:复用取消事务逻辑,Worker 触发,不走买家主动接口;C03 复用 M04 的库存回补和状态变更逻辑。 -- X01 商品评价:Completed 状态后开放评价入口,评价模块消费 OrderCompletedIntegrationEvent。 -- M09 站内消息:消费 OrderCreatedIntegrationEvent、OrderCancelledIntegrationEvent、OrderShippedIntegrationEvent、OrderCompletedIntegrationEvent。 -- M05 支付:在支付事务内更新订单状态为 Paid 并发布 OrderPaidIntegrationEvent;M09(Messaging 模块)幂等消费事件并生成站内消息。 - -## 十二、验收证据清单 - -- [ ] 正常下单:返回订单号、PendingPayment 状态、订单总额 -- [ ] 库存不足:整单拒绝,库存不扣减 -- [ ] 地址无效:整单拒绝 -- [ ] 幂等键重复:返回首次成功结果,不重复扣库存 -- [ ] 订单列表:仅返回当前买家订单,分页正确 -- [ ] 订单详情:地址快照、订单项快照、状态时间线正确 -- [ ] 买家取消:状态变为 Cancelled,库存回补,站内消息通知 -- [ ] 重复取消:幂等成功,库存只回补一次 -- [ ] 已支付/已发货/已完成/已取消订单取消被拒绝 -- [ ] 商家发货:状态变为 Shipped -- [ ] 重复发货:幂等成功 -- [ ] 买家确认收货:状态变为 Completed,站内消息通知 -- [ ] 支付与取消/发货竞争:条件更新保证最终只有一个成功 -- [ ] C03 超时取消:状态变为 Cancelled,库存回补,站内消息通知 +| 购物车提交订单 | A301 | 必填幂等标识、选中购物车条目标识、本人地址;服务端金额 / 商家 / 截止时间;整单原子结果 | 待重建详细契约 | +| 买家订单列表 | A302 | 本人隔离、五状态筛选、稳定分页、最新摘要 | 待重建详细契约 | +| 买家订单详情 | A303 | 全部快照、截止时间、状态时间线、真实支付来源、售后与操作入口事实 | 待重建详细契约 | +| 买家取消订单 | A304 | 本人授权、首次取消或幂等重放、按时间确定取消原因、原库存通道完整回补 | 待重建详细契约 | +| 买家确认收货 | A308 | 本人 `Shipped → Completed`、二次确认、主动 / 自动完成竞争的当前结果 | 待重建详细契约 | +| 支付状态推进 | Payment 内部应用契约 | 应付金额、截止时间和 `PendingPayment → Paid` 唯一竞争 | 待按流程补齐 | +| 过期取消 | Ordering 内部应用契约 | 系统身份、到期复核、统一取消与幂等回补 | 待按流程补齐 | +| 自动完成 | Ordering 内部应用契约 | 到期复核、`Shipped → Completed` 与主动确认竞争 | 待按流程补齐 | + +A305~A307 由 M06-02 商家履约流程派生,本文件不以旧商家接口反向定义商家页面。 + +接口阶段必须满足: + +1. A301 的幂等标识为必填;请求只传选中购物车条目标识和地址标识,不传最终数量清单、价格、总额或商家。 +2. A304 对已 `Cancelled` 的本人订单重放首次成功,对 `Paid` / `Shipped` / `Completed` 明确拒绝;调用时达到截止时间则使用过期原因。 +3. A303 在过期但尚未取消成功时必须表达“不可支付、取消待重试”,不能仅凭 `PendingPayment` 展示支付按钮。 +4. Payment 内部契约同时校验状态和截止时间;成功来源可为小金库或受控模拟通道。 +5. A308 返回唯一完成时间和方式;已完成重试不重复通知。 +6. 所有资源归属都由服务端当前身份判断,错误响应不泄露他人订单存在性。 + +## 十三、验收证据清单 + +- [ ] 提交订单只使用本人选中购物车条目、本人地址和必填幂等标识;服务端重读数量、价格、库存和默认商家。 +- [ ] 正常下单原子形成库存扣减、订单 / 订单项快照、购物车清理和创建通知事实,并返回固定支付截止时间。 +- [ ] 任一商品不可售、库存不足、地址无效、默认商家不可用或可靠事实失败时整单不成立,库存和购物车不留部分变化。 +- [ ] 同幂等标识同内容返回首次订单;换内容拒绝;未知结果先查询原订单,不重复扣库存。 +- [ ] 默认商家分配与账号禁用并发时只有一个合法结果,不产生无人处理订单。 +- [ ] 买家列表和详情严格隔离,五种状态筛选、快照、金额、时间线和真实支付来源正确。 +- [ ] 截止时间前可支付;达到截止时间后即使 C03 未运行也不能支付。 +- [ ] 买家取消、M05 过期触发和 C03 Worker 复用同一取消能力;重复取消不重复回补或通知。 +- [ ] 普通和秒杀订单均按订单项原库存来源回补;活动结束或取消不阻止历史取消回补,也不重新开放抢购。 +- [ ] 取消与支付并发只有 `Paid` 或 `Cancelled` 一个结果;过期取消失败时订单不可支付且可恢复。 +- [ ] 指定商家和 M10 售后快照共同约束发货,部分退款只发剩余数量,全部退款不得发货。 +- [ ] 买家主动确认与发货满 7 天自动完成只有一个完成时间、方式和通知事实;重复与 Worker 重启不重复处理。 +- [ ] 完成后评价和售后入口分别遵守 X01 与 M10 规则,退款不覆盖订单核心状态。 +- [ ] 保存下单、库存竞争、幂等重放、默认商家异常、支付截止、主动 / 超时取消、普通 / 秒杀回补、发货、主动 / 自动完成和越权场景的真实证据。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" index 01c5952..dc7fbdd 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" @@ -157,11 +157,12 @@ flowchart TD J -- "是" --> L["原子增加当前买家的活动限购占用,且不得超过上限"] L --> M{"限购占用是否成功?"} M -- "否" --> K - M -- "是" --> N["创建共享待支付订单与订单项快照,记录秒杀来源、活动和成交价"] - N --> O["可靠记录订单已创建事实,并绑定本次稳定请求结果"] + M -- "是" --> N["在同一原子边界调用 M04 统一订单创建能力"] + N --> N1["M04 生成共享待支付订单、指定商家、固定支付截止时间、地址与订单项快照,并记录秒杀来源、活动和成交价"] + N1 --> O["可靠记录订单已创建事实,并绑定本次稳定请求结果"] O --> P{"库存、限购、订单、快照、可靠事实与请求结果是否整体提交?"} P -- "否" --> T["整体回滚;属于未形成确定结果的瞬态失败,原标识可重试"] - P -- "是" --> Q["返回同一订单号、购买结果和提交后的权威剩余库存,进入 M05 支付"] + P -- "是" --> Q["返回同一订单号、购买结果、支付截止时间和提交后的权威剩余库存,进入 M05 支付"] Z --> W["先把确定业务结果与稳定请求标识持久绑定"] G --> W K --> W @@ -173,7 +174,7 @@ flowchart TD 不可变核心事实: - 秒杀库存扣减必须使用数据库条件更新,一次同时约束目标活动、`Ongoing` 状态、权威时间窗口与剩余量;未命中就失败,禁止在应用层“先读取、后递减”。 -- 秒杀库存扣减、买家限购占用、共享订单、订单项快照、可靠订单事实和幂等结果属于同一个原子成功结果;任一步失败全部回滚,不产生支付前置记录。 +- 秒杀库存扣减、买家限购占用、M04 共享订单、指定商家、固定支付截止时间、地址与订单项快照、可靠订单事实和幂等结果属于同一个原子成功结果;任一步失败全部回滚,不产生支付前置记录。 - 完成身份与固定输入校验后,必须先查询稳定请求结果,再进入限流、时间、库存和限购判断。同标识同请求重放首次确定结果,不得因当前活动、库存、限购或流量变化重新裁决。 - 成功、未开始、已结束、已取消、售罄、超限、地址无效和已正式返回的过载结果都属于可重放的确定业务结果,必须先与稳定请求标识持久绑定再返回。数据库连接中断、事务提交失败等无法确认结果的瞬态故障不得伪装成确定业务失败,也不得固化;客户端使用原标识重试。 - 买家限购以“同一活动下当前有效占用量”作为唯一并发事实;待支付与已支付订单都占用名额,只有取消成功才释放。 @@ -231,7 +232,7 @@ flowchart TD - **M02 Catalog / M06-01 商家运营**:提供商品归属、销售状态、普通库存和当前价格;发布时完成普通库存到秒杀库存的原子划拨,之后两个通道互不混用。 - **M03 Cart**:秒杀立即抢购绕过购物车,成功、失败、取消和回补均不读写购物车条目。 -- **M04 Ordering**:接收 C01 的原子下单要求并生成共享订单与快照;订单必须保留秒杀来源、活动和成交价,供查询、取消与追溯。 +- **M04 Ordering**:C01 在同一原子边界调用其统一订单创建能力;M04 生成共享 `PendingPayment` 订单、唯一启用的默认指定商家、固定支付截止时间、地址与订单项快照,并保留秒杀来源、活动和成交价,供查询、取消与追溯。C01 不建立第二套订单创建路径。 - **M05 Payment / C08 回调**:秒杀订单沿用统一支付和幂等回写;支付成功与取消竞争由订单状态条件推进裁决。 - **C03 超时取消**:只触发 M04 公开取消入口,由订单来源决定回补普通库存还是原秒杀库存,不得直接改写 C01 数据。 - **M06-02 履约、M09 消息、M10 售后**:分别沿用发货、可靠消息和售后流程,不为秒杀建立第二套通道。 @@ -252,7 +253,7 @@ flowchart TD | 商家活动详情 | A225 | 返回本人活动、库存与订单汇总;越权不泄露存在性 | 待重建详细契约 | | 买家公开活动列表 | A226 | 返回 `Published` / `Ongoing` 活动及需求规定的完整展示信息 | 待重建详细契约 | | 买家公开活动详情 | A227 | 返回权威倒计时、剩余库存、已售数量及当前买家限购提示 | 待重建详细契约 | -| 买家秒杀下单 | A228 | 稳定请求重放、过载保护、条件扣减、限购占用、共享订单与快照原子提交 | 待重建详细契约 | +| 买家秒杀下单 | A228 | 稳定请求重放、过载保护、条件扣减、限购占用,并由 M04 统一生成共享订单、指定商家、固定支付截止时间与快照 | 待重建详细契约 | | 秒杀订单查询 | 复用 A302 / A303 | 按买家归属查询共享订单和秒杀追溯信息 | 由 M04 契约承载 | | 取消与秒杀回补 | 复用 M04 公开取消契约 | 首次成功取消时按原通道回补并释放限购 | 由 M04 / C03 契约承载 | @@ -266,7 +267,7 @@ HTTP 路径、方法、请求响应字段、状态码、错误码、幂等标识 4. 数据库必须表达活动状态、计划量、已划拨总量、剩余量、已售量、每名买家当前占用量、共享订单追溯信息、稳定请求结果和取消是否已回补;具体表名与字段在统一数据库设计中确定。 5. 活动期间必须满足“剩余量 + 已售量 = 已划拨总量”;本期不引入冻结量。取消成功时剩余量增加、已售量减少,二者仍保持恒等。 6. 同一活动的买家当前占用量不得超过单用户限购;同一订单最多释放一次,同一稳定请求最多形成一个确定订单结果。 -7. 共享订单必须能区分普通购买与秒杀购买,并能追溯原活动、成交价和原库存通道;取消时不得依赖客户端告诉系统回补到哪里。 +7. 共享订单必须由 M04 统一生成指定商家、固定支付截止时间和快照,能区分普通购买与秒杀购买,并能追溯原活动、成交价和原库存通道;取消时不得依赖客户端告诉系统回补到哪里。 8. 活动结束或取消后的剩余量继续归属原活动且不可售,不自动并回普通库存;数据库设计不能把这部分库存丢失或误计为普通可售。 9. 公开库存展示必须从权威库存事实派生;接口需提供足够的结果顺序或版本信息,保证旧刷新结果不能覆盖新结果。 10. 接口与数据库完成后,必须回到本文逐项验证动作、状态、异常和原子结果;若实现成本暴露设计缺口,记录缺口并修正下游设计,不能擅自改变已确认业务语义。 @@ -280,6 +281,7 @@ HTTP 路径、方法、请求响应字段、状态码、错误码、幂等标识 - [ ] 不少卖:请求充足且不存在身份、时间、限购等业务失败时,10 份库存全部形成 10 笔成功订单,限流配置不提前截断全部有效请求。 - [ ] 单用户限购:同一买家的当前有效秒杀数量不超过上限;并发请求不能绕过;取消成功后按数量释放。 - [ ] 幂等:同一买家、活动和稳定标识重复提交只形成一笔订单并返回同一结果;同标识不同请求被拒绝。 +- [ ] 订单统一创建:C01 只负责活动库存和限购;M04 在同一原子边界生成共享订单、指定商家、固定支付截止时间和快照,不存在第二套秒杀订单创建路径。 - [ ] 事务回滚:库存、限购、订单、订单项快照、可靠订单事实或请求结果任一步失败时,全部恢复原状。 - [ ] 时间窗口:开始前、结束后、取消后的请求均不扣库存;应用实例时钟偏差不改变结果。 - [ ] 取消回补:买家主动取消或 C03 超时取消只回补原活动一次;不增加普通库存;活动结束 / 取消后回补量仍不可售。 -- Gitee From 04237e06af2d04e6be6a16c613715130d2feff90 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 20:17:50 +0800 Subject: [PATCH 094/118] =?UTF-8?q?docs(process):=20=E8=A1=A5=E9=BD=90=20M?= =?UTF-8?q?06-02=20=E5=95=86=E5=AE=B6=E5=B1=A5=E7=BA=A6=EF=BC=9B=E5=86=BB?= =?UTF-8?q?=E7=BB=93=E5=94=AE=E5=90=8E=E4=B8=8E=E7=A6=81=E7=94=A8=E8=B4=A3?= =?UTF-8?q?=E4=BB=BB=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...41\347\220\206\346\265\201\347\250\213.md" | 19 +- ...42\345\215\225\346\265\201\347\250\213.md" | 2 +- ...45\347\272\246\346\265\201\347\250\213.md" | 232 ++++++++++++++++++ 3 files changed, 242 insertions(+), 11 deletions(-) create mode 100644 "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M06-02-\345\225\206\345\256\266\345\261\245\347\272\246\346\265\201\347\250\213.md" diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" index b18e0a2..5dd9538 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -86,7 +86,7 @@ flowchart TD E -- “正常” --> F[“在同一受控事务内调用 Ordering 公开应用契约复核阻断条件”] F --> G[“在同一受控事务内调用 AfterSales 公开应用契约复核阻断条件”] G --> H[“在同一受控事务内调用 Seckill 公开应用契约复核阻断条件”] - H --> I{“是否存在待支付/待履约订单、未结束售后或未结束活动?”} + H --> I{“是否仍有核心订单责任、售后申请窗口、非终态售后或未结束活动?”} I -- “是” --> Z[“回滚事务,拒绝禁用,账号及业务归属保持不变”] Z --> Z1[“将复核失败原因写入结构化日志,等待管理员决策”] I -- “否” --> J[“条件更新:仅在状态为正常且令牌版本匹配禁用版本基准时改为禁用并提升令牌版本号”] @@ -103,7 +103,7 @@ flowchart TD 禁用约束: -- 默认商家和仍有待处理业务的商家必须拒绝禁用,账号及业务归属保持不变。 +- 默认商家始终拒绝禁用。非默认商家的阻断口径在流程阶段固定为:存在 `PendingPayment` 订单、仍有可履约数量的 `Paid` 订单、`Shipped` 订单、完成后 7 天售后窗口内订单、任一非终态售后申请,或任一未结束秒杀活动。已全量退款、剩余可履约数量为 0 且无非终态售后的 `Paid` 订单不再单凭状态名永久阻断;账号及业务归属保持不变。 - 业务归属复核必须发生在 PostgreSQL 事务内部,与账号状态条件更新共享同一事务边界;Ordering、AfterSales、Seckill 必须提供能在该受控事务内阻止新业务归属的版本事实或行锁,否则本流程不能消除 TOCTOU 漏洞。具体事务边界由系统架构设计承接,本流程不规定应用层 HTTP 调用顺序。 - PostgreSQL 事务以目标账号为锁起点;条件更新除匹配 `status = 正常` 外还必须匹配复核阶段读取的令牌版本号,避免复核到提交之间状态被并发改变。 - PostgreSQL 与 Redis 不能组成同一事务;PostgreSQL 提交后 Redis 撤销不可用时,必须返回服务暂不可用,不允许回滚已经持久化的状态变更(避免出现”禁用后又回滚导致旧登录态生效”的更大问题),同时也不允许返回虚假成功。 @@ -172,7 +172,7 @@ flowchart TD | M06-03 | 禁用/启用命令 | M01 Identity | 账号状态与令牌版本提升 | | M06-03 | 商家禁用约束 | M06-01 | 商品维护可继续遵守账号状态 | | M06-03 | 商家禁用约束 | M06-02 | 商家发货和售后审核遵守账号状态 | -| M06-03 | 业务归属复核 | Ordering、AfterSales、Seckill 公开应用契约 | 最新待支付/待履约/售后/活动状态 | +| M06-03 | 业务责任复核 | Ordering、AfterSales、Seckill 公开应用契约 | 最新核心订单责任、售后窗口、非终态售后和活动状态 | | M06-03 | 操作记录 | 管理员查询 | 操作人、时间、`traceId` 可追踪 | 衔接约束: @@ -194,12 +194,11 @@ flowchart TD ## 九、由流程反查出的接口与数据待评审项 -1. 默认商家和“仍有待处理业务”的判断口径需要在数据库设计中确认:是否包含进行中售后、待发货订单、未结束秒杀活动等。 -2. 多实例令牌失效的可见延迟需在 C10 与 M00 评审中明确,本流程不预设具体延迟。 -3. 操作记录是否长期保留或定期归档需在数据库设计中明确,本流程只承诺最小记录字段。 -4. 禁用或启用操作是否需要支持批量,本期不实现。 -5. 商家禁用的“业务归属转移”本期不实现,需明确告知管理员原因。 -6. Redis 撤销不可用时的返回码(503 / AUTH.TOKEN_SERVICE_UNAVAILABLE)和前端降级策略需在接口设计中明确。 +1. 多实例令牌失效的可见延迟需在 C10 与 M00 评审中明确,本流程不预设具体延迟。 +2. 操作记录是否长期保留或定期归档需在数据库设计中明确,本流程只承诺最小记录字段。 +3. 禁用或启用操作是否需要支持批量,本期不实现。 +4. 商家禁用的“业务归属转移”本期不实现,需明确告知管理员原因。 +5. Redis 撤销不可用时的返回码(503 / AUTH.TOKEN_SERVICE_UNAVAILABLE)和前端降级策略需在接口设计中明确。 ## 十、验收证据清单 @@ -211,4 +210,4 @@ flowchart TD - [ ] 买家、商家和游客不能访问管理接口,管理员账号和角色字段不能通过本模块操作。 - [ ] 重复与并发状态变更保持幂等,安全追踪记录不包含敏感凭据。 - [ ] 默认商家和存在待处理业务的商家禁用保护可复现,普通可禁用商家仍可完成禁用/启用验收。 -- [ ] 保存列表、禁用、启用、旧登录态失效、越权和角色注入拦截证据。 \ No newline at end of file +- [ ] 保存列表、禁用、启用、旧登录态失效、越权和角色注入拦截证据。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" index ee9136a..b7dc3de 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" @@ -19,7 +19,7 @@ M04 拥有订单创建、买家订单查询、待支付订单取消、订单核 | M03 购物车与 M01 地址 | 已校准或待整合 | 提供本人购物车条目和地址事实 | | M02 / C01 库存来源 | 已定义或已校准 | 下单扣减、取消与售后按原通道回补 | | M05 / C08 支付 | 已校准 | 只在截止时间前竞争待支付状态 | -| M06-02 商家履约 | 缺失,待本轮补齐 | 使用指定商家与订单状态公开能力 | +| M06-02 商家履约 | 已补齐、待交叉评审 | 使用指定商家、售后履约快照与订单状态公开能力 | | M10 售后 | 已重构 | 提供发货阻断和已退款数量 | | 本文业务流程 | 已重构、待交叉评审 | 作为接口与后续数据设计输入 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M06-02-\345\225\206\345\256\266\345\261\245\347\272\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M06-02-\345\225\206\345\256\266\345\261\245\347\272\246\346\265\201\347\250\213.md" new file mode 100644 index 0000000..e6d2dee --- /dev/null +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M06-02-\345\225\206\345\256\266\345\261\245\347\272\246\346\265\201\347\250\213.md" @@ -0,0 +1,232 @@ +# M06-02 商家履约流程 + +> 负责人:韦乾强 +> 覆盖:M06-02、F12 +> 基础核心流程:F08、F09、F10、F11、X04 +> 直接协作:张海洋(M10 AfterSales)、罗皓晨(M09 Messaging)、唐宇昊(M01 Identity) +> 文档状态:已按需求补齐,可作为接口设计输入;待 Ordering、AfterSales、Messaging 交叉评审 +> 需求事实源:[需求规格说明书 M06-02](../../../01-需求文档/需求规格说明书.md) 的“M06-02 后台订单管理(F12)”完整七节 + +## 一、范围与设计顺序 + +M06-02 为商家提供授权订单列表、状态筛选、履约详情和发货动作。它不拥有订单状态,不修改金额、支付、地址或商品快照,不建设真实物流平台,也不建立商户租户、商品归属、拆单和结算模型。 + +本流程先确定商家范围、可发货状态、售后阻断、实际发货数量、并发结果和最小数据,再映射商家接口。旧商家接口只能在第九章做下游映射,不能反向扩大商家权限或让客户端决定履约数量。 + +| 设计对象 | 当前成熟度 | 本文处理 | +|---|---|---| +| M06-02 / F12 需求 | 完整定义 | 作为商家履约事实源 | +| M04 订单状态与指定商家 | 已重构 | 提供权威订单、快照和 `Paid → Shipped` 动作 | +| M10 售后履约快照 | 已重构 | 提供非终态阻断与已退款数量 | +| M09 发货通知 | 已定义、待整合 | 只消费已提交发货事实 | +| 本文业务流程 | 新增并已校准、待交叉评审 | 补齐此前缺失的 M06-02 设计 | + +## 二、参与者、事实归属与权限边界 + +```mermaid +flowchart LR + MERCHANT["状态正常的商家"] -->|"授权订单查询与发货"| FULFILL["M06-02 履约入口"] + ORDER["M04 Ordering
assignedMerchantUserId、状态、快照、购买数量"] <--> FULFILL + AFTER["M10 AfterSales
非终态申请、已退款数量、剩余可履约数量"] --> FULFILL + FULFILL -->|"唯一 Paid → Shipped 结果"| ORDER + FULFILL -->|"已提交发货事实,只通知订单买家"| MESSAGE["M09 Messaging"] + + GUEST["游客"] --> DENY["无后台订单数据"] + BUYER["买家"] --> DENY + ADMIN["管理员"] --> DENY + OTHER["未被分配该订单的商家"] --> DENY +``` + +事实归属: + +- M04 拥有订单状态、订单项与地址快照、金额、指定商家、发货时间、完成时间和每项实际发货数量。 +- M10 拥有售后申请和已退款数量,并向履约提供当前一致快照。 +- M06-02 是商家入口与业务编排,不复制订单或售后数据,不直接修改 Payment、Catalog 或 Seckill。 +- M09 只根据已提交发货事实通知当前订单买家。 + +权限边界: + +- 只有状态正常的 Merchant 可以进入本模块。 +- 每次列表、详情和发货都必须强制 `assignedMerchantUserId = 当前商家`,不能仅在页面隐藏其他订单。 +- 默认商家也只能看明确分配给自己的订单;本期不存在“Merchant 角色共享全站订单”。 +- 游客、买家和管理员不能调用商家查询或发货入口,管理员本期不代替商家履约。 +- 订单不在当前商家范围时统一返回不可访问结果,不泄露订单是否存在或属于哪个商家。 + +## 三、商家订单列表 + +```mermaid +flowchart TD + A["状态正常的商家进入后台订单列表"] --> B["只按当前 assignedMerchantUserId 读取订单"] + B --> C["按可选核心状态筛选,按创建时间倒序稳定分页"] + C --> D{"本页有订单?"} + D -- "否" --> E["展示正常空状态和清除筛选入口"] + D -- "是" --> F["展示订单号、买家最小摘要、总额、状态、创建 / 支付 / 发货 / 完成 / 取消相关时间"] + F --> G["Paid 且可能可履约的订单显示查看详情入口;是否可发货由详情重新判断"] +``` + +列表规则: + +- 支持全部、`PendingPayment`、`Paid`、`Shipped`、`Completed`、`Cancelled` 五种受控状态筛选。 +- 结果按创建时间倒序,并使用稳定次序保证翻页不重复、不跳项。 +- 列表只返回买家必要摘要,不返回完整地址、手机号、钱包、支付流水、售后原因或无关资料。 +- `Paid` 只表示可以进入履约复核,不保证一定可发货;售后申请和退款可能在打开详情前发生。 +- 空状态、加载、分页、筛选非法和查询失败都有明确页面反馈,不把失败伪装为空集合。 +- 商家账号禁用后所有查询和主动发货均被拒绝;M06-03 必须在存在待处理订单或售后时阻止正常禁用。 + +## 四、履约详情与服务端发货数量 + +### 4.1 详情内容 + +```mermaid +flowchart TD + A["商家打开授权订单"] --> B["M04 校验订单仍分配给当前商家"] + B --> C["读取订单项、地址快照、金额、状态和时间线"] + C --> D["读取 M10 当前履约快照"] + D --> E["按订单项展示购买数量、处理中售后数量、已退款数量、剩余可履约数量"] + E --> F["按最新状态和阻断原因派生发货入口"] +``` + +详情只展示履约所需信息: + +- 订单号、核心状态、金额和已提交支付摘要; +- 商品名称、图片、成交单价、购买数量; +- 当前售后阻断摘要、已退款数量和剩余可履约数量; +- 下单时地址快照,包括发货所需收件人、联系电话和完整配送地址;联系电话只在当前授权订单详情展示,不进入列表或其他订单响应; +- 创建、支付、发货、完成或取消时间线; +- 已发货时的实际发货数量和必要说明。 + +详情不展示: + +- 买家钱包余额、完整支付凭证或其他订单; +- 非当前订单的地址、手机号或个人资料; +- 其他商家的订单与售后; +- 可由商家修改的订单金额、快照、支付来源或买家身份。 + +### 4.2 履约数量 + +每个订单项的剩余可履约数量由服务端计算: + +```text +剩余可履约数量 += 订单项购买数量 +- 已退款数量 +``` + +- `PendingReview`、`PendingReturn`、`PendingReceipt`、`Refunding`、`RefundFailed` 任一非终态售后申请会阻断整单发货,不能边处理售后边发剩余数量。 +- `Rejected`、`Cancelled` 售后申请不阻断。 +- 非终态申请结束为 `Refunded` 后,已退款数量永久从可履约数量中扣除。 +- 部分退款完成后可以一次发出所有剩余可履约数量;全部数量已退款时不允许发货。 +- 商家或客户端不能提交一套自选发货数量覆盖服务端结果。本期不做拆单、分批发货或部分收货。 + +## 五、发货主流程 + +```mermaid +flowchart TD + A["指定商家在授权 Paid 订单详情确认发货"] --> B["提交本次动作的稳定请求标识和必要发货说明"] + B --> C["重新读取订单 assignedMerchantUserId、核心状态和已有发货结果"] + C --> D{"当前商家有权且订单仍为 Paid?"} + D -- "已为 Shipped 且同一动作" --> R["重放首次发货时间、实际数量和结果"] + D -- "不在范围或其他状态" --> X["拒绝并返回可安全公开的当前结果"] + D -- "是" --> E["与 M10 售后申请串行读取最新履约快照"] + E --> F{"是否存在非终态售后?"} + F -- "是" --> Y["阻断发货,返回售后处理中和最新状态"] + F -- "否" --> G["服务端计算每项已退款和剩余可履约数量"] + G --> H{"整单剩余可履约数量是否大于零?"} + H -- "否" --> Z["拒绝发货:全部数量已退款"] + H -- "是" --> I["再次确认发货与新售后申请的唯一提交顺序"] + I --> J{"发货是否唯一胜出?"} + J -- "否" --> K["返回新售后阻断或已有 Shipped 结果"] + J -- "是" --> L["形成完整原子结果:Paid → Shipped、发货时间、每项实际发货数量、必要说明和买家通知事实"] + L --> M{"完整结果提交?"} + M -- "否" --> T["全部不生效;订单保持 Paid,可安全重试"] + M -- "是" --> U["刷新为 Shipped;买家可查询并收到发货通知"] +``` + +发货规则: + +- 只有 `Paid` 可以首次发货。`PendingPayment`、`Cancelled`、`Completed` 明确拒绝;已 `Shipped` 的相同动作重放已有结果。 +- 发货前必须重新校验当前商家、订单状态和 M10 履约快照,列表页或早先详情的结果不能作为提交承诺。 +- 发货请求的稳定身份与内容绑定:同一身份、同一说明重放首次结果;同一身份换内容拒绝;状态已变后不能覆盖首次发货说明。 +- 发货与售后申请对同一订单履约事实串行复核: + - 发货先提交,订单进入 `Shipped`;M10 后续按已发货规则处理新申请; + - 售后申请先提交,非终态申请阻断发货; + - 不能把同一数量既按未发货退款处理又记录为已发出。 +- 发货成功只记录一次时间、每项实际发货数量、必要说明和可靠发货事实。 +- 本期不接真实物流平台,不查询轨迹,不要求商家提供可验证的真实运单;必要说明只用于当前演示履约。 +- 发货成功后 M09 只通知订单买家,不广播给其他买家、商家或管理员。 + +## 六、重复、并发与售后竞争 + +```mermaid +flowchart TD + A["Paid 订单同时出现发货与售后申请"] --> B{"哪个动作先取得同一订单履约提交资格?"} + B -- "发货先" --> C["原子提交 Shipped 和实际发货数量"] + C --> D["售后重新读取 Shipped,按已发货仅退款 / 退货退款规则处理"] + B -- "售后先" --> E["原子提交非终态售后和数量占用"] + E --> F["发货重新读取阻断并拒绝"] +``` + +| 场景 | M06-02 结果 | 不变量 | +|---|---|---| +| 同一请求网络重试 | 返回首次 `Shipped` 结果 | 不重复发货时间或通知 | +| 两台设备同时发货 | 一个首次提交,另一个读取当前结果 | 只形成一次发货 | +| 不同内容并发发货 | 首次合法内容胜出,其他不可覆盖 | 说明和实际数量唯一 | +| 发货前出现非终态售后 | 阻断并展示原因 | 不把处理中数量发出 | +| 售后部分退款已完成 | 发剩余数量 | 已退款数量不再发货 | +| 售后全部退款已完成 | 拒绝发货 | 零数量不进入 `Shipped` | +| 发货先于新售后申请 | 发货成功;售后按 `Shipped` 规则复核 | 不误按未发货库存回补 | +| 订单已取消或完成 | 拒绝 | 不跳转状态 | +| 发货结果提交失败 | 保持 `Paid` | 无发货时间、数量或通知半结果 | +| 通知暂时失败 | 保持 `Shipped` | M09 重试,不回滚发货 | + +## 七、页面反馈与状态变化 + +- 列表与详情加载期间显示加载状态;真实空集合与请求失败分别展示。 +- 发货按钮提交期间防止当前页面重复点击,但最终幂等仍由服务端业务结果保证。 +- 权限、状态、售后阻断、全部退款、并发冲突和暂时失败使用不同的可理解提示。 +- 发货成功后立即刷新订单状态、发货时间、实际发货数量和可用动作。 +- 另一页面、Worker 或售后流程先改变状态时,当前页面读取并展示最新事实,不以旧快照强行覆盖。 +- 买家在 M04 订单详情看到同一 `Shipped` 状态、实际数量和时间;商家页面不建立第二份履约状态。 + +## 八、跨模块必须承接的事实 + +- **M04 Ordering**:拥有 `assignedMerchantUserId`、核心状态和 `Paid → Shipped`。M06-02 不直接写订单内部数据。 +- **M05 / C08 Payment**:只有已经提交的 `Paid` 订单可履约;发货不检查或修改买家钱包。 +- **M10 AfterSales**:提供非终态阻断、已退款数量和剩余可履约数量;发货与新申请串行复核。 +- **M09 Messaging**:发货完整结果提交后通知订单买家;通知失败独立重试。 +- **M06-03 Identity**:默认商家始终拒绝禁用。非默认商家只要仍有 `PendingPayment`、仍有可履约数量的 `Paid`、`Shipped`、完成后 7 天售后窗口内订单、非终态售后或未结束秒杀活动,均仍负有业务责任并拒绝禁用;已全量退款且无非终态售后的 `Paid` 不得被状态名永久阻断。禁用后不能主动查询或发货。 +- **M04-04**:发货后由买家确认或满 7 天自动完成;M06-02 不代替买家确认、不自行推进 `Completed`。 + +## 九、由流程派生的接口契约映射 + +| 已确认业务能力 | 派生接口 | 接口必须承载的业务结果 | 当前状态 | +|---|---|---|---| +| 商家订单列表 | A305 | 当前 `assignedMerchantUserId` 强制范围、五状态筛选、稳定分页和最小买家摘要 | 待重建详细契约 | +| 商家订单详情 | A306 | 授权快照、最小敏感信息、售后阻断、已退款和剩余可履约数量 | 待重建详细契约 | +| 商家发货 | A307 | 必填稳定请求身份、服务端实际发货数量、售后竞争、`Paid → Shipped` 和幂等重放 | 待重建详细契约 | +| 售后履约快照 | AfterSales 内部应用契约 | 非终态申请、已退款数量、剩余可履约数量和同订单串行复核 | 待按流程补齐 | +| 发货状态推进 | Ordering 内部应用契约 | 指定商家、`Paid → Shipped`、时间 / 数量 / 说明和可靠通知事实 | 待按流程补齐 | + +接口阶段必须满足: + +1. A305、A306、A307 均从认证身份取得当前商家,不接收客户端指定 `merchantUserId` 扩大范围。 +2. A307 请求不接收订单金额、状态、支付结果、地址、买家或自选发货数量,只承载稳定请求身份和必要说明。 +3. A307 响应返回服务端计算的每项实际发货数量、发货时间、当前状态和冲突后的最新事实。 +4. 已 `Shipped` 的同一动作返回首次结果;非同一内容不能修改已经提交的发货事实。 +5. A306 的手机号和地址只为当前授权订单履约展示,列表不返回完整敏感字段。 +6. HTTP 状态与错误码必须区分未认证、角色不符、资源不在范围、非法状态、售后阻断、全部退款和并发结果,但不得泄露其他商家的订单。 + +## 十、验收证据清单 + +- [ ] 商家列表和详情只出现 `assignedMerchantUserId` 为当前账号的订单,其他商家、买家、管理员和游客无法访问。 +- [ ] 列表支持五种核心状态筛选、稳定分页、创建时间倒序、真实空状态和失败反馈。 +- [ ] 详情只展示履约所需快照和最小买家信息,包含非终态售后、已退款和剩余可履约数量。 +- [ ] 只有授权 `Paid` 订单可首次发货;其他状态明确拒绝,已 `Shipped` 的相同动作重放已有结果。 +- [ ] 发货数量完全由服务端按购买数量减已退款数量计算,客户端不能指定。 +- [ ] 任一非终态售后阻断整单发货;`Rejected`、`Cancelled` 不阻断。 +- [ ] 部分退款后只发全部剩余数量,全部退款后不能发货;本期无拆单或分批发货。 +- [ ] 发货与售后申请并发时只有一个先提交,后续动作按最新 `Paid` / `Shipped` 与售后事实重新判断。 +- [ ] 网络重试、多设备并发和不同内容竞争只形成一个发货时间、一组实际数量、一份说明和一条可靠发货事实。 +- [ ] 发货完整结果失败时订单保持 `Paid`;通知失败时订单保持 `Shipped` 并由 M09 重试。 +- [ ] 买家订单详情与商家履约详情展示同一个 M04 权威状态和发货结果,不存在第二份履约状态。 +- [ ] 保存授权筛选、越权、正常发货、非法状态、非终态售后、部分 / 全部退款、并发发货、发货与售后竞争、事务失败和通知重试的真实证据。 -- Gitee From 81125460415d1a5934fa675d6be666df0a21cd01 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 20:36:23 +0800 Subject: [PATCH 095/118] =?UTF-8?q?docs(process):=20=E6=A0=A1=E5=87=86?= =?UTF-8?q?=E5=95=86=E5=93=81=E5=88=86=E7=B1=BB=E4=B8=8E=E9=94=80=E5=94=AE?= =?UTF-8?q?=E7=8A=B6=E6=80=81=EF=BC=9B=E6=98=8E=E7=A1=AE=E5=81=9C=E7=94=A8?= =?UTF-8?q?=E5=88=86=E7=B1=BB=E5=92=8C=E7=BC=93=E5=AD=98=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 68 +++++--- ...06\345\223\201\346\265\201\347\250\213.md" | 109 +++++++----- ...41\347\220\206\346\265\201\347\250\213.md" | 160 ++++++++++-------- ...22\346\235\200\346\265\201\347\250\213.md" | 6 +- 4 files changed, 197 insertions(+), 146 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 6854709..ae20fc1 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -1,6 +1,6 @@ # 电子商城需求规格说明书 -> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.3 +> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.4 > > 文档状态:内容和格式已完成内部统一,待全组与指导教师评审后冻结 > 截止:第 1 周周三提交初稿 @@ -12,6 +12,7 @@ | v0.1 | 2026-07-24 | 全体成员(罗皓晨统稿) | 形成并统一需求规格,明确四类角色、必做功能、4 项选做、7 项挑战、单店 B2C 边界、核心状态与六人模块职责 | | v0.2 | 2026-07-24 | 罗皓晨 | 消解 C01 库存展示绝对一致与“不预占库存”的冲突,统一为数据库权威快照、旧结果防覆盖及并发失败后同一交互刷新 | | v0.3 | 2026-07-24 | 罗皓晨 | 冻结 F10 同步钱包与 C08 受控模拟回调的互斥入账边界,并明确回调同样受订单状态和支付截止时间约束 | +| v0.4 | 2026-07-24 | 罗皓晨 | 冻结商品三态、分类停用、售罄展示与 C07 缓存范围,明确统一经营目录及商品列表、搜索不进入本期缓存 | ## 业务流程设计入口 @@ -484,7 +485,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | M02-01-FR03 | 关键词搜索 | 对输入去除首尾空白并校验长度;F05 至少支持商品名称模糊匹配 | | M02-01-FR04 | 组合筛选 | 关键词、分类、价格区间和仅看有货可组合使用,条件之间按“同时满足”处理 | | M02-01-FR05 | 排序 | 支持经过白名单约束的排序项及方向,非法字段不得直接拼接为查询语句 | -| M02-01-FR06 | 状态过滤 | 购物端查询始终只返回已上架商品,下架、草稿或已删除商品不得泄露 | +| M02-01-FR06 | 状态过滤 | 购物端查询始终只返回已上架商品;草稿、已下架商品不得泄露,已物理删除商品已不存在,不能再作为可查询状态返回 | | M02-01-FR07 | 条件恢复 | 关键词、筛选、排序和页码反映在页面地址或等效可恢复状态中,刷新或返回列表时无需重新选择 | | M02-01-FR08 | 状态反馈 | 首次加载显示骨架或加载状态;无结果时说明原因并提供清空筛选入口;失败时保留原条件并允许重试 | | M02-01-FR09 | 角色化操作 | 买家可进入收藏、加购和购买流程;游客获得登录引导;商家和管理员不显示买家专属操作 | @@ -501,6 +502,9 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, #### 5. 业务规则与权限 - 商品可见状态和价格、库存均以服务端返回为准,前端不得自行放宽上下架条件。 +- 商品销售状态只有草稿、已上架和已下架三种;满足删除条件并完成物理删除后,商品作为终止结果不再存在,“已删除”不是第四种销售状态。 +- 已上架商品即使库存为零仍保留在公开列表、搜索与详情中,并明确标记“售罄”;仅购买、加购或结算动作因库存不足被拒绝。 +- 停用分类只从购物端分类筛选入口移除,不自动下架或隐藏其下已上架商品;用户仍可从全部商品、关键词搜索、收藏或历史记录进入这些商品。 - 页码、每页数量、价格范围和排序项必须校验;价格下限大于上限时应明确提示。 - 空结果属于正常结果,返回空集合和正确分页信息,不以系统异常处理。 - 搜索输入必须参数化处理,不得拼接 SQL;响应不得包含商品内部备注或未公开状态。 @@ -511,16 +515,17 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | 场景 | 处理 | |---|---| | 无效页码、超限页容量或非法排序字段 | 拒绝请求并返回可定位到字段的参数错误 | -| 分类不存在或已停用 | 不返回越权商品;页面提示分类不可用并允许返回全部商品 | +| 分类不存在或已停用 | 不执行该分类条件;页面提示分类不可用并允许返回全部商品,全部商品和关键词搜索仍可展示其中已上架商品 | | 关键词或组合条件无匹配 | 展示空结果、当前条件和“清空筛选”入口 | | 请求失败或网络中断 | 保留用户已选条件,显示简短原因并允许重试 | -| 查询期间商品刚被下架 | 以服务端最终状态为准,不再出现在后续结果;进入详情时显示不可售状态 | +| 查询期间商品刚被下架 | 商品列表和搜索的下一次查询直接按 PostgreSQL 已提交状态过滤;商品详情若命中 C07 旧值,只能在已约定的一致性窗口内短暂展示,任何购买动作仍按 PostgreSQL 拒绝 | #### 7. 验收标准与证据 - 分页数据、总数和翻页结果正确,刷新或返回后查询条件仍可恢复。 - 分类筛选、关键词模糊搜索、价格区间、库存条件和白名单排序可独立及组合生效。 -- 购物端任何身份均无法搜索到草稿、下架或已删除商品。 +- 购物端任何身份均无法搜索到草稿或已下架商品,已物理删除商品不再存在;库存为零的已上架商品仍可见并标记售罄。 +- 停用分类不再作为筛选入口,但其下已上架商品仍能通过全部商品、关键词搜索和详情入口访问。 - 游客、买家、商家和管理员看到符合权限的操作入口,不能仅靠隐藏按钮代替服务端鉴权。 - 空结果、参数错误和网络失败均有明确、可恢复的界面反馈。 @@ -550,7 +555,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | M02-02-FR05 | 游客衔接 | 游客触发登录后保留目标商品和原操作意图,减少重复查找 | | M02-02-FR06 | 评价展示 | 已选 X01 时展示评分汇总和公开评价列表入口;评价提交资格由 M07 判断 | | M02-02-FR07 | 状态反馈 | 提供加载、无数据、图片失败、商品不存在和商品不可售状态,并给出返回列表入口 | -| M02-02-FR08 | 数据刷新 | 后台改价、改库存或上下架后,详情重新获取时按服务端最新数据展示 | +| M02-02-FR08 | 数据刷新 | 后台改价、改库存、上下架或修改内容后,详情按 C07 已确认的一致性窗口收敛到 PostgreSQL 最新值;超过窗口不得继续返回旧值,购买动作始终实时重检 | | M02-02-FR09 | 内容安全 | 商品描述按受控内容展示,不执行脚本或不可信嵌入内容 | #### 4. 主流程 @@ -564,7 +569,9 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, #### 5. 业务规则与权限 - 购物端详情只公开已上架商品;已下架商品的旧链接不得继续提供购买操作。 -- 展示价格、库存和状态以服务端最新数据为准,页面缓存不得作为下单依据。 +- 已上架且库存为零的商品详情继续公开并标记售罄,不提供加购或购买入口。 +- 展示价格、库存和状态以服务端结果为准;C07 仅允许商品详情在有限一致性窗口内出现旧公开值,页面缓存和 Redis 均不得作为下单依据。 +- 商品所属分类被停用时,商品只要仍为已上架就继续公开;分类停用只影响筛选入口,不改变商品销售状态。 - 买家专属操作必须同时受前端入口和后端角色、资源归属校验保护。 - 商品图片由 S3 Compatible Object Storage 提供;单个商品最多 8 张,单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 400~4096 像素;第一张作为主图并生成方形缩略图。 - 本期不提供关联推荐、商家评价回复、评价点赞或通过详情页直接编辑商品。 @@ -574,15 +581,17 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | 场景 | 处理 | |---|---| | 商品不存在 | 展示“商品不存在”状态,不暴露内部异常,并提供返回列表入口 | -| 商品已下架或被删除 | 展示“暂不可售”,禁用购买操作;历史订单中的商品快照不受影响 | -| 库存为零或数量超限 | 禁用或拒绝购买并明确提示库存不足;不得显示虚假成功 | +| 商品已下架 | 展示“暂不可售”,禁用购买操作;历史订单中的商品快照不受影响 | +| 商品已物理删除或商品 ID 不存在 | 展示“商品不存在”,不把已删除实体伪装成下架状态;历史订单中的商品快照不受影响 | +| 已上架商品库存为零 | 详情继续公开并标记“售罄”,禁用加购和购买 | +| 请求数量超过当前库存 | 服务端拒绝操作并明确提示库存不足;不得显示虚假成功 | | 图片加载失败 | 使用占位图,不阻断价格、库存和描述浏览 | | 加载失败或网络中断 | 保留当前页面,允许重试;不把旧缓存价格当作最新价格 | -| 操作时状态发生变化 | 服务端拒绝失效操作,页面刷新商品状态并说明原因 | +| 操作时状态发生变化 | 服务端按 PostgreSQL 当前事实拒绝失效操作,页面刷新商品状态并说明原因 | #### 7. 验收标准与证据 -- 商品名称、图片、描述、价格、库存和分类展示正确,并与后台最新有效修改一致。 +- 商品名称、图片、描述、价格、库存和分类展示正确,并在 C07 已约定的一致性窗口内收敛到后台最新有效修改。 - 有货、售罄、下架、不存在和加载失败状态均能清楚区分。 - 游客登录引导、买家操作、商家和管理员非购买态符合身份边界。 - 已选 X01 的评分与评价入口展示正常,但未满足条件的用户不能从详情页绕过订单资格提交评价。 @@ -1223,8 +1232,8 @@ M04 提供订单金额、状态和支付截止时间,M09 接收支付结果通 | 编号 | 功能 | 详细要求 | |---|---|---| | M06-01-FR01 | 分类查询 | 展示分类名称、层级、排序和启停状态,供商品编辑和购物端筛选使用 | -| M06-01-FR02 | 分类维护 | 商家可新增、编辑、启用或停用分类;名称、父级关系和状态必须校验 | -| M06-01-FR03 | 分类保护 | 存在商品或历史引用时不得进行破坏性物理删除,可采用停用方式退出购物端 | +| M06-01-FR02 | 分类维护 | 商家可新增、编辑、启用或停用分类;名称、父级关系和状态必须校验,编辑分类元数据不受商品引用关系阻断 | +| M06-01-FR03 | 分类保护 | 只有物理删除需要检查商品或历史引用;存在引用时拒绝删除,可采用停用方式退出购物端分类筛选入口 | | M06-01-FR04 | 商品列表 | 支持按关键词、分类、上下架状态分页查询,清楚展示价格、库存和状态 | | M06-01-FR05 | 新建商品 | 录入名称、分类、价格、库存、主图/图片和描述,校验通过后保存 | | M06-01-FR06 | 编辑商品 | 可修改允许变更的商品信息;保存成功后展示最新数据 | @@ -1239,15 +1248,19 @@ M04 提供订单金额、状态和支付截止时间,M09 接收支付结果通 1. 商家登录后进入商品管理,按关键词、分类或状态查找目标商品。 2. 新建或编辑商品时填写必填信息并上传合规图片,页面即时提示字段问题。 -3. 服务端再次校验身份、字段、分类有效性和并发状态,在事务内保存商品。 +3. 服务端再次校验身份、字段和分类有效性;编辑既有商品时再校验并发标记,创建商品不要求不存在的旧版本。 4. 事务成功后返回明确结果;搜索索引随商品数据同步更新,缓存失效在事务提交后处理。 5. 商家执行上架后,购物端可浏览该商品;执行下架后,购物端不再提供购买入口。 -6. 商家删除有关联订单的商品时,系统拒绝破坏性删除并引导改为下架。 +6. 商家删除存在订单、购物车、收藏、浏览、评价、秒杀等任何历史关联的商品时,系统拒绝物理删除并引导改为下架。 #### 5. 业务规则与权限 -- 商品名称、有效分类、当前价格、库存和主图为必填;价格不得为负,库存必须为非负整数。 -- 上架前必须满足完整性校验;已停用分类不能用于新上架商品。 +- 商品名称、合法的分类关联、当前价格、库存和主图为必填;价格不得为负,库存必须为非负整数。新建、切换分类或上架时分类必须已启用,既有商品在原分类后来停用时仍可修改不改变分类归属的字段。 +- 商品销售状态只有草稿、已上架和已下架三种;物理删除是实体不存在的终止结果,不得保存为“已删除”状态。 +- 上架前必须满足完整性校验;新建商品不得绑定已停用分类,已有商品切换分类时不得选择已停用分类。 +- 分类停用后,已有已上架商品继续公开展示;它们可以继续修改不改变分类归属的字段,但一旦下架,必须先迁移到启用分类或重新启用原分类才能再次上架。 +- 已上架商品库存降为零时保持已上架并在购物端标记售罄,不自动下架。 +- 本期为单店 B2C 统一经营目录;所有正常商家账号维护同一套分类和商品,不按商家账号隔离商品所有权。 - 下架不删除购物车记录、收藏记录、浏览记录或历史订单快照,由对应模块显示不可售状态。 - 商品公开可见范围由服务端状态过滤保证,不能只依赖前端隐藏。 - 商品与分类写操作仅允许商家;游客、买家和管理员均不能越权调用。 @@ -1259,9 +1272,12 @@ M04 提供订单金额、状态和支付截止时间,M09 接收支付结果通 | 场景 | 处理 | |---|---| | 字段、价格、库存或分类非法 | 拒绝保存并返回字段级错误,前端保留用户已填写内容 | +| 编辑仍有商品引用的分类名称、层级或排序 | 允许在校验通过后保存;引用关系只阻止物理删除 | +| 停用仍有已上架商品的分类 | 分类从购物端筛选入口移除,已有已上架商品继续公开且不自动下架 | +| 已停用分类下商品尝试重新上架 | 拒绝上架,提示迁移到启用分类或先重新启用原分类 | | 图片上传失败 | 明确标记失败图片并允许重试,不清空其他表单字段 | | 两名操作人并发编辑 | 后提交者收到冲突提示,不静默覆盖已生效修改 | -| 删除存在历史订单的商品 | 拒绝物理删除,提示改为下架 | +| 删除存在订单、购物车、收藏、浏览、评价、秒杀等任何历史关联的商品 | 拒绝物理删除,提示改为下架并永久保留商品事实 | | 上架条件不完整 | 拒绝上架并指出缺失字段 | | 事务提交失败 | 数据回滚,页面显示保存失败且允许安全重试 | | 缓存失效失败 | 不回滚已经正确提交的商品事务;由缓存处理器重试并记录可追踪错误 | @@ -1272,6 +1288,8 @@ M04 提供订单金额、状态和支付截止时间,M09 接收支付结果通 - 商家可完成分类维护,以及商品新增、查询、编辑、受约束删除和上下架。 - 商品必填项、价格、库存、分类和图片校验在前后端均生效。 - 下架商品不会出现在购物端列表和搜索结果,旧链接不再允许购买;历史订单快照保持可读。 +- 停用分类不会自动隐藏其下已上架商品;库存为零的已上架商品保持可见并显示售罄。 +- 商家账号共同维护单店统一经营目录,不按当前操作人过滤商品归属。 - 游客、买家和管理员无法进入或调用商家写操作,后端返回正确的认证或授权结果。 - 并发编辑、图片失败、保存失败和删除受限时均有明确反馈,已填写内容不会无故丢失。 - 商品变更后,PostgreSQL `pg_trgm`/GIN 索引随数据同步保持一致;缓存失效过程可追踪、可重试,答辩能说明两者边界。 @@ -1801,7 +1819,7 @@ C01 要求在面向买家的高并发秒杀场景下保证库存“只减不超 - 幂等与拒绝顺序:完成认证和固定字段校验后,先按 PostgreSQL 幂等记录处理相同 Key;同指纹直接重放首次结果,不再受当前限流、时间、库存或限购变化影响,不同指纹返回 409。只有全新 Key 才依次进入限流、时间窗口、售罄、单用户限购和其他业务校验;前端不把 5xx 误判为“已售罄”。 - 数据隔离:秒杀活动接口、订单接口和库存接口在读写上都必须按活动 ID、买家 ID 双重过滤;活动维度数据不暴露他人抢购明细,只返回当前请求可见信息。 - 日志脱敏:秒杀日志记录买家 ID、活动 ID、行为结果和 traceId;不输出完整 Token、密码或支付卡号;截图和答辩材料中订单金额、库存数据按需脱敏。 -- 与 C03、C08、C10、C07 的衔接:超时取消使用秒杀库存回补通道;支付回调幂等(C08)也作用于秒杀订单;多 API 实例(C10)下秒杀入口由任一实例受理,最终一致性仍以数据库为准;缓存(C07)可用于活动列表和商品详情读取,但秒杀库存不参与缓存。 +- 与 C03、C08、C10、C07 的衔接:超时取消使用秒杀库存回补通道;支付回调幂等(C08)也作用于秒杀订单;多 API 实例(C10)下秒杀入口由任一实例受理,最终一致性仍以数据库为准;C07 只用于固定首页商品摘要和商品详情,秒杀活动列表、活动状态和秒杀库存均不缓存。 #### 6. 异常与边界场景 @@ -1814,7 +1832,7 @@ C01 要求在面向买家的高并发秒杀场景下保证库存“只减不超 - 服务器时钟漂移:活动开始/结束时间以数据库 UTC 时间为权威;应用节点间时钟漂移不影响业务结果;活动开始前 1 秒到达的请求仍可能被延迟到 0.x 秒后处理,文档说明允许秒级误差。 - 取消与回补竞争:买家主动取消或 C03 超时取消时回补秒杀库存并释放限购名额;已支付订单不走取消回补,后续退款/退货按 M10 处理。同笔订单的重复取消请求只回补一次。 - 活动提前取消:商家在“已发布”或“进行中”状态下取消活动,已存在订单继续按既有流程走完;未提交的请求直接拒绝;本期不回收已分配库存,避免与普通购买混淆。 -- 缓存不一致:秒杀库存不进入缓存;活动列表、商品基础信息走缓存时必须按定义好的失效策略更新;活动开始/结束/售罄状态变化必须以数据库为准重新加载活动详情。 +- 缓存不一致:秒杀活动列表、活动状态和库存均不进入缓存;商品详情若命中 C07 旧公开值,只能存在于已约定的一致性窗口,参与资格、活动状态和库存始终重新读取数据库事实。 - 越权访问:买家只能查看自己的秒杀订单和抢购资格,不允许通过订单 ID、买家 ID 或活动 ID 直接访问他人的购买明细;接口不存在或无权限返回统一错误,不暴露是否命中目标记录。 - 后台人工干预:演示时手动将 `remaining` 调整为 0 以模拟售罄、压测后重置库存必须使用唯一、明确的运维脚本;不允许直接在生产库上手动 UPDATE 后未记录原因。 - 数据库与连接池抖动:连接池耗尽或主库短暂不可用时,秒杀接口应快速失败而非长期阻塞;压测前评估连接池上限并预留普通接口余量。 @@ -1957,7 +1975,7 @@ C04 在 M02-01 的基础搜索入口上增加中文分词模糊搜索、多条 #### 5. 业务规则与权限 - 搜索关键词和筛选参数必须参数化处理,排序字段使用白名单,不得拼接不可信 SQL。 -- 公开搜索强制过滤草稿、下架和已删除商品;任何身份都不能通过请求参数绕过。 +- 公开搜索强制过滤草稿和已下架商品;已物理删除商品已不存在,任何身份都不能通过请求参数恢复或绕过状态过滤。 - 商品数据与搜索索引由同一 PostgreSQL 实例维护,不存在独立搜索服务的异步数据副本。 - 相关度排序应有明确、可答辩的规则;当结果分值相同时使用稳定的次级排序。 - 降级不是静默制造错误结果:系统需记录所用实现与原因,并保证核心过滤和权限不变。 @@ -2082,8 +2100,8 @@ C07 使用 Redis 优化首页商品摘要和商品详情两个高频只读场景 | 身份 | 是否涉及 | 缓存处理要求 | |---|---|---| -| 游客 | 是 | 读取已上架商品的公开首页摘要和详情缓存,不得看到草稿、下架商品、商家字段或任何用户个性化数据。 | -| 会员(买家) | 是 | 浏览阶段可复用与游客一致的公开商品缓存;收藏状态、购物车数量等个人字段不得混入公共缓存;下单时重新从数据库校验价格、库存和可售状态。 | +| 游客 | 是 | 只读取固定首页摘要和已上架商品详情的公开缓存;下架后不得写入新缓存,旧详情必须在已约定的一致性窗口内失效,且不得包含商家字段或用户个性化数据。 | +| 会员(买家) | 是 | 浏览阶段可复用与游客一致的固定首页摘要和商品详情缓存;旧详情不得在一致性窗口外继续返回,收藏状态、购物车数量等个人字段不得混入公共缓存;下单时重新从数据库校验价格、库存和可售状态。 | | 商家(运营人员) | 是 | 商品新增、改价、改库存、上下架或修改内容成功后触发相关缓存失效;商家管理查询以授权后的事实数据为准,不直接使用可能隐藏管理字段的公开缓存。 | | 管理员 | 否 | 当前 C07 只优化首页和商品详情,不为管理员账号治理页面增加缓存,避免扩大范围。 | @@ -2093,7 +2111,7 @@ C07 使用 Redis 优化首页商品摘要和商品详情两个高频只读场景 | 编号 | 功能 | 详细要求 | |---|---|---| -| C07-FR01 | 缓存对象 | 至少缓存首页商品摘要和商品详情;缓存内容只包含接口返回所需且允许公开的数据,不缓存管理员字段、连接信息或用户敏感数据。 | +| C07-FR01 | 缓存对象 | 本期只缓存固定首页商品摘要和商品详情;分类、普通商品列表、关键词搜索、组合筛选和秒杀活动均不缓存。缓存内容只包含接口返回所需且允许公开的数据,不缓存管理员字段、连接信息或用户敏感数据。 | | C07-FR02 | Cache-Aside 读取 | 查询先读取 Redis;命中时返回缓存值,未命中时查询 PostgreSQL、生成与原接口一致的响应并写入有限 TTL 的缓存。 | | C07-FR03 | Key 隔离 | Key 必须包含环境、模块、资源类型、资源 ID 或稳定查询标识及必要版本信息,避免不同环境、不同查询条件和不同数据结构互相污染。 | | C07-FR04 | 写后失效 | 商品改价、库存调整、上下架、名称/图片/描述变更的数据库事务提交后,删除受影响的详情和首页缓存;事务回滚时不得提前删除并生成错误的新值。 | @@ -2464,7 +2482,7 @@ Electron 后续原则上复用 PC Web 原型,只补充窗口、桌面导航和 | C03 | 订单超时取消—韦乾强 | 订单倒计时、Worker 验收脚本 | 复用 A304;超时扫描为 Worker 内部契约 | 待挑战测试登记 | Worker 扫描、取消复用和原库存通道回补已定义,待数据库与测试评审 | | C04 | 商品搜索进阶—顾欣月 | 商品搜索页、性能对比报告 | A102 | 待挑战测试登记 | 接口草案已汇总,待交叉评审 | | C06 | 实时消息推送—罗皓晨 | 实时通知、连接状态、消息中心 | SignalR 契约(无 Axxx),补查复用 A501~A505 | 待挑战测试登记 | 实时推送、持久化事实与断线补查边界已定义,待部署与测试评审 | -| C07 | 缓存与性能优化—罗皓晨,顾欣月协作 | 首页、商品详情、压测报告 | 复用 A102、A103 | 待挑战测试登记 | 不新增业务 HTTP,缓存约定待确认 | +| C07 | 缓存与性能优化—罗皓晨,顾欣月协作 | 首页、商品详情、压测报告 | 复用 A102 的固定首页摘要调用、A103 商品详情;不缓存普通列表与搜索 | 待挑战测试登记 | 不新增业务 HTTP,缓存约定待确认 | | C08 | 支付回调幂等与对账—张海洋 | 回调重放脚本、管理端对账 | A421~A425、A432~A433;退款入账使用 Payment 应用契约 | 待挑战测试登记 | A431 为取消历史号,待数据库、OpenAPI 与交叉评审 | | C10 | 容器化与负载均衡—罗皓晨 | 统一访问入口、健康与实例验证 | A506、A507 | 待部署验收登记 | 健康检查草案已汇总,待交叉评审 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" index f84ec8c..6f3da02 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" @@ -4,21 +4,21 @@ > 覆盖:M02-01、M02-02、F04、F05、F06 > 基础核心流程:F01~F02、M01 Identity;与根文档 [`业务流程设计.md`](../业务流程设计.md) 第 3.3、3.8.1 节保持一致 > 直接协作:朱惠惠(M03 购物车)、韦乾强(M04 订单)、唐宇昊(M01 身份)、罗皓晨(C07 缓存协作) -> 文档状态:初稿,待顾欣月自审及 Cart/Ordering/Identity 交叉评审 -> 升级标记:在初稿基础上补强 C07 缓存接入边界、C04 搜索契约衔接、回归核心结果表述和验收对照 +> 文档状态:完整定义,已完成统稿审计,待负责人及 Catalog/Cart/Ordering/Identity/C07 协作确认 +> 升级标记:已统一商品三态、停用分类、售罄展示、统一经营目录和 C07 缓存边界;本轮阻断项已关闭 > 需求事实源:[需求规格说明书 M02-01](../../../01-需求文档/需求规格说明书.md) 与 [M02-02](../../../01-需求文档/需求规格说明书.md) 的完整七节 ## 一、范围与事实来源 本模块负责购物端商品发现入口,覆盖分类筛选、关键词搜索、价格区间、库存条件、排序与组合查询,以及商品详情页的信息展示、可售状态判断、买家/游客操作衔接和评价入口衔接。它不承接商家后台的商品维护(属于 M06-01),不替代购物车的库存和归属校验,也不修改商品在历史订单中的快照。 -本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。Catalog 接口编号落在 A101~A128 范围(M02 公开浏览 A101~A103,M06-01 后台写操作 A110~A128,M07 评价读取 A140~A144 引用);该编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。Catalog 接口编号落在 A101~A128 范围(M02 公开浏览 A101~A103,M06-01 后台写操作 A110~A128);M07 的公开评价读取能力由其自身流程另行派生。接口编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M02-01/F04、F05 需求 | 完整定义 | 作为业务语义事实源 | | M02-02/F06 需求 | 完整定义 | 作为业务语义事实源 | -| 本文业务流程 | 初稿 | 先确认角色、状态、分支和模块出入口 | +| 本文业务流程 | 完整定义、待交叉评审 | 已确认角色、状态、分支和模块出入口 | | 分类与商品接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | | DBxxx 分类/商品表 | 部分定义 | 本文不发明表字段、状态码和索引 | | C07 缓存协作 | 待细化(罗皓晨主责、顾欣月协作失效规则) | 只登记接入点,不混入 F04~F06 核心浏览口径;具体 TTL 与失效策略由缓存主责确认 | @@ -29,45 +29,52 @@ ```mermaid flowchart LR ID["M01 Identity
已认证买家、游客角色识别"] -->|"公开浏览对游客和买家均开放"| CAT["M02 Catalog
已上架商品、分类、实时价格与库存"] - ADM["M06-01 商家后台商品管理"] -->|"分类与商品维护命令(事务提交后)"| CAT - CACHE["C07 性能缓存层
命中则直返
商品事务提交后失效"] -. "读取前缓存" .-> CAT + ADM["M06-01 商家后台商品管理"] -->|"统一经营目录中的分类与商品维护结果"| CAT + CACHE["C07 固定首页摘要与商品详情缓存协作"] -. "仅接入预定义首页摘要" .-> HOME + CACHE -. "接入商品详情读取" .-> DET SEARCH["C04 进阶搜索实现
替换 F05 底层查询"] -. "替换 F05 底层实现" .-> CAT CAT -->|"已上架商品与实时价格库存"| CART["M03 Cart 加购与失效标记"] CAT -->|"下单重读与库存条件更新"| ORD["M04 Ordering"] - CAT -->|"分类与已上架商品第一页(强制已上架过滤)"| LIST["购物端列表/搜索页 F04/F05"] + CAT -->|"预定义、不可由用户任意组合参数的已上架商品摘要"| HOME["购物端固定首页商品摘要"] + CAT -->|"启用分类与已上架商品(强制已上架过滤)"| LIST["购物端普通列表/搜索页 F04/F05"] CAT -->|"商品公开信息与可售状态"| DET["商品详情页 F06"] LIST -->|"商品 ID"| DET CART -. "收藏、加购或购买意图从详情页进入" .-> CAT REV["M07 商品评价"] -->|"公开评价汇总与列表"| DET ID -->|"账号禁用或角色越权"| X["按身份禁止越权操作"] - ADM -->|"草稿、下架或已删除商品"| Y["购物端不得出现在公开浏览结果中"] - CAT -->|"商品不存在、已下架或库存归零"| Z["详情显示不可售,不提供购买入口"] - CACHE -->|"缓存不可用或命中陈旧"| W["回退事实源直读;缓存命中时无法可靠感知数据库已变更,存在受 TTL 约束的短暂旧值窗口"] + ADM -->|"草稿或已下架商品"| Y["购物端不得出现在公开浏览结果中"] + CAT -->|"商品 ID 不存在或实体已物理删除"| Z1["详情显示商品不存在"] + CAT -->|"商品已下架"| Z2["详情显示暂不可售,不提供购买入口"] + CAT -->|"商品已上架且库存为零"| Z3["详情继续公开并标记售罄
不提供加购或购买入口"] + CACHE -->|"缓存不可用或命中旧详情"| W["回退事实源直读;旧详情只允许存在于已约定的一致性窗口内"] SEARCH -->|"进阶查询失败或索引损坏"| V["回退基础模糊查询并记录降级原因"] ``` 边界约束: -- 公开浏览只暴露已上架商品;草稿、下架或已删除商品不得通过搜索参数绕过。 +- 公开浏览只暴露已上架商品;草稿和已下架商品不得通过搜索参数绕过。满足删除条件并完成物理删除后,商品已不存在,“已删除”不是可查询的销售状态。 +- 本期是单店 B2C 统一经营目录,所有正常商家账号维护同一套分类和商品;M02 不按创建人或操作人分割公开商品。 +- 已上架商品库存为零时仍公开展示并标记售罄;库存只控制加购、结算和购买资格,不自动改变商品销售状态。 +- 停用分类只从购物端分类筛选入口移除,不自动下架或隐藏其下已上架商品。已有链接、全部商品和关键词搜索仍可发现这些商品。 - **公开浏览对游客和买家均开放**:携带过期或无效令牌访问公开商品接口时,按游客处理并正常返回商品数据,不得因令牌状态拒绝。401/403 仅在受保护写操作(收藏、加购、购买、评价提交)出现。 - 购物端的价格和库存只能作为浏览口径,下单与购物车写入必须由服务端在 M03、M04 中重新校验。 - 商品模块不直接访问用户私有数据;买家专属操作(收藏、加购、购买)由 M08、M03、M04 提供,商品模块只提供事实输入和入口。 - 评价(X01)汇总来自 M07,商品详情只做公开读取,不修改评价事实。 -- C07 缓存只能放在事实查询路径之前;缓存命中时无法可靠感知底层 PostgreSQL 已发生的变更,因此存在受 TTL 约束的短暂旧值窗口;缓存失效或不可用时回退到事实源(PostgreSQL)直读,由缓存主责约定 TTL 与主动失效策略,本文不擅自承诺"绝不返回旧值"。 +- C07 本期只缓存预定义且不可由用户任意组合查询参数的固定首页商品摘要,以及商品详情。除该固定首页调用外,M02 分类、普通列表、关键词搜索和组合筛选不得接入 C07,每次都按 PostgreSQL 已提交事实执行。首页摘要和详情缓存只允许在 C07 已确认的一致性窗口内短暂返回旧公开值,缓存失效或不可用时回退 PostgreSQL。 - C04 在 M02-01 列表入口上替换底层搜索实现,对外参数白名单、已上架过滤和返回口径与本文保持一致;接口与数据待评审项在第十章集中登记。 -- 扩展完成后回到的核心结果:F04 公开列表仍只返回已上架商品、F05 关键词查询仍按统一搜索契约返回、F06 详情仍按服务端最新状态展示。扩展失败不能改变上述核心结果。 +- 扩展完成后回到的核心结果:F04 公开列表仍只返回已上架商品、F05 关键词查询仍按统一搜索契约返回、F06 详情仍遵守已上架、售罄和有限一致性窗口口径。扩展失败不能改变上述核心结果。 ## 三、购物端商品列表与组合筛选 ```mermaid flowchart TD - A["游客/买家/商家/管理员进入商品列表"] --> B["加载有效分类与已上架商品第一页
关键词去除首尾空白,参数校验通过"] + A["游客/买家/商家/管理员进入商品列表"] --> B["从事实源加载启用分类和已上架商品第一页
关键词去除首尾空白"] B --> C{"参数是否合法?"} C -- "否" --> X["返回字段级错误并保留查询条件"] - C -- "是" --> D["服务端强制过滤为已上架商品
分类、价格区间、仅看有货、白名单排序组合生效"] + C -- "是" --> D["服务端强制过滤为已上架商品
分类、价格区间、仅看有货、白名单排序组合生效
不因所属分类停用而隐藏商品"] D --> E{"是否有匹配结果?"} E -- "否" --> F["返回空集合与正确分页信息
展示当前条件并提供清空筛选入口"] E -- "是" --> G["展示主图、名称、当前价格、库存摘要"] @@ -81,6 +88,8 @@ flowchart TD 组合筛选规则: - 关键词、分类、价格区间和仅看有货之间按“同时满足”处理;价格下限不得大于上限。 +- 分类筛选入口只列出启用分类;停用分类不再作为可选条件。停用分类下已有已上架商品仍出现在未指定分类的列表和关键词搜索结果中。 +- “仅看有货”未选中时,库存为零的已上架商品正常返回并标记售罄;选中后才排除库存为零的商品。 - 排序项和方向必须走白名单,禁止把客户端字段直接拼为查询语句。 - 条件恢复:关键词、筛选、排序和页码应当反映在页面地址或等效可恢复状态中,刷新或返回时无需重新选择。 - 失败反馈:首次加载显示骨架或加载状态;请求失败时保留原条件并允许重试,不以系统异常处理空结果。 @@ -90,9 +99,11 @@ flowchart TD ```mermaid flowchart TD A["用户从列表/搜索/收藏/历史进入详情"] --> B["按商品 ID 加载公开信息"] - B --> C{"商品是否存在且为已上架?"} - C -- "否" --> X["展示不存在或暂不可售,禁用购买入口
提供返回列表入口"] - C -- "是" --> D["展示名称、主图/图片、描述、当前价格、库存和分类"] + B --> C{"商品是否存在?"} + C -- "否" --> X["展示商品不存在
提供返回列表入口"] + C -- "是" --> S{"当前销售状态?"} + S -- "草稿或已下架" --> Y["展示暂不可售,禁用购买入口
提供返回列表入口"] + S -- "已上架" --> D["展示名称、主图/图片、描述、当前价格、库存状态和分类
分类停用不改变商品可见性"] D --> E{"当前角色?"} E -- "游客" --> F["显示登录引导并保留目标商品与原操作意图"] E -- "商家或管理员" --> G["仅展示公开效果,不显示买家专属操作"] @@ -104,9 +115,11 @@ flowchart TD 关键规则: -- 价格、库存和上下架状态以服务端最新数据为准,页面缓存不得作为下单依据。 +- 价格、库存和上下架状态以服务端结果为准;C07 商品详情缓存可以在已约定的一致性窗口内短暂返回旧公开值,但不得作为加购、结算或下单依据。 +- 已上架且库存为零时保留详情并明确标记售罄,不提供加购或购买入口。 +- 所属分类停用时,只要商品仍为已上架就继续公开;详情可以展示分类信息,但购物端分类筛选入口不再提供该分类。 - 商品主图加载失败时使用占位图,不阻断其他信息浏览。 -- 后台改价、改库存或上下架后,详情重新获取时按服务端最新数据展示,不复用旧缓存。 +- 后台改价、改库存、上下架或修改内容后,详情在 C07 已约定的一致性窗口内收敛到 PostgreSQL 最新值;超过窗口不得继续返回旧值。 - 图片合规:单个商品最多 8 张,单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 400~4096 像素,第一张作为主图。 - 已下架商品的旧链接只能显示不可售状态,不提供购买入口;历史订单中的商品快照仍可读,但不受当前上下架状态影响。 @@ -116,20 +129,19 @@ flowchart TD stateDiagram-v2 [*] --> 草稿: M06-01 商家创建并保存 草稿 --> 已上架: 完整性校验通过并主动上架 - 草稿 --> 已删除: 无历史关联且确认删除 已上架 --> 已下架: 商家主动下架 已下架 --> 已上架: 重新校验通过并上架 - 已下架 --> 已删除: 无历史关联且确认删除 - 已删除 --> [*] + 草稿 --> [*]: 无任何历史关联且确认物理删除 + 已下架 --> [*]: 无任何历史关联且确认物理删除 ``` -购物端浏览口径只承认 `已上架` 状态,其他状态在公开列表、搜索和详情入口中均不出现。下架商品的历史订单快照、购物车、收藏与浏览记录由对应模块显示不可售,不被本模块删除。 +商品销售状态只有 `草稿`、`已上架`、`已下架` 三种。物理删除后商品实体不再存在,是终止结果而不是第四种状态;已上架商品必须先下架,且无任何历史关联时才可物理删除。购物端浏览口径只承认 `已上架`,下架商品的历史订单快照、购物车、收藏与浏览记录由对应模块显示不可售,不被本模块删除。 并发与一致性: -- 商品事务提交后由 M06-01 触发缓存失效;缓存命中时无法可靠感知数据库已变更,存在受 TTL 约束的短暂旧值窗口;缓存不可用或失效失败时按 M02 直读 PostgreSQL 处理,不掩饰错误。 -- 购物端读取始终以 PostgreSQL 为事实来源;Redis 仅承担性能缓冲,不得覆盖浏览口径。 -- 商品在买家浏览瞬间被下架,详情页必须按服务端最新状态展示暂不可售,不复用缓存中的已上架结果。 +- 商品事务提交后由 M06-01 通知 C07 失效固定首页摘要和目标商品详情;分类、列表与搜索没有缓存失效责任,因为本期不缓存这些查询。 +- PostgreSQL 始终是商品事实来源;Redis 仅为固定首页摘要和详情提供有限性能缓冲,不得覆盖浏览、加购或下单口径。 +- 商品在买家浏览瞬间被下架时,列表与搜索的下一次查询直接过滤该商品;详情若在一致性窗口内命中旧值,买家发起操作时仍由 M03/M04 按 PostgreSQL 当前状态拒绝,并刷新页面。 ## 六、结果反馈与页面衔接 @@ -162,15 +174,17 @@ flowchart TD |---|---|---| | 公开浏览携带过期/无效令牌 | 按游客处理并正常返回公开商品数据 | 公开接口不依赖有效令牌 | | 公开浏览携带账号禁用令牌 | 按游客处理并正常返回公开商品数据;保护写操作时返回 401 | 公开接口与受保护接口分开校验 | -| 商家或管理员在购物端尝试越权操作 | 服务端按 Policy 拒绝;前端隐藏入口不替代后端 | 401/403 由对应模块返回 | -| 商品不存在 | 返回”商品不存在”,提供返回列表入口 | 不暴露内部异常 | -| 商品已下架或被删除 | 显示”暂不可售”,禁用购买 | 历史订单快照仍可读 | -| 库存为零或数量超限 | 显示售罄或拒绝购买 | 由 M03/M04 决定是否调大或重新选择 | +| 商家或管理员在购物端尝试买家专属操作 | 服务端按角色拒绝;前端隐藏入口不替代后端 | 由收藏、购物车或订单模块返回无权限结果 | +| 商品 ID 不存在或商品已物理删除 | 返回“商品不存在”,提供返回列表入口 | 不暴露内部异常;删除不是销售状态 | +| 商品已下架 | 显示“暂不可售”,禁用购买 | 历史订单快照仍可读 | +| 商品已上架但库存为零 | 继续展示并标记售罄 | 不提供加购、结算或购买入口 | +| 商品所属分类被停用 | 分类不再出现在筛选入口,商品继续按已上架状态公开 | 不自动下架,不从全部商品或关键词搜索隐藏 | +| 请求数量超过当前库存 | 拒绝加购、结算或购买并刷新库存事实 | 由 M03/M04 决定是否调小或重新选择 | | 关键词、分类、价格或排序非法 | 字段级错误,保留查询条件 | 不执行查询 | | 图片加载失败 | 使用占位图 | 不阻断价格、库存和描述浏览 | | 加载失败或网络中断 | 保留当前页面,允许重试 | 不把旧缓存价格当作最新价格 | -| 商品在浏览瞬间被下架 | 服务端按最新状态返回不可售 | 不复用缓存 | -| C07 缓存失效或不可用 | 回退事实源直读 | 缓存命中时存在受 TTL 约束的短暂旧值窗口,不掩饰错误 | +| 商品在浏览瞬间被下架 | 列表/搜索下一次查询不再返回;详情旧值只可存在于一致性窗口 | M03/M04 始终按事实源拒绝购买 | +| C07 首页摘要或详情缓存失效、损坏或不可用 | 回退事实源直读 | 仅固定首页摘要和详情使用本期缓存;普通列表、分类、搜索直读,旧值不得超过一致性窗口 | ## 八、由流程派生的接口契约映射 @@ -178,16 +192,16 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 查询有效分类 | A101 Catalog 分类 | 返回购物端筛选入口使用的有效分类,停用分类不出现在筛选入口 | 待交叉评审 | -| 查询商品列表与组合筛选/关键词搜索 | A102 Catalog 列表/搜索 | 强制已上架过滤、分页、白名单排序、组合筛选和空结果正常返回;F05 基础模糊查询与 C04 进阶实现共用同一接口契约 | 待交叉评审 | -| 查询商品详情 | A103 Catalog 详情 | 仅返回当前已上架商品的最新价格、库存、图片和描述 | 待交叉评审 | -| 公开评价汇总与列表(X01 衔接) | 由 M07 派生(A140~A144) | 商品详情只读取 M07 公开结果,不在此模块内实现评价提交 | 待交叉评审 | +| 查询启用分类 | A101 Catalog 分类 | 只返回购物端筛选入口使用的启用分类;分类停用不改变其下商品销售状态 | 待交叉评审 | +| 查询固定首页摘要、普通列表与组合筛选/关键词搜索 | A102 Catalog 列表/搜索 | 固定首页摘要调用可由 C07 缓存;所有用户控制的普通列表、关键词和组合筛选每次按 PostgreSQL 强制已上架过滤、分页、白名单排序并正常返回空结果;库存为零时标记售罄,所属分类停用时仍可通过全部商品和关键词命中;F05 与 C04 共用同一契约 | 待交叉评审 | +| 查询商品详情 | A103 Catalog 详情 | 仅公开已上架商品;允许库存为零并标记售罄,允许所属分类停用;C07 旧值受一致性窗口约束 | 待交叉评审 | +| 公开评价汇总与列表(X01 衔接) | 由 M07 的公开读取能力派生,编号待 M07 流程确认后映射 | 商品详情只读取公开评价和评分汇总,不混入上传、提交或资格判断能力 | 待交叉评审 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 ## 九、扩展接入边界 -- C07 缓存:在商品详情和分类/列表查询路径前使用缓存层读取公开商品;M06-01 提交商品事务后失效缓存,TTL 与主动失效策略由缓存主责人统一确认。PostgreSQL 仍是事实来源;缓存不可用时回退数据库直读,不掩盖错误。 +- C07 缓存:A102 中只有预定义且不可由用户任意组合参数的固定首页摘要调用可以接入缓存,A103 商品详情也可接入;A101 分类及 A102 的普通列表、关键词搜索和组合筛选不进入本期缓存。M06-01 提交商品事务后只失效受影响的首页摘要和目标商品详情,TTL、一致性窗口与主动失效策略由 C07 主责确认。 - C04 中文搜索:在 M02-01 列表查询入口上替换底层搜索实现,返回口径与基础模糊查询一致;公开浏览口径、参数白名单和已上架过滤不变。 - M03 购物车:只接收本模块输出的已上架商品与实时价格库存;下架或库存归零由 M03 标记失效,不反向修改商品状态。 - M04 订单:下单时由 M04 重读本模块的最新事实进行条件扣减,不信任购物端传入的金额和库存。 @@ -196,11 +210,11 @@ flowchart TD ## 十、由流程反查出的接口与数据待评审项 1. 公开浏览接口(A101~A103)必须明确"无登录或令牌失效时按游客返回"的契约;接口设计需与 M01 的 JWT 鉴权边界统一,避免公开接口误判为受保护接口。 -2. 列表与详情对已上架过滤必须服务端强制;接口需要确认是否在响应中显式携带”不可售原因”或仅按 HTTP 状态码区分,由 M02 与接口设计共同决定。 +2. 列表与详情对已上架过滤必须服务端强制;库存为零的已上架商品需要返回可区分的售罄结果,已下架与不存在的详情结果由接口契约分别定义。 3. 排序白名单字段集尚未在需求中枚举;接口设计前需要 M02 与评审人员确认价格、时间、相关度的默认与可选顺序。 4. 图片合规校验在前端完成上传限制后仍需服务端再次校验;接口字段需要明确”主图”与”附加图”的上传顺序和替换规则。 5. 商品详情是否暴露最新库存数或仅暴露”有货/无货”摘要,由需求决定展示口径;接口返回字段需要和前端展示要求对齐。 -6. 评价公开汇总字段(平均分、总条数的计算时机)与缓存策略相关,需要与 M07、C07 共同确认。 +6. 评价公开汇总字段(平均分、总条数的计算时机)由 M07 派生;本期不得把它混入 C07 商品详情缓存,避免评价变更扩大商品缓存失效范围。 7. C04 进阶搜索替换 F05 基础模糊查询时,需要保留 A102 商品列表/搜索接口的请求字段和返回口径,避免前端按字段名硬编码查询逻辑。 8. DBxxx 分类/商品表字段尚未形成可实施的完整定义,数据库字段、约束、索引和 GIN/pg_trgm 维护策略必须由数据库设计任务另行确认。 9. C07 的 TTL 与主动失效上限需要在本流程评审前完成,避免缓存值与商品最新事实长期不一致。 @@ -211,11 +225,12 @@ flowchart TD - [ ] F04:分页数据、总数和翻页结果正确,刷新或返回后查询条件仍可恢复;分类筛选有效,停用分类不出现在购物端筛选入口。 - [ ] F05:关键词模糊搜索、组合筛选和白名单排序可独立及组合生效;F05 由 C04 替换底层实现后口径不变。 -- [ ] F06:商品名称、图片、描述、价格、库存和分类展示正确,并与后台最新有效修改一致;有货、售罄、下架、不存在和加载失败状态均能清楚区分。 -- [ ] N04:购物端任何身份均无法搜索到草稿、下架或已删除商品;游客、买家、商家和管理员看到符合权限的操作入口,服务端鉴权生效。 +- [ ] F06:商品名称、图片、描述、价格、库存和分类展示正确,并在 C07 一致性窗口内收敛到后台最新有效修改;有货、售罄、下架、不存在和加载失败状态均能清楚区分。 +- [ ] N04:购物端任何身份均无法搜索到草稿或已下架商品,已物理删除商品不再存在;库存为零的已上架商品仍可见并标记售罄。 - [ ] N02:图片失败或接口失败时页面仍可理解、可返回或可重试,不出现空白页。 - [ ] N05:Chrome / Edge 最新版正常显示,无明显样式错乱。 -- [ ] 缓存:缓存命中时承认存在受 TTL 约束的短暂旧值窗口;缓存失效或不可用时回退事实源直读,不掩饰错误。 +- [ ] 分类:停用分类不再作为筛选入口,但其下已上架商品仍能通过全部商品、关键词搜索和详情访问。 +- [ ] 缓存:A102 仅固定首页摘要调用和 A103 商品详情允许存在受一致性窗口约束的旧值;分类、普通列表和搜索直读 PostgreSQL;缓存不可用时回退事实源。 - [ ] X01 衔接:已选 X01 的评分与评价入口展示正常,但未满足条件的用户不能从详情页绕过订单资格提交评价。 ## 十二、提交前自检与升级路径 @@ -224,12 +239,12 @@ flowchart TD - [ ] 已写清基础 F、核心接入状态和回归结果;本图不改变 F04~F06 核心结果。 - [ ] Mermaid 图内部包含直接上游模块输入(M01、M06-01)和直接下游模块出口(M03、M04、M07)。 - [ ] 主流程、拒绝分支、失败分支和最终结果齐全;扩展不破坏核心权限、金额、库存、快照和事实来源。 -- [ ] 状态名称与需求规格说明书一致;没有新增核心商品状态。 +- [ ] 商品只有草稿、已上架、已下架三种销售状态;物理删除是实体不存在的终止结果,没有“已删除”状态。 - [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径、DTO、技术模式名称(如 Cache-Aside)或 SQL 片段代替流程;没有自行发明接口编号、字段名或数据库状态码。 -- [ ] 公开浏览接口不因令牌失效而拒绝;身份逻辑与 M01 Policy 边界一致。 -- [ ] 接口编号落在 Catalog/Review 范围 A101~A200(M02 公开浏览 A101~A103,M06-01 后台写操作 A110~A128,M07 评价 A140~A144),不混用其他模块编号。 +- [ ] 公开浏览不因令牌失效而拒绝;受保护操作的身份与账号状态判断由 M01 提供。 +- [ ] M02 仅映射公开 Catalog 的 A101~A103;M06-01 使用 A110~A128,M07 公开评价读取由 M07 流程独立派生,不用整段编号反推本流程。 - [ ] 已对照根文档 3.3 校准主流程、状态机和模块出入口,与 M06-01 边界一致。 升级到”待交叉评审”的条件:自检完成、主流程与异常分支齐全、公开身份逻辑已对齐 M01、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 -升级到”已确认”的条件:直接协作人(Cart、Ordering、Identity)共同确认边界,C07 主责确认缓存接入边界,根文档 3.3 中对应追踪项成熟度同步更新。 \ No newline at end of file +升级到”已确认”的条件:直接协作人(Cart、Ordering、Identity)共同确认边界,C07 主责确认缓存接入边界,根文档 3.3 中对应追踪项成熟度同步更新。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" index 9292cf2..eb7bb84 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -4,8 +4,8 @@ > 覆盖:M06-01、F11 > 基础核心流程:F02、F04~F06、M02 Catalog;与根文档 [`业务流程设计.md`](../业务流程设计.md) 第 3.3、3.7 节保持一致 > 直接协作:罗皓晨(M00 公共基建、C07 缓存主责)、唐宇昊(M01 Identity)、朱惠惠(M03 购物车)、韦乾强(M04 订单) -> 文档状态:初稿,待顾欣月自审及 Identity/Catalog/缓存协作交叉评审 -> 升级标记:在初稿基础上补强 MerchantOnly 鉴权链路、并发保护方式、缓存失效责任、回归核心结果和验收对照 +> 文档状态:完整定义,已完成统稿审计,待负责人及 Identity/Catalog/Cart/Ordering/C07 协作确认 +> 升级标记:已统一经营目录、商品三态、分类停用、售罄展示和 C07 失效范围;本轮阻断项已关闭 > 需求事实源:[需求规格说明书 M06-01](../../../01-需求文档/需求规格说明书.md) 的完整七节 ## 一、范围与事实来源 @@ -19,7 +19,7 @@ | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M06-01/F11 需求 | 完整定义 | 作为业务语义事实源 | -| 本文业务流程 | 初稿 | 先确认角色、状态、分支和模块出入口 | +| 本文业务流程 | 完整定义、待交叉评审 | 已确认角色、状态、分支和模块出入口 | | 商家端写操作接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | | DBxxx 分类/商品表 | 部分定义 | 本文不发明表字段、状态码和索引 | | C07 缓存失效协作 | 待细化 | 只登记接入点,不混入商家写操作核心结果 | @@ -29,59 +29,64 @@ ```mermaid flowchart LR - ID["M01 Identity
MerchantOnly 认证与账号状态"] -->|"MerchantOnly + 账号正常"| ADM["M06-01 商家后台入口"] - ADM -->|"分类与商品维护命令(事务提交后)"| CAT["M02 Catalog
商品事实、分类、销售状态"] + ID["M01 Identity
已认证且账号正常的商家"] -->|"商家身份有效"| ADM["M06-01 商家后台入口"] + ADM -->|"维护单店统一经营目录"| CAT["M02 Catalog
商品事实、分类、销售状态"] CAT -->|"最新商品销售状态、分类与价格库存"| LIST["F04~F06 购物端浏览"] CAT -->|"商品事实被 M06-01 修改"| CART["M03 Cart 失效条目重检"] CAT -->|"下单重读与库存条件更新"| ORD["M04 Ordering"] - CAT -. "事务提交后事件" .-> CACHE["C07 性能缓存层
缓存主责统一失效与重建"] - CAT -. "事务提交后索引同步" .-> SEARCH["C04 进阶搜索索引
由事实源数据库同步维护"] + CAT -. "事实提交后通知" .-> CACHE["C07 固定首页摘要与商品详情
失效与重建"] + CAT -. "事实提交后可查询" .-> SEARCH["C04 进阶搜索
读取同一商品事实"] - ID -->|"游客、买家、管理员或账号禁用"| X["403 或 401,拒绝后台访问"] + ID -->|"游客、买家、管理员或账号禁用"| X["拒绝后台访问"] ADM -->|"字段非法或并发冲突"| Y["拒绝保存并保留表单内容"] - ADM -->|"删除存在历史订单的商品"| Z["拒绝破坏性删除,引导改为下架"] + ADM -->|"删除存在任何历史引用的商品"| Z["拒绝物理删除,引导改为下架"] CACHE -->|"缓存失效失败"| W["不回滚商品事务,由缓存处理器重试"] SEARCH -->|"索引异常"| V["暂停进阶搜索,回退基础查询"] ``` 边界约束: -- M06-01 仅拥有商家身份入口;游客、买家和管理员都不能调用任何写接口,前端隐藏入口不能替代服务端 Policy。 +- M06-01 仅允许已认证且账号正常的商家进入;游客、买家、管理员和已禁用商家都不能读取后台经营数据或执行写操作。前端隐藏入口不能替代服务端身份校验。 +- 本项目是单店 B2C,所有正常商家账号共同维护一套分类和商品目录,不按创建人或当前操作人过滤商品所有权。 - 商品模块只暴露分类与商品事实;商家不得修改买家账号、支付事实或订单金额。 -- 商品名称、有效分类、当前价格、库存和主图为必填;价格不得为负,库存必须为非负整数。 -- 商品事务提交后才允许触发缓存失效与搜索索引同步;事务失败时不发起任何外部动作。 -- 删除约束:存在历史订单关联时禁止破坏性删除,由系统建议改为下架。 -- 并发保护:编辑与上下架使用并发标记(version/etag)或数据库条件更新防止静默覆盖,与 M04 订单并发口径由接口设计统一;具体技术实现见接口与数据库设计。 -- 缓存与索引责任划分:缓存失效与重建由 C07 主责统一执行,本模块只提交“商品事务已提交”信号;搜索索引由 PostgreSQL 同步维护,本模块不创建独立同步任务。 +- 商品名称、合法的分类关联、当前价格、库存和主图为必填;价格不得为负,库存必须为非负整数。 +- 商品事务提交后才通知 C07 失效固定首页摘要和目标商品详情;分类、后台列表及用户控制的购物端普通列表和搜索本期不缓存。事务失败时不发布成功结果。 +- 删除约束:存在订单、购物车、收藏、浏览、评价、秒杀等任何历史关联时禁止物理删除,由系统建议改为下架。 +- 并发保护:编辑与上下架必须使用条件更新防止静默覆盖;业务要求由本流程确定,具体并发字段和失败响应再由接口与数据库设计承接。 +- 缓存与搜索责任划分:缓存失效与重建由 C07 主责统一执行,本模块只提供已提交的商品变更事实;C04 读取同一 PostgreSQL 商品事实,本模块不创建第二份搜索事实或独立同步任务。 - 扩展完成后回到的核心结果:F04~F06 公开浏览口径、M02 商品销售状态机、M03 购物车失效标记契约、M04 下单重读条件均不变;商家写操作不能绕过这些核心结果。 ## 三、分类维护 ```mermaid flowchart TD - A["M06-01 入口:商家通过 MerchantOnly 进入分类管理"] --> B["查询当前分类列表(含名称、层级、排序、启停状态)"] + A["账号正常的商家进入分类管理"] --> B["查询统一经营目录的分类列表
含名称、层级、排序、启停状态"] B --> C{"选择操作?"} C -- "新增" --> D["填写名称、父级关系、排序和初始启停状态"] - C -- "编辑" --> E{"该分类是否被商品或历史引用?"} - C -- "启用或停用" --> F["切换启停状态并校验依赖"] - C -- "申请删除" --> E - E -- "是" --> X["拒绝破坏性删除,建议改为停用"] - E -- "否" --> G["完成删除或编辑保存"] - D --> H{"字段是否合法?"} - H -- "否" --> Y["字段级错误,保留已填内容"] - H -- "是" --> G - F --> I{"停用分类是否仍被商品引用?"} - I -- "是" --> J["允许停用但禁止新建或编辑该分类下的商品上架
购物端不再作为筛选入口"] - I -- "否" --> K["直接停用或启用,结果立即生效"] - G --> L["保存分类事实并返回最新分类列表"] - K --> L - J --> L + C -- "编辑" --> E["修改名称、父级或排序
引用关系不阻止编辑"] + C -- "启用或停用" --> F["切换启停状态"] + C -- "申请删除" --> G{"是否存在商品或历史引用?"} + G -- "是" --> X["拒绝物理删除,建议改为停用"] + G -- "否" --> N["确认后物理删除分类"] + D --> V{"字段是否合法?"} + E --> V + V -- "否" --> Y["字段级错误,保留已填内容"] + V -- "是" --> I["保存分类元数据"] + F --> J{"切换为停用?"} + J -- "否" --> K["启用分类并恢复购物端筛选入口"] + J -- "是" --> L["停用分类并移出购物端筛选入口
不改变已有商品销售状态"] + I --> M["返回最新分类列表"] + K --> M + L --> M + N --> M ``` 关键规则: -- 停用分类不再作为购物端筛选入口;新建或编辑商品时不允许把停用分类作为上架分类。 -- 分类层级、名称和排序由商家维护;存在商品或历史引用时禁止破坏性物理删除。 +- 分类只有启用和停用两种状态。停用分类不再作为购物端筛选入口,但不自动下架或隐藏其下已有已上架商品。 +- 新建商品不得绑定停用分类;已有商品切换分类时不得选择停用分类。停用分类下的既有商品可继续修改不改变分类归属的字段。 +- 停用分类下的商品一旦处于草稿或已下架,必须迁移到启用分类或重新启用原分类后才能上架。 +- 分类层级、名称和排序可以在校验通过后正常编辑;商品或历史引用只阻止物理删除,不能阻止分类元数据编辑。 - 分类名称、父级关系和启停状态必须校验;非法输入返回字段级错误并保留已填内容。 ## 四、商品创建与编辑 @@ -91,20 +96,23 @@ flowchart TD A["商家进入商品管理"] --> B{"选择操作?"} B -- "新建商品" --> C["填写名称、分类、价格、库存、主图/图片和描述"] B -- "编辑商品" --> D["按商品 ID 加载当前内容并保留已填字段"] - C --> E{"必填项、价格、库存、分类和图片合规?"} - D --> E - E -- "否" --> X["字段级错误,保留表单内容并标记失败字段"] - E -- "是" --> F{"并发标记或版本条件是否一致?"} + C --> NC{"必填项、价格、库存、启用分类和图片合规?"} + NC -- "否" --> X["字段级错误,保留表单内容并标记失败字段"] + NC -- "是" --> G["原子保存商品事实"] + D --> EC{"字段是否合法?
若切换分类,目标分类是否启用?"} + EC -- "否" --> X + EC -- "是" --> F{"编辑依据的并发标记是否仍有效?"} F -- "否" --> Y["返回冲突提示,不静默覆盖已生效修改"] - F -- "是" --> G["开启商品事务并保存商品事实"] + F -- "是" --> G G --> H{"事务提交成功?"} H -- "否" --> Z["整体回滚,提示保存失败并允许安全重试"] - H -- "是" --> I["提交后触发缓存失效;pg_trgm/GIN 由 PostgreSQL 事务内同步维护
返回最新商品事实"] + H -- "是" --> I["返回最新商品事实
通知 C07 失效目标详情和受影响的固定首页摘要"] ``` 商品字段与图片校验: -- 商品名称、有效分类、当前价格、库存和主图为必填;价格不得为负,库存必须为非负整数。 +- 商品名称、合法的分类关联、当前价格、库存和主图为必填;价格不得为负,库存必须为非负整数。新建、改绑分类和上架时分类必须已启用;既有商品的原分类后来停用时仍可修改不改变分类归属的字段。 +- 新建商品只能绑定启用分类;既有商品在停用分类下可以修改名称、价格、库存、图片和描述,但不能改绑另一个停用分类。 - 图片上传到 S3 Compatible Object Storage;单商品最多 8 张,单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 400~4096 像素,第一张作为主图。 - 图片上传失败时明确标记失败图片并允许重试,不清空其他表单字段。 - 编辑商品时使用并发标记或条件更新防止静默覆盖;冲突时保留已填写内容并提示刷新确认。 @@ -114,17 +122,19 @@ flowchart TD ```mermaid flowchart TD A["商家选择目标商品"] --> B{"选择操作?"} - B -- "上架" --> C{"完整性校验通过且分类已启用?"} - C -- "否" --> X["拒绝上架并指出缺失字段"] + B -- "上架" --> US{"当前销售状态?"} + US -- "已上架" --> U0["返回既有已上架结果
不重复产生状态变化"] + US -- "草稿或已下架" --> C{"完整性校验通过且分类已启用?"} + C -- "否" --> X["拒绝上架并指出缺失字段或停用分类"] C -- "是" --> D["事务内将商品状态置为已上架"] - B -- "下架" --> E["事务内将商品状态置为已下架
购物端列表与搜索不再返回该商品"] - B -- "删除" --> F{"是否存在历史订单关联?"} - F -- "是" --> Y["拒绝破坏性删除,建议改为下架"] - F -- "否" --> G{"当前是否已上架?"} - G -- "是" --> H["先执行下架,状态变为已下架"] - G -- "否" --> I["完成物理删除,进入终止结果"] - H --> I - D --> J["提交后触发缓存失效;pg_trgm/GIN 由 PostgreSQL 事务内同步维护,不发起独立同步任务"] + B -- "下架" --> DS{"当前销售状态?"} + DS -- "已上架" --> E["原子推进为已下架
购物端列表与搜索不再返回该商品"] + DS -- "已下架" --> R["返回既有已下架结果
不重复产生状态变化"] + DS -- "草稿" --> T["拒绝无意义的下架请求
保持草稿"] + B -- "删除" --> F{"当前是否为草稿或已下架
且不存在任何历史关联?"} + F -- "否" --> Y["拒绝物理删除
已上架则先下架,有历史关联则永久保留"] + F -- "是" --> I["确认后物理删除
商品实体不再存在"] + D --> J["提交后通知 C07 失效目标详情和受影响的固定首页摘要"] E --> J I --> J ``` @@ -133,7 +143,9 @@ flowchart TD - 下架不删除购物车记录、收藏记录、浏览记录或历史订单快照,由对应模块显示不可售状态。 - 下架商品的旧链接只能显示不可售状态,不提供购买入口;历史订单快照不受影响。 -- 已上架但库存为 0 的商品仍可展示详情,但必须标记售罄并禁用购买;是否允许继续“已上架 + 售罄”展示由本期业务口径决定。 +- 已上架但库存为 0 的商品继续出现在公开列表、搜索和详情中,明确标记售罄并禁用购买,不自动下架。 +- 所属分类停用不改变已有商品状态;已有已上架商品继续公开。只有后续重新上架时才要求分类已启用。 +- 物理删除只允许草稿或已下架且没有订单、购物车、收藏、浏览、评价、秒杀等任何历史关联的商品;已上架商品必须先完成下架。 ## 六、商品销售状态机与并发边界 @@ -141,17 +153,17 @@ flowchart TD stateDiagram-v2 [*] --> 草稿: 商家创建并保存 草稿 --> 已上架: 完整性校验通过并主动上架 - 草稿 --> 已删除: 无历史关联且确认删除 已上架 --> 已下架: 商家主动下架 已下架 --> 已上架: 重新校验通过并上架 - 已下架 --> 已删除: 无历史关联且确认删除 - 已删除 --> [*] + 草稿 --> [*]: 无任何历史关联且确认物理删除 + 已下架 --> [*]: 无任何历史关联且确认物理删除 ``` 并发与一致性: +- `草稿`、`已上架`、`已下架` 是仅有的商品销售状态;物理删除后商品实体不再存在,不保存“已删除”状态。 - 商品写操作使用并发标记(version/etag)或 `WHERE` 条件更新防止静默覆盖;冲突时返回明确提示,不覆盖已生效数据。 -- 事务提交后才允许触发缓存失效与搜索索引同步;事务失败时不发起任何外部动作。 +- 事务提交后才通知 C07 失效固定首页摘要和目标商品详情;事务失败时不发起失效动作。 - PostgreSQL 的 `pg_trgm`/GIN 数据库索引随商品数据同步维护,不通过异步处理器复制搜索索引。 - 缓存失效失败不回滚已正确提交的商品事务;由缓存处理器重试并记录可追踪错误。 @@ -185,9 +197,13 @@ flowchart TD |---|---|---| | 游客、买家或管理员访问后台写接口 | 拒绝 | 401/403,不返回后台数据 | | 字段、价格、库存或分类非法 | 拒绝保存 | 字段级错误,前端保留用户已填写内容 | +| 编辑存在商品引用的分类元数据 | 校验名称、层级和排序后允许保存 | 引用关系不阻止编辑 | +| 停用存在已上架商品的分类 | 停用分类筛选入口 | 已有已上架商品继续公开,不自动下架 | +| 停用分类下商品尝试重新上架 | 拒绝上架 | 迁移到启用分类或先重新启用原分类 | +| 已上架商品库存降为零 | 保持已上架并标记售罄 | 不提供加购、结算和购买 | | 图片上传失败 | 标记失败图片并允许重试 | 不清空其他表单字段 | | 两名操作人并发编辑 | 后提交者收到冲突提示 | 不静默覆盖已生效修改 | -| 删除存在历史订单的商品 | 拒绝物理删除 | 提示改为下架 | +| 删除存在订单、购物车、收藏、浏览、评价、秒杀等任何历史引用的商品 | 拒绝物理删除 | 提示改为下架并永久保留商品事实 | | 上架条件不完整 | 拒绝上架 | 指出缺失字段 | | 事务提交失败 | 数据回滚 | 页面显示保存失败,允许安全重试 | | 缓存失效失败 | 不回滚商品事务 | 由缓存处理器重试并记录可追踪错误 | @@ -200,18 +216,18 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 商家分页查询商品(全状态) | A120 后台商品列表 | 按商家身份过滤、分页、关键词、分类和上下架状态组合查询 | 待交叉评审 | +| 商家分页查询商品(全状态) | A120 后台商品列表 | 对所有正常商家返回同一经营目录,支持分页、关键词、分类和三种销售状态组合查询,不按操作人过滤商品归属 | 待交叉评审 | | 后台商品详情 | A121 后台商品详情 | 返回含 version 字段的全状态商品事实,供编辑并发校验 | 待交叉评审 | -| 新建商品 | A122 商品创建 | 校验字段、分类、图片和并发状态,事务内保存商品事实 | 待交叉评审 | -| 编辑商品 | A123 商品编辑 | 乐观并发保护、字段校验、事务保存并返回最新商品事实 | 待交叉评审 | -| 商品上架 | A125 商品上架 | 校验上架完整性;事务内将状态置为已上架 | 待交叉评审 | +| 新建商品 | A122 商品创建 | 校验字段、启用分类和图片,保存为草稿并返回商品事实 | 待交叉评审 | +| 编辑商品 | A123 商品编辑 | 允许停用分类下既有商品修改非分类字段;改绑分类只能选择启用分类;并发保护后保存并返回最新事实 | 待交叉评审 | +| 商品上架 | A125 商品上架 | 校验完整性和启用分类;从草稿或已下架变为已上架 | 待交叉评审 | | 商品下架 | A126 商品下架 | 事务内将状态置为已下架;购物端列表与搜索立即不再返回 | 待交叉评审 | -| 商品后台删除 | A124 商品删除 | 仅允许无历史关联时物理删除;存在历史订单时返回拒绝并建议下架 | 待交叉评审 | +| 商品后台删除 | A124 商品删除 | 仅允许草稿或已下架且无任何历史关联时物理删除;删除后实体不存在,不返回“已删除”状态 | 待交叉评审 | | 后台分类列表 | A110 后台分类列表 | 返回全状态分类,购物端只返回启用分类 | 待交叉评审 | | 新建分类 | A111 新建分类 | 校验名称、父级、排序和初始状态,事务内保存 | 待交叉评审 | -| 编辑分类 | A112 编辑分类 | 校验字段和依赖,事务内保存并返回最新分类 | 待交叉评审 | +| 编辑分类 | A112 编辑分类 | 校验名称、父级与排序后保存;商品或历史引用不能阻止元数据编辑 | 待交叉评审 | | 启用分类 | A113 启用分类 | 切换启停状态并校验依赖 | 待交叉评审 | -| 停用分类 | A114 停用分类 | 切换启停状态并校验依赖;停用后 A101/A102 不再以其作为筛选入口 | 待交叉评审 | +| 停用分类 | A114 停用分类 | 移出 A101 分类筛选入口,但不改变已有商品状态;A102 仍可在全部商品和关键词搜索中返回其下已上架商品 | 待交叉评审 | | 上传商品图片 | A127 商品图片上传 | 按合规要求上传到对象存储,返回主图与附加图标识 | 待交叉评审 | | 删除商品图片 | A128 商品图片删除 | 删除商品图片关联与对象存储对象,保持引用一致 | 待交叉评审 | @@ -219,7 +235,7 @@ flowchart TD ## 十、扩展接入边界 -- C07 缓存:商家端商品事务提交后由架构确定的可靠机制触发缓存失效;缓存不可用时不影响商品事务,由缓存处理器重试失效动作。 +- C07 缓存:商品事务提交后失效受影响的固定首页摘要和目标商品详情;分类、后台商品列表及用户控制的购物端普通列表和搜索本期不缓存。缓存不可用时不影响商品事务,由 C07 重试失效动作并以 TTL 约束旧首页摘要和详情窗口。 - C04 搜索:`pg_trgm`/GIN 由 PostgreSQL 事务内同步维护;本模块不建设独立的索引同步任务,事务回滚时索引同样回滚;进阶搜索暂时不可用时回退基础模糊查询。 - M03 购物车:商品下架、库存归零或被禁用后由购物车模块按 M03 规则标记失效,不反向写入商品状态。 - M04 订单:商品事务不修改历史订单的地址或商品快照;价格或上下架变更不影响已有订单。 @@ -228,11 +244,11 @@ flowchart TD ## 十一、由流程反查出的接口与数据待评审项 1. 商品并发保护方式尚未在需求中枚举(version/etag、`WHERE updated_at` 等),需要在接口设计前与 Ordering 的并发口径统一。 -2. 删除判断“是否存在历史订单关联”的查询口径需要明确按订单状态筛选还是全量包含已取消订单,避免商家误判。 -3. 停用分类下已上架商品的可见性:是否允许继续展示直到商家主动下架或编辑,需要在接口层明确返回字段。 +2. 商品物理删除必须按全量历史关联判断,不按订单状态排除已取消订单;订单、购物车、收藏、浏览、评价和秒杀等任何历史引用均阻止删除,后续由数据库设计落实约束。 +3. 停用分类下已有已上架商品继续公开已冻结;接口需要确保 A101 移除分类筛选入口时,A102/A103 不按分类启停状态额外隐藏商品。 4. 图片上传顺序和替换规则的接口字段(主图上传后是否自动替换旧主图)尚未定义,需在接口设计前与命名规范统一。 -5. 缓存失效失败的处理需要记录可追踪错误并由缓存处理器重试;接口响应不得返回缓存失效状态,避免商家误以为商品未上架。 -6. 搜索索引异常时的降级语义需要在接口和缓存层达成一致;商家端不感知底层使用哪种索引实现。 +5. 缓存失效失败的处理需要记录可追踪错误并由 C07 重试;商品写接口按商品事务结果响应,不能把缓存失效失败伪装成商品保存失败。 +6. 搜索索引异常时的降级语义需要在接口契约和 C04 搜索实现之间达成一致;搜索不接入 C07,商家端不感知底层使用哪种索引实现。 7. DBxxx 分类/商品表字段尚未形成可实施的完整定义,数据库字段、约束、索引和 GIN/pg_trgm 维护策略必须由数据库设计任务另行确认。 8. 商品图片上传接口与对象存储的兼容边界需要与系统架构设计同步,避免不同商家端入口使用不同的上传契约。 @@ -242,10 +258,12 @@ flowchart TD - [ ] F11:商家可完成分类维护,以及商品新增、查询、编辑、受约束删除和上下架;商品必填项、价格、库存、分类和图片校验在前后端均生效。 - [ ] F11:下架商品不会出现在购物端列表和搜索结果,旧链接不再允许购买;历史订单快照保持可读。 +- [ ] F11:所有正常商家账号维护同一经营目录;分类元数据编辑不受引用阻断,物理删除才检查全部历史引用。 +- [ ] F11:停用分类只移出筛选入口,已有已上架商品继续公开;库存为零的已上架商品保持可见并显示售罄。 - [ ] N04:游客、买家和管理员无法进入或调用商家写操作,后端返回正确的认证或授权结果。 - [ ] N02:并发编辑、图片失败、保存失败和删除受限时均有明确反馈,已填写内容不会无故丢失。 - [ ] 索引:商品变更随事务提交后,PostgreSQL `pg_trgm`/GIN 索引在事务内同步保持一致;事务回滚时索引同样回滚,本模块不建设独立索引同步任务。 -- [ ] 缓存:缓存失效失败不回滚商品事务,由缓存处理器重试并记录可追踪错误。 +- [ ] 缓存:只失效固定首页摘要和商品详情;分类、列表和搜索不缓存;失效失败不回滚商品事务,由 C07 重试并记录错误。 - [ ] 保存正常和异常操作的页面截图、并发冲突提示和图片失败标记证据。 - [ ] 答辩能够说明商家事务与缓存失效的边界,以及搜索索引同步维护的责任划分。 @@ -255,12 +273,12 @@ flowchart TD - [ ] 已写清基础 F、核心接入状态和回归结果;商家写操作不改变 M02 核心销售状态机。 - [ ] Mermaid 图内部包含直接上游模块输入(M01)和直接下游模块出口(M02、M03、M04、C07、C04)。 - [ ] 主流程、拒绝分支、失败分支和最终结果齐全;并发保护与缓存失效责任划分清晰。 -- [ ] 状态名称与需求规格说明书一致;没有新增核心商品状态。 +- [ ] 商品只有草稿、已上架、已下架三种销售状态;物理删除是实体不存在的终止结果,没有“已删除”状态。 - [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径、DTO、技术模式名称(如 Cache-Aside、pg_trgm/GIN)或 SQL 片段代替流程;没有自行发明接口编号、字段名或数据库状态码。 - [ ] 接口编号落在 Catalog/Review 范围 A101~A200(M06-01 占 A110~A128),不混用其他模块编号。 -- [ ] MerchantOnly 鉴权链路由 M01 提供,M06-01 不重复定义角色判断规则。 +- [ ] 商家身份、账号正常状态与旧凭据失效由 M01 提供,M06-01 不重复定义身份规则。 - [ ] 已对照根文档 3.7 校准后台角色与操作边界,与 M06-02/M06-03 边界一致。 升级到”待交叉评审”的条件:自检完成、主流程与异常分支齐全、并发与缓存责任划分清楚、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 -升级到”已确认”的条件:Identity 主责确认 MerchantOnly 鉴权链路,C07 主责确认缓存失效责任,M02 与 M03 主责确认下游事实回退口径,根文档 3.7 中对应追踪项成熟度同步更新。 \ No newline at end of file +升级到”已确认”的条件:Identity 主责确认商家身份链路,C07 主责确认缓存失效责任,M02 与 M03 主责确认下游事实回退口径,根文档 3.7 中对应追踪项成熟度同步更新。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" index dc7fbdd..b85b701 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" @@ -131,7 +131,7 @@ flowchart TD - 公开列表和详情必须返回活动、商品、主图、秒杀价、原价、开始 / 结束时间、权威剩余库存与已售数量;不能只返回“是否售罄”替代数量。 - 倒计时使用服务端给出的权威 UTC 时间口径。客户端本地计时只负责平滑展示,每次刷新都以服务端结果校正。 -- 商品名称、主图等静态信息可以缓存;剩余库存、已售数量和“已售罄”派生结果不得使用脱离数据库事实的缓存值。 +- 秒杀活动列表与详情响应中的商品名称、主图、活动状态、剩余库存、已售数量和“已售罄”派生结果全部从数据库事实生成,不读取 C07;只有用户另行进入普通商品详情时,才可使用 C07 的商品详情缓存。 - 轮询或推送每次只能发布刚从权威库存事实得到的结果;前端按结果先后顺序应用更新,旧结果不得覆盖新结果。 - 每次抢购完成或失败后,响应都要带回本次处理后的权威活动结果,页面在同一交互中更新。并发请求抢走最后库存时,未抢到的买家应立即看到剩余为 0 和“已售罄”,不能继续显示可抢。 - 无法取得权威库存时,页面展示“正在刷新”并暂时禁用提交,不得猜测为“有库存”或“已售罄”。 @@ -224,7 +224,7 @@ flowchart TD 流量与日志规则: - 秒杀入口与普通商品查询使用可独立配置的承载上限;超过上限的秒杀请求快速失败,不能拖垮普通列表与详情。 -- 队列可以削峰或传递通知,但“进入队列”不等于抢购成功;缓存可以服务静态活动信息,但不能决定库存与限购。 +- 队列可以削峰或传递通知,但“进入队列”不等于抢购成功;C07 只能服务固定首页商品摘要和商品详情,不能缓存秒杀活动事实,也不能决定库存与限购。 - 每个秒杀提交都记录买家、活动、商品、请求数量、结果类别、数据库影响结果、成功订单号和链路标识;不得记录 Token、密码或支付卡号。 - 对成功、售罄、超限、重复、未开始、已结束、已取消、过载和系统失败分别统计,压测报告记录吞吐与 P50/P95/P99。 @@ -236,7 +236,7 @@ flowchart TD - **M05 Payment / C08 回调**:秒杀订单沿用统一支付和幂等回写;支付成功与取消竞争由订单状态条件推进裁决。 - **C03 超时取消**:只触发 M04 公开取消入口,由订单来源决定回补普通库存还是原秒杀库存,不得直接改写 C01 数据。 - **M06-02 履约、M09 消息、M10 售后**:分别沿用发货、可靠消息和售后流程,不为秒杀建立第二套通道。 -- **C07 缓存**:可缓存商品与活动静态信息;权威剩余库存、已售数量和售罄结果不从缓存判定。 +- **C07 缓存**:只复用固定首页商品摘要和商品详情缓存;秒杀活动列表、活动状态、权威剩余库存、已售数量和售罄结果全部从数据库事实判定。 - **C10 多实例**:任一实例都可受理秒杀请求,最终结果只由共享数据库事务决定,不能依赖进程内状态。 ## 九、由流程派生的接口契约映射 -- Gitee From 71e4e32a830bb773054b77c6adc1664d8828b80b Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 20:42:32 +0800 Subject: [PATCH 096/118] =?UTF-8?q?docs(process):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=95=86=E5=93=81=E8=AF=84=E4=BB=B7=E6=B5=81=E7=A8=8B=EF=BC=9B?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E8=B5=84=E6=A0=BC=E9=87=8D=E6=A3=80=E5=92=8C?= =?UTF-8?q?=E5=85=AC=E5=BC=80=E8=AE=A1=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 13 +- ...04\344\273\267\346\265\201\347\250\213.md" | 118 ++++++++---------- 2 files changed, 60 insertions(+), 71 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index ae20fc1..3eb6f61 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -1,6 +1,6 @@ # 电子商城需求规格说明书 -> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.4 +> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.5 > > 文档状态:内容和格式已完成内部统一,待全组与指导教师评审后冻结 > 截止:第 1 周周三提交初稿 @@ -13,6 +13,7 @@ | v0.2 | 2026-07-24 | 罗皓晨 | 消解 C01 库存展示绝对一致与“不预占库存”的冲突,统一为数据库权威快照、旧结果防覆盖及并发失败后同一交互刷新 | | v0.3 | 2026-07-24 | 罗皓晨 | 冻结 F10 同步钱包与 C08 受控模拟回调的互斥入账边界,并明确回调同样受订单状态和支付截止时间约束 | | v0.4 | 2026-07-24 | 罗皓晨 | 冻结商品三态、分类停用、售罄展示与 C07 缓存范围,明确统一经营目录及商品列表、搜索不进入本期缓存 | +| v0.5 | 2026-07-24 | 罗皓晨 | 冻结 X01 评价提交入口、提交时资格重检和公开计分口径,明确评价数据不进入 C07 商品详情缓存 | ## 业务流程设计入口 @@ -1464,7 +1465,7 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 | M07-FR04 | 提交校验 | 服务端重新校验身份、订单项归属、订单状态、评分、文字、图片和重复提交 | | M07-FR05 | 幂等与唯一性 | 同一订单项只能形成一条评价;重复点击或重复请求不得新增多条记录 | | M07-FR06 | 公开列表 | 商品详情分页展示评分、文字、图片、评价时间和评价提交时形成的脱敏买家展示名快照,不返回手机号、邮箱等敏感信息 | -| M07-FR07 | 评分汇总 | 根据有效评价计算总数和平均分;新增评价后结果最终更新且可追踪 | +| M07-FR07 | 评分汇总 | 根据全部成功提交的评价计算总数和平均分;本期没有审核、隐藏或删除状态,新增评价提交成功后立即成为公开计分事实 | | M07-FR08 | 提交反馈 | 提交期间防止重复点击;成功后明确标记“已评价”,失败时保留文字和已上传状态 | | M07-FR09 | 内容安全 | 文字按纯文本或受控内容展示,图片经过类型和大小校验,不执行脚本或泄露存储凭据 | @@ -1473,7 +1474,7 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 1. 买家进入订单详情,系统为已完成且未评价的订单项显示“评价商品”入口。 2. 买家选择 1~5 分、填写文字并按需上传图片;页面显示输入与上传状态。 3. 买家提交后,服务端按当前登录用户重新校验订单项归属、完成状态和唯一性。 -4. 校验通过后保存评价及图片关联,返回成功;订单项变为已评价,重复操作被阻止。 +4. 校验通过后原子保存评价、脱敏展示名快照及图片关联并返回成功;“已评价”由 M07 的唯一评价事实派生,M04 订单和订单项仍保持 Completed,重复操作返回既有已评价结果。 5. 用户进入商品详情,可分页查看公开评价和更新后的评分汇总。 #### 5. 业务规则与权限 @@ -1481,6 +1482,8 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 - 评分只能为 1~5 的整数,评价必须关联真实商品和订单项。 - 只有订单项所属买家且订单状态为已完成时可以提交评价。 - 同一订单项只能评价一次,数据库唯一约束或等效机制必须作为最终保障。 +- 页面最初展示入口只代表当时的提示结果;正式提交时必须重新校验当前买家、订单项归属、订单已完成和尚未评价,不能沿用打开表单时的旧资格。 +- 本期评价没有待审核、已隐藏、已删除等状态;提交事务成功的评价全部公开并进入总数与平均分,任何角色都没有隐藏或修改入口。 - 图片为可选;每条评价最多 6 张,单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 200~4096 像素;前端提示与服务端校验必须一致。 - 评价公开展示时不得返回手机号、邮箱、内部用户标识等不必要的敏感信息。 - 公开展示名在提交评价时形成脱敏快照;用户以后修改资料不改变历史评价展示,也不得为评价列表逐条查询用户资料。 @@ -2111,7 +2114,7 @@ C07 使用 Redis 优化首页商品摘要和商品详情两个高频只读场景 | 编号 | 功能 | 详细要求 | |---|---|---| -| C07-FR01 | 缓存对象 | 本期只缓存固定首页商品摘要和商品详情;分类、普通商品列表、关键词搜索、组合筛选和秒杀活动均不缓存。缓存内容只包含接口返回所需且允许公开的数据,不缓存管理员字段、连接信息或用户敏感数据。 | +| C07-FR01 | 缓存对象 | 本期只缓存固定首页商品摘要和商品详情中的商品自身公开字段;分类、普通商品列表、关键词搜索、组合筛选、秒杀活动、M07 评价汇总和评价列表均不缓存。缓存内容不含管理员字段、连接信息或用户敏感数据。 | | C07-FR02 | Cache-Aside 读取 | 查询先读取 Redis;命中时返回缓存值,未命中时查询 PostgreSQL、生成与原接口一致的响应并写入有限 TTL 的缓存。 | | C07-FR03 | Key 隔离 | Key 必须包含环境、模块、资源类型、资源 ID 或稳定查询标识及必要版本信息,避免不同环境、不同查询条件和不同数据结构互相污染。 | | C07-FR04 | 写后失效 | 商品改价、库存调整、上下架、名称/图片/描述变更的数据库事务提交后,删除受影响的详情和首页缓存;事务回滚时不得提前删除并生成错误的新值。 | @@ -2474,7 +2477,7 @@ Electron 后续原则上复用 PC Web 原型,只补充窗口、桌面导航和 | F11 | M06-01 后台商品管理—顾欣月 | 商家分类管理、商品管理 | A110~A114、A120~A128 | 待测试计划登记 | 创建、图片顺序与同步索引边界已统一,待数据库、OpenAPI 与交叉评审 | | F12 | M06-02 后台订单管理—韦乾强 | 商家订单列表、详情和发货入口 | A305~A307 | 待测试计划登记 | Ordering 命名、状态与商家归属已统一,待数据库、OpenAPI 与交叉评审 | | F13 | M06-03 后台用户管理—唐宇昊 | 管理端用户管理 | A015~A017 | 待测试计划登记 | 禁用、启用和旧令牌失效语义已统一,待数据库、OpenAPI 与交叉评审 | -| X01 | M07 商品评价与晒图—顾欣月 | 订单评价入口、商品评价区 | A140~A144 | 待测试计划登记 | 单条评价读取与图片顺序已闭合,待数据库、OpenAPI 与交叉评审 | +| X01 | M07 商品评价与晒图—顾欣月 | 订单评价入口、商品评价区 | A140~A143;A144 无业务来源,接口整合时取消 | 待测试计划登记 | 订单入口、提交重检、唯一评价、公开计分和图片边界已统一,待数据库、OpenAPI 与交叉评审 | | X02 | M08 收藏与浏览历史—唐宇昊 | 商品收藏、收藏页、浏览历史页 | A018~A025 | 待测试计划登记 | 浏览记录写入与设置查询已闭合,待数据库、OpenAPI 与交叉评审 | | X03 | M09 站内消息—罗皓晨 | 买家/商家消息中心 | A501~A505 | 待测试计划登记 | HTTP、集成事件和接收人边界已统一,待数据库、OpenAPI 与来源模块交叉评审 | | X04 | M10 售后流程—张海洋 | 买家售后申请、商家售后审核 | A411~A417、A419、A432~A434;退款入账使用 Payment 应用契约 | 待测试计划登记 | A418/A431 为取消历史号,待数据库、OpenAPI 与交叉评审 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" index b2c904b..366ee7b 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" @@ -3,9 +3,9 @@ > 负责人:顾欣月 > 覆盖:M07、X01 > 基础核心流程:F06、F09;与根文档 [`业务流程设计.md`](../业务流程设计.md) 第 3.6 节中评价接入点保持一致 -> 直接协作:韦乾强(M04 订单)、唐宇昊(M01 身份)、顾欣月本人(M02 商品详情)、罗皓晨(C07 缓存协作) -> 文档状态:初稿,待顾欣月自审及 Ordering/Identity 交叉评审 -> 升级标记:在初稿基础上补强多订单项并发、图片上传状态机、脱敏快照生成时机、回归核心结果和验收对照 +> 直接协作:韦乾强(M04 订单)、唐宇昊(M01 身份)、顾欣月本人(M02 商品详情) +> 文档状态:完整定义,已完成统稿审计,待负责人及 Review/Ordering/Identity/Catalog 协作确认 +> 升级标记:已收敛订单入口、提交时资格重检、唯一评价、公开计分和图片业务边界;本轮阻断项已关闭 > 需求事实源:[需求规格说明书 M07](../../../01-需求文档/需求规格说明书.md) 的完整七节 ## 一、范围与事实来源 @@ -14,33 +14,31 @@ 本模块不包含追评、评价点赞、买家自删、匿名评价、商家回复或隐藏、自动内容审核和评价运营后台。 -本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。M07 评价接口编号落在 A140~A144 范围,与 M02 公开浏览 A101~A103、M06-01 商家写操作 A110~A128 同属 Catalog/Review 编号段(A101~A200);该编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。当前流程派生 A140~A143;旧 A144 没有业务入口,应在下游接口整合中取消。接口编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M07/X01 需求 | 完整定义 | 作为业务语义事实源 | -| 本文业务流程 | 初稿 | 先确认角色、状态、分支和模块出入口 | +| 本文业务流程 | 完整定义、待交叉评审 | 已确认角色、状态、分支和模块出入口 | | 评价接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | -| DBxxx 评价表 | 模板/占位 | 本文不发明表字段、状态码和索引 | +| DBxxx 评价表 | 模板/占位 | 本文不发明表字段、暂存结构、状态码和索引 | | F06 评价公开读取 | 完整定义 | 商品详情只读取,不在本模块内重复实现 | ## 二、模块直接出入口 ```mermaid flowchart LR - ID["M01 Identity
BuyerOnly 认证与账号状态"] -->|"BuyerOnly + 账号正常"| RV["M07 Review
本人已完成订单项的评价"] + ID["M01 Identity
已认证且账号正常的买家"] -->|"买家身份有效"| RV["M07 Review
本人已完成订单项的评价"] ORD["M04 Ordering
Completed 订单项与归属事实"] -->|"本人订单项归属与完成状态"| RV - DET["F06 商品详情"] -->|"读取公开评价与评分汇总"| RV - CACHE["C07 评价读取缓存"] -. "读取前缓存" .-> RV + DET["F06 商品详情"] -->|"只请求公开评价与评分汇总"| RV - RV -->|"本人评价记录"| ORD + RV -->|"是否已评价的派生结果"| ORD RV -->|"公开评价与评分汇总"| DET RV -->|"买家脱敏展示名快照"| DET - ID -->|"游客、商家或管理员"| X["403 或 401,拒绝提交评价"] + ID -->|"游客、商家、管理员或账号已禁用"| X["拒绝提交评价"] ORD -->|"订单未完成或不属于本人"| Y["拒绝提交,不泄露他人订单信息"] RV -->|"同一订单项已有评价"| Z["返回已评价结果,不新增重复记录"] - CACHE -->|"缓存不可用或命中失效"| W["回退 PostgreSQL 直读"] ``` 边界约束: @@ -51,7 +49,8 @@ flowchart LR - 公开展示名在提交评价时形成脱敏快照;用户以后修改资料不改变历史评价展示,也不得为评价列表逐条查询用户资料。 - **M07 不修改 M04 订单项状态**:"订单项是否已评价"由 M07 的唯一评价事实派生(按订单项 ID 关联查询得到是否已有评价记录);订单项本身的状态机只由 M04 维护,不允许 Review 越界修改 Ordering 的内部订单项状态。 - 商品详情只读取公开评价,不在本模块内实现评价提交。 -- 缓存只能放在公开评价读取路径之前;评价事务提交后由缓存主责统一失效,缓存不可用时回退数据库直读,不返回旧数据。 +- 本期没有评价审核、隐藏、删除、商家回复或管理员治理状态;成功提交的评价立即成为公开事实,并全部进入总数和平均分。 +- M07 公开评价、评分汇总和图片不进入 C07 商品详情缓存;每次公开读取都以 PostgreSQL 已提交评价事实为准。 - 扩展完成后回到的核心结果:F06 商品详情仍只读取公开评价,F09 Completed 订单项状态保持不变,订单快照和支付事实不被评价结果覆盖。 ## 三、评价提交主流程 @@ -64,14 +63,14 @@ flowchart TD C --> D["买家填写评分(1~5)、文字(1~500 字)并按需上传图片"] D --> E{"评分、文字和图片均合规?"} E -- "否" --> X1["字段级错误,保留已填内容"] - E -- "是" --> F["买家提交并附带防重复标识"] - F --> G{"同一订单项是否已存在评价记录?"} - G -- "是" --> Y["返回已评价结果,不新增重复记录"] - G -- "否" --> H["开启事务并保存评价及图片关联"] + E -- "是" --> F["买家正式提交评价"] + F --> G{"重新校验当前买家、订单项归属、Completed 状态
以及是否已有评价,是否全部通过?"} + G -- "已有评价" --> Y["返回已评价结果,不新增重复记录"] + G -- "身份、归属或状态无效" --> Y1["拒绝提交并返回可理解原因
不泄露他人订单信息"] + G -- "全部通过" --> H["原子保存评价、脱敏展示名快照和成功图片关联"] H --> I{"事务提交成功?"} I -- "否" --> Z["整体回滚,提示稍后重试并保留已填内容"] - I -- "是" --> J["评价记录落库,订单项状态由 M04 维护,不在 M07 中改动"] - J --> K["商品详情公开评价和评分汇总最终更新"] + I -- "是" --> J["评价立即公开并进入评分汇总
订单项状态仍由 M04 维护"] ``` 关键规则: @@ -80,6 +79,7 @@ flowchart TD - 图片合规:单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 200~4096 像素;逐张显示上传状态,失败图片可重试或移除。 - 评分只能为 1~5 的整数;评价必须关联真实商品和订单项。 - 只有订单项所属买家且订单状态为已完成时可以提交评价。 +- 打开表单时的资格只用于展示入口;正式提交时重新校验身份、归属、Completed 状态和唯一性。 - 提交期间防止重复点击;成功后明确标记“已评价”,失败时保留文字和已上传状态。 ## 四、公开评价与评分汇总 @@ -93,16 +93,13 @@ flowchart TD E --> F{"加载是否成功?"} F -- "否" --> G["展示加载失败提示并允许重试"] F -- "是" --> H["展示评分汇总(总数与平均分)和公开评价列表"] - H --> I{"当前身份?"} - I -- "游客" --> J["不显示提交入口,未登录请求被拒绝"] - I -- "买家" --> K["根据 M07 资格判断是否显示提交入口"] - I -- "商家或管理员" --> L["仅展示公开评价,不显示提交入口"] + H --> I["任何身份都只查看公开评价
商品详情不显示评价提交入口"] ``` 公开展示约束: - 评价展示名在提交评价时形成脱敏快照,不在列表页按评价逐条查询用户资料。 -- 评分汇总根据有效评价计算总数和平均分;新增评价后结果最终更新且可追踪。 +- 评分汇总根据全部成功提交的评价计算总数和平均分;新增评价事务成功后立即计入,不存在待审核或隐藏评价。 - 商品详情页面不暴露评价人手机号、邮箱或内部用户标识。 - 评价列表为空或加载失败时展示友好空状态或重试入口,不显示空白页。 @@ -111,50 +108,43 @@ flowchart TD ```mermaid stateDiagram-v2 [*] --> 未评价: F09 订单项进入 Completed - 未评价 --> 已评价: M07 评价事务提交成功 - 未评价 --> 未评价: 重复提交被拒绝 - 已评价 --> 已评价: 重复提交或重复点击被拦截 + 未评价 --> 已评价: M07 评价提交成功 + 已评价 --> 已评价: 重复提交返回既有已评价结果 ``` +`未评价`、`已评价` 是按订单项是否存在唯一评价事实派生的展示结果,不是 M04 订单项的新状态,也不存在待审核、隐藏或已删除等评价状态。 + 图片上传状态机: ```mermaid stateDiagram-v2 [*] --> 待上传: 买家选择本地图片 - 待上传 --> 上传中: 服务端校验并上传至对象存储 - 上传中 --> 已上传: 上传成功并返回对象键 + 待上传 --> 上传中: 校验并提交图片 + 上传中 --> 已上传: 图片通过校验并可随评价提交 上传中 --> 失败: 类型/大小/尺寸不合规或网络错误 失败 --> 上传中: 买家点击重试 失败 --> 已移除: 买家移除失败图片 已上传 --> 已移除: 买家在提交前移除图片 - 已上传 --> 已绑定: 评价事务提交成功,图片关联到评价 + 已上传 --> 已公开: 评价事务提交成功,图片随评价公开 已移除 --> [*] ``` +该图只定义用户可感知的图片选择、校验、重试、移除和随评价公开结果,不指定对象键、暂存表、清理任务或上传协议;这些实现细节由后续接口、数据库与对象存储设计承接。 + 并发与一致性: - 同一订单项重复评价或重复点击必须由数据库唯一约束或等效机制阻止,不能依赖前端去重。 - 同一订单项在两个浏览器同时提交时,仅一个事务成功,另一个由数据库唯一约束或等效机制返回已评价结果。 - 同一订单的多订单项并发提交评价:每个订单项独立判断资格与唯一性,互不影响;任一订单项评价事务失败不影响其他订单项。 - 评价事务与图片关联在同一受控事务内提交,任一写入失败时整体回滚,不留下"评价已存但图片缺失"的部分结果。 -- 评价公开读取最终以 PostgreSQL 为事实来源;缓存失效或读取失败时回退到数据库直读,不返回旧数据。 - -**暂存图片孤儿清理**: - -评价图片采用"上传即暂存"模式(A141 返回 `imageId` 与对象键)。买家在评价事务提交前可能放弃提交、关闭页面、评价事务失败或被防重复规则拦截,导致对象存储里出现未被评价引用的对象。这些对象不属于已完成评价事实,但持续占用对象存储容量,必须按以下边界清理: - -- A141 为非幂等上传;每次上传或重试都生成新的暂存图片和对象键,不复用先前上传的对象键。 -- A142 只关联当前买家通过 `imageIds` 引用且仍有效的暂存图片;评价与图片关联在同一受控事务内提交,事务失败时不产生已完成评价关联。 -- 用户提交前移除图片、放弃提交、评价事务失败或重复评价被拦截时,未被评价引用的暂存图片保留到清理窗口,由后台清理任务回收对象存储对象及对应元数据。 -- 清理任务只能处理超过保留窗口且仍未被评价引用的图片,不能删除已被评价关联的对象;具体持久化结构、识别字段和索引由数据库设计确认,本流程不预先指定独立暂存表。 -- 保留窗口、清理调度频率、失败重试上限和告警方式仍是待评审项;清理失败必须可追踪,不允许静默吞掉。 +- 评价公开读取始终以 PostgreSQL 已提交事实为准,不进入 C07 商品详情缓存。 ## 六、结果反馈与页面衔接 ```mermaid flowchart TD A["买家在订单详情完成评价提交"] --> B{"提交结果"} - B -- "成功" --> C["评价记录落库;商品详情评价列表与评分汇总最终更新"] + B -- "成功" --> C["评价记录落库并立即公开
商品详情评价列表与评分汇总按已提交事实更新"] B -- "已评价" --> D["提示已评价,不重复写入"] B -- "字段或图片不合规" --> E["字段级错误并保留可恢复的表单内容"] B -- "部分图片上传失败" --> F["标记失败项,允许重试或移除"] @@ -183,9 +173,6 @@ flowchart TD | 同一订单项重复评价或重复点击 | 返回已评价结果 | 不新增重复记录 | | 评分、文字或图片不合规 | 字段级错误 | 保留可恢复的表单内容 | | 部分图片上传失败 | 标记失败项 | 允许重试或移除 | -| 评价事务失败、用户放弃提交或防重复拦截 | 暂存图片保持未被评价引用 | 保留至暂存保留窗口后由清理任务删除对象存储对象 | -| 同一买家重新上传或重试 | 生成新的暂存图片与对象键 | 原未引用对象等待清理,不复用旧对象键 | -| 清理任务失败 | 记录重试次数与最后错误 | 超过重试上限由可观测性告警并保留记录,不静默吞掉 | | 提交时登录失效 | 引导重新登录 | 保留未提交内容,重新提交时执行完整资格校验 | | 评价事务失败 | 整体回滚 | 提示稍后重试 | | 评价列表为空或加载失败 | 友好空状态或重试入口 | 不显示空白页 | @@ -198,33 +185,31 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 上传评价图片(提交前暂存) | A141 评价图片上传 | 校验合规,每次生成新的暂存图片和对象键并返回 `imageId`;具体持久化结构由数据库设计确认 | 待交叉评审 | -| 提交商品评价(幂等) | A142 评价提交 | 校验身份、订单项归属、完成状态、唯一性、字段与图片并原子写入;在同一事务内关联有效的暂存图片 | 待交叉评审 | -| 查询订单项评价资格/结果 | A143 评价资格 | 按当前买家返回 Completed 订单项的可评价状态与已提交评价 | 待交叉评审 | -| 商品公开评价分页 + 评分汇总 | A140 公开评价 | 分页返回评分、文字、图片、时间和脱敏展示名,并返回总数与平均分汇总 | 待交叉评审 | -| 单条公开评价详情查询 | A144 评价详情 | 返回单条公开评价的完整字段,供评价详情或举报链路使用 | 待交叉评审 | +| 上传评价图片 | A141 评价图片上传 | 先校验当前买家与目标订单项当时具备评价资格,再校验数量、类型、大小和尺寸;上传结果仅归当前买家和目标订单项使用,A142 仍须重检最终资格;不在流程中预设对象键或暂存表结构 | 待交叉评审 | +| 提交商品评价 | A142 评价提交 | 正式提交时重新校验身份、订单项归属、Completed 状态、唯一性、字段与图片,并原子形成评价、脱敏展示名快照及图片关联 | 待交叉评审 | +| 查询订单项评价资格/结果 | A143 评价资格 | 仅为当前买家的订单详情返回可评价或已评价提示;该结果不替代 A142 提交时重检 | 待交叉评审 | +| 商品公开评价分页 + 评分汇总 | A140 公开评价 | 无需登录即可分页返回全部成功评价的评分、文字、图片、时间和脱敏展示名,并返回总数与平均分 | 待交叉评审 | -接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 +接口详细定义与实现必须承接上述流程结果。现有 A144“单条评价详情/举报链路”没有需求与流程入口,后续接口整合应取消该编号,不得反向新增评价详情页或举报流程。HTTP 状态码、请求字段和错误码不得反向写入业务图。 ## 九、扩展接入边界 - F06 商品详情:只读取 M07 公开评价与评分汇总,不在商品详情页内提交评价;评价提交入口由订单详情提供。 - F09 订单完成:评价入口只能从本人 Completed 订单项接入,不能由商品详情绕过。 -- M09 站内消息:评价成功落库属于已确认业务事实,通知发送由 M09 决定;本模块不直接发送通知。 -- C07 缓存:评价公开读取可使用缓存层加速;评价事务提交后由架构确定的可靠机制失效缓存,缓存不可用时回退数据库直读。 +- M09 站内消息:本期评价提交不产生站内消息,不新增买家、商家或管理员接收人。 +- C07 缓存:M07 评价汇总、公开列表和图片不进入本期缓存,商品详情组合展示时分别读取商品缓存结果和 PostgreSQL 评价事实。 - 商家回复、隐藏或点赞:本期不实现;后续如需扩展,必须先修订主需求和本文档的边界约束。 ## 十、由流程反查出的接口与数据待评审项 -1. 评价唯一约束需要确认是数据库唯一索引还是等效应用层机制,并在数据库设计中明确。 -2. 评价图片上传顺序、是否允许后续追加图片以及失败重试上限,需要在接口层确定,避免买家多次提交不同图片集。 -3. 评分汇总字段(平均分、总条数)的计算时机与缓存策略相关,需要与 C07 缓存主责人共同确认。 -4. 评价公开列表的脱敏展示名规则需要在接口和前端达成一致;脱敏快照生成时机是提交时还是读取时需要确认。 -5. 商品详情读取评价是否要求登录状态、是否区分登录与游客可见范围,需要在接口设计中明确。 -6. 评价事务失败的回滚语义需要与图片上传失败处理保持一致,避免“评价已存但图片缺失”的部分结果。 -7. DBxxx 评价表字段尚未形成可实施的完整定义,数据库字段、约束和索引必须由数据库设计任务另行确认。 -8. 评价与图片元数据的具体持久化结构、未引用图片识别字段和清理索引尚未确认,需在数据库设计中统一定义;本流程不预设独立暂存表。 -9. 暂存图片保留窗口、清理任务调度频率、清理重试上限和告警方式需要在数据库与运维设计中明确,避免对象存储无限增长或被激进清理误删。 +1. 同一订单项唯一评价必须具有并发下可执行的最终保障,具体唯一约束与字段由数据库设计承接,接口不得仅依赖前端防重复。 +2. A141 需要定义当前买家可引用的上传结果和失败重试反馈;评价提交成功后不允许追加、替换或删除图片,因为本期没有评价编辑能力。 +3. A140 的平均分与总数只基于全部已提交评价,直接从 PostgreSQL 评价事实计算或读取由数据库设计维护的等价汇总,不接入 C07。 +4. 脱敏展示名在 A142 成功提交时形成快照,A140 直接返回该快照;具体脱敏格式由接口与前端统一。 +5. A140 是公开读取,游客、买家、商家和管理员使用同一公开字段;A142、A143 才要求当前买家身份。 +6. 评价、脱敏展示名快照和成功图片关联必须形成一个完整业务结果,任一必要写入失败时不得留下可公开的部分评价。 +7. DBxxx 评价表及图片关联字段尚未形成可实施的完整定义,由后续数据库设计统一派生;流程不预设对象键、暂存表或清理调度。 +8. A144 缺少独立业务入口,应在接口整合中转为历史取消号;不得为保留旧编号而补造举报或评价详情需求。 ## 十一、验收证据清单 @@ -234,12 +219,13 @@ flowchart TD - [ ] X01:未购买、订单未完成、他人订单项、游客、商家和管理员提交评价均被服务端拒绝。 - [ ] X01:同一订单项重复提交不会产生第二条评价,连续点击也不会重复写入。 - [ ] X01:公开列表与评分汇总正确,不泄露买家敏感信息;商品详情页不开放绕过订单资格的评价提交入口。 +- [ ] X01:全部成功提交的评价立即公开并计入总数和平均分,本期没有审核、隐藏或删除状态。 - [ ] X01:图片不合规、网络失败和登录失效时反馈清楚,用户已输入内容可恢复。 -- [ ] N04:评价提交与图片关联在同一事务内提交,任一步失败整体回滚;评分与文字字段级错误不写入数据库。 +- [ ] N04:评价、脱敏展示名快照和图片关联形成完整原子结果,任一步失败不公开部分评价;评分与文字字段级错误不写入数据库。 - [ ] N02:评价列表为空或加载失败时展示友好空状态或重试入口,不显示空白页。 - [ ] 多订单项并发:同一订单的多个订单项独立评价互不影响;两个浏览器同时提交同一订单项时仅一个成功。 -- [ ] 孤儿图片:评价失败、用户放弃提交或防重复拦截产生的暂存图片在保留窗口后被清理任务删除;清理任务失败时记录重试次数与最后错误。 -- [ ] 答辩能够说明唯一约束或等效机制如何阻止重复评价、脱敏快照生成时机、评价事务与图片关联的原子性、暂存图片孤儿清理策略。 +- [ ] 缓存边界:评价汇总、公开列表和图片不进入 C07;商品详情读取评价时以 PostgreSQL 已提交事实为准。 +- [ ] 答辩能够说明唯一约束或等效机制如何阻止重复评价、提交时资格重检、脱敏快照生成时机和评价与图片关联的原子结果。 ## 十二、提交前自检与升级路径 @@ -250,7 +236,7 @@ flowchart TD - [ ] 状态名称与需求规格说明书一致;没有新增订单状态。 - [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径、DTO、技术模式名称或 SQL 片段代替流程;没有自行发明接口编号、字段名或数据库状态码。 - [ ] **M07 不修改 M04 订单项状态**:流程图与状态机中不出现”订单项变为已评价”,已评价事实由 Review 唯一评价记录派生;订单项状态机只由 Ordering 维护。 -- [ ] 接口编号落在 Review 范围 A140~A144,不混用其他模块编号。 +- [ ] 当前流程只派生 A140~A143;A144 无业务来源,应在接口整合时取消,不为编号补造需求。 - [ ] 已对照根文档 3.6 节中评价接入点校准入口位置,X01 不可绕过订单资格。 升级到”待交叉评审”的条件:自检完成、主流程与异常分支齐全、并发场景验证、订单项越界问题已修正、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 -- Gitee From e7e4fe019f2ba8d846ac359a0908f60188406472 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 20:47:46 +0800 Subject: [PATCH 097/118] =?UTF-8?q?docs(process):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=B8=AD=E6=96=87=E6=90=9C=E7=B4=A2=E6=B5=81=E7=A8=8B=EF=BC=9B?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=BC=BA=E5=88=B6=E8=BF=87=E6=BB=A4=E5=92=8C?= =?UTF-8?q?=E6=80=A7=E8=83=BD=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 19 +-- ...34\347\264\242\346\265\201\347\250\213.md" | 137 +++++++++--------- ...41\347\220\206\346\265\201\347\250\213.md" | 7 +- 3 files changed, 84 insertions(+), 79 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 3eb6f61..ec3808c 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -1,6 +1,6 @@ # 电子商城需求规格说明书 -> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.5 +> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.6 > > 文档状态:内容和格式已完成内部统一,待全组与指导教师评审后冻结 > 截止:第 1 周周三提交初稿 @@ -14,6 +14,7 @@ | v0.3 | 2026-07-24 | 罗皓晨 | 冻结 F10 同步钱包与 C08 受控模拟回调的互斥入账边界,并明确回调同样受订单状态和支付截止时间约束 | | v0.4 | 2026-07-24 | 罗皓晨 | 冻结商品三态、分类停用、售罄展示与 C07 缓存范围,明确统一经营目录及商品列表、搜索不进入本期缓存 | | v0.5 | 2026-07-24 | 罗皓晨 | 冻结 X01 评价提交入口、提交时资格重检和公开计分口径,明确评价数据不进入 C07 商品详情缓存 | +| v0.6 | 2026-07-24 | 罗皓晨 | 冻结 C04 关键词分流、统一过滤与降级口径,明确搜索不缓存且正式 60 秒性能采样不包含预热 | ## 业务流程设计入口 @@ -1242,7 +1243,7 @@ M04 提供订单金额、状态和支付截止时间,M09 接收支付结果通 | M06-01-FR08 | 删除约束 | 无历史关联时可按设计删除;已有订单关联时禁止破坏性删除并建议下架 | | M06-01-FR09 | 图片管理 | 图片上传到 S3 Compatible Object Storage;单商品最多 8 张,单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 400~4096 像素,第一张作为主图 | | M06-01-FR10 | 并发保护 | 编辑依据并发标记或条件更新防止静默覆盖;冲突时保留已填写内容并提示刷新确认 | -| M06-01-FR11 | 变更传播 | 商品事务提交后触发缓存失效;`pg_trgm`/GIN 数据库索引随 PostgreSQL 商品数据同步维护,不通过异步处理器复制搜索索引 | +| M06-01-FR11 | 变更传播 | 商品事务提交后触发对应首页摘要和详情缓存失效;商品创建、编辑、上下架、删除或关联分类名称修改时,检索文本、销售状态和数据库索引随同一 PostgreSQL 事实同步维护,不通过异步处理器复制搜索索引 | | M06-01-FR12 | 操作反馈 | 保存、上下架和删除均显示明确结果;危险操作需要确认,失败时保留可恢复的表单数据 | #### 4. 主流程 @@ -1960,7 +1961,7 @@ C04 在 M02-01 的基础搜索入口上增加中文分词模糊搜索、多条 | C04-FR02 | 多条件筛选 | 支持关键词与分类、价格区间、仅看有货和已上架条件组合使用 | | C04-FR03 | 排序 | 至少支持相关度及经过白名单约束的价格或时间排序;相同条件下顺序应稳定 | | C04-FR04 | 搜索适配器 | 应用层依赖统一的 `IProductSearch` 或等效契约,不直接依赖某个分词器或索引产品 | -| C04-FR05 | 索引更新 | 商品创建、编辑、上下架或删除成功后,数据库索引随商品数据同步更新,不建设独立索引同步任务 | +| C04-FR05 | 索引更新 | 商品创建、编辑、上下架、删除或关联分类名称修改成功后,商品检索文本、销售状态和数据库索引在同一 PostgreSQL 事实提交中同步更新,不建设独立索引同步任务 | | C04-FR06 | 结果一致性 | 搜索结果最终以商品当前状态为准,索引中的旧数据不得让下架商品重新公开 | | C04-FR07 | 降级处理 | 进阶搜索暂时不可用时,可在保证已上架过滤和参数安全的前提下回退基础模糊查询,并记录降级原因 | | C04-FR08 | 用户反馈 | 保留用户查询条件;加载、无结果、降级和失败状态提供简洁说明、清空条件或重试入口 | @@ -1970,8 +1971,8 @@ C04 在 M02-01 的基础搜索入口上增加中文分词模糊搜索、多条 #### 4. 主流程 1. 用户输入中文关键词,并按需选择分类、价格、库存和排序条件。 -2. 服务端校验输入后调用统一搜索契约,搜索实现完成分词、召回、筛选与排序。 -3. 返回结果前再次保证商品为已上架状态,并返回分页数据与当前筛选结果。 +2. 服务端先校验全部参数:有关键词时优先执行进阶中文搜索,进阶能力不可用时回退安全的基础模糊查询;无关键词时直接查询商品事实,不执行无意义的分词。 +3. 无论进阶、基础降级还是无关键词路径,都必须统一强制已上架过滤、组合筛选、稳定排序和分页后再返回。 4. 页面展示结果并保留查询状态;无结果时显示当前条件并允许清空或调整。 5. 商品发生变更后,数据库维护对应搜索索引;下一次查询使用最新商品状态和检索文本。 @@ -1985,12 +1986,12 @@ C04 在 M02-01 的基础搜索入口上增加中文分词模糊搜索、多条 #### 6. 异常与边界场景 -**性能对比要求:**使用不少于 10000 条商品数据、50 并发、持续 60 秒的固定场景,对进阶搜索和 `LIKE/ILIKE` 基线使用相同查询词与筛选条件。进阶搜索成功率不低于 99%,P95 不高于 500 ms,且应比基线降低至少 30%;同时核对结果正确性并保留原始数据。 +**性能对比要求:**使用不少于 10000 条商品数据、50 并发的固定场景,对进阶搜索和 `LIKE/ILIKE` 基线使用相同查询词与筛选条件。两种实现必须分别完成独立预热,预热请求不计入统计;随后各自持续 60 秒正式采样。进阶搜索成功率不低于 99%,P95 不高于 500 ms,且应比基线降低至少 30%;同时核对结果正确性并保留原始数据。 | 场景 | 处理 | |---|---| | 进阶搜索执行失败 | 记录错误并回退安全的基础模糊查询;无法保证正确性时明确提示稍后重试 | -| 索引缺失或损坏 | 停止使用进阶查询并提示维护,重建索引后恢复;任何情况下均过滤非公开商品 | +| 索引缺失或损坏 | 服务端记录内部维护日志并停止使用进阶查询;能够保证正确性时回退基础模糊查询,否则向用户显示通用重试提示,不暴露索引等内部细节 | | 特殊字符或超长关键词 | 参数校验与安全转义,不返回数据库内部错误 | | 条件无匹配 | 返回正常空集合,展示调整关键词或清空筛选入口 | | 高并发下响应变慢 | 通过压测记录瓶颈和资源参数,不以缓存掩盖错误结果 | @@ -1999,9 +2000,9 @@ C04 在 M02-01 的基础搜索入口上增加中文分词模糊搜索、多条 - 中文多词查询能够通过分词或等效方案获得合理结果,并能解释为什么命中。 - 关键词、分类、价格、库存和已上架条件可组合筛选,相关度、价格或时间排序结果正确且稳定。 -- 新建、修改、上下架商品后搜索结果立即遵守最新数据;重建索引前后均不会公开下架商品。 +- 新建、修改、上下架、删除商品或修改关联分类名称后,搜索结果立即遵守同一 PostgreSQL 已提交事实;进阶查询不可用时也不会公开下架商品。 - 进阶搜索异常时降级行为可观察、结果口径不越权,页面保留条件并提供明确反馈。 -- 在不少于 10000 条商品、50 并发、60 秒场景下完成与 `LIKE/ILIKE` 的可重复对比,达到既定成功率和 P95 目标。 +- 在不少于 10000 条商品、50 并发场景下,对两种实现分别预热后各完成 60 秒正式采样;正式统计不包含预热,并达到既定成功率和 P95 目标。 - 答辩能够说明 N-gram、倒排索引、同步更新、排序、降级和性能对比方法。 ### C06 实时消息推送 — 罗皓晨 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" index e713797..a50447e 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" @@ -3,9 +3,9 @@ > 负责人:顾欣月 > 覆盖:C04 > 基础核心流程:F04、F05、F06、M02-01;与根文档 [`业务流程设计.md`](../业务流程设计.md) 第 3.3 节中搜索接入点保持一致 -> 直接协作:顾欣月本人(M02-01 商品列表与搜索)、韦乾强(M04 订单)、罗皓晨(C07 缓存协作) -> 文档状态:初稿,待顾欣月自审及 Catalog/Ordering 交叉评审 -> 升级标记:在初稿基础上补强统一搜索契约、降级细节、性能压测口径、回归核心结果和验收对照 +> 直接协作:顾欣月本人(M02-01 商品列表与搜索)、韦乾强(M04 订单) +> 文档状态:完整定义,已完成统稿审计,待负责人及 Search/Catalog/Ordering 协作确认 +> 升级标记:已统一关键词分流、强制过滤、同步检索事实、降级与独立预热口径;本轮阻断项已关闭 > 需求事实源:[需求规格说明书 C04](../../../01-需求文档/需求规格说明书.md) 的完整七节 ## 一、范围与事实来源 @@ -14,12 +14,12 @@ C04 在 M02-01 的基础搜索入口上增加中文分词模糊搜索、多条 本期使用字符 N-gram 等效分词和 PostgreSQL 倒排索引完成中文模糊搜索,不引入独立搜索引擎。本期不实现个性化排序、搜索广告、热词榜、搜索审核、同义词词典或后台全状态商品检索。 -本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。C04 不新增独立 Axxx 接口编号,而是替换 M02-01 列表查询(A101~A103)的底层搜索实现,对外接口契约完全沿用 M02 的 A102 商品列表/搜索等编号。 +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。C04 不新增独立 Axxx 接口编号,只替换 M02-01 的 A102 商品列表/搜索底层实现;不改变 A101 分类或 A103 商品详情。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | C04 需求 | 完整定义 | 作为业务语义事实源 | -| 本文业务流程 | 初稿 | 先确认角色、状态、分支和模块出入口 | +| 本文业务流程 | 完整定义、待交叉评审 | 已确认角色、分支、降级和模块出入口 | | 搜索适配器接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | | DBxxx 索引维护 | 部分定义 | 本文不发明表字段、索引名和分词参数 | | 性能对比原始结果 | 缺失 | 本文登记对比场景与口径,不预填压测结论 | @@ -32,45 +32,44 @@ flowchart LR AD -->|"使用中文分词与倒排索引召回"| DB["PostgreSQL 商品事实"] AD -->|"返回口径一致的搜索结果"| UI BASE["M02-01 基础模糊查询"] -. "降级回退" .-> AD - CACHE["C07 性能缓存层"] -. "读取前缓存" .-> AD - M06["M06-01 商家商品事务"] -->|"商品事实与索引同步"| DB + M06["M06-01 商家商品或分类变更"] -->|"检索文本、销售状态与索引同一事实提交"| DB AD -->|"调用方读取最新已上架商品"| LIST["M02-01 列表与 F04 公开浏览"] AD -->|"商品 ID 与详情事实"| DET["F06 商品详情"] UI -->|"越权或参数非法"| X["字段级错误,保留查询条件"] - DB -->|"索引缺失或损坏"| Y["停止进阶查询并提示维护"] + DB -->|"索引缺失或损坏"| Y["记录内部日志并停止进阶查询"] AD -->|"进阶搜索执行失败"| Z["记录原因并回退基础模糊查询"] - CACHE -->|"缓存命中但索引缺失"| W["按降级处理,不复用旧进阶结果"] ``` 边界约束: - C04 替换 F05 基础模糊查询的底层实现;F04 列表与 F06 详情的业务口径、参数白名单和已上架过滤不变。 - 搜索关键词和筛选参数必须参数化处理,排序字段使用白名单,不得拼接不可信 SQL。 -- 公开搜索强制过滤草稿、下架和已删除商品;任何身份都不能通过请求参数绕过。 +- 公开搜索强制过滤草稿和已下架商品;物理删除后的商品已不存在,任何身份都不能通过请求参数恢复或绕过状态过滤。 +- 停用分类不再作为分类筛选项,但其下已有已上架商品仍可在未指定分类的普通列表和关键词搜索中命中。 - 商品数据与搜索索引由同一 PostgreSQL 实例维护,不存在独立搜索服务的异步数据副本。 - 进阶搜索暂时不可用时,可在保证已上架过滤和参数安全的前提下回退基础模糊查询,并记录降级原因。 -- 缓存命中但底层索引缺失时按降级处理,不复用旧的进阶结果;缓存写入与失效由 C07 主责统一约定。 +- C04、F05 关键词搜索和 A102 的用户控制查询不进入 C07;进阶搜索不可用时只能回退 PostgreSQL 基础模糊查询,不能返回缓存的旧搜索结果。 - 扩展完成后回到的核心结果:F04 公开列表仍只返回已上架商品,F05 关键词查询仍按统一搜索契约返回同口径结果,F06 详情与下单重读条件不变;进阶搜索失败不改变这些核心结果。 ## 三、中文搜索主流程 ```mermaid flowchart TD - A["用户输入中文关键词并按需选择分类、价格、库存和排序"] --> B["服务端校验输入并调用统一搜索能力"] - B --> C["搜索实现对关键词进行中文分词"] - C --> D["按分词结果召回候选商品"] - D --> E{"是否触发进阶查询?"} - E -- "是" --> F["基于倒排索引完成相关度排序"] - E -- "否" --> BAS["回退基础模糊查询"] - F --> G["应用多条件筛选:关键词、分类、价格区间、仅看有货、已上架"] - BAS --> G - G --> H{"结果是否需要再过滤已上架状态?"} - H -- "是" --> I["返回结果前再次保证商品为已上架"] - H -- "否" --> J["直接返回结果"] - I --> K["分页返回数据与当前筛选结果"] - J --> K + A["用户输入关键词并按需选择分类、价格、库存和排序"] --> B{"全部参数是否合法?"} + B -- "否" --> X["返回字段级错误并保留查询条件"] + B -- "是" --> C{"是否有非空关键词?"} + C -- "否" --> D["直接从商品事实取得候选集"] + C -- "是" --> E{"进阶中文搜索是否可用?"} + E -- "是" --> F["分词并按倒排索引召回候选商品"] + E -- "否" --> G["记录内部降级原因
执行安全的基础模糊查询"] + F --> H["按相关度形成候选顺序"] + D --> I["统一强制已上架过滤
应用启用分类、价格区间和仅看有货条件"] + G --> I + H --> I + I --> J["执行白名单排序及稳定次级排序"] + J --> K["按同一分页规则返回结果与总数"] K --> L{"是否有结果?"} L -- "否" --> M["展示空集合和当前条件,并允许清空或调整"] L -- "是" --> N["展示商品摘要并保留查询状态"] @@ -79,27 +78,27 @@ flowchart TD 关键规则: - 中文分词对商品名称、分类名称和商品描述进行字符 N-gram 等效分词,支持中文多词查询、部分匹配和可解释的模糊召回。 -- 多条件筛选支持关键词与分类、价格区间、仅看有货和已上架条件组合使用。 -- 排序至少支持相关度及经过白名单约束的价格或时间排序;相同条件下顺序应稳定。 +- 有关键词时优先使用进阶中文搜索;只有进阶能力不可用时才回退基础模糊查询。无关键词时不执行分词,直接进入统一筛选。 +- 无论进阶、基础降级还是无关键词路径,都必须强制已上架过滤,并应用分类、价格区间、仅看有货、白名单排序和分页,任何路径都不能跳过。 +- 分类筛选只接受启用分类;停用分类下的已有已上架商品仍可在未指定分类和关键词搜索中返回。 +- 排序至少支持相关度及经过白名单约束的价格或时间排序;相关度相同时按上架时间和商品 ID 稳定排序,价格或时间相同时按商品 ID 稳定排序。 - 结果一致性:搜索结果最终以商品当前状态为准,索引中的旧数据不得让下架商品重新公开。 ## 四、索引更新与一致性 ```mermaid flowchart TD - A["M06-01 商家商品事务提交"] --> B["商品事实写入 PostgreSQL"] - B --> C["搜索索引随商品数据同步更新"] - C --> D["下一次搜索查询使用最新商品状态和检索文本"] - D --> E{"下架商品是否仍出现在索引中?"} - E -- "是" --> F["结果过滤阶段强制按已上架状态过滤"] - E -- "否" --> G["直接返回最新结果"] - A -. "事务失败" .-> H["不更新索引,搜索结果保持原状"] + A["M06-01 保存商品或修改关联分类名称"] --> B["在同一 PostgreSQL 事实提交中更新
商品字段、检索文本、销售状态和数据库索引"] + B --> C{"完整提交是否成功?"} + C -- "否" --> X["整体失败,不产生部分新检索事实"] + C -- "是" --> D["下一次搜索使用同一已提交商品事实"] + D --> E["无论索引候选如何,最终仍强制已上架过滤"] ``` 索引约束: -- 商品创建、编辑、上下架或删除成功后,数据库索引随商品数据同步更新,不建设独立索引同步任务。 -- 索引缺失或损坏时停止使用进阶查询并提示维护,重建索引后恢复;任何情况下均过滤非公开商品。 +- 商品创建、编辑、上下架、删除或关联分类名称修改成功后,检索文本、销售状态和数据库索引随同一 PostgreSQL 事实同步提交,不建设独立索引同步任务。 +- 索引缺失或损坏时记录内部维护日志并停止进阶查询;能够保证正确性时回退基础模糊查询,否则向用户返回通用重试提示。任何情况下均过滤非公开商品。 - 进阶搜索异常时记录错误并回退安全的基础模糊查询;无法保证正确性时明确提示稍后重试。 ## 五、降级处理 @@ -107,35 +106,38 @@ flowchart TD ```mermaid flowchart TD A["搜索请求进入搜索实现"] --> B{"进阶搜索是否可用?"} - B -- "是" --> C["执行进阶查询并返回结果"] + B -- "是" --> C["执行进阶查询并得到候选集"] B -- "否" --> D["记录降级原因(索引缺失、查询失败等)"] D --> E{"基础模糊查询能否保证已上架过滤和参数安全?"} - E -- "是" --> F["执行基础模糊查询"] - E -- "否" --> G["返回明确提示稍后重试,不返回错误数据"] - F --> H["返回结果前再次过滤已上架商品"] - H --> I["分页返回数据并标注降级原因"] - C --> I + E -- "是" --> F["执行基础模糊查询并得到候选集"] + E -- "否" --> G["返回通用重试提示,不暴露索引或数据库内部细节"] + C --> H["统一强制已上架过滤、组合筛选、稳定排序和分页"] + F --> H + H --> I["返回同一结构的搜索结果
内部记录本次实际搜索路径"] ``` 降级约束: - 降级不是静默制造错误结果:系统需记录所用实现与原因,并保证核心过滤和权限不变。 -- 缓存命中但底层索引缺失时仍然按降级处理,不复用旧的进阶结果。 +- 搜索不读取 C07 或其他结果缓存;底层索引缺失时只能按上述路径回退事实源查询,不复用旧进阶结果。 - 降级日志需在可观测性中暴露(指标或事件),便于答辩与压测现场说明。 -- 进阶搜索长时间不可用且无法回退时,禁止返回陈旧的进阶结果或半成品数据,只能返回明确提示。 +- 索引缺失、查询失败等内部原因只进入日志与指标;用户只看到正常结果、搜索能力暂时受限的通用说明或重试提示。 +- 进阶搜索长时间不可用且无法回退时,禁止返回陈旧的进阶结果或半成品数据。 ## 六、性能对比要求 ```mermaid flowchart TD A["生成不少于 10000 条商品演示数据"] --> B["固定查询词、筛选条件和并发参数"] - B --> C["使用 50 并发持续 60 秒执行进阶搜索"] - C --> D["使用 50 并发持续 60 秒执行基础模糊查询基线"] - D --> E["汇总成功率、P95、QPS、CPU 和 I/O"] - E --> F{"进阶搜索对比基线是否满足目标?"} - F -- "是" --> G["记录原始数据、环境参数和命令"] - F -- "否" --> H["调整索引或查询实现并复测"] - G --> I["保留可重复执行脚本与原始结果文件"] + B --> C["单独预热进阶搜索
预热请求不计入统计"] + C --> D["使用 50 并发正式采样进阶搜索 60 秒"] + D --> E["单独预热基础模糊查询
预热请求不计入统计"] + E --> F["使用 50 并发正式采样基础模糊查询 60 秒"] + F --> G["汇总成功率、P95、QPS、CPU 和 I/O"] + G --> H{"进阶搜索对比基线是否满足目标?"} + H -- "是" --> I["记录原始数据、环境参数、预热和正式命令"] + H -- "否" --> J["调整索引或查询实现并重新执行完整对比"] + I --> K["保留可重复执行脚本与原始结果文件"] ``` 性能压测口径(按需求固化): @@ -144,7 +146,7 @@ flowchart TD |---|---|---|---| | 商品数据规模 | ≥ 10000 条 | ≥ 10000 条 | 数据集、查询词、筛选条件在两次对比中完全一致 | | 并发 | 50 | 50 | 同一压测工具与脚本 | -| 持续时间 | 60 秒 | 60 秒 | 包含预热与正式采样两段 | +| 持续时间 | 独立预热后正式采样 60 秒 | 独立预热后正式采样 60 秒 | 预热请求不进入正式成功率、延迟或吞吐统计 | | 成功率 | ≥ 99% | 记录基线 | 含 5xx 视为失败 | | P95 延迟 | ≤ 500 ms | 记录基线 | 与基线对比应降低至少 30% | | 结果正确性 | 进阶实现独立验证 | 基线 | 不要求 Top-N 排序完全一致;按已上架过滤、参数安全、预期相关结果集合和分页一致性核对 | @@ -175,7 +177,7 @@ flowchart TD B -- "有结果" --> C["展示商品摘要并保留查询状态"] B -- "无结果" --> D["展示空集合、当前条件和清空筛选入口"] B -- "加载失败" --> E["保留查询条件并显示简短原因,允许重试"] - B -- "降级" --> F["展示结果并提示当前使用基础模糊查询"] + B -- "降级" --> F["展示同结构结果;确有体验影响时仅提示搜索能力暂时受限"] B -- "进阶搜索不可用且无法回退" --> G["提示稍后重试并保留查询条件"] C --> H["用户继续翻页、调整条件或进入详情"] D --> H @@ -187,20 +189,21 @@ flowchart TD 用户反馈约束: - 用户反馈:保留用户查询条件;加载、无结果、降级和失败状态提供简洁说明、清空条件或重试入口。 -- 降级提示不应让用户误以为系统异常,只说明“搜索实现已切换”,不影响后续操作。 +- 索引、分词器和查询实现等内部原因只写日志;确有结果能力差异时,页面只提示“搜索能力暂时受限”,不暴露技术维护细节。 ## 八、异常、回滚与责任 | 场景 | C04 处理 | 最终状态/责任 | |---|---|---| | 进阶搜索执行失败 | 记录错误并回退基础模糊查询 | 不返回数据库内部错误 | -| 索引缺失或损坏 | 停止进阶查询并提示维护 | 重建索引后恢复 | +| 索引缺失或损坏 | 记录内部日志并停止进阶查询;能保证正确性则回退基础查询,否则返回通用重试提示 | 重建索引后恢复,不向用户暴露内部维护细节 | | 特殊字符或超长关键词 | 参数校验与安全转义 | 不返回数据库内部错误 | | 条件无匹配 | 返回正常空集合 | 展示调整关键词或清空筛选入口 | | 高并发下响应变慢 | 通过压测记录瓶颈和资源参数 | 不以缓存掩盖错误结果 | -| 商家修改商品后下架 | 数据库索引同步更新 | 过滤阶段强制按已上架状态过滤 | -| 商家删除商品 | 数据库索引同步删除 | 不让下架商品重新出现在结果中 | -| 缓存命中但索引缺失 | 按降级处理 | 不复用旧的进阶结果 | +| 商家修改商品、分类名称或销售状态 | 检索文本、销售状态与数据库索引随同一事实提交 | 下一次搜索读取同一已提交结果 | +| 商家物理删除商品 | 商品事实与数据库索引同步移除 | 不再返回已不存在商品 | +| 所选分类已停用 | 拒绝该分类筛选条件并提供返回全部商品入口 | 该分类下已上架商品仍可在未指定分类或关键词搜索中命中 | +| 索引缺失且基础查询可用 | 记录内部原因并回退基础模糊查询 | 仍统一执行已上架过滤、筛选、稳定排序与分页 | ## 九、由流程派生的接口契约映射 @@ -208,9 +211,9 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 中文分词模糊搜索 | 复用 M02-01 A102 商品列表/搜索接口 | 按统一搜索契约返回与基础模糊查询同口径的结果 | 待交叉评审 | +| 中文分词模糊搜索 | 仅复用 M02-01 A102 商品列表/搜索接口 | 有关键词时优先进阶搜索,不可用时回退基础模糊查询;无关键词不执行分词 | 待交叉评审 | | 多条件筛选与排序 | 复用 M02-01 A102 接口参数 | 关键词、分类、价格、库存和排序组合生效 | 待交叉评审 | -| 进阶搜索降级 | 由 M02-01 A102 接口返回结果 | 降级时返回结果与原因日志,不改变公开口径 | 待交叉评审 | +| 进阶搜索降级 | 由 M02-01 A102 接口返回同结构结果 | 内部记录原因;所有成功路径强制已上架过滤、组合筛选、稳定排序和分页,不暴露内部索引错误 | 待交叉评审 | | 性能对比压测 | 不通过业务接口暴露 | 保留测试脚本与原始结果 | 待交叉评审 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 @@ -221,7 +224,7 @@ flowchart TD - F05:基础模糊查询由 C04 进阶实现替换;返回字段、参数白名单和分页口径保持一致。 - M02-01:搜索适配器由 M02-01 内部实现替换,前端和接口契约不变。 - M06-01:商品事务提交后索引随 PostgreSQL 数据同步维护;不通过异步处理器复制搜索索引。 -- C07:缓存只能放在搜索查询路径之前;缓存命中但索引缺失时按降级处理,不复用旧的进阶结果。 +- C07:A102 的普通列表、关键词搜索和组合筛选不进入缓存;C04 只读取 PostgreSQL 商品事实。 - M04:下单时由 M04 重读商品事实进行条件扣减,不信任搜索结果中的价格或库存。 ## 十一、由流程反查出的接口与数据待评审项 @@ -229,11 +232,10 @@ flowchart TD 1. 字符 N-gram 的最小长度和最大长度参数需要在数据库设计中明确,避免过短导致误命中或过长导致索引过大。 2. 倒排索引的具体列(商品名称、分类名称、描述)需要在数据库设计中确认是否全列建立或部分建立;具体索引类型(pg_trgm/GIN 等)由数据库设计统一约定,本流程图与文字不重复枚举。 3. 相关度排序的”相同条件下顺序应稳定”需要明确次级排序字段,建议在数据库设计中统一。 -4. 进阶搜索降级是否需要在响应中携带降级原因字段,需要与前端展示要求对齐。 +4. A102 不返回索引缺失、查询计划等内部原因;如需表达能力受限,只返回与前端约定的通用提示标记,具体错误留在日志和指标。 5. 性能对比环境的固定参数(CPU、内存、PostgreSQL 配置、连接池大小)需要在执行前统一记录,避免环境差异影响结论。 -6. 缓存命中与索引缺失同时发生时是否需要主动清除缓存,避免长时间返回错误结果。 -7. 进阶搜索失败时的告警和可观测性要求,需要与 M00 公共基建的可观测性约束一致。 -8. DBxxx 索引维护语句与分词参数需要在数据库设计任务中给出可执行定义,不在本流程中预填。 +6. 进阶搜索失败时的告警和可观测性要求,需要与 M00 公共基建的可观测性约束一致。 +7. DBxxx 索引维护语句与分词参数需要在数据库设计任务中给出可执行定义,不在本流程中预填。 ## 十二、验收证据清单 @@ -243,7 +245,7 @@ flowchart TD - [ ] C04:关键词、分类、价格、库存和已上架条件可组合筛选,相关度、价格或时间排序结果正确且稳定。 - [ ] C04:新建、修改、上下架商品后搜索结果立即遵守最新数据;重建索引前后均不会公开下架商品。 - [ ] C04:进阶搜索异常时降级行为可观察、结果口径不越权,页面保留条件并提供明确反馈。 -- [ ] C04:在不少于 10000 条商品、50 并发、60 秒场景下完成与 `LIKE/ILIKE` 的可重复对比,达到既定成功率和 P95 目标。 +- [ ] C04:在不少于 10000 条商品、50 并发场景下,两种实现分别独立预热后各正式采样 60 秒;预热不进入统计,并达到既定成功率和 P95 目标。 - [ ] C04:保留数据生成方式、环境参数、执行命令、原始结果和汇总报告,使结果能够复现。 - [ ] 答辩能够说明 N-gram、倒排索引、同步更新、排序、降级和性能对比方法。 - [ ] 性能压测结果按第六章“结果回填位”填入真实数据,不留空。 @@ -252,14 +254,15 @@ flowchart TD - [ ] 图中的 F、X、C、M 编号与需求一致,与根文档 3.3 节中搜索接入点一致。 - [ ] 已写清基础 F、核心接入状态和回归结果;进阶搜索失败或降级不改变 F04~F06 核心结果。 -- [ ] Mermaid 图内部包含直接上游模块输入(M02-01、M06-01)和直接下游模块出口(F04、F06)。 +- [ ] Mermaid 图内部包含直接上游模块输入(M02-01、M06-01)和直接下游模块出口(F04、F06),未把 C07 接入搜索路径。 - [ ] 主流程、降级分支、索引更新与一致性、性能对比齐全。 - [ ] 状态名称与需求规格说明书一致;没有新增商品状态或排序项。 - [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径、DTO、技术模式名称(如 N-gram、pg_trgm/GIN、LIKE/ILIKE)或 SQL 片段代替流程;没有自行发明接口编号、字段名或数据库状态码。 - [ ] C04 不另起独立接口,复用 M02-01 A102 商品列表/搜索接口契约;进阶实现替换时参数白名单与返回口径保持一致。 +- [ ] 所有搜索路径统一执行已上架过滤、组合筛选、稳定排序和分页;停用分类只移除筛选入口,不隐藏其下已上架商品。 - [ ] 降级不是静默失败:日志或可观测性指标需暴露降级原因,不在前端构造虚假”全部成功”反馈。 - [ ] 已对照根文档 3.3 节中搜索接入点校准入口位置,C04 不另起一套主链路。 升级到”待交叉评审”的条件:自检完成、主流程与降级分支齐全、性能压测脚本可执行、第六章”结果回填位”保留、Mermaid 节点已剔除技术细节、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 -升级到”已确认”的条件:Catalog 主责确认统一搜索契约与参数白名单,Ordering 主责确认下单重读不受搜索实现影响,C07 主责确认缓存失效责任,根文档 3.3 节中搜索接入点对应追踪项成熟度同步更新;性能压测结果按口径填入第六章”结果回填位”。 \ No newline at end of file +升级到”已确认”的条件:Catalog 主责确认统一搜索契约与参数白名单,Ordering 主责确认下单重读不受搜索实现影响,根文档 3.3 节中搜索接入点对应追踪项成熟度同步更新;性能压测结果按口径填入第六章“结果回填位”。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" index eb7bb84..b1696ba 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -88,6 +88,7 @@ flowchart TD - 停用分类下的商品一旦处于草稿或已下架,必须迁移到启用分类或重新启用原分类后才能上架。 - 分类层级、名称和排序可以在校验通过后正常编辑;商品或历史引用只阻止物理删除,不能阻止分类元数据编辑。 - 分类名称、父级关系和启停状态必须校验;非法输入返回字段级错误并保留已填内容。 +- 分类名称修改成功时,关联商品用于 C04 的检索文本和数据库索引必须在同一 PostgreSQL 事实提交中同步反映新名称;不能先返回分类成功再等待独立索引任务。 ## 四、商品创建与编辑 @@ -225,7 +226,7 @@ flowchart TD | 商品后台删除 | A124 商品删除 | 仅允许草稿或已下架且无任何历史关联时物理删除;删除后实体不存在,不返回“已删除”状态 | 待交叉评审 | | 后台分类列表 | A110 后台分类列表 | 返回全状态分类,购物端只返回启用分类 | 待交叉评审 | | 新建分类 | A111 新建分类 | 校验名称、父级、排序和初始状态,事务内保存 | 待交叉评审 | -| 编辑分类 | A112 编辑分类 | 校验名称、父级与排序后保存;商品或历史引用不能阻止元数据编辑 | 待交叉评审 | +| 编辑分类 | A112 编辑分类 | 校验名称、父级与排序后保存;商品或历史引用不能阻止元数据编辑;分类名称变更与关联商品检索事实同步提交 | 待交叉评审 | | 启用分类 | A113 启用分类 | 切换启停状态并校验依赖 | 待交叉评审 | | 停用分类 | A114 停用分类 | 移出 A101 分类筛选入口,但不改变已有商品状态;A102 仍可在全部商品和关键词搜索中返回其下已上架商品 | 待交叉评审 | | 上传商品图片 | A127 商品图片上传 | 按合规要求上传到对象存储,返回主图与附加图标识 | 待交叉评审 | @@ -236,7 +237,7 @@ flowchart TD ## 十、扩展接入边界 - C07 缓存:商品事务提交后失效受影响的固定首页摘要和目标商品详情;分类、后台商品列表及用户控制的购物端普通列表和搜索本期不缓存。缓存不可用时不影响商品事务,由 C07 重试失效动作并以 TTL 约束旧首页摘要和详情窗口。 -- C04 搜索:`pg_trgm`/GIN 由 PostgreSQL 事务内同步维护;本模块不建设独立的索引同步任务,事务回滚时索引同样回滚;进阶搜索暂时不可用时回退基础模糊查询。 +- C04 搜索:商品创建、编辑、上下架、删除或关联分类名称修改时,检索文本、销售状态与 PostgreSQL 数据库索引同步维护;本模块不建设独立索引同步任务,事务回滚时检索事实同样回滚;进阶搜索不可用时由 C04 回退基础模糊查询。 - M03 购物车:商品下架、库存归零或被禁用后由购物车模块按 M03 规则标记失效,不反向写入商品状态。 - M04 订单:商品事务不修改历史订单的地址或商品快照;价格或上下架变更不影响已有订单。 - M01 Identity:本模块不修改账号、角色或令牌状态;账号禁用由 M06-03 独立流程处理。 @@ -262,7 +263,7 @@ flowchart TD - [ ] F11:停用分类只移出筛选入口,已有已上架商品继续公开;库存为零的已上架商品保持可见并显示售罄。 - [ ] N04:游客、买家和管理员无法进入或调用商家写操作,后端返回正确的认证或授权结果。 - [ ] N02:并发编辑、图片失败、保存失败和删除受限时均有明确反馈,已填写内容不会无故丢失。 -- [ ] 索引:商品变更随事务提交后,PostgreSQL `pg_trgm`/GIN 索引在事务内同步保持一致;事务回滚时索引同样回滚,本模块不建设独立索引同步任务。 +- [ ] 索引:商品变更或关联分类名称修改随同一 PostgreSQL 事实提交后,检索文本、销售状态和数据库索引保持一致;事务回滚时检索事实同样回滚,本模块不建设独立索引同步任务。 - [ ] 缓存:只失效固定首页摘要和商品详情;分类、列表和搜索不缓存;失效失败不回滚商品事务,由 C07 重试并记录错误。 - [ ] 保存正常和异常操作的页面截图、并发冲突提示和图片失败标记证据。 - [ ] 答辩能够说明商家事务与缓存失效的边界,以及搜索索引同步维护的责任划分。 -- Gitee From 4965477493888345f18c16496a8fc863ab79b680 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 21:03:08 +0800 Subject: [PATCH 098/118] =?UTF-8?q?docs(process):=20=E9=87=8D=E6=9E=84=20M?= =?UTF-8?q?01=20=E8=BA=AB=E4=BB=BD=E6=B5=81=E7=A8=8B=EF=BC=9B=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E6=B3=A8=E5=86=8C=E7=99=BB=E5=BD=95=E5=92=8C=E5=9C=B0?= =?UTF-8?q?=E5=9D=80=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 51 ++++--- ...50\345\206\214\346\265\201\347\250\213.md" | 85 ++++++----- ...00\345\207\272\346\265\201\347\250\213.md" | 99 +++++++------ ...60\345\235\200\346\265\201\347\250\213.md" | 134 +++++++++--------- 4 files changed, 202 insertions(+), 167 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index ec3808c..e9155e3 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -1,6 +1,6 @@ # 电子商城需求规格说明书 -> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.6 +> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.7 > > 文档状态:内容和格式已完成内部统一,待全组与指导教师评审后冻结 > 截止:第 1 周周三提交初稿 @@ -15,6 +15,7 @@ | v0.4 | 2026-07-24 | 罗皓晨 | 冻结商品三态、分类停用、售罄展示与 C07 缓存范围,明确统一经营目录及商品列表、搜索不进入本期缓存 | | v0.5 | 2026-07-24 | 罗皓晨 | 冻结 X01 评价提交入口、提交时资格重检和公开计分口径,明确评价数据不进入 C07 商品详情缓存 | | v0.6 | 2026-07-24 | 罗皓晨 | 冻结 C04 关键词分流、统一过滤与降级口径,明确搜索不缓存且正式 60 秒性能采样不包含预热 | +| v0.7 | 2026-07-24 | 罗皓晨 | 冻结 F01~F03 注册后登录、单一登录凭证、全部旧凭证失效、资料字段和地址默认切换边界 | ## 业务流程设计入口 @@ -271,7 +272,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | 身份 | 处理方式 | |---|---| -| 游客 | 可进入注册页并创建买家账号,成功后进入买家身份 | +| 游客 | 可进入注册页并创建买家账号;成功后仍为未登录状态,需显式进入登录流程 | | 买家 | 已登录时不重复展示注册入口,可返回个人中心 | | 商家 | 不允许通过公开注册接口创建或改变商家账号 | | 管理员 | 不允许通过公开注册接口创建管理员账号 | @@ -288,21 +289,23 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | M01-01-FR04 | 密码保护 | 系统必须安全保存用户凭据;明文密码和确认密码不得被保存、返回或记录 | | M01-01-FR05 | 用户名生成 | 自动生成以 `u_` 开头、后接 8 位不易混淆字符的全局唯一用户名;发生冲突时安全重试,最终不能产生重复账号 | | M01-01-FR06 | 默认资料 | 创建正常状态买家账号并设置统一默认头像 | -| M01-01-FR07 | 角色固定 | 忽略或拒绝客户端传入的角色,公开注册结果始终为买家 | -| M01-01-FR08 | 操作反馈 | 提交期间防止重复点击;成功后展示用户名和下一步入口,失败时保留非敏感输入 | +| M01-01-FR07 | 角色固定 | 注册请求不接受角色字段;客户端传入角色时拒绝整次请求,任何成功注册结果始终为买家 | +| M01-01-FR08 | 操作反馈 | 提交期间防止重复点击;成功后展示用户名和明确登录入口,不自动建立登录态;失败时保留非敏感输入 | #### 4. 主流程 1. 游客填写手机号、密码和确认密码并提交。 -2. 服务端校验字段格式、密码强度和手机号唯一性。 -3. 系统生成唯一用户名,对密码执行可靠哈希并创建正常状态买家账号。 -4. 系统设置默认头像并返回不含敏感信息的账号摘要。 -5. 页面提示注册成功,展示用户名并引导登录或按确认方案建立登录态。 +2. 服务端校验手机号、基础密码强度、确认密码和手机号唯一性。 +3. 系统生成唯一用户名,并继续校验密码不得等于该用户名。 +4. 系统对密码执行可靠哈希,在同一注册事务中创建正常状态买家账号、唯一用户名和默认头像。 +5. 页面提示注册成功并展示用户名;用户显式进入登录流程后再建立登录态。 #### 5. 业务规则与权限 - 手机号作为账号标识和登录凭据,用户名仅用于页面展示;个人中心和管理端默认显示掩码手机号,例如 `138****8888`。 - 公开注册只创建买家账号,客户端不得指定或修改角色。 +- 已登录买家不重复进入注册流程,直接返回个人中心;已登录商家或管理员不能通过公开注册入口再创建买家账号。 +- 注册成功只返回账号摘要,不签发登录凭证;登录态只能由 M01-02 在用户重新提交手机号和密码后建立。 - 密码原文、完整认证凭据和内部异常不得出现在响应或日志中。 - 手机号和用户名必须保持全局唯一,并发注册不能产生重复账号。 - 用户名生成规则、手机号格式和密码强度以本需求规格为准;后续接口设计与各客户端必须保持一致,不得自行放宽。 @@ -315,7 +318,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | 手机号已注册 | 拒绝重复注册,不暴露其他账户资料 | | 两个请求同时注册同一手机号 | 仅一个成功,另一个返回明确的重复注册提示 | | 用户名生成冲突 | 在受控次数内重新生成;仍冲突时不创建账号并提示“系统繁忙,请稍后重试” | -| 请求注入商家或管理员角色 | 忽略或拒绝角色字段,绝不提升权限 | +| 请求注入任何角色字段 | 拒绝整次注册请求,绝不创建账号或提升权限 | | 网络重试或重复点击 | 不产生重复账号,页面给出确定结果 | #### 7. 验收标准与证据 @@ -329,7 +332,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, #### 1. 功能目标与范围 -用户使用手机号和密码登录,系统签发可由任一 API 实例验证的 JWT;刷新页面后能够恢复有效登录态,退出后当前令牌立即失效。本期使用统一登录入口,不提供用户名登录、第三方登录或找回密码流程。 +用户使用手机号和密码登录,系统签发可由任一 API 实例验证的单一 JWT 登录凭证;刷新页面后能够恢复有效登录态,退出后当前令牌立即失效。本期使用统一登录入口,不提供刷新凭证、用户名登录、第三方登录或找回密码流程。 #### 2. 身份处理与协作边界 @@ -349,10 +352,10 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | M01-02-FR01 | 登录表单 | 只接收手机号和密码,并提供必要格式校验 | | M01-02-FR02 | 凭据校验 | 通过手机号定位账号并安全验证密码;账号不存在或密码错误统一返回“账号或密码错误” | | M01-02-FR03 | 状态校验 | 禁用账号不得登录;错误文案应说明账号停用但不暴露内部信息 | -| M01-02-FR04 | 登录凭证 | 登录成功后签发有明确有效期的登录凭证,并保证多实例访问结果一致 | +| M01-02-FR04 | 登录凭证 | 登录成功后只签发一个有明确有效期的 JWT 登录凭证,不提供刷新凭证,并保证多实例访问结果一致 | | M01-02-FR05 | 登录态恢复 | 页面刷新后根据有效令牌恢复账号摘要和正确路由 | | M01-02-FR06 | 角色路由 | 登录成功后按服务端确认的角色进入购物端、商家端或管理端 | -| M01-02-FR07 | 安全退出 | 清理前端登录态,并使当前令牌在自然过期前不可继续使用;退出只影响当前令牌 | +| M01-02-FR07 | 安全退出 | 前端立即停止发起受保护请求并清理本地凭证,服务端使当前令牌在自然过期前不可继续使用;退出只影响当前令牌,撤销结果无法确认时不得显示退出成功 | | M01-02-FR08 | 用户反馈 | 登录、恢复和退出过程均有明确状态,重复点击不得产生混乱结果 | #### 4. 主流程 @@ -361,7 +364,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, 2. 服务端校验格式、账号状态和密码哈希。 3. 校验成功后签发 JWT,返回账号摘要和角色。 4. 前端保存登录态,按角色进入对应端并展示用户名与头像。 -5. 用户刷新页面时验证令牌并恢复状态;退出时清理本地状态并登记当前令牌失效。 +5. 用户刷新页面时验证令牌并恢复状态;退出时前端立即停止受保护请求、清理本地凭证,并由服务端登记当前令牌失效。 #### 5. 业务规则与权限 @@ -369,8 +372,11 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, - 被禁用账号返回明确停用提示,并禁止签发新令牌。 - 401 表示未认证或登录已失效,403 表示身份有效但无权执行当前操作。 - 退出只影响当前令牌;多设备退出范围如需扩大,必须另行确认并写入接口设计。 +- 手机号修改或账号禁用会使该账号此前签发的全部登录凭证失效;重新启用账号只允许重新登录,不恢复任何旧凭证。 +- 本期不提供刷新凭证;登录凭证到期、退出或失效后必须重新提交手机号和密码登录。 - 系统无法确认登录凭证是否已经失效时不得继续放行,必须明确提示服务暂不可用。 - 前端路由不能代替后端授权,角色和资源归属均以服务端为准。 +- 本期交付入口仅覆盖 PC Web;Electron 和 Android 仅作为后续客户端规划,不属于本流程当前验收范围。 #### 6. 异常与边界场景 @@ -378,7 +384,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, |---|---| | 手机号或密码错误 | 返回统一错误,不说明具体错误字段 | | 账号被禁用 | 拒绝登录并提示联系管理员 | -| 登录凭证过期、无效或已退出 | 清理失效登录态并引导重新登录 | +| 登录凭证过期、无效、已退出或属于修改手机号/禁用账号前签发 | 清理失效登录态并引导重新登录 | | 买家访问商家或管理接口 | 返回 403,不泄露目标资源内容 | | 商家从购物端入口登录 | 登录成功后按角色跳转商家端,不误报密码错误 | | 登录凭证失效能力暂时不可用 | 无法确认登录态的受保护请求提示服务暂不可用;公开且不依赖认证的能力不受影响 | @@ -395,7 +401,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, #### 1. 功能目标与范围 -买家可以查看并维护个人资料以及收货地址,清楚知道修改内容和生效影响。范围包括用户名、手机号展示与变更规则、默认头像,以及地址新增、查询、编辑、删除和默认地址切换;本期不开放自定义头像上传和商家资料维护。 +买家可以查看并维护本人账号资料以及收货地址,清楚知道修改内容和生效影响。资料范围仅包括系统生成用户名、掩码手机号和默认头像;可变更动作仅包括一次用户名重置和手机号修改。地址范围包括新增、查询、编辑、删除和独立的默认地址切换;本期不开放展示名、简介、自定义头像上传和商家资料维护。 #### 2. 身份处理与协作边界 @@ -416,7 +422,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | M01-03-FR02 | 用户名修改 | 本项目期内允许用户自助重置一次;重置时由服务端按 M01-01-FR05 重新生成唯一用户名,不接受客户端指定任意用户名 | | M01-03-FR03 | 手机号修改 | 要求重新验证当前密码,新手机号遵守 M01-01-FR02 格式与唯一性;修改成功后立即使该用户修改前签发的全部令牌失效并要求重新登录 | | M01-03-FR04 | 地址列表 | 查询本人地址,清楚标记默认地址 | -| M01-03-FR05 | 新增地址 | 录入收件人、手机号、省市区和详细地址等必填信息 | +| M01-03-FR05 | 新增地址 | 录入收件人、手机号、省市区和详细地址等必填信息;新地址固定为非默认地址,如需设为默认必须另行执行默认地址切换 | | M01-03-FR06 | 编辑地址 | 只能修改本人地址,并重新校验全部字段 | | M01-03-FR07 | 删除地址 | 删除本人地址;删除默认地址时不静默指定其他地址 | | M01-03-FR08 | 默认地址 | 同一买家最多一个默认地址,切换操作保持唯一性 | @@ -433,11 +439,15 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, #### 5. 业务规则与权限 - 用户名只用于展示,全站唯一;每位买家在本项目期内最多自助重置一次,成功后页面立即刷新为新用户名。 +- 用户名重置机会只在新用户名成功提交后消耗;两个并发重置请求最多一个成功,失败请求不得额外消耗次数。 - 手机号变更属于敏感操作,必须重新验证当前密码;成功后撤销修改前签发的全部令牌并要求重新登录。 +- 两个基于同一旧手机号状态的并发修改请求最多一个成功;后到请求必须按资料已变化处理,不能覆盖先成功结果。 - 地址必须属于当前买家;收件人、联系电话、省市区和详细地址为必填。 +- 新增地址不接受默认标记;设置默认地址必须通过独立动作完成。本期不设置单买家地址数量上限。 - 每名买家最多一个默认地址,默认地址切换需原子完成或提供等效一致性保障。 - 删除默认地址后不自动选择其他地址,下单时由买家明确确认。 - 所有资料和地址接口按当前用户过滤,不能仅凭资源 ID 访问。 +- M01-03 负责校验地址归属并返回当前地址数据;M04 负责在创建订单的同一事务中保存订单地址快照,M01-03 不承担订单事务。 #### 6. 异常与边界场景 @@ -447,6 +457,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | 商家或管理员调用买家接口 | 返回 403,不返回买家字段 | | 用户名重复或修改次数已用完 | 拒绝修改并显示可理解说明 | | 手机号校验失败或已被使用 | 不变更账号,保留当前有效登录信息 | +| 并发重置用户名或修改手机号 | 最多一个请求成功;其他请求读取最新资料后明确返回冲突,不覆盖成功结果 | | 地址不存在或属于他人 | 返回资源不存在或无权限,不泄露归属信息 | | 删除当前默认地址 | 删除后明确提示需要重新选择默认地址 | | 并发设置多个默认地址 | 最终只保留一个默认地址 | @@ -454,7 +465,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, #### 7. 验收标准与证据 - 买家可以查看和修改允许变更的个人资料,并完成地址增删改查。 -- 用户名唯一和自助修改次数限制生效;手机号变更后旧登录态不能继续执行敏感操作。 +- 用户名唯一和自助修改次数限制生效;手机号变更后此前签发的全部登录凭证不能再访问任何受保护能力。 - 默认地址始终最多一个,删除默认地址后下单流程不会擅自选择其他地址。 - 买家不能访问他人地址,游客、商家和管理员不能越权调用买家资料接口。 - 保存资料修改、地址 CRUD、默认地址切换和越权拦截证据,并能解释敏感变更与资源归属规则。 @@ -2465,9 +2476,9 @@ Electron 后续原则上复用 PC Web 原型,只补充窗口、桌面导航和 | 教师编号 | 模块与负责人 | 页面或操作入口 | 接口契约(Axxx) | 测试用例 | 当前状态 | |---|---|---|---|---|---| -| F01 | M01-01 用户注册—唐宇昊 | 注册页 | A001 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | -| F02 | M01-02 登录与退出—唐宇昊 | 统一登录页、全端退出入口 | A002~A005 | 待测试计划登记 | 接口草案已汇总,待 OpenAPI 与交叉评审 | -| F03 | M01-03 个人信息与地址—唐宇昊 | 买家个人中心、地址管理 | A006~A014 | 待测试计划登记 | 已统一为买家专属,待 OpenAPI 与交叉评审 | +| F01 | M01-01 用户注册—唐宇昊 | PC Web 注册页 | A001 | 待测试计划登记 | 注册后显式登录、角色拒绝和原子账号创建已统一,待接口同步 | +| F02 | M01-02 登录与退出—唐宇昊 | PC Web 统一登录页、退出入口 | A002~A004;A005 无业务来源,接口整合时取消 | 待测试计划登记 | 单一 JWT、退出和全部旧凭证失效已统一,待接口同步 | +| F03 | M01-03 个人信息与地址—唐宇昊 | PC Web 买家个人中心、地址管理 | A006~A008、A010~A014;A009 无业务来源,接口整合时取消 | 待测试计划登记 | 资料字段、并发修改和独立默认地址切换已统一,待接口同步 | | F04 | M02-01 商品列表与分类—顾欣月 | 购物端商品列表、分类入口 | A101、A102 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | | F05 | M02-01 商品搜索—顾欣月 | 购物端搜索框、商品列表 | A102 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | | F06 | M02-02 商品详情—顾欣月 | 商品详情页 | A103 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" index 4686363..de42b59 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" @@ -3,10 +3,10 @@ > - 覆盖:M01-01、F01 > - 主责人:唐宇昊 > - 需求来源:[《需求规格说明书》M01-01](../../../01-需求文档/需求规格说明书.md) 的“M01-01 用户注册(F01)— 唐宇昊”完整七节 -> - 基础核心流程:F02(账号体系入口);为 M03、M04、M05、M06、M08、M09 提供“正常”买家账号前提 +> - 基础核心流程:F01;注册成功后显式进入 F02,并为 M03、M04、M05、M06、M07、M08、M09 提供“正常”买家账号前提 > - 直接入口:游客在公共注册入口提交手机号、密码和确认密码 > - 直接出口:M01 创建“正常”买家账号,引导进入 F02 登录流程;账号事实后续供 M03、M04、M05、M06、M07、M08、M09 复用 -> - 回归核心结果:F02 已签发登录态保持有效;F03 已维护资料与地址不变;F13 账号治理结果不变 +> - 回归核心结果:注册不建立或改变 F02 登录态;F03 已维护资料与地址不变;F13 账号治理结果不变 > - 不得改变:管理员和商家账号必须由初始化或受控流程创建;公开注册流程不能创建管理员或商家账号 ## 一、范围与事实来源 @@ -18,7 +18,7 @@ A001 由本流程派生,仅在流程评审通过后用于契约映射;现有 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M01-01/F01 需求 | 完整定义 | 作为业务语义事实源 | -| 本文业务流程 | 初稿 | 先确认角色、判断、状态、事务边界和模块出入口 | +| 本文业务流程 | 完整定义 | 已确认角色、判断、状态、事务边界和模块出入口 | | A001 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | | DBxxx 用户表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | | M00 公共认证能力 | 内部 P0 | 只登记接入点,规则由 M00 维护 | @@ -27,17 +27,20 @@ A001 由本流程派生,仅在流程评审通过后用于契约映射;现有 ```mermaid flowchart LR - VIS["游客"] -->|"公共注册入口"| REG["M01-01 Identity
注册事务"] + VIS["未登录游客"] -->|"公共注册入口"| REG["M01-01 Identity
注册事务"] REG -->|"正常买家账号 + 默认资料"| F02["M01-02 登录流程"] REG -->|"账号摘要"| UI["注册成功页"] REG -. "失败字段级提示" .-> FAIL["保留非敏感输入并提示原因"] - VIS -. "尝试指定商家或管理员" .-> REJ["忽略或拒绝角色字段"] + VIS -. "尝试传入角色字段" .-> REJ["拒绝整次注册请求"] VIS -. "重复手机号或非法输入" .-> FAIL + BUYER["已登录买家"] --> PC["返回个人中心"] + STAFF["已登录商家或管理员"] --> BLOCK["拒绝公开注册动作"] ``` 边界约束: - 公开注册流程只能产出买家账号;管理员和商家账号由初始化或另行确认的受控流程提供。 +- 公开注册只接受未登录游客;已登录买家返回个人中心,已登录商家或管理员不得通过该入口创建买家账号。 - 公开注册不与任何受保护业务共享事务边界;账号创建成功后由 F02 独立负责登录态。 - 默认资料由系统生成,不允许用户上传头像或指定用户名。 @@ -45,19 +48,23 @@ flowchart LR ```mermaid flowchart TD - A["游客进入公开注册页"] --> B["填写手机号、密码和确认密码"] - B --> C{"字段格式、密码强度和两次输入一致?"} + A["用户进入公开注册页"] --> A1{"当前是否已有有效登录态?"} + A1 -- "买家" --> A2["返回个人中心,不重复注册"] + A1 -- "商家或管理员" --> A3["拒绝公开注册动作"] + A1 -- "否" --> B["填写手机号、密码和确认密码"] + B --> B1{"请求是否夹带角色字段?"} + B1 -- "是" --> B2["拒绝整次请求,不创建账号"] + B1 -- "否" --> C{"手机号、基础密码强度和两次输入一致?"} C -- "否" --> X["保留非敏感输入并提示字段错误"] C -- "是" --> D{"手机号是否已注册?"} D -- "是" --> Y["拒绝重复注册,不泄露其他账号资料"] - D -- "否" --> E{"客户端是否尝试传入角色字段?"} - E -- "是" --> E1["忽略或拒绝该字段,始终创建买家"] - E -- "否" --> F["服务端按 F01 规则生成唯一用户名"] - F --> G{"用户名生成成功?"} - G -- "否" --> G1["提示系统繁忙,不创建账号"] - G -- "是" --> H["对密码执行可靠哈希"] + D -- "否" --> F["服务端按 F01 规则生成唯一用户名"] + F --> G{"生成结果?"} + G -- "受控重试后仍冲突" --> G1["提示系统繁忙,不创建账号"] + G -- "密码等于用户名" --> G2["拒绝密码过于简单,不创建账号"] + G -- "唯一且密码不同" --> H["对密码执行可靠哈希"] H --> I["开启注册事务"] - I --> J["写入正常买家账号、默认头像和唯一用户名"] + I --> J["原子写入正常买家账号、唯一用户名和默认头像"] J --> K{"事务提交成功?"} K -- "否" --> Z["整体回滚,不创建账号"] K -- "是" --> L["返回不含敏感信息的账号摘要"] @@ -68,30 +75,36 @@ flowchart TD 主流程要求: - 手机号作为账号标识,用户名仅用于展示;前端不得展示完整手机号或明文密码。 +- 请求体不接受角色字段;检测到角色注入时拒绝整次请求,不能创建任何账号。 - 密码哈希必须使用可靠算法;明文密码、确认密码和哈希前的中间结果不得写入日志、响应或数据库。 - 用户名生成冲突必须在受控次数内安全重试;多次冲突时不创建账号,避免重复账号。 - 注册成功不默认建立登录态,必须由用户进入 F02 重新提交凭据。 -## 四、注册输入校验 +## 四、注册输入与前置校验 ```mermaid flowchart TD A["接收注册请求"] --> B["读取手机号、密码、确认密码"] - B --> C{"手机号符合中国大陆手机号格式且无前后空白?"} + B --> B1{"是否夹带角色字段?"} + B1 -- "是" --> X0["拒绝整次请求"] + B1 -- "否" --> C{"手机号符合中国大陆手机号格式且无前后空白?"} C -- "否" --> X1["拒绝:手机号格式错误"] C -- "是" --> D{"密码长度 8~16 且同时包含字母和数字?"} D -- "否" --> X2["拒绝:密码强度不足"] - D -- "是" --> E{"密码不等于手机号、手机号倒序或生成后用户名?"} + D -- "是" --> E{"密码是否等于手机号或手机号倒序?"} E -- "是" --> X3["拒绝:密码过于简单"] E -- "否" --> F{"确认密码与密码一致?"} F -- "否" --> X4["拒绝:两次密码输入不一致"] - F -- "是" --> G["字段校验通过,进入手机号唯一性检查"] + F -- "是" --> G{"手机号当前未被占用?"} + G -- "否" --> X5["拒绝重复注册"] + G -- "是" --> H["进入用户名生成与最终密码校验"] ``` 校验要求: - 拒绝国际区号、空格、连字符或固话号码;只接受中国大陆 11 位手机号(具体格式校验在接口设计中给出,本流程不展开)。 - 不得强制特殊字符、大小写混合或验证码;本期不存储历史密码。 +- “密码不得等于生成后用户名”只能在生成候选用户名后判断,不能在用户名尚不存在时假装完成校验。 - 校验失败时,前端保留已填写的手机号和提示,密码字段不回显。 - 字段级错误不得泄露其他账号是否存在或格式细节。 @@ -103,9 +116,11 @@ flowchart TD B --> C{"全局唯一?"} C -- "否,受控次数内重试" --> B C -- "仍冲突" --> X["提示系统繁忙,请稍后重试"] - C -- "是" --> D["对密码执行可靠哈希"] - D --> E["创建账号:状态=正常、角色=买家、默认头像、唯一用户名"] - E --> F["登记创建时间并返回账号摘要"] + C -- "是" --> D{"密码是否等于候选用户名?"} + D -- "是" --> X1["拒绝密码过于简单,不创建账号"] + D -- "否" --> E["对密码执行可靠哈希"] + E --> F["同一事务创建正常买家账号、唯一用户名和默认头像"] + F --> G["提交成功后返回账号摘要"] ``` 默认资料约束: @@ -113,6 +128,7 @@ flowchart TD - 用户名生成使用 `u_` 前缀加 8 位不易混淆字符;字符集由接口设计在评审前确认。 - 默认头像由系统提供统一资源,不接受用户上传。 - 创建成功后账号状态只能为“正常”,本期注册流程不提供“待激活”或“待验证”中间状态。 +- 账号、唯一用户名和默认头像是一个不可拆分的注册结果;任一写入失败都不得留下部分账号事实。 ## 六、并发、幂等与异常 @@ -126,8 +142,8 @@ flowchart TD B2 -- "无变化" --> N["不产生重复账号"] A3["用户名生成多次冲突"] --> B3["在受控次数内重试"] B3 -- "仍冲突" --> Z["提示系统繁忙,不创建账号"] - A4["客户端请求注入商家或管理员角色"] --> B4["忽略或拒绝角色字段"] - B4 --> C2["永远创建买家账号"] + A4["客户端请求注入任意角色字段"] --> B4["拒绝整次请求"] + B4 --> C2["不创建任何账号"] A5["数据库或哈希计算失败"] --> B5["整体回滚"] B5 --> R["提示稍后重试,不泄露内部错误"] ``` @@ -137,7 +153,7 @@ flowchart TD - 重复注册不暴露已注册账号的用户名、创建时间或状态。 - 用户名生成冲突必须显式可重试且不消耗未受控次数;最终不创建账号。 - 注册事务任一步失败(手机号唯一性冲突、用户名冲突、哈希失败、数据库写入失败)必须整体回滚。 -- 网络重试或前端防抖失败时,重复提交按字段级错误返回,不创建第二张账号。 +- 网络重试或前端防抖失败时,如果首个请求已经提交成功,后续同手机号请求返回已注册,不创建第二张账号。 ## 七、与核心模块的衔接 @@ -145,12 +161,12 @@ flowchart TD |---|---|---|---| | 公共注册页 | 手机号、密码、确认密码 | M01-01 | 创建正常买家账号并返回账号摘要 | | M01-01 | 正常买家账号摘要 | M01-02(F02) | 用户进入登录流程 | -| M01-01 | 注册成功事实 | M00 认证能力 | 用户名、密码哈希、默认资料登记到公共认证上下文 | +| M00 公共认证能力 | 密码哈希与安全配置 | M01-01 | Identity 在注册事务中保存密码哈希;账号、用户名和默认资料仍由 Identity 持有 | | M01-01 | 重复手机号、非法字段或角色注入 | M01-01 | 字段级错误或重复注册提示,不创建账号 | 衔接约束: -- 公开注册完成后必须立即进入 F02;不默认建立登录态,避免令牌与账号状态耦合。 +- 公开注册完成后只展示明确的 F02 登录入口;用户需主动进入并重新提交凭据,不默认建立登录态。 - 公开注册不写任何业务模块事实;M03、M04、M05、M06、M07、M08、M09 只能在用户后续登录后接入。 - 注册事件事实目前不进入 M09;如未来纳入通知,需另行评审事件、接收人和文案。 @@ -158,17 +174,16 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 公开注册创建买家账号 | A001 | 校验字段、手机号唯一性、密码强度、用户名生成和角色固定 | 待交叉评审 | +| 公开注册创建买家账号 | A001 | 拒绝角色字段,依次完成基础校验、手机号唯一性、用户名生成、最终密码校验,并原子创建账号与默认资料;成功后不签发登录凭证 | 流程已确认,待接口同步 | 接口仅承载“创建买家账号”这一业务结果;字段格式、错误码和 `traceId` 由接口设计在评审前统一,不得反向写入业务流程。 -## 九、由流程反查出的接口与数据待评审项 +## 九、下游契约与数据约束 -1. 是否需要在 A001 响应中返回默认头像 URL;当前需求仅要求返回账号摘要。 -2. 用户名字符集需在接口设计中明确,避免与命名规范冲突。 -3. 注册成功是否同时建立短效令牌用于“注册即登录”;当前需求固定要求重新登录。 -4. 数据库是否需要为用户名预留历史记录字段;当前需求仅要求全局唯一。 -5. 注册事件是否进入 M09;当前需求未包含,本流程不预设接入。 +1. A001 成功响应必须返回生成后的用户名和默认头像摘要,但不得返回任何登录凭证、密码或密码哈希。 +2. 用户名字符集必须满足 `u_` 加 8 位不易混淆字符,并由数据库唯一约束兜底;受控重试次数属于实现配置,不改变业务结果。 +3. 注册数据设计必须保证手机号、用户名唯一,并让账号、角色、状态、用户名和默认头像在同一事务中提交。 +4. 本流程不建立用户名历史、注册通知或自动登录事实;如需求以后变化,必须先回到需求与流程重新评审。 ## 十、验收证据清单 @@ -178,5 +193,7 @@ flowchart TD - [ ] 密码以可靠哈希存储,响应、日志和数据库中不出现明文密码。 - [ ] 用户名和手机号全局唯一,并发注册不产生重复账号。 - [ ] 重复点击、前端防抖失败和网络重试不创建第二张账号。 +- [ ] 注册成功后仍处于未登录状态,必须显式进入 F02 并重新提交手机号和密码。 +- [ ] 已登录买家返回个人中心,已登录商家或管理员不能使用公开注册动作创建买家账号。 - [ ] 系统仍存在由初始化或受控流程创建的商家和管理员账号;公开注册流程不能创建这些账号。 -- [ ] 保存正常与异常注册截图,并能说明凭据保护、唯一性和角色固定规则。 \ No newline at end of file +- [ ] 保存正常与异常注册截图,并能说明凭据保护、唯一性和角色固定规则。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" index 2f3e18d..d41233b 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" @@ -4,22 +4,23 @@ > - 主责人:唐宇昊 > - 需求来源:[《需求规格说明书》M01-02](../../../01-需求文档/需求规格说明书.md) 的“M01-02 用户登录与退出(F02)— 唐宇昊”完整七节 > - 基础核心流程:F01、M00 公共认证;后续 F03、F04~F06、F07、F08、F09、F10、F11、F12、F13、X02 -> - 直接入口:游客或低登录态用户提交手机号和密码;前端刷新或退出动作触发登录态恢复与失效 +> - 直接入口:未登录用户提交手机号和密码;PC Web 刷新或退出动作触发登录态恢复与失效 > - 直接出口:M01 返回有效登录凭证、服务端确认的角色与账号状态;下游模块据此决定是否继续受理 > - 回归核心结果:F01 已注册账号、F03 个人资料、F13 账号治理结果保持不变 > - 不得改变:账号、订单、支付事实与权限结果;本期不引入第三方登录或多设备登录联动 ## 一、范围与事实来源 -本流程负责公开登录入口、统一收银台登录、商家端和管理端登录、登录态恢复、主动退出和令牌失效判定。它不提供用户名登录、第三方登录、找回密码、密码强度提示升级或多设备同步退出。 +本流程负责 PC Web 的统一登录入口、登录态恢复、主动退出和登录凭证失效判定。它只签发一个有明确有效期的 JWT 登录凭证,不提供刷新凭证、用户名登录、第三方登录、找回密码或多设备同步退出。 -A002~A005 由本流程派生,仅在流程评审通过后用于契约映射;现有清单不能反向决定或拼接业务流程。 +A002~A004 由本流程派生;历史清单中的 A005 刷新凭证没有业务来源并取消。接口编号只在流程确认后用于契约映射,现有清单不能反向决定或拼接业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M01-02/F02 需求 | 完整定义 | 作为业务语义事实源 | -| 本文业务流程 | 初稿 | 先确认角色、判断、状态、事务边界和模块出入口 | -| A002~A005 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| 本文业务流程 | 完整定义 | 已确认角色、判断、状态、失效边界和模块出入口 | +| A002~A004 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| A005 刷新凭证 | 无需求来源 | 取消,不进入实现 | | DBxxx 账号/令牌表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | | C10 多实例认证 | 部分定义 | 只登记接入点;多实例令牌验证规则由 C10 评审 | @@ -32,8 +33,10 @@ flowchart LR LOG -->|"有效登录态 + 角色 + 账号状态"| M04["M04 Ordering"] LOG -->|"有效登录态 + 角色 + 账号状态"| M05["M05 Payment"] LOG -->|"有效登录态 + 角色 + 账号状态"| M06["M06 后台"] + LOG -->|"有效登录态 + 角色 + 账号状态"| M07["M07 评价"] LOG -->|"有效登录态 + 角色 + 账号状态"| M08["M08 收藏与历史"] - LOG -->|"退出后当前令牌失效"| UI["前端清理登录态并返回登录页"] + LOG -->|"有效登录态 + 角色 + 账号状态"| M09["M09 消息"] + LOG -->|"本地立即退出;服务端撤销当前令牌"| UI["返回登录页"] F13["M06-03 禁用/启用"] -->|"账号状态变更"| LOG F01["M01-01 注册成功"] -->|"正常买家账号"| LOG VIS -. "越权访问" .-> REJ["按角色规则拒绝访问"] @@ -45,6 +48,7 @@ flowchart LR - M01-02 只产出可被任一 API 实例验证的登录态,并返回当前账号状态,不直接通知业务模块。 - 登录态失效结果无法确认时,受保护请求必须失败关闭,不允许因依赖异常继续放行。 - 退出只使当前令牌失效;多设备或全设备退出需另行评审并写入接口设计。 +- 手机号修改或账号禁用属于账号级安全变化,必须使此前签发的全部登录凭证失效;重新启用不恢复旧凭证。 ## 三、登录主流程 @@ -61,7 +65,7 @@ flowchart TD G -- "否" --> X2 G -- "是" --> H{"账号状态正常?"} H -- "否" --> X3["拒绝登录并提示账号停用"] - H -- "是" --> I["签发有明确有效期的登录态和刷新凭证"] + H -- "是" --> I["签发一个有明确有效期的 JWT 登录凭证"] I --> J["返回账号摘要、服务端确认的角色和当前状态"] J --> K["前端保存登录态,按角色进入对应端"] K --> L{"按角色进入?"} @@ -74,7 +78,8 @@ flowchart TD - 账号不存在和密码错误必须返回统一的“账号或密码错误”提示,禁止区分错误字段。 - 账号禁用需明确告知用户联系管理员,但不暴露内部状态码或异常。 -- 登录态包含明确有效期;登录态有效期、刷新策略和签名配置由接口设计统一。 +- 登录凭证包含明确有效期;有效期到达后必须重新登录,本期没有刷新凭证或静默续期。 +- 登录凭证的签名与验证配置由公共认证能力统一,接口不能额外派生第二种凭证。 - 多实例环境下,登录态验签配置必须一致;任一实例可独立验证同一有效登录态。 ## 四、登录态恢复与角色路由 @@ -104,30 +109,30 @@ flowchart TD ```mermaid flowchart TD - A[“用户在任一端点击退出”] --> B[“前端使用当前登录态调用退出动作”] - B --> C{“退出动作成功?”} - C -- “是” --> D[“服务端登记当前登录态失效,返回成功”] - D --> E[“前端清理本地登录态和刷新凭证”] - C -- “否” --> F[“前端无论结果如何都清理本地登录态和刷新凭证”] - F --> G[“前端提示服务端撤销状态待确认,建议用户假设凭据已泄露并尽快修改密码”] - E --> H[“跳转登录页”] - G --> H - A2[“受保护接口仍使用旧登录态”] --> B2[“校验时返回登录失效”] - B2 --> C2[“前端清理登录态并引导重新登录”] - A3[“M06-03 禁用账号”] --> B3[“服务端提升账号令牌版本,旧登录态按版本失效”] - B3 --> C3[“受保护请求返回登录失效”] - A4[“手机号修改成功”] --> B4[“服务端提升账号令牌版本,修改前签发的全部登录态失效”] - B4 --> C4[“受保护请求返回登录失效”] + A["用户在 PC Web 点击退出"] --> B["前端立即进入退出中状态"] + B --> C["停止新的受保护请求并清除持久化凭证"] + C --> D["仅使用退出时持有的当前 JWT 调用服务端撤销"] + D --> E{"服务端能否确认当前 JWT 已失效?"} + E -- "是" --> F["显示退出成功并进入登录页"] + E -- "否" --> G["说明本地已退出、服务端撤销结果无法确认"] + G --> H["不得显示退出成功;后续受保护请求失败关闭"] + A2["旧 JWT 再次访问受保护接口"] --> B2["服务端校验凭证、账号状态和失效事实"] + B2 --> C2["已失效则拒绝并引导重新登录"] + A3["M06-03 禁用账号"] --> B3["账号状态与全部旧凭证失效形成确定结果"] + B3 --> C3["重新启用后仍需重新登录"] + A4["M01-03 手机号修改"] --> B4["新手机号与全部旧凭证失效形成确定结果"] + B4 --> C4["修改成功后必须重新登录"] ``` 退出与失效约束: -- 退出顺序必须先使用当前登录态调用退出动作;前端无论服务端是否成功,都必须清理本地登录态和刷新凭证,避免公共电脑等场景保留凭据造成安全风险。 -- 服务端撤销失败时前端必须提示”服务端撤销状态待确认”,并建议用户假设凭据已泄露、尽快修改密码;不允许保留本地登录态等待重试。 +- 点击退出后,前端必须立即停止新的受保护请求并清理持久化登录凭证;为完成一次撤销调用而暂存的当前 JWT 不得重新写回本地状态。 +- 只有服务端明确确认当前 JWT 已失效时才显示“退出成功”;无法确认时说明“本地已退出,服务端撤销结果无法确认”,不允许恢复本地登录态或给出虚假成功。 - 主动退出只影响当前登录态;本期不自动撤销同账号的其他设备登录态。 -- M06-03 禁用账号、手机号修改成功后必须提升对应账号令牌版本,使修改前签发的登录态失效;新登录态签发前用户必须重新登录。 +- M06-03 禁用账号、M01-03 修改手机号必须让业务变化与“此前全部登录凭证失效”形成确定结果;无法确认全部失效时不得把业务变化报告为成功。 +- 重新启用账号不会恢复禁用前签发的凭证;用户必须重新提交手机号和密码。 - 登录态失效结果需在多实例之间保持一致;失效状态无法确认时受保护请求失败关闭。 -- 退出后用户的所有个人页和受保护页必须退出到登录态,禁止出现”看似已退出但仍能访问”的状态。 +- 退出后用户的所有个人页和受保护页必须退出到登录态,禁止出现“看似已退出但仍能访问”的状态。 ## 六、并发、幂等与异常 @@ -146,15 +151,15 @@ flowchart TD A6["买家尝试访问商家或管理入口"] --> B6["按角色规则拒绝访问,不泄露目标数据"] A7["商家从购物端入口登录"] --> B7["登录成功后按角色跳转商家端"] B7 --> N7["不误报密码错误"] - A8["登录态即将过期"] --> B8["调用刷新凭证换取新登录态"] - B8 --> N8["旧登录态随刷新撤销"] + A8["登录凭证到期"] --> B8["拒绝受保护请求并清理本地状态"] + B8 --> N8["重新提交手机号和密码登录"] ``` 异常约束: - 未认证或登录已失效与身份有效但无权执行当前操作是两类不同的拒绝结果;具体错误码和 HTTP 状态由接口设计统一。 - 撤销状态共享不可用时不得返回虚假成功,也不得静默放行受保护请求。 -- 退出动作的幂等性由撤销登记机制保证;重复退出返回一致结果。 +- 同一 JWT 已明确撤销后重复提交退出,服务端返回同一已失效结果;撤销结果未知时不得伪造为幂等成功。 ## 七、与核心模块的衔接 @@ -162,42 +167,44 @@ flowchart TD |---|---|---|---| | M01-01 注册成功 | 正常买家账号 | M01-02 | 用户进入登录流程 | | 公开登录入口 | 手机号和密码 | M01-02 | 登录态、角色与账号状态 | -| M01-02 | 有效登录态 + 角色 | M03、M04、M05、M06、M08 | 业务模块按各自资源规则受理 | -| M06-03 禁用/启用 | 账号状态变更 | M01-02 | 旧登录态失效或恢复正常登录 | -| M01-03 修改手机号 | 旧登录态集合 | M01-02 | 修改前全部登录态失效,需重新登录 | +| M01-02 | 有效登录态 + 角色 | M03、M04、M05、M06、M07、M08、M09 | 业务模块按各自角色与资源规则受理 | +| M06-03 禁用/启用 | 账号状态变更 | M01-02 | 禁用使全部旧凭证失效;启用只允许重新登录,不恢复旧凭证 | +| M01-03 修改手机号 | 账号安全事实变化 | M01-02 | 修改前全部登录凭证失效,需重新登录 | | C10 多实例环境 | 共享登录态验签配置 | M01-02 | 任一实例可独立验证同一有效登录态 | 衔接约束: - 下游模块只接收登录态解析后的身份和角色,不接受客户端自行声明的接收人。 -- 退出或登录态失效结果必须可被任何 API 实例在合理时间内观察到;具体延迟由 C10 和共享撤销能力共同确定。 -- 登录入口不区分 PC Web、Electron 和 Android;同一登录态在各客户端均有效,但路由和入口展示仍由前端按角色控制。 +- 退出、手机号修改或账号禁用只有在所有 API 实例都能一致拒绝相应旧凭证后才能返回成功;无法确认一致失效时不得返回成功,依赖该失效事实的受保护请求必须失败关闭。 +- 本期仅验收 PC Web 登录入口与角色路由;Electron 和 Android 作为后续客户端规划,不进入当前流程或验收证据。 ## 八、由流程派生的接口契约映射 | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 提交手机号和密码登录 | A002 | 校验凭据与账号状态、签发登录态与刷新凭证、返回角色与摘要 | 待交叉评审 | -| 主动退出当前登录态 | A003 | 登记当前登录态与刷新凭证失效 | 待交叉评审 | -| 登录态恢复与当前账号查询 | A004 | 校验登录态有效性并返回当前账号状态 | 待交叉评审 | -| 使用刷新凭证换取新登录态 | A005 | 校验刷新凭证有效性并签发新登录态,旧凭证同步撤销 | 待交叉评审 | +| 提交手机号和密码登录 | A002 | 校验凭据与账号状态,只签发一个有明确有效期的 JWT,返回角色与账号摘要 | 流程已确认,待接口同步 | +| 主动退出当前登录态 | A003 | 撤销当前 JWT;已明确撤销时可重复返回相同结果,未知时不得返回成功 | 流程已确认,待接口同步 | +| 登录态恢复与当前账号查询 | A004 | 校验 JWT、账号状态和失效事实,返回当前账号摘要与角色 | 流程已确认,待接口同步 | +| 刷新登录凭证 | A005 | 无需求来源;本期凭证到期后重新登录 | 取消,保留历史编号 | 接口必须承载“当前账号可登录”这一业务结果;HTTP 状态码、错误码和 `traceId` 由接口设计在评审前统一,不得反向写入业务流程。 -## 九、由流程反查出的接口与数据待评审项 +## 九、下游契约与数据约束 -1. 多设备或全设备退出范围需另行确认;A003 当前仅承诺单登录态失效。 -2. 登录失败次数限制和锁定策略本期是否实现,需在接口设计中明确。 -3. 撤销状态共享不可用时的降级策略需要在 C10 与 M00 协作下进一步评审。 -4. 角色路由在多端(PC Web、Electron、Android)上的跳转目标是否一致需另行确认。 -5. 登录态即将过期时的静默刷新策略需在接口和前端设计中明确。 +1. A002 响应只返回一个 JWT、有效期、账号摘要和服务端角色,不得出现刷新令牌或刷新入口。 +2. A003 只撤销请求携带的当前 JWT;多设备退出没有需求来源,不得由接口自行扩大。 +3. A004 必须同时校验 JWT 有效期、签名、当前账号状态和失效事实;任何必需事实无法确认时按服务暂不可用失败关闭。 +4. 登录失败次数限制、密码修改和账号锁定均不属于本期业务范围;不得因实现便利写入契约。 +5. 多实例必须共享一致的签名配置、账号安全变化和凭证失效判断,但具体存储机制不进入业务接口。 ## 十、验收证据清单 - [ ] 正确账号可登录,刷新后登录态保持,买家、商家和管理员落地路由正确。 - [ ] 错误密码不泄露账号存在性,被禁用账号无法登录并获得清楚提示。 -- [ ] 退出时先由服务端登记当前登录态失效,再清理本地登录态;服务端未成功时本地登录态保留并提示重试。 +- [ ] 登录响应只包含一个有明确有效期的 JWT;到期后重新登录,不存在刷新凭证或 A005 调用。 +- [ ] 点击退出后立即停止受保护请求并清理本地凭证;仅在服务端确认撤销后显示退出成功,未知时保持本地已退出并明确提示。 - [ ] 退出后旧登录态不能再访问受保护入口;跨角色访问按角色规则被拒绝。 +- [ ] 修改手机号或禁用账号后此前全部凭证失效;重新启用账号不会恢复旧凭证。 - [ ] 在两个 API 实例间切换请求时,同一有效登录态得到一致认证结果。 - [ ] 撤销状态共享不可用时,受保护请求提示服务暂不可用,不静默放行。 -- [ ] 保存登录、刷新、退出、禁用账号、登录态失效能力不可用和越权访问证据。 \ No newline at end of file +- [ ] 保存登录、刷新、退出、禁用账号、登录态失效能力不可用和越权访问证据。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" index 4f2001a..e1b102e 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" @@ -5,21 +5,22 @@ > - 需求来源:[《需求规格说明书》M01-03](../../../01-需求文档/需求规格说明书.md) 的“M01-03 个人信息与收货地址(F03)— 唐宇昊”完整七节 > - 基础核心流程:F02、F08 > - 直接入口:M01-02 提供已登录买家身份;M04 在 F08 提交订单时按地址快照契约读取 -> - 直接出口:买家资料与地址的查看、修改和默认地址结果;M04 据此完成地址归属校验和快照 +> - 直接出口:买家账号资料与地址的查看、允许变更和默认地址结果;M04 据此取得地址归属校验结果和当前地址数据 > - 回归核心结果:F02 已签发登录态在非敏感修改后保持有效;F08 已下单订单的地址快照不变;修改手机号后必须重新登录 > - 不得改变:订单金额、商品快照、支付事实与他人资料归属 ## 一、范围与事实来源 -本流程负责买家个人中心的资料查看与维护、地址新增、查询、编辑、删除、默认地址切换和敏感修改的登录态处理。它不提供自定义头像上传、商家资料维护、管理员代修改或多地址簿切换。 +本流程负责买家个人中心的账号资料查看、一次用户名重置、手机号修改、地址新增/查询/编辑/删除、独立默认地址切换和敏感修改后的登录凭证失效。资料字段仅包含系统生成用户名、掩码手机号和默认头像;它不提供展示名、简介、自定义头像上传、商家资料维护或管理员代修改。 -A006~A014 由本流程派生,仅在流程评审通过后用于契约映射;现有清单不能反向决定或拼接业务流程。 +A006~A008、A010~A014 由本流程派生;历史清单中的 A009 展示资料修改没有业务来源并取消。接口编号只在流程确认后用于契约映射,现有清单不能反向决定或拼接业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M01-03/F03 需求 | 完整定义 | 作为业务语义事实源 | -| 本文业务流程 | 初稿 | 先确认角色、判断、状态、事务边界和模块出入口 | -| A006~A014 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| 本文业务流程 | 完整定义 | 已确认角色、判断、状态、事务边界和模块出入口 | +| A006~A008、A010~A014 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| A009 展示资料修改 | 无需求来源 | 取消,不进入实现 | | DBxxx 用户资料/地址表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | | M04 地址快照契约 | 部分定义 | 只登记接入点;快照字段由 Ordering 评审 | @@ -30,8 +31,8 @@ flowchart LR B["已登录买家"] -->|"个人中心入口"| PROF["M01-03 Identity
资料与地址"] PROF -->|"用户名、默认头像、掩码手机号"| UI["个人中心页"] PROF -->|"地址列表 + 默认地址标记"| ADDR["地址管理页"] - PROF -->|"敏感修改后提升令牌版本"| TOK["M01-02 登录与退出"] - PROF -->|"地址归属校验和地址快照"| ORD["M04 Ordering
F08 提交订单"] + PROF -->|"手机号修改后全部旧凭证失效"| TOK["M01-02 登录与退出"] + PROF -->|"地址归属校验 + 当前地址数据"| ORD["M04 Ordering
F08 提交订单"] ORD -->|"地址快照随订单持久化"| SNAP["历史订单地址快照"] GUEST["游客"] -. "访问个人中心" .-> REJ["引导登录并保留安全返回目标"] MERCH["商家或管理员"] -. "访问买家资料" .-> FORB["拒绝访问买家私人资源"] @@ -39,8 +40,8 @@ flowchart LR 边界约束: -- 资料与地址只能由当前买家修改;M04 只读取快照数据,不能修改地址。 -- 敏感修改(手机号)成功后必须提升账号令牌版本,使修改前签发的全部登录态失效,由 M01-02 处理登录态失效。 +- 资料与地址只能由当前买家修改;M04 只通过公开应用契约读取本次校验的当前地址数据,不能修改地址。 +- 手机号修改与“修改前全部登录凭证失效”必须形成确定结果;无法确认凭证失效时不得报告手机号修改成功。 - 地址列表与默认地址切换必须按当前买家 ID 隔离,禁止跨用户访问。 ## 三、资料维护主流程 @@ -51,30 +52,32 @@ flowchart TD B -- "否" --> X["拒绝访问并引导登录"] B -- "是" --> C["返回用户名、默认头像和掩码手机号"] C --> D{"选择资料操作?"} - D -- "查看资料" --> E["返回展示资料和自助重置用户名状态"] - D -- "修改展示资料" --> F{"字段合法?"} - F -- "否" --> F1["字段级错误,保留可恢复输入"] - F -- "是" --> F2["保存展示资料并返回最新结果"] + D -- "查看资料" --> E["返回账号资料和用户名重置机会状态"] D -- "重置用户名" --> G{"本项目期内是否仍有一次机会?"} G -- "否" --> H["拒绝修改并说明次数已用完"] G -- "是" --> I["服务端按注册规则重新生成全局唯一用户名"] - I --> J["保存并返回最新资料"] + I --> I1{"原子提交新用户名并消耗重置机会?"} + I1 -- "否" --> I2["不消耗机会,读取最新资料并返回冲突或失败"] + I1 -- "是" --> J["返回新用户名和已用完状态"] D -- "修改手机号" --> K["要求重新验证当前密码"] K --> K1{"当前密码正确?"} K1 -- "否" --> L["保持原资料并提示原因"] K1 -- "是" --> M{"新手机号格式正确且全局唯一?"} M -- "否" --> L - M -- "是" --> N["保存新手机号并提升账号令牌版本"] - N --> O["M01-02:修改前签发的全部登录态失效,需重新登录"] + M -- "是" --> N{"账号仍处于本次读取的旧状态?"} + N -- "否" --> N1["拒绝并返回资料已变化,不覆盖最新结果"] + N -- "是" --> O["原子提交新手机号和全部旧凭证失效结果"] + O --> P["清理本地登录态并要求重新登录"] ``` 资料维护约束: - 用户名只能由服务端重新生成,不接受客户端指定任意字符串。 -- 用户名自助修改次数仅一次,成功后页面立即刷新为新用户名。 -- 手机号修改必须验证当前密码;成功后提升账号令牌版本,使修改前签发的全部登录态失效。 +- 用户名自助重置机会仅一次,只有新用户名成功提交后才消耗;生成冲突、保存失败或并发落败不得消耗机会。 +- 手机号修改必须验证当前密码,并基于开始修改时读取的旧账号状态做条件提交;同一旧状态上的并发请求最多一个成功。 +- 手机号修改成功必须同时形成“修改前全部登录凭证失效”的确定结果;无法确认时手机号保持不变。 - 资料修改不得在响应或日志中泄露完整手机号、密码哈希或内部异常。 -- 修改展示资料不改变登录态;只有修改手机号需要重新登录。 +- 本期不存在展示名、简介或其他任意资料编辑;查看资料和用户名重置不改变登录态,修改手机号后必须重新登录。 ## 四、地址维护主流程 @@ -82,17 +85,12 @@ flowchart TD flowchart TD A["已登录买家进入地址管理"] --> B["按当前买家查询本人地址列表"] B --> C{"选择操作?"} - C -- "新增" --> D{"当前地址数量已达上限?"} - D -- "是" --> D1["拒绝新增并提示已达上限"] - D -- "否" --> E["录入收件人、联系电话、省市区、详细地址和可选默认标记"] + C -- "新增" --> E["录入收件人、联系电话、省市区和详细地址"] E --> E1{"字段合法?"} E1 -- "否" --> E2["保留输入并提示字段错误"] - E1 -- "是" --> E3{"isDefault = true?"} - E3 -- "是" --> E4["在同一事务内取消旧默认地址"] - E3 -- "否" --> E5["保存地址"] - E4 --> E6["保存地址"] - E6 --> E7["刷新列表"] - E5 --> E7 + E1 -- "是" --> E3["创建非默认地址"] + E3 --> E4["如需设为默认,另行执行设默认动作"] + E4 --> E5["刷新列表"] C -- "编辑" --> F{"地址属于当前买家?"} F -- "否" --> Y["返回不存在或无权限,不泄露归属"] F -- "是" --> F1["录入收件人、联系电话、省市区、详细地址(不含默认标记)"] @@ -115,9 +113,9 @@ flowchart TD - 每名买家最多一个默认地址,切换操作需原子完成或提供等效一致性保障。 - 删除默认地址后不自动选择其他地址,下单时由买家明确选择。 -- 新增地址时可携带默认地址标记;在同一事务内将其他地址的默认标记取消,保证唯一性。 +- 新增地址固定创建为非默认地址,不接收默认标记;如需设为默认,必须在创建成功后另行执行设默认动作。 - 编辑地址不允许直接修改默认标记;默认地址切换必须通过设默认动作完成。 -- 单买家地址上限暂定 20 条;超出时拒绝新增并提示已达上限。 +- 本期不设置单买家地址数量上限,接口和数据设计不得自行加入固定上限。 - 同一买家对地址的读写始终按当前用户过滤,禁止仅凭资源 ID 跨用户访问。 - 地址字段变更不影响历史订单的地址快照,下单时间点确定的快照始终保留。 @@ -128,8 +126,8 @@ flowchart LR A["买家在 F08 提交订单时选择地址 ID"] --> B["M04 向 M01-03 校验地址归属"] B --> C{"地址属于当前买家且存在?"} C -- "否" --> X["拒绝整次下单并提示原因"] - C -- "是" --> D["M01-03 返回地址快照数据"] - D --> E["M04 在同一事务内保存地址快照"] + C -- "是" --> D["M01-03 返回本次校验时的当前地址数据"] + D --> E["M04 在订单创建事务内写入订单地址快照"] E --> F["地址变更不影响历史订单"] G["买家在地址管理中删除或修改地址"] --> H["M01-03 仅影响当前买家"] H --> I["历史订单快照保持不变"] @@ -138,7 +136,8 @@ flowchart LR 衔接约束: - M04 必须在提交订单时再次校验地址归属,不能依赖前端传入或本地缓存。 -- 地址快照需保存下单时刻的完整收件人、联系电话、省市区和详细地址。 +- M01-03 只负责按当前买家校验归属并返回本次读取的完整收件人、联系电话、省市区和详细地址;不参与订单事务。 +- M04 使用该返回结果构造订单地址快照,并在 M04 自己的订单创建事务中原子提交;模块之间不得共享仓储或直接写表。 - M01-03 不接收订单、支付或售后模块直接写入;资料和地址是买家私人数据。 ## 六、并发、幂等与异常 @@ -147,19 +146,18 @@ flowchart LR flowchart TD A1["并发设置多个默认地址"] --> B1["原子切换或等效机制保证最终唯一"] B1 --> N1["不出现两个默认地址"] - A2["两个请求同时修改手机号"] --> B2["先成功者写入;后者按唯一性失败"] - B2 --> N2["提升令牌版本只发生一次"] + A2["两个请求基于同一旧状态修改手机号"] --> B2["条件提交保证最多一个成功"] + B2 --> N2["后到请求返回资料已变化,不覆盖先成功结果"] A3["敏感修改时密码错误"] --> B3["拒绝修改,不泄露账号存在性"] B3 --> N3["登录态保持有效"] A4["地址归属错误或他人地址"] --> B4["返回不存在或无权限"] B4 --> N4["不泄露地址是否真实存在"] - A5["用户名重置次数已用完"] --> B5["拒绝并说明规则"] - B5 --> N5["不再次扣减次数"] + A5["两个请求并发重置用户名"] --> B5["原子提交用户名与机会消耗"] + B5 --> N5["最多一个成功;失败请求不消耗机会"] A6["删除默认地址后未选新地址直接下单"] --> B6["M04 拒绝并提示先选择地址"] B6 --> N6["不自动选择其他地址"] - A7["资料字段格式非法"] --> B7["字段级错误,保留可恢复输入"] - A8["地址数量已达上限仍尝试新增"] --> B8["拒绝新增并提示已达上限"] - A9["修改展示资料时夹带手机号或用户名"] --> B9["字段被忽略,不修改对应内容"] + A7["手机号或地址字段格式非法"] --> B7["字段级错误,保留可恢复输入"] + A8["新增地址夹带默认标记"] --> B8["拒绝无来源字段,要求另行设置默认地址"] ``` 异常约束: @@ -167,58 +165,60 @@ flowchart TD - 任何敏感修改的失败都必须保持现有账号状态、登录态和地址不变。 - 资料和地址接口不允许通过仅凭资源 ID 跨用户访问,越权请求统一返回“资源不存在或无权限”。 - 并发修改场景下,最终结果必须保持一致;不出现“两个默认地址”或“两个不同手机号同时生效”的状态。 -- 修改展示资料不允许改变手机号、用户名、角色、状态、用户标识或头像;这些字段只能通过专门接口修改。 +- A006 手机号修改需要携带可验证的旧账号状态条件;不能只依赖新手机号唯一约束解决并发覆盖。 +- 本期没有任意展示资料修改动作;用户名只能通过一次重置动作变化,默认头像保持系统统一资源。 ## 七、与核心模块的衔接 | 上游 | 入口事实 | 下游 | 出口结果 | |---|---|---|---| | M01-02 | 已登录买家身份 | M01-03 | 资料与地址的读写权限 | -| M01-03 | 当前买家地址 ID | M04 Ordering | 地址归属校验和快照数据 | -| M04 | 已下单订单 | 历史订单 | 地址快照随订单持久化,不被后续修改覆盖 | -| M01-03 | 敏感修改成功 | M01-02 | 提升账号令牌版本,使修改前签发的全部登录态失效 | +| M01-03 | 当前买家地址 ID | M04 Ordering | 地址归属校验和当前地址数据 | +| M04 | 订单创建事务 | 历史订单 | M04 写入地址快照并随订单持久化,不被后续修改覆盖 | +| M01-03 | 手机号修改成功 | M01-02 | 修改前签发的全部登录凭证失效 | | F13 M06-03 | 账号状态变更 | M01-03 | 禁用账号后不能再修改资料或地址 | 衔接约束: -- 资料与地址结果对 M04 来说只读快照;M04 不得反向修改 M01-03 的数据。 +- 资料与地址结果对 M04 来说是只读的当前地址数据;M04 不得反向修改 M01-03 的数据。 - M01-03 不为商家或管理员提供读写入口;后台管理端不得通过本流程访问买家私人数据。 -- 任何地址快照写入必须发生于下单事务;下单失败时不能留下新的地址快照事实。 +- 任何地址快照写入都由 M04 在订单创建事务中完成;下单失败时不能留下新的地址快照事实。 ## 八、由流程派生的接口契约映射 | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 修改手机号 | A006 | 验证当前密码、校验格式与唯一性、提升令牌版本 | 待交叉评审 | -| 重置用户名 | A007 | 在限次规则内由服务端生成唯一用户名 | 待交叉评审 | -| 获取本人资料 | A008 | 返回展示资料、掩码手机号和自助重置状态 | 待交叉评审 | -| 修改本人展示资料 | A009 | 维护展示名、简介等展示字段;不接受手机号、用户名、角色、状态等敏感字段 | 待交叉评审 | -| 查询本人地址列表 | A010 | 按当前买家分页返回地址列表与默认标记 | 待交叉评审 | -| 新增地址 | A011 | 校验字段、检查地址数量上限、支持默认地址标记 | 待交叉评审 | -| 编辑地址 | A012 | 仅修改本人地址、重新校验字段、不允许修改默认标记 | 待交叉评审 | -| 删除地址 | A013 | 仅删除本人地址,删除默认地址时不自动指定其他地址 | 待交叉评审 | -| 设置默认地址 | A014 | 原子切换默认地址,保证最终唯一 | 待交叉评审 | +| 修改手机号 | A006 | 验证当前密码和旧账号状态,校验新手机号格式与唯一性,原子形成新手机号和全部旧凭证失效结果 | 流程已确认,待接口同步 | +| 重置用户名 | A007 | 由服务端生成唯一用户名,仅在提交成功后消耗唯一重置机会,并发最多一个成功 | 流程已确认,待接口同步 | +| 获取本人资料 | A008 | 只返回用户名、默认头像、掩码手机号和用户名重置机会状态 | 流程已确认,待接口同步 | +| 修改本人展示资料 | A009 | 展示名、简介等字段没有需求来源 | 取消,保留历史编号 | +| 查询本人地址列表 | A010 | 按当前买家返回本人地址列表与默认标记 | 流程已确认,待接口同步 | +| 新增地址 | A011 | 校验必填字段并固定创建非默认地址,不接受默认标记或固定数量上限 | 流程已确认,待接口同步 | +| 编辑地址 | A012 | 仅修改本人地址并重新校验字段,不允许修改默认标记 | 流程已确认,待接口同步 | +| 删除地址 | A013 | 仅删除本人地址,删除默认地址时不自动指定其他地址 | 流程已确认,待接口同步 | +| 设置默认地址 | A014 | 独立动作原子切换默认地址,保证最终最多一个 | 流程已确认,待接口同步 | 接口仅承载本流程结果;错误码、HTTP 状态码和 `traceId` 由接口设计统一,不得反向写入业务流程。 -## 九、由流程反查出的接口与数据待评审项 +## 九、下游契约与数据约束 -1. 用户名自助重置次数的存储位置与查询语义需要数据库和接口设计统一。 -2. 地址省市区数据是否采用受控字典或开放输入,需另行评审。 -3. 修改手机号时令牌版本提升的并发场景(同一秒既有登录又有修改请求)需要在多实例下保持一致。 -4. 默认地址切换的“等效一致性”具体实现方式(事务、唯一约束、补偿)需在数据库设计中明确。 -5. 商家或管理员尝试调用 A006~A014 的具体错误码和返回体需在接口设计中定义。 -6. 地址数量上限(20 条)的最终值需在数据库约束和接口响应中确认一致。 +1. 用户名重置机会必须与新用户名在一个原子提交边界内更新,并由唯一用户名约束兜底;接口不得先消耗机会再尝试保存。 +2. A006 必须携带并校验服务端可识别的旧账号状态条件;手机号变化和全部旧凭证失效必须作为一个确定业务结果对外返回。 +3. A008 不得出现展示名、简介、自定义头像等字段;A009 取消后不得用通用资料接口绕开专用变更规则。 +4. A011 请求不得包含默认标记,也不得校验固定地址数量上限;A012 与 A014 使用不同业务动作。 +5. 默认地址切换的数据设计必须保证同一买家最终最多一个默认地址;删除默认地址允许留下零个默认地址。 +6. M01-03 的公开应用契约只返回地址归属与当前地址数据;订单地址快照由 M04 自己的数据设计和事务负责。 ## 十、验收证据清单 - [ ] 买家可以查看和修改允许变更的个人资料,并完成地址增删改查。 -- [ ] 用户名唯一和自助修改次数限制生效;手机号变更后旧登录态不能继续执行敏感操作。 +- [ ] 用户名唯一和自助修改次数限制生效;手机号变更后此前签发的全部登录凭证不能访问任何受保护能力。 +- [ ] 用户名只有成功提交后才消耗重置机会;并发重置最多一个成功。 +- [ ] 两个基于同一旧状态的手机号修改最多一个成功,失败请求不覆盖新资料。 - [ ] 默认地址始终最多一个,删除默认地址后下单流程不会擅自选择其他地址。 -- [ ] 单买家地址数量不超过上限;超出时拒绝新增。 -- [ ] 新增地址时携带默认地址标记能在同一事务内取消旧默认地址。 +- [ ] 新增地址固定为非默认且不受固定数量上限约束;设置默认必须另行调用独立动作。 - [ ] 编辑地址不能直接修改默认地址标记;只能通过设默认动作完成切换。 -- [ ] 修改展示资料时不接受手机号、用户名、角色、状态等敏感字段。 +- [ ] 本期不存在展示名、简介或通用展示资料修改能力,A009 保持取消。 - [ ] 买家不能访问他人地址,游客、商家和管理员不能越权调用买家资料接口。 - [ ] F08 提交订单时地址归属校验和快照写入正确;历史订单地址快照不被后续修改覆盖。 -- [ ] 保存资料修改、地址 CRUD、默认地址切换和越权拦截证据。 \ No newline at end of file +- [ ] 保存资料修改、地址 CRUD、默认地址切换和越权拦截证据。 -- Gitee From ab5c3260afea9e098e1ad87abdbab5d4366178b7 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 21:10:39 +0800 Subject: [PATCH 099/118] =?UTF-8?q?docs(process):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=E8=B4=A6=E5=8F=B7=E6=B2=BB=E7=90=86=EF=BC=9B?= =?UTF-8?q?=E5=86=BB=E7=BB=93=E4=B9=B0=E5=AE=B6=E5=92=8C=E5=95=86=E5=AE=B6?= =?UTF-8?q?=E7=A6=81=E7=94=A8=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 35 +++- ...41\347\220\206\346\265\201\347\250\213.md" | 191 ++++++++++-------- 2 files changed, 130 insertions(+), 96 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index e9155e3..08238dd 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -1,6 +1,6 @@ # 电子商城需求规格说明书 -> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.7 +> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.8 > > 文档状态:内容和格式已完成内部统一,待全组与指导教师评审后冻结 > 截止:第 1 周周三提交初稿 @@ -16,6 +16,7 @@ | v0.5 | 2026-07-24 | 罗皓晨 | 冻结 X01 评价提交入口、提交时资格重检和公开计分口径,明确评价数据不进入 C07 商品详情缓存 | | v0.6 | 2026-07-24 | 罗皓晨 | 冻结 C04 关键词分流、统一过滤与降级口径,明确搜索不缓存且正式 60 秒性能采样不包含预热 | | v0.7 | 2026-07-24 | 罗皓晨 | 冻结 F01~F03 注册后登录、单一登录凭证、全部旧凭证失效、资料字段和地址默认切换边界 | +| v0.8 | 2026-07-24 | 罗皓晨 | 冻结 F13 买家与商家分流、非默认商家责任清单、禁用竞争顺序和全部旧凭证失效边界 | ## 业务流程设计入口 @@ -1399,10 +1400,10 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 | 编号 | 功能 | 详细要求 | |---|---|---| | M06-03-FR01 | 用户列表 | 分页展示买家和商家账号的用户名、掩码手机号、角色、状态和注册时间 | -| M06-03-FR02 | 筛选查询 | 支持按用户名、手机号、角色和状态等已确认条件筛选 | -| M06-03-FR03 | 禁用账号 | 将可禁用的正常买家或商家变为禁用状态,并立即使旧令牌失效;默认商家及仍有待处理业务的商家必须拒绝禁用 | -| M06-03-FR04 | 启用账号 | 将禁用账号恢复为正常状态;禁用前令牌不恢复,用户必须重新登录获取新令牌 | -| M06-03-FR05 | 状态幂等 | 重复禁用或重复启用返回当前结果,不产生矛盾状态 | +| M06-03-FR02 | 筛选查询 | 用户名、完整手机号、角色和状态使用独立字段精确筛选;列表按注册时间倒序、账号 ID 倒序稳定分页,响应手机号始终掩码 | +| M06-03-FR03 | 禁用账号 | 正常买家可直接禁用且既有业务事实保持不变;默认商家始终拒绝禁用;非默认商家只有在固定责任清单全部为空时才可禁用;任何禁用成功都立即使此前全部登录凭证失效 | +| M06-03-FR04 | 启用账号 | 将禁用账号恢复为正常状态;禁用前登录凭证不恢复,用户必须重新登录获取新凭证 | +| M06-03-FR05 | 状态幂等 | 禁用和启用均携带稳定请求标识;同标识同目标同动作重放首次确定结果,同标识换目标或动作被拒绝;使用新标识重复禁用或启用时返回当前状态且不重复产生副作用 | | M06-03-FR06 | 敏感信息保护 | 手机号默认掩码,只返回管理操作所需字段 | | M06-03-FR07 | 安全追踪 | 禁用和启用记录操作人、目标账号、原状态、新状态、时间和 `traceId`,不扩展为通用后台操作日志功能 | | M06-03-FR08 | 用户反馈 | 危险操作需确认,成功后立即刷新状态,失败时说明原因 | @@ -1411,9 +1412,11 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 1. 管理员进入用户管理页,分页筛选买家或商家账号。 2. 管理员选择目标账号并确认禁用或启用。 -3. 服务端校验管理员身份、目标角色和当前状态。 -4. 禁用时更新账号状态并登记现有令牌失效;启用时恢复账号状态。 -5. 页面展示最新状态;被禁用户再次登录或使用旧令牌时均被拒绝。 +3. 服务端校验管理员身份、稳定请求标识和目标 ID 固定格式;若原请求已有确定结果则直接重放。 +4. 全新请求读取目标角色和当前状态,并先区分买家与商家。 +5. 禁用买家时不检查或修改其订单、支付、售后事实;禁用商家时先保护默认商家,再按固定责任清单复核是否仍承担业务。 +6. 允许禁用时形成“账号已禁用 + 此前全部登录凭证失效”的确定结果;启用只恢复账号状态,不恢复旧凭证。 +7. 页面展示最新状态;被禁用户再次登录或使用旧凭证时均被拒绝。 #### 5. 业务规则与权限 @@ -1421,8 +1424,14 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 - 本模块不提供角色修改字段,禁用和启用不能改变账号角色。 - 多实例下的账号禁用和登录凭证失效结果必须一致。 - 重复禁用或启用必须保持结果一致,不得产生相互矛盾的账号状态。 +- 完成管理员身份与固定格式校验后,状态变更必须先按稳定请求标识查询既有确定结果,再读取当前账号状态和商家责任;网络重试不能越过后来发生的反向状态变更重新执行旧动作。 - 单店模式由 Identity 保证至多一个启用的默认商家,并由部署/种子数据保证实际存在一个;本期不通过 A016 迁移默认商家,默认账号直接禁用返回冲突。 -- 非默认商家仍有关联待履约订单、售后期限内订单、未完成售后或未结束秒杀活动时不得禁用;不自动把业务归属改给其他账号。 +- 买家禁用不以订单、支付、售后、收藏或历史记录作为阻断条件,也不删除或改写这些既有事实;禁用只阻止其后续登录和受保护业务操作。 +- 非默认商家的固定禁用阻断责任为:`PendingPayment` 订单、仍有可履约数量的 `Paid` 订单、`Shipped` 订单、完成后 7 天售后窗口内订单、任一非终态售后申请,或任一未结束秒杀活动;任一存在即拒绝禁用,且不自动改派业务归属。 +- 已全量退款、剩余可履约数量为 0 且没有非终态售后的 `Paid` 订单不再仅因状态名阻断禁用。 +- 商品属于单店统一经营目录,不按商家账号划分所有权;商品引用、创建或编辑记录不是非默认商家禁用条件。 +- 禁用与新责任受理必须形成唯一先后结果:新责任先成立则禁用被拒绝;禁用先成立则后续订单分配、售后受理和秒杀活动创建/推进拒绝该商家。 +- 禁用只有在账号状态和此前全部登录凭证失效形成确定结果后才可返回成功;任何实例无法确认账号状态或失效事实时,相关受保护请求必须失败关闭。 - 手机号默认掩码,列表不得返回密码哈希、完整 Token 或私人业务数据。 - 只有管理员 Policy 可以调用列表和状态变更接口。 @@ -1434,9 +1443,11 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 | 目标账号不存在或不是买家/商家 | 拒绝操作,不泄露额外信息 | | 尝试修改角色或管理员账号 | 接口不接受相关字段或明确拒绝 | | 重复禁用或启用 | 返回当前状态,不重复产生副作用 | +| 网络结果未知后重试 | 使用原稳定请求标识重放首次确定结果;同标识换目标或动作被拒绝,瞬态失败不固化 | | 并发状态变更 | 仅符合当前状态的一次更新成功,其余返回最新状态 | | 禁用默认商家或仍有待处理业务的商家 | 返回 409 和明确原因,账号及业务归属不变 | | 登录凭证失效能力暂时不可用 | 禁用操作不得返回虚假成功;无法确认登录态的受保护请求提示服务暂不可用 | +| 禁用与新商家责任并发 | 只允许一个先成立;落败动作返回最新账号状态或明确责任阻断,不出现已禁用商家获得新责任 | #### 7. 验收标准与证据 @@ -1446,7 +1457,9 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 - 买家、商家和游客不能访问管理接口,管理员账号和角色字段不能通过本模块操作。 - 重复与并发状态变更保持幂等,安全追踪记录不包含敏感凭据。 - 默认商家和存在待处理业务的商家禁用保护可复现,普通可禁用商家仍可完成禁用/启用验收。 -- 保存列表、禁用、启用、旧令牌失效、越权和角色注入拦截证据。 +- 买家禁用不因其既有订单、支付或售后事实被阻断,且这些事实不被删除或改写。 +- 新商家责任与禁用并发时只能一个先成立;禁用成功后该商家不能再获得新的订单、售后或秒杀责任。 +- 保存列表、禁用、启用、旧凭证失效、越权和角色注入拦截证据。 ### M07 商品评价与晒图(X01)— 顾欣月 @@ -2488,7 +2501,7 @@ Electron 后续原则上复用 PC Web 原型,只补充窗口、桌面导航和 | F10 | M05-01 模拟支付—张海洋 | 小金库、统一收银台、支付记录 | A401~A408 | 待测试计划登记 | 重复支付与幂等语义已统一,待数据库、OpenAPI 与交叉评审 | | F11 | M06-01 后台商品管理—顾欣月 | 商家分类管理、商品管理 | A110~A114、A120~A128 | 待测试计划登记 | 创建、图片顺序与同步索引边界已统一,待数据库、OpenAPI 与交叉评审 | | F12 | M06-02 后台订单管理—韦乾强 | 商家订单列表、详情和发货入口 | A305~A307 | 待测试计划登记 | Ordering 命名、状态与商家归属已统一,待数据库、OpenAPI 与交叉评审 | -| F13 | M06-03 后台用户管理—唐宇昊 | 管理端用户管理 | A015~A017 | 待测试计划登记 | 禁用、启用和旧令牌失效语义已统一,待数据库、OpenAPI 与交叉评审 | +| F13 | M06-03 后台用户管理—唐宇昊 | 管理端用户管理 | A015~A017 | 待测试计划登记 | 买家/商家分流、固定责任清单、禁用竞争和全部旧凭证失效已统一,待接口同步 | | X01 | M07 商品评价与晒图—顾欣月 | 订单评价入口、商品评价区 | A140~A143;A144 无业务来源,接口整合时取消 | 待测试计划登记 | 订单入口、提交重检、唯一评价、公开计分和图片边界已统一,待数据库、OpenAPI 与交叉评审 | | X02 | M08 收藏与浏览历史—唐宇昊 | 商品收藏、收藏页、浏览历史页 | A018~A025 | 待测试计划登记 | 浏览记录写入与设置查询已闭合,待数据库、OpenAPI 与交叉评审 | | X03 | M09 站内消息—罗皓晨 | 买家/商家消息中心 | A501~A505 | 待测试计划登记 | HTTP、集成事件和接收人边界已统一,待数据库、OpenAPI 与来源模块交叉评审 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" index 5dd9538..3547ff4 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -3,34 +3,34 @@ > - 覆盖:M06-03、F13 > - 主责人:唐宇昊 > - 需求来源:[《需求规格说明书》M06-03](../../../01-需求文档/需求规格说明书.md) 的“M06-03 后台用户管理(F13)— 唐宇昊”完整七节 -> - 基础核心流程:F02、M06-01 与 M06-02 的后台角色入口 +> - 基础核心流程:F02 认证授权;商家禁用约束直接衔接 M04、M06-02、M10 与 C01 的责任事实 > - 直接入口:管理员在管理端入口发起账号治理;M01-02 提供登录态校验与角色判断 -> - 直接出口:M01 账号状态变更和令牌版本提升;商家发货(M06-02)和商品维护(M06-01)继续遵守新状态 +> - 直接出口:M01 账号状态与全部旧凭证失效结果;后续所有受保护能力和商家责任受理遵守新状态 > - 回归核心结果:买家订单、支付和售后事实不变;商品事实不变 > - 不得改变:管理员账号不可操作,角色字段不可修改,禁用默认商家和仍有待处理业务的商家必须被拒绝 ## 一、范围与事实来源 -本流程负责管理员分页查看买家和商家账号、按受控条件筛选、对可禁用账号执行禁用或启用,并协调令牌版本提升和默认商家保护。它不提供管理员账号管理、角色修改、提权或普通用户资料编辑。 +本流程负责管理员分页查看买家和商家账号、按受控条件筛选、对可操作账号执行禁用或启用,并保护默认商家、非默认商家既有责任和全部旧登录凭证失效。它不提供管理员账号管理、角色修改、提权、普通用户资料编辑、商家责任改派或批量状态变更。 A015~A017 由本流程派生,仅在流程评审通过后用于契约映射;现有清单不能反向决定或拼接业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M06-03/F13 需求 | 完整定义 | 作为业务语义事实源 | -| 本文业务流程 | 初稿 | 先确认角色、判断、状态、事务边界和模块出入口 | +| 本文业务流程 | 完整定义 | 已确认角色分流、责任阻断、竞争结果、状态与模块出入口 | | A015~A017 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | | DBxxx 账号/操作记录表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | -| C10 多实例令牌失效 | 部分定义 | 只登记接入点;具体失效范围和延迟由 C10 评审 | +| C10 多实例凭证校验 | 部分定义 | 只登记接入点;所有实例必须一致遵守 Identity 的账号状态与失效事实 | ## 二、模块直接出入口 ```mermaid flowchart LR ADM["管理员"] -->|"管理端用户管理"| MGMT["M06-03 Admin
账号治理"] - MGMT -->|"最新账号状态 + 令牌版本提升"| M01["M01 Identity
登录态、令牌"] - MGMT -->|"商家禁用约束"| M06P["M06-01 商家商品入口"] - MGMT -->|"商家禁用约束"| M06O["M06-02 商家履约入口"] + MGMT -->|"最新账号状态 + 全部旧凭证失效"| M01["M01 Identity
账号与认证"] + MGMT -->|"账号状态约束"| M06P["M06-01 商品管理入口"] + MGMT -->|"账号状态与责任约束"| M06O["M06-02 商家履约入口"] MGMT -->|"查询过滤"| DB["账号与角色事实"] MGMT -->|"业务归属复核"| ORD["M04 Ordering
应用契约"] MGMT -->|"业务归属复核"| AFT["M10 AfterSales
应用契约"] @@ -44,8 +44,9 @@ flowchart LR 边界约束: - M06-03 只对买家和商家账号执行禁用或启用,不允许修改角色或操作管理员账号。 -- 禁用账号必须提升账号令牌版本,使修改前签发的全部登录态失效;启用后旧登录态不恢复,用户必须重新登录。 -- 商家禁用约束由 M06-03 强制执行,具体可执行操作由 M06-01 和 M06-02 依据账号状态决定。 +- 禁用成功必须同时形成账号禁用与此前全部登录凭证失效的确定结果;启用后旧凭证不恢复,用户必须重新登录。 +- 买家禁用不查询业务责任;商家禁用先保护默认商家,再通过 Ordering、AfterSales、Seckill 的公开应用契约复核固定责任清单。 +- 商品属于统一经营目录,不以商品引用或维护记录阻断非默认商家禁用;M06-01 只在后续操作时遵守账号状态。 ## 三、列表与筛选主流程 @@ -56,7 +57,7 @@ flowchart TD B -- "是" --> C["按分页和筛选条件查询账号"] C --> D{"筛选条件?"} D -- "无" --> E["返回当前页买家和商家账号"] - D -- "用户名/手机号关键词" --> F["按已确认字段精确或模糊匹配"] + D -- "用户名/手机号" --> F["按独立字段精确匹配"] D -- "角色" --> G["按买家或商家过滤"] D -- "状态" --> H["按正常或禁用过滤"] E --> I["返回账号摘要:用户名、掩码手机号、角色、状态、注册时间"] @@ -68,7 +69,9 @@ flowchart TD 列表要求: -- 分页参数必须校验:`page` 从 1 开始,`pageSize` 默认 10、上限 50。 +- 分页参数必须遵守接口文档统一约定;非法页码或页大小拒绝,不在本流程单独发明另一套默认值和上限。 +- 用户名和手机号使用独立筛选字段并精确匹配;手机号输入需满足账号手机号格式,响应仍只返回掩码值。 +- 列表按注册时间倒序、账号 ID 倒序形成稳定分页;角色和状态只接受已确认枚举。 - 列表不返回密码哈希、完整手机号或非必要私人数据。 - 筛选条件必须使用受控字段,禁止拼接任意字段作为查询条件。 - 管理员账号不出现在列表中;不允许通过筛选绕过隐藏。 @@ -77,108 +80,122 @@ flowchart TD ```mermaid flowchart TD - A[“管理员选择目标账号并确认禁用”] --> B{“目标账号合法?”} - B -- “否” --> X[“拒绝操作并提示原因”] - B -- “是” --> C[“开启 PostgreSQL 事务并按目标账号条件锁”] - C --> D[“读取当前账号状态和令牌版本号,记录为禁用版本基准”] - D --> E{“当前状态?”} - E -- “已禁用” --> Y[“回滚事务,幂等返回当前状态,不重复产生副作用”] - E -- “正常” --> F[“在同一受控事务内调用 Ordering 公开应用契约复核阻断条件”] - F --> G[“在同一受控事务内调用 AfterSales 公开应用契约复核阻断条件”] - G --> H[“在同一受控事务内调用 Seckill 公开应用契约复核阻断条件”] - H --> I{“是否仍有核心订单责任、售后申请窗口、非终态售后或未结束活动?”} - I -- “是” --> Z[“回滚事务,拒绝禁用,账号及业务归属保持不变”] - Z --> Z1[“将复核失败原因写入结构化日志,等待管理员决策”] - I -- “否” --> J[“条件更新:仅在状态为正常且令牌版本匹配禁用版本基准时改为禁用并提升令牌版本号”] - J --> K{“条件更新是否影响行?”} - K -- “否” --> K1[“回滚事务,拒绝并返回最新状态”] - K -- “是” --> L[“提交 PostgreSQL 事务”] - L --> M[“PostgreSQL 为事实,Redis 撤销集合随后失效旧登录态”] - M --> N{“Redis 撤销状态共享是否可用?”} - N -- “否” --> N1[“返回服务暂不可用,账号状态和令牌版本仍按已提交结果生效”] - N -- “是” --> O[“记录操作人、目标账号、原状态、新状态、时间和 traceId”] - O --> P[“返回最新账号状态”] - P --> Q[“被禁账号再次登录或使用旧登录态被拒绝”] + A["管理员选择目标账号并确认禁用"] --> A0["校验管理员身份、请求标识和目标 ID 格式"] + A0 --> A1{"稳定请求标识已有结果?"} + A1 -- "同目标同动作" --> A2["重放首次确定结果"] + A1 -- "换目标或动作" --> A3["拒绝请求标识复用"] + A1 -- "全新" --> B{"目标账号存在且角色为买家或商家?"} + B -- "否" --> X["拒绝操作,不泄露额外账号信息"] + B -- "是" --> C{"当前状态?"} + C -- "已禁用" --> Y["幂等返回当前 Disabled,不重复产生副作用"] + C -- "正常买家" --> D["不查询或修改订单、支付、售后等既有事实"] + C -- "正常商家" --> E{"是否为默认商家?"} + E -- "是" --> E1["拒绝禁用,默认商家保持启用"] + E -- "否" --> F["通过公开应用契约复核固定商家责任清单"] + F --> G{"任一阻断责任存在?"} + G -- "是" --> G1["拒绝禁用,返回明确责任原因且不改派业务"] + G -- "否" --> H["与新商家责任受理形成唯一先后结果"] + H --> I{"哪一方先成立?"} + I -- "新责任" --> G1 + I -- "禁用" --> J["后续新订单、售后和秒杀责任不得再分配给该商家"] + D --> K["提交账号禁用、全部旧凭证失效和最小安全追踪事实"] + J --> K + K --> L{"结果是否确定提交?"} + L -- "否" --> L1["不返回成功;重试读取最新状态,受保护请求失败关闭"] + L -- "是" --> M["返回 Disabled 和状态变化时间"] + M --> N["全部旧凭证不能再访问任何受保护能力"] ``` 禁用约束: -- 默认商家始终拒绝禁用。非默认商家的阻断口径在流程阶段固定为:存在 `PendingPayment` 订单、仍有可履约数量的 `Paid` 订单、`Shipped` 订单、完成后 7 天售后窗口内订单、任一非终态售后申请,或任一未结束秒杀活动。已全量退款、剩余可履约数量为 0 且无非终态售后的 `Paid` 订单不再单凭状态名永久阻断;账号及业务归属保持不变。 -- 业务归属复核必须发生在 PostgreSQL 事务内部,与账号状态条件更新共享同一事务边界;Ordering、AfterSales、Seckill 必须提供能在该受控事务内阻止新业务归属的版本事实或行锁,否则本流程不能消除 TOCTOU 漏洞。具体事务边界由系统架构设计承接,本流程不规定应用层 HTTP 调用顺序。 -- PostgreSQL 事务以目标账号为锁起点;条件更新除匹配 `status = 正常` 外还必须匹配复核阶段读取的令牌版本号,避免复核到提交之间状态被并发改变。 -- PostgreSQL 与 Redis 不能组成同一事务;PostgreSQL 提交后 Redis 撤销不可用时,必须返回服务暂不可用,不允许回滚已经持久化的状态变更(避免出现”禁用后又回滚导致旧登录态生效”的更大问题),同时也不允许返回虚假成功。 -- 状态变更可追踪:操作人、目标账号、原状态、新状态、时间和 `traceId` 写入结构化日志,不写入通用操作审计。 +- 完成管理员身份、请求标识和目标 ID 固定格式校验后,必须先查询稳定请求结果,再读取账号状态和商家责任;同标识同目标同动作重放首次确定结果,同标识换目标或动作拒绝。 +- 已确定的成功、默认商家拒绝、责任阻断和当前状态结果可以绑定稳定请求标识重放;依赖不可用、提交结果未知等瞬态失败不得固化为确定业务结果。 +- 买家禁用不以既有订单、支付、售后、收藏或历史记录为阻断条件,也不删除、取消、退款或改写这些事实;禁用后只阻止新的登录和受保护业务操作。 +- 默认商家始终拒绝禁用。非默认商家的固定阻断责任为:`PendingPayment` 订单、仍有可履约数量的 `Paid` 订单、`Shipped` 订单、完成后 7 天售后窗口内订单、任一非终态售后申请,或任一未结束秒杀活动。 +- 已全量退款、剩余可履约数量为 0 且没有非终态售后的 `Paid` 订单不再仅凭 `Paid` 状态永久阻断。 +- 商品属于单店统一经营目录,不按商家账号划分所有权;商品引用、创建或编辑记录不是禁用阻断条件。 +- Ordering、AfterSales、Seckill 只通过公开应用契约提供责任事实和新责任受理门槛,不允许 M06-03 直接读取其内部表、仓储或上下文。 +- 商家责任复核与后续账号禁用不能留下检查到提交之间的空档:新责任先成立则禁用被阻断;禁用先成立则后续责任受理必须拒绝该商家。具体并发控制由架构、接口和数据设计承接,业务流程不规定跨模块共享事务、行锁或缓存实现。 +- 禁用是 Identity 内的单一确定结果:账号状态、此前全部登录凭证失效边界、最小安全追踪事实和稳定请求结果必须一起成功或一起失败。结果未知时不得返回成功,重试使用原请求标识确认首次结果。 +- 最小安全追踪只记录操作人、目标账号、原状态、新状态、时间和 `traceId`,不扩展为通用后台操作审计,也不记录密码、JWT 或完整手机号。 ## 五、启用账号 ```mermaid flowchart TD - A["管理员选择目标账号并确认启用"] --> B{"目标账号合法?"} + A["管理员选择目标账号并确认启用"] --> A0["校验管理员身份、请求标识和目标 ID 格式"] + A0 --> A1{"稳定请求标识已有结果?"} + A1 -- "同目标同动作" --> A2["重放首次确定结果"] + A1 -- "换目标或动作" --> A3["拒绝请求标识复用"] + A1 -- "全新" --> B{"目标账号合法?"} B -- "否" --> X["拒绝操作并提示原因"] - B -- "是" --> C["开启 PostgreSQL 事务并按目标账号条件锁"] - C --> D["读取当前状态并记录为启用版本基准"] - D --> E["条件更新:仅在状态为禁用且令牌版本匹配启用版本基准时改为正常"] - E --> F{"条件更新是否影响行?"} - F -- "否且当前已正常" --> Y["回滚事务,幂等返回当前状态"] - F -- "否且当前已是其他状态" --> Y1["回滚事务,拒绝并说明原因"] - F -- "是" --> G["提交 PostgreSQL 事务"] - G --> H["不恢复任何旧登录态,账号令牌版本保持当前值"] - H --> I["记录操作人、目标账号、原状态、新状态、时间和 traceId"] - I --> J["返回最新账号状态"] - J --> K["用户重新登录获取新登录态"] + B -- "是" --> C{"当前状态?"} + C -- "已正常" --> Y["幂等返回当前 Normal,不重复产生副作用"] + C -- "已禁用" --> D["提交账号启用和最小安全追踪事实"] + D --> E{"结果是否确定提交?"} + E -- "否" --> E1["不返回成功;重试读取最新状态"] + E -- "是" --> F["返回 Normal 和状态变化时间"] + F --> G["禁用前全部登录凭证仍保持失效"] + G --> H["用户重新提交手机号和密码登录"] ``` 启用约束: +- 启用使用与禁用相同的稳定请求规则;原启用请求的网络重试不能因为账号后来又被禁用而重新执行一次启用。 - 启用只改变账号状态,不恢复任何旧登录态;用户必须重新登录。 -- 启用不修改 Redis 撤销集合;旧登录态即使未过期也无法继续使用。 -- 启用与禁用共用同一受控事务边界,按目标账号条件锁起始;条件更新除匹配状态外还匹配启用版本基准,避免并发覆盖。 +- 启用不清除或回退禁用时形成的旧凭证失效边界;旧凭证即使未过期也无法继续使用。 +- 启用不重新检查或改写买家、商家的既有业务事实;后续新业务仍在受理时检查当前账号角色和状态。 +- 启用只在 Identity 内提交账号状态和最小安全追踪事实,具体并发与存储机制由后续设计承接。 - 重复启用必须幂等,不产生相互矛盾的状态或重复副作用。 ## 六、并发、幂等与异常 ```mermaid flowchart TD - A1["两名管理员并发禁用同一账号"] --> B1["目标账号条件锁串行化;后提交者条件更新不命中令牌版本"] - B1 --> N1["回滚事务并返回最新状态,不重复产生副作用"] - A2["同一账号在禁用与启用之间切换"] --> B2["按状态与令牌版本条件更新,最终状态唯一确定"] - B2 --> N2["不出现两种状态同时生效"] + A1["两名管理员并发禁用同一账号"] --> B1["各请求标识形成独立且可排序的确定结果"] + B1 --> N1["最多一次形成状态变化,其余返回最新 Disabled"] + A2["同一账号并发禁用与启用"] --> B2["按已提交状态形成唯一先后顺序"] + B2 --> N2["各请求返回自己观察到的确定结果,不出现双状态"] A3["尝试修改角色或管理员账号"] --> B3["接口不接受相关字段或明确拒绝"] B3 --> N3["不返回修改后的角色"] - A4["Redis 撤销状态共享暂时不可用"] --> B4["返回服务暂不可用"] - B4 --> N4["账号状态仍按已提交结果生效,不允许回滚"] - A5["商家在事务内新产生订单或售后"] --> B5["Ordering、AfterSales、Seckill 必须提供能阻止新业务归属的版本事实或行锁"] - B5 --> N5["本事务已锁定的版本事实保证新业务不会绕过阻断条件"] - A6["应用契约不能在同一受控事务内阻止新业务归属"] --> B6["视为架构级能力缺口,必须在系统架构和接口设计中补齐"] - B6 --> N6["否则本流程不能消除 TOCTOU,本流程文档不构成完成"] + A4["账号状态或旧凭证失效事实无法确认"] --> B4["状态变更不返回成功"] + B4 --> N4["依赖该事实的受保护请求失败关闭"] + A5["非默认商家禁用与新责任并发"] --> B5["公开应用契约形成唯一先后结果"] + B5 --> N5["新责任先成立则阻断禁用;禁用先成立则拒绝新责任"] + A6["责任复核应用契约不可用"] --> B6["不把未知当作无责任"] + B6 --> N6["拒绝本次禁用并提示服务暂不可用"] A7["非管理员访问管理端入口"] --> B7["拒绝访问,不返回账号列表"] A8["筛选或分页参数非法"] --> B8["字段级错误,保留可恢复输入"] - A9["商家仍有未完成售后或关联订单"] --> B9["业务归属复核返回存在待处理业务"] + A9["商家存在固定清单内责任"] --> B9["业务责任复核返回具体阻断原因"] B9 --> N9["拒绝禁用并说明原因,不自动改派业务归属"] + A10["买家存在订单、支付或售后"] --> B10["不进入商家责任复核"] + B10 --> N10["既有事实保持不变,允许执行账号禁用"] ``` 异常约束: -- 任何并发变更的最终结果必须保持一致;不允许出现“禁用后又启用”或反之的中间结果被外部观察。 +- 任何并发变更都必须形成可排序的提交顺序;同一时点只有一个当前状态,后续请求读取最近已提交结果。 - 禁用或启用操作必须记录操作人、目标账号、原状态、新状态、时间和 `traceId`,不写入密码或完整登录态。 -- PostgreSQL 与 Redis 的状态变更天然不在同一事务,必须以 PostgreSQL 状态和令牌版本为安全事实;Redis 撤销集合只在 PostgreSQL 提交成功后追加。 -- 重复禁用或启用必须幂等返回当前状态,不重复产生副作用。 +- 账号状态和旧凭证失效边界是 Identity 的安全事实;缓存或其他基础设施不得成为唯一事实,也不得在无法确认时放行。 +- 商家责任模块只暴露完成复核与新责任受理门槛所需的公开应用契约;实现不得以跨模块直接读表或共享仓储代替。 +- 同一稳定请求标识重放首次确定结果;使用新标识重复禁用或启用时返回当前状态,不重复产生副作用。 ## 七、与核心模块的衔接 | 上游 | 入口事实 | 下游 | 出口结果 | |---|---|---|---| | M01-02 | 管理员登录态 | M06-03 | 后台访问权限 | -| M06-03 | 禁用/启用命令 | M01 Identity | 账号状态与令牌版本提升 | -| M06-03 | 商家禁用约束 | M06-01 | 商品维护可继续遵守账号状态 | -| M06-03 | 商家禁用约束 | M06-02 | 商家发货和售后审核遵守账号状态 | +| M06-03 | 禁用/启用命令 | M01 Identity | 账号状态、全部旧凭证失效边界和最小安全追踪 | +| M06-03 | 买家禁用结果 | M03、M04、M05、M07、M08、M09、M10 | 既有事实保持不变;新的受保护动作按禁用状态拒绝 | +| M06-03 | 商家账号状态 | M06-01 | 商品属于统一经营目录;后续商品维护按账号状态受理 | +| M06-03 | 商家账号状态与责任约束 | M06-02 | 商家履约和售后审核按账号状态与既有责任受理 | | M06-03 | 业务责任复核 | Ordering、AfterSales、Seckill 公开应用契约 | 最新核心订单责任、售后窗口、非终态售后和活动状态 | -| M06-03 | 操作记录 | 管理员查询 | 操作人、时间、`traceId` 可追踪 | +| M06-03 | 最小安全追踪事实 | 运行观测与问题定位 | 可按操作人、目标账号、时间和 `traceId` 关联,不新增管理员查询接口 | 衔接约束: -- 商家禁用约束由 M06-03 在治理事务中校验;M06-01 和 M06-02 仍需在自身业务动作前再次校验账号状态。 -- 禁用或启用结果必须在多实例之间一致;具体失效延迟由 C10 和 Redis 撤销能力共同保证。 +- 商家禁用由 M06-03 发起责任复核;Ordering、AfterSales、Seckill 在接收新责任时仍必须重新校验商家当前状态并参与唯一先后判定。 +- 禁用或启用结果必须被所有实例一致执行;无法确认 Identity 安全事实时,受保护请求失败关闭,不允许设置“稍后才失效”的成功窗口。 - 管理员账号治理不直接修改订单、支付或售后事实;只通过账号状态影响后续业务受理。 - 业务归属复核必须使用目标模块的公开应用契约,不能直接读取目标模块内部表。 @@ -186,28 +203,32 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 分页查询账号 | A015 | 返回买家和商家账号摘要与筛选结果 | 待交叉评审 | -| 禁用账号 | A016 | 校验可禁用条件、提升令牌版本、按 Redis 可用性返回结果 | 待交叉评审 | -| 启用账号 | A017 | 校验账号并恢复状态,旧登录态不恢复 | 待交叉评审 | +| 分页查询账号 | A015 | 只返回买家和商家账号摘要、受控筛选结果与稳定分页 | 流程已确认,待接口同步 | +| 禁用账号 | A016 | 使用稳定请求标识;先按买家/商家分流,商家保护默认账号并复核固定责任清单;成功时账号禁用、全部旧凭证失效和最小追踪形成确定结果 | 流程已确认,待接口同步 | +| 启用账号 | A017 | 使用稳定请求标识恢复账号状态但不恢复任何旧凭证;重放首次结果或返回当前状态 | 流程已确认,待接口同步 | 接口仅承载本流程结果;错误码、HTTP 状态码和 `traceId` 由接口设计统一,不得反向写入业务流程。 -## 九、由流程反查出的接口与数据待评审项 +## 九、下游契约与数据约束 -1. 多实例令牌失效的可见延迟需在 C10 与 M00 评审中明确,本流程不预设具体延迟。 -2. 操作记录是否长期保留或定期归档需在数据库设计中明确,本流程只承诺最小记录字段。 -3. 禁用或启用操作是否需要支持批量,本期不实现。 -4. 商家禁用的“业务归属转移”本期不实现,需明确告知管理员原因。 -5. Redis 撤销不可用时的返回码(503 / AUTH.TOKEN_SERVICE_UNAVAILABLE)和前端降级策略需在接口设计中明确。 +1. A015 只允许管理员查询买家和商家摘要;手机号始终掩码,管理员账号不在结果中。分页上下限服从接口文档统一约定,不在业务流程另造一套。 +2. A016 必须先区分买家和商家。买家不调用商家责任复核;非默认商家的每类阻断责任要返回稳定、可理解的原因,默认商家始终返回冲突。 +3. Ordering、AfterSales、Seckill 的内部应用契约必须同时支持“读取当前责任”和“新责任受理时拒绝已禁用商家”,并与 A016 形成唯一先后结果;具体锁、版本或消息机制由后续架构和数据设计选择。 +4. A016、A017 都必须携带稳定请求标识:同标识同目标同动作重放首次确定结果,同标识换目标或动作拒绝;新标识面对已处于目标状态的账号返回当前状态且不重复产生追踪事实。 +5. Identity 数据设计必须让账号状态、旧凭证失效边界、最小安全追踪和稳定请求结果成为一个确定提交结果;启用不得回退旧凭证失效边界,瞬态失败不得固化。 +6. 本期不支持批量禁用/启用、商家责任迁移或商品所有权转移,也不扩展通用后台操作审计。 ## 十、验收证据清单 - [ ] 管理员可以分页筛选买家和商家账号,手机号默认掩码。 - [ ] 失效能力正常时,禁用后不能再次登录,禁用前的登录态也不能继续访问;启用后旧登录态仍不可用,用户可重新登录。 -- [ ] Redis 撤销不可用时返回服务暂不可用,不返回虚假成功;恢复后多个服务实例必须得到一致结果。 -- [ ] 业务归属复核在禁用提交前完成;存在待处理业务的商家禁用被拒绝,且不影响业务归属。 -- [ ] PostgreSQL 状态和令牌版本与 Redis 撤销集合明确分工:PostgreSQL 为安全事实,Redis 为共享加速,PostgreSQL 提交后不允许回滚。 +- [ ] 账号状态或旧凭证失效事实无法确认时不返回虚假成功,相关受保护请求失败关闭;恢复后多个服务实例得到一致结果。 +- [ ] 买家即使存在订单、支付或售后事实也可禁用,既有事实不被删除、取消或改写。 +- [ ] 业务责任复核在商家禁用提交前完成;存在固定清单内责任的商家禁用被拒绝,且不影响业务归属。 +- [ ] 禁用与新商家责任并发时只有一个先成立;禁用成功后不会再分配新订单、售后或秒杀责任。 - [ ] 买家、商家和游客不能访问管理接口,管理员账号和角色字段不能通过本模块操作。 - [ ] 重复与并发状态变更保持幂等,安全追踪记录不包含敏感凭据。 +- [ ] 原禁用/启用请求在网络结果未知后使用同一请求标识重试,只重放首次确定结果;其间发生反向状态变更也不会让旧请求再次执行。 - [ ] 默认商家和存在待处理业务的商家禁用保护可复现,普通可禁用商家仍可完成禁用/启用验收。 +- [ ] 商品引用或维护记录不会被误判为非默认商家禁用阻断条件。 - [ ] 保存列表、禁用、启用、旧登录态失效、越权和角色注入拦截证据。 -- Gitee From 93449ba1556d88d167b15f5d106821f61dc79d4f Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 21:15:39 +0800 Subject: [PATCH 100/118] =?UTF-8?q?docs(process):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E6=94=B6=E8=97=8F=E4=B8=8E=E6=B5=8F=E8=A7=88=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=EF=BC=9B=E7=BB=9F=E4=B8=80=E5=B9=82=E7=AD=89=E5=92=8C=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E4=B8=8A=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 35 +++-- ...06\345\217\262\346\265\201\347\250\213.md" | 144 ++++++++++-------- 2 files changed, 101 insertions(+), 78 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 08238dd..8225481 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -1,6 +1,6 @@ # 电子商城需求规格说明书 -> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.8 +> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.9 > > 文档状态:内容和格式已完成内部统一,待全组与指导教师评审后冻结 > 截止:第 1 周周三提交初稿 @@ -17,6 +17,7 @@ | v0.6 | 2026-07-24 | 罗皓晨 | 冻结 C04 关键词分流、统一过滤与降级口径,明确搜索不缓存且正式 60 秒性能采样不包含预热 | | v0.7 | 2026-07-24 | 罗皓晨 | 冻结 F01~F03 注册后登录、单一登录凭证、全部旧凭证失效、资料字段和地址默认切换边界 | | v0.8 | 2026-07-24 | 罗皓晨 | 冻结 F13 买家与商家分流、非默认商家责任清单、禁用竞争顺序和全部旧凭证失效边界 | +| v0.9 | 2026-07-24 | 罗皓晨 | 冻结 X02 收藏幂等、浏览历史默认开启与最近 200 条上限,明确关闭记录不隐藏旧历史并取消清空历史能力 | ## 业务流程设计入口 @@ -1554,32 +1555,38 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 | 编号 | 功能 | 详细要求 | |---|---|---| -| M08-FR01 | 收藏商品 | 买家收藏商品,同一买家和商品只保留一条记录 | +| M08-FR01 | 收藏商品 | 已有本人收藏时直接返回既有结果;没有记录时只允许收藏当前存在且已上架的商品,同一买家和商品只保留一条记录 | | M08-FR02 | 取消收藏 | 买家取消本人收藏,重复取消保持幂等 | | M08-FR03 | 收藏列表 | 按最近收藏时间倒序分页展示商品名称、主图、当前价格和状态 | -| M08-FR04 | 记录浏览 | 买家查看商品详情时记录或更新最近浏览时间 | +| M08-FR04 | 记录浏览 | 历史开关开启时,买家成功打开已上架商品详情后记录或更新最近浏览时间;重复浏览更新时间并移动到最近位置 | | M08-FR05 | 浏览历史 | 按最近浏览时间倒序分页展示,重复浏览不新增重复记录 | -| M08-FR06 | 记录上限 | 按已确认上限保留最近记录,超出后移除最早记录 | -| M08-FR07 | 历史开关 | 买家可关闭或开启后续浏览记录,关闭不等于自动删除已有历史 | -| M08-FR08 | 下架提示 | 下架商品记录继续展示,但购买入口禁用并说明原因 | +| M08-FR06 | 记录上限 | 每名买家最多保留最近 200 条浏览记录;每次写入或更新时间时同步移除最早记录,提交结果不得超过 200 条 | +| M08-FR07 | 历史开关 | 默认开启;关闭只停止后续浏览记录写入,不删除、不隐藏已有历史,历史列表始终可分页查看 | +| M08-FR08 | 不可用提示 | 商品下架时个人记录继续展示并标记“已下架”;M06-01 必须阻止删除仍有收藏或浏览引用的商品,若防御性读取仍遇到商品事实缺失,则展示“商品已不存在”占位并禁用详情与购买入口 | | M08-FR09 | 状态反馈 | 收藏、取消、加载和空列表提供即时、可恢复反馈 | #### 4. 主流程 -1. 买家在商品详情收藏商品,服务端按买家和商品完成幂等写入。 +1. 买家在商品详情收藏商品,服务端先查询本人既有收藏;已存在则直接返回,没有记录时再校验商品已上架并创建,唯一约束处理并发兜底。 2. 买家进入收藏列表,按最近时间查看商品当前摘要和可售状态。 3. 买家取消收藏,列表立即更新;失败时恢复原状态并允许重试。 -4. 买家查看商品详情时,若历史记录开关开启,系统新增或更新该商品的最近浏览时间。 -5. 浏览记录超过上限时移除最早记录;商品下架后保留记录并显示不可售。 +4. 买家查看已上架商品详情时,若历史记录开关开启,系统新增或更新该商品的最近浏览时间并移动到最近位置。 +5. 每次记录或更新后同步裁剪到最近 200 条;关闭开关只停止后续写入,旧历史仍可查看;商品下架后保留记录并显示不可售。 #### 5. 业务规则与权限 - 收藏和浏览历史均以当前买家 ID 隔离,任何请求不得只凭记录 ID 访问。 - 同一买家对同一商品最多一条收藏和一条浏览记录。 - 收藏和浏览列表按各自最近活动时间倒序,排序结果稳定。 +- 收藏创建必须先读本人既有记录;已有记录直接返回,只有不存在时才校验商品当前已上架并创建,唯一约束只作为并发兜底。 +- 取消收藏按当前买家和商品处理;本人记录不存在时直接返回未收藏成功结果,不把重复取消误报为越权。 - 商品下架不删除个人记录,但不能从记录直接进入无效购买流程。 +- 收藏或浏览引用会按 M06-01 阻止商品物理删除;若数据异常导致列表读取不到商品事实,也不得把缺失伪装为下架或可售,统一展示“商品已不存在”占位。 - 游客不创建匿名历史;商家和管理员不能读取或维护买家私人数据。 -- 浏览记录上限及开关默认值在接口设计前统一确认。 +- 浏览历史默认开启,每名买家最多 200 条;重复浏览必须更新时间,写入/更新与裁剪形成一个确定结果,不能依赖异步清理最终收敛。 +- 记录浏览时服务端必须重新确认商品当前存在且已上架,不能相信客户端声称已打开详情;商品不存在或已下架时不新增或更新记录。 +- 历史开关只控制未来写入,不控制旧记录查询;关闭后列表仍返回已有历史。本期不提供清空浏览历史能力。 +- 开关切换与详情浏览并发时按提交先后处理:浏览写入先成立则保留该次记录,关闭先成立则该次不写入。 #### 6. 异常与边界场景 @@ -1588,6 +1595,8 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 | 商品不存在 | 拒绝新增记录,已有记录按不可用状态处理 | | 重复收藏或重复浏览 | 不新增重复行,只返回已有结果或更新时间 | | 重复取消收藏 | 返回幂等成功,不报系统异常 | +| 历史开关关闭 | 不新增或更新浏览记录,旧历史仍可正常分页查看 | +| 并发浏览导致超过 200 条 | 写入或更新与裁剪一并提交,最终只保留最近 200 条 | | 越权访问他人记录 | 返回资源不存在或无权限,不泄露归属 | | 商品被下架 | 保留记录并清楚标记不可购买 | | 网络失败 | 页面恢复原状态并允许重试,不制造假成功 | @@ -1598,7 +1607,9 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 - 重复收藏、重复浏览和重复取消满足幂等要求,不产生重复记录。 - 收藏和历史按最近时间倒序,数据严格按买家隔离。 - 下架商品仍保留记录并明确不可购买;游客、商家和管理员不能越权访问。 -- 保存正常操作、重复操作、下架占位、记录上限和跨账号隔离证据。 +- 历史默认开启且最多保留最近 200 条;关闭后不再新增或更新,但旧历史仍可分页查看。 +- 本期没有清空浏览历史入口或接口;重复取消本人不存在的收藏仍返回幂等成功。 +- 保存正常操作、重复操作、下架占位、200 条裁剪、开关切换、关闭后查看旧历史和跨账号隔离证据。 ### M09 站内消息通知(X03)— 罗皓晨 @@ -2503,7 +2514,7 @@ Electron 后续原则上复用 PC Web 原型,只补充窗口、桌面导航和 | F12 | M06-02 后台订单管理—韦乾强 | 商家订单列表、详情和发货入口 | A305~A307 | 待测试计划登记 | Ordering 命名、状态与商家归属已统一,待数据库、OpenAPI 与交叉评审 | | F13 | M06-03 后台用户管理—唐宇昊 | 管理端用户管理 | A015~A017 | 待测试计划登记 | 买家/商家分流、固定责任清单、禁用竞争和全部旧凭证失效已统一,待接口同步 | | X01 | M07 商品评价与晒图—顾欣月 | 订单评价入口、商品评价区 | A140~A143;A144 无业务来源,接口整合时取消 | 待测试计划登记 | 订单入口、提交重检、唯一评价、公开计分和图片边界已统一,待数据库、OpenAPI 与交叉评审 | -| X02 | M08 收藏与浏览历史—唐宇昊 | 商品收藏、收藏页、浏览历史页 | A018~A025 | 待测试计划登记 | 浏览记录写入与设置查询已闭合,待数据库、OpenAPI 与交叉评审 | +| X02 | M08 收藏与浏览历史—唐宇昊 | 商品收藏、收藏页、浏览历史页 | A018~A022、A024、A025;A023 无业务来源,接口整合时取消 | 待测试计划登记 | 收藏幂等、默认开启、最近 200 条及关闭后旧历史可见已统一,待接口同步 | | X03 | M09 站内消息—罗皓晨 | 买家/商家消息中心 | A501~A505 | 待测试计划登记 | HTTP、集成事件和接收人边界已统一,待数据库、OpenAPI 与来源模块交叉评审 | | X04 | M10 售后流程—张海洋 | 买家售后申请、商家售后审核 | A411~A417、A419、A432~A434;退款入账使用 Payment 应用契约 | 待测试计划登记 | A418/A431 为取消历史号,待数据库、OpenAPI 与交叉评审 | | C01 | 秒杀与防超卖—朱惠惠 | 秒杀活动页、商家活动管理、压测脚本 | A220~A228;订单查询复用 A302/A303;生命周期为 Worker 内部契约 | 待挑战测试登记 | 原子限购、库存通道与状态推进已定义,待数据库、OpenAPI 与跨模块联调 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" index 97885d6..aa3e4d6 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" @@ -11,15 +11,16 @@ ## 一、范围与事实来源 -本流程负责买家收藏商品、取消收藏、查看收藏列表、查看商品详情时记录或更新浏览时间、按上限保留历史记录、清空历史、开启或关闭后续记录。它不提供收藏分组、分享、跨账号迁移或推荐。 +本流程负责买家收藏商品、取消收藏、查看收藏列表、查看商品详情时记录或更新浏览时间、保留最近 200 条历史,以及开启或关闭后续记录。历史默认开启;关闭只停止未来写入,不删除或隐藏旧历史。本期不提供清空历史、收藏分组、分享、跨账号迁移或推荐。 -A018~A025 由本流程派生,仅在流程评审通过后用于契约映射;现有清单不能反向决定或拼接业务流程。 +A018~A022、A024、A025 由本流程派生;历史清单中的 A023 清空历史没有业务来源并取消。接口编号只在流程确认后用于契约映射,现有清单不能反向决定或拼接业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M08/X02 需求 | 完整定义 | 作为业务语义事实源 | -| 本文业务流程 | 初稿 | 先确认角色、判断、状态、事务边界和模块出入口 | -| A018~A025 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| 本文业务流程 | 完整定义 | 已确认角色、幂等、开关、记录上限、并发和模块出入口 | +| A018~A022、A024、A025 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| A023 清空浏览历史 | 无需求来源 | 取消,不进入实现 | | DBxxx 收藏/浏览表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | | M02 商品事实 | 部分定义 | 只登记接入点;价格、库存与销售状态由 Catalog 评审 | @@ -43,30 +44,36 @@ flowchart LR - M08 只读取商品事实,不修改商品销售状态、价格或库存。 - 收藏与浏览历史均按当前买家 ID 隔离,禁止跨用户访问。 - 游客不创建匿名记录;商家和管理员不能查看或维护买家私人数据。 +- 历史设置默认开启,只控制后续浏览写入;收藏与旧历史查询不受开关影响。 ## 三、收藏与取消主流程 ```mermaid flowchart TD - A["买家在商品详情或列表点击收藏"] --> B["服务端按买家和商品完成幂等写入"] - B --> C{"记录已存在?"} - C -- "否" --> D["新增收藏并记录收藏时间"] - C -- "是" --> E["保留原收藏时间,返回幂等成功"] - D --> F["返回最新收藏状态"] - E --> F - F --> G["收藏列表按最近收藏时间倒序展示"] - H["买家点击取消收藏"] --> I{"记录属于当前买家?"} - I -- "否" --> X["返回不存在或无权限,不泄露归属"] - I -- "是" --> J["删除收藏记录并刷新列表"] - J --> K{"取消成功?"} - K -- "是" --> L["页面立即更新为空"] - K -- "否" --> M["恢复原状态并允许重试"] + A["买家在商品详情或列表点击收藏"] --> B{"本人对该商品已有收藏?"} + B -- "是" --> C["返回既有收藏时间和当前商品可用状态"] + B -- "否" --> D{"商品当前存在且为 OnSale?"} + D -- "否" --> X["拒绝新建收藏并返回不可收藏原因"] + D -- "是" --> E["创建本人收藏并记录收藏时间"] + E --> F{"并发唯一性冲突?"} + F -- "是" --> G["读取并返回并发请求已创建的本人收藏"] + F -- "否" --> H["返回新收藏结果"] + C --> I["收藏列表按收藏时间倒序、记录 ID 倒序展示"] + G --> I + H --> I + J["买家点击取消收藏"] --> K["按当前买家和商品删除本人收藏"] + K --> L{"删除结果?"} + L -- "删除一条" --> M["返回未收藏并刷新列表"] + L -- "原本不存在" --> M + L -- "结果未知" --> N["不制造成功,页面恢复原状态并允许重试"] ``` 收藏约束: - 同一买家和同一商品最多一条收藏记录,重复点击按幂等处理。 -- 取消收藏同样按当前买家 ID 过滤,重复取消返回幂等成功。 +- 收藏动作先读取本人既有记录;已有记录直接返回,不因商品后来下架而创建第二条或丢失既有结果。只有不存在记录时才校验商品当前存在且已上架,然后创建。 +- 唯一约束只作为并发兜底:两个新建请求竞争时最多一条成功写入,另一个读回同一本人收藏结果。 +- 取消收藏按当前买家和商品删除;本人记录不存在时直接返回未收藏成功结果,不把重复取消误报为不存在或越权。 - 商品下架不删除收藏记录,但购买入口必须失效。 ## 四、浏览记录与历史列表 @@ -74,63 +81,66 @@ flowchart TD ```mermaid flowchart TD A["买家成功打开已上架商品详情"] --> B{"历史记录开关是否开启?"} - B -- "否" --> X["不写入浏览记录,返回开关关闭提示"] - B -- "是" --> C["开启浏览记录写入事务"] - C --> D["按买家和商品唯一约束写入或更新最近浏览时间"] - D --> E["按稳定排序裁剪到本买家上限以内(默认 200 条)"] - E --> F["提交事务,返回本次写入结果和清理条数"] - F --> G["浏览历史列表按最近浏览时间倒序展示"] - H["买家进入浏览历史列表"] --> I{"历史记录开关是否开启?"} - I -- "否" --> I1["返回空列表,关闭不等于删除已有历史"] - I -- "是" --> J["按当前买家分页返回浏览历史"] + B -- "否" --> X["不新增或更新记录;旧历史保持可查"] + B -- "是" --> C["写入新记录或更新该商品最近浏览时间"] + C --> D["按浏览时间倒序、记录 ID 倒序保留最近 200 条"] + D --> E["写入/更新与裁剪一并提交"] + E --> F["返回本次记录结果"] + H["买家进入浏览历史列表"] --> I["不受开关状态影响,按当前买家分页查询旧历史"] + I --> J["按浏览时间倒序、记录 ID 倒序返回"] + K["买家开启或关闭历史记录"] --> L["提交目标开关值"] + L --> M["返回当前开关;关闭不删除或隐藏旧历史"] ``` 浏览约束: -- 浏览记录上限默认 200 条,超出后按稳定排序裁剪;裁剪必须在同一事务内完成,避免多个详情并发写入时基于旧数量判断导致超限。 -- 历史开关关闭时浏览历史列表返回空数组;调用修改开关动作可重新开启。 -- 历史开关关闭不等于删除已有历史;重新开启后继续按规则更新最近浏览时间。 -- 同一买家和同一商品最多一条浏览记录;按唯一约束写入或更新。 +- 历史开关默认开启;没有单独设置事实时查询结果也必须为开启,查询本身不得为了默认值产生写操作。 +- A024 不能相信客户端声称已经打开详情;每次写入前必须重新确认商品当前存在且为 OnSale,商品不存在或已下架时不新建或更新浏览记录。 +- 每名买家最多保留最近 200 条浏览记录;写入或更新与稳定裁剪必须形成一个提交结果,多个详情并发写入后也不能超过 200 条,不能依赖异步清理。 +- 历史开关关闭只停止未来新增和最近浏览时间更新,浏览历史列表仍正常返回已有记录;重新开启后从下一次成功打开已上架商品详情开始继续记录。 +- 同一买家和同一商品最多一条浏览记录;重复浏览更新时间并移动到最近位置,不新增重复行。 +- 开关切换与详情浏览并发时按确定提交顺序处理:浏览记录先提交则保留该次结果,关闭先提交则该次不写入;不能出现关闭已先成功仍写入的结果。 - 浏览历史不得影响商品事实;商品下架、库存变化或价格调整均由 M02 决定。 ## 五、收藏与浏览列表的展示 ```mermaid flowchart TD - A["买家进入收藏列表"] --> B["按当前买家和最近收藏时间倒序分页"] + A["买家进入收藏列表"] --> B["按收藏时间倒序、记录 ID 倒序分页"] B --> C["读取商品摘要:名称、主图、当前价格、销售状态"] C --> D{"商品是否仍可售?"} D -- "是" --> E["展示可售状态和进入详情/加购入口"] D -- "否" --> F["保留记录并标记不可购买"] F --> G["提供返回列表或查看历史的入口"] - H["买家进入浏览历史列表"] --> I{"历史记录开关是否开启?"} - I -- "否" --> I1["返回空列表"] - I -- "是" --> J["按最近浏览时间倒序分页"] + H["买家进入浏览历史列表"] --> J["不检查开关,按浏览时间倒序、记录 ID 倒序分页"] J --> K["读取商品摘要与销售状态"] K --> L["下架商品标记不可购买,重复浏览不新增重复记录"] ``` 展示约束: -- 列表按各自最近活动时间倒序,排序结果稳定。 +- 列表按各自活动时间倒序、记录 ID 倒序,形成稳定分页。 - 下架商品保留记录并明确标记不可购买,但不删除个人记录。 +- 收藏或浏览引用应按 M06-01 阻止商品物理删除;若防御性读取仍遇到商品事实缺失,返回“商品已不存在”的不可用占位,不伪装成已下架,也不提供详情或购买入口。 - 收藏与浏览列表不返回他人或他人的历史条目,禁止仅凭记录 ID 跨用户访问。 +- 浏览历史列表始终可查询已有记录,不能因开关关闭返回空列表或隐藏旧数据。 ## 六、并发、幂等与异常 ```mermaid flowchart TD - A1["重复收藏同一商品"] --> B1["保持单条记录,重复请求幂等成功"] - A2["重复浏览同一商品"] --> B2["仅更新时间,不新增重复记录"] - A3["重复取消收藏"] --> B3["幂等成功,不报系统异常"] - A4["商品不存在"] --> B4["拒绝新增记录,已有记录按不可用状态处理"] - A5["越权访问他人记录"] --> B5["返回资源不存在或无权限,不泄露归属"] - A6["浏览记录超过上限"] --> B6["在同一事务内按稳定排序裁剪到上限以内"] - B6 --> N6["并发写入不会超过上限"] + A1["重复或并发收藏同一商品"] --> B1["先读本人记录,唯一约束兜底"] + B1 --> N1["保持单条记录并返回同一收藏结果"] + A2["重复浏览同一商品"] --> B2["更新时间并移到最近位置,不新增重复记录"] + A3["重复取消本人不存在的收藏"] --> B3["返回未收藏成功结果,不报不存在或越权"] + A4["商品不存在或已下架"] --> B4["不新建收藏或浏览记录;已有记录保留为不可用"] + A5["越权读取他人列表或记录"] --> B5["按当前买家隔离,不泄露归属"] + A6["并发浏览可能超过 200 条"] --> B6["写入/更新与稳定裁剪一并提交"] + B6 --> N6["最终只保留最近 200 条"] A7["网络或保存失败"] --> B7["恢复原状态,允许重试,不制造假成功"] - A8["历史开关关闭时仍尝试记录浏览"] --> B8["不写入新记录,不返回错误"] - A9["清空浏览历史"] --> B9["按当前买家物理删除全部历史记录"] - B9 --> N9["不影响历史开关状态"] + A8["历史开关关闭后打开商品详情"] --> B8["不新增或更新,旧历史仍正常可查"] + A9["关闭开关与记录浏览并发"] --> B9["按提交顺序只保留一个先成立结果"] + B9 --> N9["关闭先成立则不写;浏览先成立则保留该次记录"] ``` 异常约束: @@ -138,8 +148,8 @@ flowchart TD - 重复操作必须保持幂等,不产生重复记录或重复副作用。 - 商品下架不影响收藏和浏览记录的存在,但展示中必须标记不可购买。 - 任何写操作的失败都必须可重试,不留下半成功的状态。 -- 浏览记录写入与裁剪必须在同一事务内完成;不能先写入后异步清理。 -- 清空浏览历史后再次浏览商品仍按当前开关决定是否写入。 +- 浏览记录写入/更新与 200 条裁剪必须形成一个提交结果;不能先写入后异步清理。 +- 本期没有清空浏览历史动作或接口;任何客户端都不得通过 A022 开关变更暗中删除旧记录。 ## 七、与核心模块的衔接 @@ -160,32 +170,34 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 收藏列表 | A018 | 按当前买家分页返回收藏与商品摘要 | 待交叉评审 | -| 收藏商品 | A019 | 按买家和商品幂等写入收藏记录 | 待交叉评审 | -| 取消收藏 | A020 | 仅删除本人收藏记录,重复取消幂等 | 待交叉评审 | -| 浏览历史列表 | A021 | 按当前买家分页返回浏览历史,开关关闭时返回空列表 | 待交叉评审 | -| 修改浏览记录开关 | A022 | 开启或关闭后续浏览记录写入,不删除已有历史 | 待交叉评审 | -| 清空浏览历史 | A023 | 按当前买家物理删除全部历史记录,不影响开关状态 | 待交叉评审 | -| 记录浏览历史 | A024 | 在历史开启时写入或更新最近浏览时间,并按稳定排序裁剪到上限以内 | 待交叉评审 | -| 查询浏览记录开关 | A025 | 返回当前开关值,用于前端初始化控件状态 | 待交叉评审 | +| 收藏列表 | A018 | 按收藏时间和记录 ID 稳定分页,返回当前商品摘要与可用状态;下架或不存在商品保留占位 | 流程已确认,待接口同步 | +| 收藏商品 | A019 | 先返回本人既有收藏;没有记录时校验商品已上架再创建,并用唯一性处理并发 | 流程已确认,待接口同步 | +| 取消收藏 | A020 | 按当前买家和商品删除本人收藏;记录不存在时仍返回未收藏成功结果 | 流程已确认,待接口同步 | +| 浏览历史列表 | A021 | 不受开关影响,始终按浏览时间和记录 ID 稳定分页返回已有历史 | 流程已确认,待接口同步 | +| 修改浏览记录开关 | A022 | 设置后续浏览写入开关;关闭不删除、不隐藏旧历史,并与浏览写入形成确定顺序 | 流程已确认,待接口同步 | +| 清空浏览历史 | A023 | 无需求来源;本期保留旧历史且不提供清空能力 | 取消,保留历史编号 | +| 记录浏览历史 | A024 | 服务端重检商品存在且 OnSale,仅在开关开启时写入或更新时间,并同步保留最近 200 条 | 流程已确认,待接口同步 | +| 查询浏览记录开关 | A025 | 无设置事实时返回默认开启,查询不产生写操作 | 流程已确认,待接口同步 | 接口仅承载本流程结果;错误码、HTTP 状态码和 `traceId` 由接口设计统一,不得反向写入业务流程。 -## 九、由流程反查出的接口与数据待评审项 +## 九、下游契约与数据约束 -1. 浏览记录上限默认值(200 条)需要在数据库约束和接口响应中确认一致。 -2. 历史开关默认值(开启)需在接口和数据库设计评审中确认。 -3. 下架商品的浏览历史是否需要自动清理,由 Catalog 与本流程协商,本期默认保留。 -4. 收藏与浏览列表的分页上限需统一,避免出现无分页返回。 -5. 列表展示中下架商品标记的字段口径需与 M02 协作确认。 +1. A018、A021 服从接口文档统一分页参数,并分别按收藏/浏览时间倒序、记录 ID 倒序;返回当前商品状态,商品实体不存在时返回明确不可用占位而不是伪装为 OffSale。 +2. A019、A020 使用当前买家与商品作为业务边界,不接受客户端用户 ID 或他人记录 ID;A020 本人记录不存在时仍返回确定的未收藏状态。 +3. A021 不读取开关来决定是否返回列表;A022 只能改变未来写入设置,不得触发删除;A023 保持取消。 +4. A024 必须让同商品浏览时间更新和最近 200 条裁剪形成一个提交结果,并与 A022 关闭动作形成可排序的先后关系;不能使用异步清理造成超限窗口。 +5. A025 在没有独立设置记录时返回 `enabled = true`,且 GET 不写状态;设置与历史记录的数据设计必须支持同一买家的并发顺序。 +6. 收藏和浏览记录在商品下架后继续保留;数据设计必须让这些引用阻止商品物理删除,不得级联静默删除个人记录。若异常数据缺少商品事实,接口返回不可用占位。 ## 十、验收证据清单 -- [ ] 收藏、取消收藏、收藏列表、浏览记录、历史列表、清空历史和开关切换均正常工作。 +- [ ] 收藏、取消收藏、收藏列表、浏览记录、历史列表和开关切换均正常工作;本期无清空历史入口。 - [ ] 重复收藏、重复浏览和重复取消满足幂等要求,不产生重复记录。 - [ ] 收藏和历史按最近时间倒序,数据严格按买家隔离。 - [ ] 下架商品仍保留记录并明确不可购买;游客、商家和管理员不能越权访问。 - [ ] 浏览记录超过上限后按稳定排序在同一事务内裁剪,并发写入不会超过上限。 -- [ ] 历史开关关闭时浏览历史列表返回空列表;重新开启后恢复写入。 -- [ ] 清空浏览历史不影响开关状态,清空后再次浏览按开关决定是否写入。 -- [ ] 保存正常操作、重复操作、下架占位、记录上限、并发裁剪、开关切换和跨账号隔离证据。 \ No newline at end of file +- [ ] 历史默认开启且最多保留最近 200 条;重复浏览更新时间并移动到最近位置。 +- [ ] 历史开关关闭后不新增或更新,但浏览历史列表仍返回已有记录;重新开启后从下一次详情浏览继续写入。 +- [ ] A023 保持取消,A022 开关切换不会删除或隐藏旧历史。 +- [ ] 保存正常操作、重复操作、下架占位、记录上限、并发裁剪、开关切换和跨账号隔离证据。 -- Gitee From 225d01d9b22f773702eb54c92aff356b8d2ef6fd Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 21:29:29 +0800 Subject: [PATCH 101/118] =?UTF-8?q?docs(process):=20=E5=86=BB=E7=BB=93?= =?UTF-8?q?=E7=AB=99=E5=86=85=E6=B6=88=E6=81=AF=E4=B8=8E=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E6=8E=A8=E9=80=81=EF=BC=9B=E7=BB=9F=E4=B8=80=E6=8E=A5=E6=94=B6?= =?UTF-8?q?=E4=BA=BA=E5=92=8C=E8=BF=9E=E6=8E=A5=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 76 ++++++++---- ...50\351\200\201\346\265\201\347\250\213.md" | 97 ++++++++------- ...10\346\201\257\346\265\201\347\250\213.md" | 114 +++++++++--------- 3 files changed, 165 insertions(+), 122 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 8225481..38b829d 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -1,6 +1,6 @@ # 电子商城需求规格说明书 -> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.9 +> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.10 > > 文档状态:内容和格式已完成内部统一,待全组与指导教师评审后冻结 > 截止:第 1 周周三提交初稿 @@ -18,6 +18,7 @@ | v0.7 | 2026-07-24 | 罗皓晨 | 冻结 F01~F03 注册后登录、单一登录凭证、全部旧凭证失效、资料字段和地址默认切换边界 | | v0.8 | 2026-07-24 | 罗皓晨 | 冻结 F13 买家与商家分流、非默认商家责任清单、禁用竞争顺序和全部旧凭证失效边界 | | v0.9 | 2026-07-24 | 罗皓晨 | 冻结 X02 收藏幂等、浏览历史默认开启与最近 200 条上限,明确关闭记录不隐藏旧历史并取消清空历史能力 | +| v0.10 | 2026-07-24 | 罗皓晨 | 冻结 X03 事件接收人、整事件消息原子性和全部已读水位,并统一 C06 WebSocket、凭证失效、角标补查和固定重连边界 | ## 业务流程设计入口 @@ -1615,7 +1616,7 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 #### 1. 功能目标与范围 -M09 为已登录用户提供可持久化的站内通知,覆盖订单创建/取消/支付/发货/完成、售后申请/审核等已确认业务结果。站内消息列表是通知事实来源,C06 实时推送只是到达速度优化;用户断网、关闭浏览器或推送失败时,消息仍必须可在重新登录后查询。 +M09 为已登录用户提供可持久化的站内通知,覆盖订单创建/取消/发货/完成、支付成功,以及售后申请/寄回、审核和退款确定结果。站内消息列表是通知事实来源,C06 实时推送只是到达速度优化;用户断网、关闭浏览器或推送失败时,消息仍必须可在重新登录后查询。 本期不实现用户自由聊天、群聊、客服工单、短信、邮件、营销群发和复杂消息模板后台。业务模块只提交“已经发生的业务事实”,M09 负责生成、保存、查询和标记通知,不反向改变订单、支付或售后状态。 @@ -1628,7 +1629,7 @@ M09 为已登录用户提供可持久化的站内通知,覆盖订单创建/取 | 身份 | 是否涉及 | 接收内容 | 消息入口与操作 | |---|---|---|---| | 游客 | 否 | 游客没有稳定用户身份,本期不生成个人站内消息,也不建立未读数。 | 不显示个人消息中心;需要查看订单等个人信息时先登录。 | -| 会员(买家) | 是 | 订单创建/取消、支付结果、商家发货、售后提交和审核结果等与本人业务直接相关的通知。 | 从购物端消息入口查看;点击后只能进入本人的订单、支付或售后详情,可标记单条/全部已读。 | +| 会员(买家) | 是 | 订单创建/取消/发货/完成、支付成功、售后审核结果、退款成功或确定失败等与本人业务直接相关的通知。 | 从购物端消息入口查看;点击后只能进入本人的订单、支付或售后详情,可标记单条/全部已读。 | | 商家(运营人员) | 是 | 新的已支付待发货订单、买家售后申请及其他需要商家处理的经营通知。 | 从商家端消息入口查看;点击后进入商家角色有权处理的订单或售后页面,不得进入买家私人页面或管理端。 | | 管理员 | 否(本期) | 当前已选 X03 只覆盖订单、支付、发货和售后通知,不为管理员新增泛化系统告警或用户私人消息查看能力。 | 管理端本期不建设独立消息中心;未来若增加平台治理通知,必须另行定义事件、权限和验收。 | @@ -1636,26 +1637,38 @@ M09 为已登录用户提供可持久化的站内通知,覆盖订单创建/取 | 来源 | 职责 | |---|---| -| 订单模块 | 在订单创建、取消、支付状态确认、发货和完成成功后提供事件事实、业务 ID、买家 ID,以及确需处理时的商家接收标识。 | +| 订单模块 | 在订单创建、取消、发货和完成成功后提供事件事实、业务 ID 和买家 ID;支付成功事实还提供订单指定处理商家。 | | 支付模块 | 提供已确认且经过幂等处理的支付结果,不以尚未落库或处理中状态生成成功通知。 | -| 售后模块 | 在买家提交申请和商家完成审核后,分别生成面向商家和买家的事件事实。 | +| 售后模块 | 在买家提交申请、提交寄回说明、商家审核以及退款得到确定结果后,按固定接收人矩阵提供事件事实。 | | Messaging 模块 | 校验事件、接收身份和数据范围,按不同身份选择文案与跳转目标,完成去重、持久化、未读状态和实时推送衔接。 | 消息归属最终以接收用户 ID 和业务数据范围为准,角色只用于选择消息模板、入口和可执行动作。禁止仅按“全部买家”或“全部商家”广播包含订单、支付或售后信息的私人通知。 -本项目中的“商家”是同一 B2C 平台内的运营账号,不新增多商户入驻、租户隔离或商户结算模型。商家通知事件必须明确接收账号或受控接收范围,避免所有运营账号收到与其工作无关的重复提醒;具体接收规则在接口设计前由订单、售后和 Identity 负责人共同确认。 +本项目中的“商家”是同一 B2C 平台内的运营账号,不新增多商户入驻、租户隔离或商户结算模型。商家通知必须使用订单事实中的 `assignedMerchantUserId`,不得按“所有商家”或活动创建人广播。 + +**固定事件接收人矩阵:** + +| 已提交业务事实 | 必需接收人 | +|---|---| +| 订单创建、取消、发货、完成 | 订单买家 | +| 支付成功 | 订单买家 + `assignedMerchantUserId` | +| 售后申请提交、买家提交寄回说明 | `assignedMerchantUserId` | +| 售后审核结果、退款成功、退款确定失败 | 申请买家 | +| 支付失败、回调 `Ignored`、回调 `Difference`、对账差异发现或管理员对账处置 | 不生成 M09 消息 | + +同一事件要求的全部接收人是一个不可拆分的生成结果:任一必需接收账号缺失、角色不符或与业务归属不一致时,整事件零消息并告警,不允许部分接收人先成功。账号已禁用但身份和业务归属仍有效时仍保存消息历史,只是不允许该账号查询或接收实时推送;重新启用后可查询禁用期间形成的本人消息。 #### 3. 功能需求 | 编号 | 功能 | 详细要求 | |---|---|---| -| X03-FR01 | 生成消息 | 接收已确认的业务事件后生成站内消息,至少记录接收用户、消息类型、标题、摘要/正文、关联业务类型、关联业务 ID、创建时间和已读状态。 | +| X03-FR01 | 生成消息 | 接收已确认的业务事件后按固定接收人矩阵生成站内消息,至少记录接收用户、消息类型、标题、摘要/正文、关联业务类型、关联业务 ID、创建时间和已读状态;同一事件的全部必需接收人必须整批成功或整批失败。 | | X03-FR02 | 事件去重 | 同一业务事件重复投递时不得为同一接收人重复生成相同消息;去重依据必须稳定,并能在进程重启后继续生效。 | | X03-FR03 | 消息列表 | 用户按创建时间倒序分页查看自己的消息;支持按全部、未读和消息类型筛选,不允许一次返回无上限数据。 | | X03-FR04 | 消息详情 | 用户查看消息详情时,系统校验消息归属;关联业务仍存在且用户有权限时可返回安全的前端跳转信息,不直接暴露内部路由或敏感字段。 | | X03-FR05 | 未读数量 | 返回当前用户未读消息总数;新增消息后增加,首次成功标记已读后减少,重复标记不得重复减少。 | | X03-FR06 | 单条已读 | 用户可将自己的一条未读消息标记为已读,并记录首次已读时间;已读消息再次操作返回幂等成功。 | -| X03-FR07 | 全部已读 | 用户可将本人当前未读消息批量标记为已读;操作只影响当前用户,不影响并发到达且不在本次更新范围内的新消息。 | +| X03-FR07 | 全部已读 | 服务端先捕获本人已提交消息的稳定高水位,再将不高于该水位的当前未读消息批量标记为已读;操作只影响当前用户,高水位之后提交的新消息保持未读,不使用客户端时间或墙上时钟划分范围。 | | X03-FR08 | 实时通知衔接 | 消息持久化成功后向 C06 提交推送任务;推送载荷只包含展示所需的最小数据,前端仍可通过消息查询接口获取完整事实。 | | X03-FR09 | 可追踪性 | 消息生成失败、重复事件、推送触发和已读操作应记录消息 ID、业务类型、业务 ID、接收用户标识和 `traceId`,不得记录完整 Token 或敏感业务内容。 | | X03-FR10 | 消息中心交互 | 全站提供容易发现但不过度突出的消息入口和未读角标;列表提供加载占位、空状态、失败重试和分页加载反馈;标记已读后角标和列表状态立即更新,失败时恢复原状态并提示用户重试。 | @@ -1663,9 +1676,9 @@ M09 为已登录用户提供可持久化的站内通知,覆盖订单创建/取 #### 4. 主流程 -1. 订单、支付或售后模块完成自身事务,形成包含事件 ID、业务标识、事件类型、发生时间和接收人的事件事实。 -2. Messaging 消费事件并检查必填字段、事件类型和接收用户;无效事件进入失败记录并告警,不生成半完整消息。 -3. 系统以事件 ID、接收用户和消息类型进行幂等判断,未处理过时生成消息并保存为未读。 +1. 订单、支付或售后模块完成自身事务,形成包含事件 ID、业务标识、事件类型、发生时间及固定矩阵所需业务归属的事件事实。 +2. Messaging 校验事件、全部必需接收账号、角色和业务归属;任一项无效时整事件零消息并告警。 +3. 系统以事件 ID、接收用户和消息类型进行幂等判断;全新事件在一个提交边界内为全部必需接收人生成消息并保存为未读。 4. 数据库提交成功后触发实时推送;推送成功与否不改变数据库中的消息事实和未读状态。 5. 用户进入消息中心,分页查询消息和未读数;点击消息后查看详情并按需要标记为已读。 6. 用户断线重连或重新登录时重新查询未读消息,补偿离线期间未收到的实时通知。 @@ -1678,6 +1691,8 @@ M09 为已登录用户提供可持久化的站内通知,覆盖订单创建/取 - `createdAt` 由服务端生成并统一使用 UTC 存储;前端负责按用户时区展示。 - 未读状态以 PostgreSQL 为准,Redis 或前端角标只可作为缓存,不得成为唯一事实来源。 - 业务事件必须在原业务事务成功后才能生成通知;被回滚的订单、支付或售后操作不得产生成功通知。 +- 禁用账号不能查询消息、建立实时连接或调用已读操作,但其既有消息和禁用期间按有效业务归属生成的新消息均保留;启用后仍只能查看本人消息。 +- 消息详情返回历史正文;关联目标不存在、当前无权限或目标模块暂时无法确认权限时,安全操作入口为空并显示“目标暂不可用”,不能隐藏正文或返回未经确认的跳转。 - 默认不提供物理删除消息能力,避免破坏验收追踪;后续如需清理历史数据,应单独定义保留期和归档规则。 #### 6. 异常与边界场景 @@ -1688,12 +1703,16 @@ M09 为已登录用户提供可持久化的站内通知,覆盖订单创建/取 - SignalR/Redis 不可用时,消息仍正常落库并可查询,实时推送失败可记录并告警,但不得回滚原业务事务。 - 用户在“全部已读”操作过程中收到新消息时,新消息应保持未读,避免把用户尚未看到的消息误标已读。 - 关联订单或售后记录已不存在、已归档或当前用户无查看权限时,消息正文仍可查看,但前端不得提供无效或越权跳转。 +- 目标模块暂时不可用、无法确认关联资源权限时,消息正文仍可查看,操作入口为空并明确提示目标暂不可用;恢复后重新查询详情再判断。 #### 7. 验收标准与证据 - 分别触发订单创建/取消、支付成功、发货、订单完成和售后审核事件,正确用户能够看到类型、内容和关联对象正确的消息。 +- 支付成功只通知订单买家和指定处理商家;售后申请/寄回只通知指定商家;审核与退款确定结果只通知买家;支付失败、忽略回调、对账差异和管理员对账处置不生成 M09 消息。 +- 构造一个缺失或角色错误的必需接收人,整事件不生成任何接收人的消息并留下可追踪告警。 - 消息列表分页、类型筛选和未读筛选结果正确,排序稳定且不出现其他用户数据。 - 单条已读、重复已读和全部已读均满足幂等要求,未读数与数据库实际未读记录一致。 +- 全部已读使用稳定消息高水位;操作期间提交的新消息保持未读,即使服务端时间相同也不会被误标。 - 将同一事件重复投递至少两次,只生成一条对应消息。 - 关闭浏览器或断开实时连接后触发消息,重新登录仍可从列表查询并保持未读。 - 使用另一用户身份直接请求消息详情或已读接口时被拒绝,且响应不泄露目标消息内容。 @@ -2044,9 +2063,9 @@ C04 在 M02-01 的基础搜索入口上增加中文分词模糊搜索、多条 #### 1. 功能目标与范围 -C06 在 M09 持久化消息之上提供低延迟实时到达能力。系统使用 ASP.NET Core SignalR 建立 WebSocket 通道,在订单支付、发货、完成、取消和售后审核等消息成功落库后,将最小通知载荷推送给目标用户。实时推送不是业务事务的一部分,不承担消息永久保存,也不能作为订单或支付状态的唯一来源。 +C06 在 M09 持久化消息之上为当前 PC Web 提供低延迟实时到达能力。系统使用 ASP.NET Core SignalR 建立 WebSocket 通道,在订单、支付和售后消息成功落库后,将最小通知载荷推送给目标用户。实时推送不是业务事务的一部分,不承担消息永久保存,也不能作为消息未读数、订单或支付状态的唯一来源。 -本挑战只实现业务状态通知,不实现在线客服对话、群聊、历史聊天同步、已送达回执和端到端加密。 +本挑战只实现业务状态通知,不实现在线客服对话、群聊、历史聊天同步、已送达回执和端到端加密。当前客户端固定使用 WebSockets 并跳过 SignalR 协商,不启用 SSE 或长轮询传输;WebSocket 暂不可用时,实时能力降级为通过 M09 HTTP 接口查询或定期补查,不阻塞页面其他业务。 **用户体验目标**:连接正常时,用户无需手动刷新即可及时得知关键状态变化;连接中断时,系统应优先静默重连,不频繁弹错或打断当前操作。只有持续断线影响实时性时才显示简短、可理解的状态提示,并告知用户消息仍可在消息中心查看。重复推送不得重复弹出相同提示。 @@ -2067,38 +2086,43 @@ M09 是消息持久化事实来源,C06 只负责实时到达;C10 提供 Ngin | 编号 | 功能 | 详细要求 | |---|---|---| -| C06-FR01 | 连接鉴权 | 客户端建立 SignalR 连接时携带有效 JWT;服务端从认证上下文获取用户 ID,不接受客户端自行声明接收用户。过期、伪造、被禁用或已失效的令牌不得建立有效连接。 | +| C06-FR01 | 连接鉴权 | 客户端建立 SignalR 连接时携带有效 JWT;服务端从认证上下文获取用户 ID,不接受客户端自行声明接收用户。过期、伪造、被禁用或已失效的令牌不得建立有效连接;服务端无法确认账号状态或凭证失效事实时失败关闭,不建立或继续保留连接。 | | C06-FR02 | 用户定向推送 | 服务端按认证用户标识向目标用户的全部在线连接推送,不向无关用户或公共广播组泄露业务通知。 | | C06-FR03 | 多标签页 | 同一账号在同一浏览器或不同浏览器打开多个标签页时,每个有效连接都能收到通知;任一标签页标记已读后,共享的数据库未读状态保持一致。 | -| C06-FR04 | 自动重连 | 前端在非主动退出导致的连接中断后按照有限退避策略自动重连,并展示连接状态;重连失败不能阻塞页面其他功能。 | -| C06-FR05 | 断线补偿 | 初次连接和重连成功后,前端重新查询 M09 未读数或最近消息,不假设断线期间的推送能够重放。 | +| C06-FR04 | 自动重连 | 前端在非主动退出导致的连接中断后依次立即、2 秒、5 秒、10 秒重连;仍失败时暂停自动尝试,等待浏览器恢复在线或用户手动重试。重连状态不得阻塞页面其他功能。 | +| C06-FR05 | 事实补查 | 初次连接、重连成功和收到实时提示后,前端重新查询 M09 权威未读数,并按需要补查最近消息或消息详情;不假设断线期间的推送能够重放,也不得用本地角标直接 `+1` 推算未读数。 | | C06-FR06 | 多实例广播 | 两个 API 实例使用 Redis Backplane 或等效共享通道传播 Hub 消息;无论用户连接落在哪个实例、事件由哪个实例触发,都能收到通知。 | | C06-FR07 | 最小载荷 | 推送至少包含消息 ID、类型、标题/摘要、关联业务类型与 ID、创建时间;不发送完整订单、支付信息、地址、Token 或其他敏感字段。 | | C06-FR08 | 失败隔离 | SignalR 或 Redis 推送失败不得回滚已提交的订单、支付、售后事务和 M09 消息;失败需留下可关联的日志和指标。 | -| C06-FR09 | 连接生命周期 | 用户主动退出后关闭当前连接;服务端清理断开的连接状态,不依赖单个 API 实例内存保存跨实例唯一在线状态。 | +| C06-FR09 | 连接生命周期 | 用户主动退出、JWT 到期、手机号修改或账号禁用后,相关既有连接必须关闭,旧凭证不得重新连接;账号启用也不恢复旧凭证。服务端清理断开的连接状态,不依赖单个 API 实例内存保存跨实例唯一在线状态。 | | C06-FR10 | 非打扰式反馈 | 实时消息采用轻量提示和未读角标,不使用必须立即关闭的连续模态弹窗;相同消息 ID 只展示一次。短暂断线静默重连,持续断线才显示连接状态,重连成功后自动恢复提示并刷新未读数。 | | C06-FR11 | 身份化路由 | Hub 连接和用户通道必须来自服务端认证结果;买家与商家可以复用技术通道,但接收组、消息模板和跳转目标按身份隔离,客户端不得通过修改参数订阅其他身份或其他用户。 | +| C06-FR12 | 固定传输 | 当前 PC Web 只使用 WebSockets 并跳过 SignalR 协商;Nginx 必须支持连接升级。WebSocket 不可用时不自动切换 SSE 或长轮询,只保留 M09 HTTP 查询或定期补查能力。 | #### 4. 主流程 1. 业务模块提交事务并形成事件。 2. M09 消费事件、幂等生成站内消息并提交数据库事务。 3. 消息提交成功后调用实时推送能力,按接收用户 ID 发送最小载荷。 -4. Redis Backplane 将通知传播到持有该用户连接的 API 实例。 -5. 前端收到通知后更新角标、展示轻提示,并可按消息 ID 查询详情;客户端不得仅凭推送载荷自行修改订单最终状态。 -6. 若步骤 3~5 任一步失败,用户在重连或打开消息中心时通过 M09 查询补偿。 +4. Redis Backplane 将通知传播到持有该用户有效连接的 API 实例;推送前若不能确认连接身份仍有效,则关闭连接而不是继续发送。 +5. 前端收到通知后仅按消息 ID 去重展示轻提示,并立即通过 M09 查询权威未读数,按需要补查最近消息或详情;不得本地执行“角标 +1”,也不得仅凭推送载荷修改业务最终状态。 +6. 若步骤 3~5 任一步失败,用户在重连、网络恢复、手动重试或打开消息中心时通过 M09 HTTP 查询补偿。 #### 5. 业务规则与权限 - Hub 方法和连接组操作均基于服务端认证身份,不提供“传入任意 userId 即可订阅”的接口。 - WebSocket 握手、普通 API 和 Nginx 转发使用一致的 JWT 验签配置;Token 出现在连接参数时不得被日志完整记录。 +- 当前 PC Web 的 SignalR 客户端固定为 WebSockets 且跳过协商;部署侧不得再依赖协商请求与升级请求之间的会话亲和。 +- 实时载荷只是“有新事实可查”的提示。未读数、已读状态和消息正文始终以 M09/PostgreSQL 查询结果为准。 +- 主动退出、凭证到期、手机号修改和账号禁用均会使旧凭证及其连接失效;服务端不能安全确认撤销或账号状态时,受保护的 Hub 连接按失败关闭处理。 #### 6. 异常与边界场景 - 网络抖动造成重复连接或重复推送时,前端以消息 ID 去重展示;数据库未读数不得因重复推送增加。 -- Redis Backplane 短暂不可用时允许实时能力降级,但 M09 查询必须保持可用;恢复后不要求重放所有推送,因为持久化列表负责补偿。 +- Redis Backplane 短暂不可用时实时能力降级;只有 Identity 仍能安全确认账号与凭证状态时,M09 受保护 HTTP 查询才可继续,否则同样失败关闭。恢复后不要求重放所有推送,因为持久化列表负责补偿。 - 单个 API 实例停止后,连接到该实例的客户端应进入重连流程并切换到可用实例;系统不承诺连接完全无中断,但必须保证消息事实不丢失。 - 前端页面不可见或浏览器节流时,不以客户端收到时间作为业务发生时间,统一展示服务端消息创建时间。 +- WebSocket 被代理或网络阻断时,客户端按固定节奏完成有限重连后暂停;页面显示实时能力不可用,但仍可在鉴权可安全完成时使用 M09 HTTP 查询或定期补查,不启用 SSE 或长轮询。 #### 7. 验收标准与证据 @@ -2107,7 +2131,7 @@ M09 是消息持久化事实来源,C06 只负责实时到达;C10 提供 Ngin | 场景 | 操作 | 预期结果 | |---|---|---| | 正常推送 | 买家在线时由商家完成发货 | 买家在无需刷新页面的情况下收到发货通知,消息中心存在同一消息。 | -| 断线补偿 | 断开网络后触发订单状态消息,再恢复网络 | 客户端自动重连;即使实时提示未重放,未读数和消息列表也能查到该消息。 | +| 断线补偿 | 断开网络后触发订单状态消息,再恢复网络 | 客户端按立即、2 秒、5 秒、10 秒进行有限重连;即使实时提示未重放,M09 权威未读数和消息列表也能查到该消息。 | | 多标签页 | 同一账号打开至少两个标签页并触发一条消息 | 两个标签页均收到通知;任一标签页标记已读后,刷新另一标签页可看到一致已读状态。 | | 用户隔离 | 用户 A、B 同时在线,只触发 A 的订单消息 | 只有 A 的连接收到通知,B 的消息列表和未读数不变化。 | | 身份隔离 | 买家、被指定的商家运营账号、未被指定的商家运营账号和管理员同时在线,触发一笔订单状态变化 | 只向事件明确指定的买家或商家运营账号推送;其他在线账号不收到该私人订单通知。 | @@ -2115,6 +2139,9 @@ M09 是消息持久化事实来源,C06 只负责实时到达;C10 提供 Ngin | 单实例故障 | 保持客户端在线并停止其当前连接所在 API 实例 | 客户端重连到存活实例;重新查询后消息和未读状态完整。 | | 推送依赖故障 | 暂停 Redis/实时推送后触发消息 | 原业务和消息落库成功;恢复后用户通过列表补查,不出现消息丢失。 | | 非打扰体验 | 连续触发多条消息并制造一次短暂断线 | 当前表单或操作不被中断;相同消息不重复弹出;短暂断线自动恢复,用户仍能从消息中心查看全部消息。 | +| 权威角标 | 同一消息重复推送、跨标签页已读或断线期间新增消息 | 每次提示后均以 M09 查询结果刷新角标,不执行本地 `+1`,各标签页最终与 PostgreSQL 未读事实一致。 | +| 凭证失效 | 分别执行主动退出、JWT 到期、手机号修改和账号禁用 | 相关既有连接关闭,旧凭证不能重连;无法确认失效事实时连接失败关闭,启用账号也不恢复旧凭证。 | +| 传输降级 | 阻断 WebSocket 升级后打开页面 | 不切换到 SSE 或长轮询;实时状态提示不可用,鉴权安全时仍可通过 M09 HTTP 查询或定期补查。 | **验收证据与答辩要求:** @@ -2122,6 +2149,7 @@ M09 是消息持久化事实来源,C06 只负责实时到达;C10 提供 Ngin - 保存每次演示的 API 实例标识、连接/重连时间、消息 ID、`traceId`、关键日志和最终数据库查询结果。 - 能说明 WebSocket 与 HTTP 的差异、SignalR 的作用、JWT 如何认证连接、为什么不能相信客户端传入的用户 ID。 - 能说明 Redis Backplane 解决的是跨实例连接路由而非消息持久化,以及 M09 如何补偿断线和推送失败。 +- 能证明客户端使用 WebSockets 并跳过协商,且未读角标来自 M09 权威查询而不是累计推送次数。 ### C07 缓存与性能优化 — 罗皓晨 @@ -2515,12 +2543,12 @@ Electron 后续原则上复用 PC Web 原型,只补充窗口、桌面导航和 | F13 | M06-03 后台用户管理—唐宇昊 | 管理端用户管理 | A015~A017 | 待测试计划登记 | 买家/商家分流、固定责任清单、禁用竞争和全部旧凭证失效已统一,待接口同步 | | X01 | M07 商品评价与晒图—顾欣月 | 订单评价入口、商品评价区 | A140~A143;A144 无业务来源,接口整合时取消 | 待测试计划登记 | 订单入口、提交重检、唯一评价、公开计分和图片边界已统一,待数据库、OpenAPI 与交叉评审 | | X02 | M08 收藏与浏览历史—唐宇昊 | 商品收藏、收藏页、浏览历史页 | A018~A022、A024、A025;A023 无业务来源,接口整合时取消 | 待测试计划登记 | 收藏幂等、默认开启、最近 200 条及关闭后旧历史可见已统一,待接口同步 | -| X03 | M09 站内消息—罗皓晨 | 买家/商家消息中心 | A501~A505 | 待测试计划登记 | HTTP、集成事件和接收人边界已统一,待数据库、OpenAPI 与来源模块交叉评审 | +| X03 | M09 站内消息—罗皓晨 | 买家/商家消息中心 | A501~A505 | 待测试计划登记 | 固定接收人、整事件原子性和稳定已读水位已冻结,待数据库、接口与实现承接 | | X04 | M10 售后流程—张海洋 | 买家售后申请、商家售后审核 | A411~A417、A419、A432~A434;退款入账使用 Payment 应用契约 | 待测试计划登记 | A418/A431 为取消历史号,待数据库、OpenAPI 与交叉评审 | | C01 | 秒杀与防超卖—朱惠惠 | 秒杀活动页、商家活动管理、压测脚本 | A220~A228;订单查询复用 A302/A303;生命周期为 Worker 内部契约 | 待挑战测试登记 | 原子限购、库存通道与状态推进已定义,待数据库、OpenAPI 与跨模块联调 | | C03 | 订单超时取消—韦乾强 | 订单倒计时、Worker 验收脚本 | 复用 A304;超时扫描为 Worker 内部契约 | 待挑战测试登记 | Worker 扫描、取消复用和原库存通道回补已定义,待数据库与测试评审 | | C04 | 商品搜索进阶—顾欣月 | 商品搜索页、性能对比报告 | A102 | 待挑战测试登记 | 接口草案已汇总,待交叉评审 | -| C06 | 实时消息推送—罗皓晨 | 实时通知、连接状态、消息中心 | SignalR 契约(无 Axxx),补查复用 A501~A505 | 待挑战测试登记 | 实时推送、持久化事实与断线补查边界已定义,待部署与测试评审 | +| C06 | 实时消息推送—罗皓晨 | 实时通知、连接状态、消息中心 | SignalR 契约(无 Axxx),补查复用 A501~A505 | 待挑战测试登记 | WebSockets 跳过协商、固定重连、凭证失效与权威角标补查已冻结,待接口、部署与测试承接 | | C07 | 缓存与性能优化—罗皓晨,顾欣月协作 | 首页、商品详情、压测报告 | 复用 A102 的固定首页摘要调用、A103 商品详情;不缓存普通列表与搜索 | 待挑战测试登记 | 不新增业务 HTTP,缓存约定待确认 | | C08 | 支付回调幂等与对账—张海洋 | 回调重放脚本、管理端对账 | A421~A425、A432~A433;退款入账使用 Payment 应用契约 | 待挑战测试登记 | A431 为取消历史号,待数据库、OpenAPI 与交叉评审 | | C10 | 容器化与负载均衡—罗皓晨 | 统一访问入口、健康与实例验证 | A506、A507 | 待部署验收登记 | 健康检查草案已汇总,待交叉评审 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C06-\345\256\236\346\227\266\346\216\250\351\200\201\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C06-\345\256\236\346\227\266\346\216\250\351\200\201\346\265\201\347\250\213.md" index dff6ef5..2d2a698 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C06-\345\256\236\346\227\266\346\216\250\351\200\201\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C06-\345\256\236\346\227\266\346\216\250\351\200\201\346\265\201\347\250\213.md" @@ -2,23 +2,23 @@ > 负责人:罗皓晨 > 覆盖:C06;经 X03/M09 接入核心业务事实 -> 基础核心流程:F08、F09、F10、F12;鉴权依赖 F02、F13;X04 可追加售后来源 +> 基础核心流程:F08、F09、F10、F12;鉴权依赖 F02、F13;接入 X04 售后来源 > 直接协作:M09 Messaging、M01 Identity、C10 多实例运行环境 -> 文档状态:初稿,待罗皓晨自审及 Identity、Ordering、Payment、AfterSales、部署边界交叉评审 +> 文档状态:完整定义;业务与运行边界已冻结,待接口、部署、实现和测试承接 > 需求事实源:[需求规格说明书 C06](../../../01-需求文档/需求规格说明书.md) 的“C06 实时消息推送”完整七节 ## 一、范围与事实来源 -C06 在 M09 消息已经成功持久化之后,为已登录买家和商家提供低延迟、按本人定向的实时到达能力。实时连接和轻提示只改善到达速度;消息内容、未读状态以及订单、支付、发货和售后状态仍分别以 PostgreSQL 中的业务事实为准。 +C06 在 M09 消息已经成功持久化之后,为当前 PC Web 的已登录买家和商家提供低延迟、按本人定向的实时到达能力。实时连接和轻提示只改善到达速度;消息内容、未读状态以及订单、支付、发货和售后状态仍分别以 PostgreSQL 中的业务事实为准。 -本流程不实现在线客服、自由聊天、群聊、历史聊天同步、已送达回执、任意客户端加组或端到端加密。游客和管理员本期不建立 Messaging 实时连接。 +本流程不实现在线客服、自由聊天、群聊、历史聊天同步、已送达回执、任意客户端加组或端到端加密。游客和管理员本期不建立 Messaging 实时连接。当前 PC Web 只使用 WebSockets 并跳过 SignalR 协商;不启用 SSE 或长轮询传输,WebSocket 不可用时退回 M09 HTTP 查询或定期补查。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| -| C06 需求与教师验收 | 完整定义,待需求冻结 | 作为断线重连、多标签页、多实例和持久化补查边界 | -| 本文业务流程 | 初稿 | 明确连接生命周期、定向推送、补查和失败隔离 | -| 接口设计 4.3.7、4.3.8 | 部分定义、待交叉评审 | 由流程派生 Hub 与服务端事件映射 | -| A501~A505 | 部分定义、待交叉评审 | 用于补查、详情和已读校正,不为 SignalR 新增 Axxx | +| C06 需求与教师验收 | 完整定义 | 作为传输、断线重连、多标签页、多实例和持久化补查边界 | +| 本文业务流程 | 完整定义 | 冻结连接生命周期、定向推送、权威补查、固定重连和失败隔离 | +| 接口设计 4.3.7、4.3.8 | 部分定义,待按本文补齐 | 由流程派生 Hub 与服务端事件映射 | +| A501~A505 | 部分定义,待按 M09 补齐 | 用于补查、详情和已读校正,不为 SignalR 新增 Axxx | | Redis Backplane / C10 | 技术与部署能力待验证 | 只承接跨实例通道,不保存唯一消息事实 | ## 二、直接出入口与不可变结果 @@ -39,9 +39,10 @@ flowchart LR - C06 只能从“M09 消息事务已提交”接入,来源模块不得绕过 M09 直接广播未落库的成功事实。 - 推送失败、重复或延迟都不能改变消息未读状态,也不能回滚订单、支付、发货或售后结果。 +- 推送只表示“有新事实可查”,客户端收到提示后必须重新查询 M09 权威未读数;不得按推送次数对角标执行本地 `+1`。 - 客户端不得仅凭推送载荷修改订单、支付或售后最终状态;需要业务详情时重新调用所属模块接口。 - Redis 只解决跨实例 Hub 消息传播,不保存永久消息、唯一未读数或唯一在线状态。 -- Redis Backplane 只传播跨实例 Hub 消息,不自动解决协商请求与 WebSocket 连接升级的实例落点;会话亲和或经验证的跳过协商策略由 C06/C10 联合评审后冻结。 +- 当前 PC Web 固定只使用 WebSockets 并跳过 SignalR 协商;Nginx 负责连接升级,部署不依赖协商请求与升级请求之间的会话亲和。 ## 三、连接鉴权与生命周期 @@ -54,11 +55,11 @@ stateDiagram-v2 Connecting --> Connected: 服务端鉴权通过 Connecting --> Disconnected: 鉴权失败或主动取消 Connected --> Reconnecting: 非主动网络或实例中断 - Reconnecting --> Connected: 有限退避重连成功 - Reconnecting --> Degraded: 持续重连失败 + Reconnecting --> Connected: 立即/2秒/5秒/10秒任一次成功 + Reconnecting --> Degraded: 四次均失败 Degraded --> Reconnecting: 网络恢复或用户手动重试 - Connected --> Disconnected: 用户主动退出 - Degraded --> Disconnected: 用户主动退出 + Connected --> Disconnected: 退出/到期/改手机号/禁用 + Degraded --> Disconnected: 退出或凭证失效 ``` 连接规则: @@ -66,9 +67,10 @@ stateDiagram-v2 - 客户端携带有效 JWT 建立连接;服务端从认证上下文取得用户 ID 和角色,不接受客户端声明任意接收用户、角色或组。 - 买家与商家可以复用技术通道,但消息接收范围、文案和安全操作入口仍按身份隔离。 - 同一账号的每个有效标签页分别建立连接,服务端向该用户全部在线连接发送消息。 -- 用户主动退出后关闭当前连接;账号禁用、令牌撤销或版本失效时,不得继续建立有效连接。 +- 用户主动退出、JWT 到期、手机号修改、账号禁用或其他全部旧凭证失效动作发生后,相关既有连接必须关闭,旧凭证不得重新连接;账号启用也不恢复旧凭证。 +- 建连、重连和连接存续期间无法确认账号状态、撤销事实或凭证有效性时失败关闭,不能为可用性继续保留受保护连接。 - 主动退出、被动断网、连接超时或 API 实例中断后,服务端必须清理该实例持有的断开连接状态;跨实例唯一在线状态不得只保存在单个 API 内存中。 -- 非主动断线采用有限退避重连。具体次数、间隔和“持续断线”提示阈值尚未冻结,进入第八章待评审项。 +- 非主动断线依次立即、2 秒、5 秒、10 秒重连;四次均失败后暂停自动尝试并显示简短状态,等待浏览器恢复在线或用户手动重试。 ## 四、消息提交后的定向推送 @@ -77,19 +79,21 @@ flowchart TD A["M09:本人消息事务提交成功"] --> B["取得接收用户和最小展示载荷"] B --> C{"目标身份是否为本期支持的买家或商家?"} C -- "否" --> X["不建立实时推送
消息事实仍保留"] - C -- "是" --> D["按服务端认证用户标识发送"] - D --> E["共享实时通道把消息传播到持有连接的 API 实例"] - E --> F{"目标用户是否有在线连接?"} - F -- "否" --> Y["结束实时尝试
等待 M09 补查"] - F -- "是" --> G["向该用户全部有效连接推送"] + C -- "是" --> D{"目标连接的凭证和账号状态仍可安全确认?"} + D -- "否" --> Q["关闭失效或不可确认的连接
不继续推送"] + D -- "是" --> E["按服务端认证用户标识发送"] + E --> F["共享实时通道把消息传播到持有连接的 API 实例"] + F --> R{"目标用户是否有在线连接?"} + R -- "否" --> Y["结束实时尝试
等待 M09 补查"] + R -- "是" --> G["向该用户全部有效连接推送"] G --> H{"标签页是否已展示同一消息标识?"} H -- "是" --> I["忽略重复轻提示
不改变未读数"] - H -- "否" --> J["更新角标并显示非阻塞轻提示"] - J --> K["用户按需进入消息中心或业务详情"] - K --> L["通过 M09/目标模块重新查询确定事实"] + H -- "否" --> J["显示非阻塞轻提示
不直接累加角标"] + J --> K["查询 A503 权威未读数
按需补查 A501/A502"] + K --> L["校正角标;用户按需进入消息中心或业务详情"] - D -. "发送失败" .-> Z["记录消息标识、实例和 traceId
不回滚 M09"] - E -. "共享通道失败" .-> Z + E -. "发送失败" .-> Z["记录消息标识、实例和 traceId
不回滚 M09"] + F -. "共享通道失败" .-> Z G -. "连接中断" .-> Z Z --> Y ``` @@ -99,6 +103,7 @@ flowchart TD - 只包含消息标识、类型、标题/摘要、关联业务类型与标识、安全操作描述和服务端创建时间。 - 不发送完整订单、支付信息、收货地址、密码、完整 Token、连接配置或内部前端路由。 - 消息标识是客户端轻提示去重键;服务端创建时间是展示事实,不使用浏览器实际收到时间替代。 +- 载荷中的关联信息不能代替详情授权;目标不存在或权限暂不可确认时沿用 M09 的“目标暂不可用”结果。 ## 五、断线重连、多标签页与补查 @@ -109,13 +114,12 @@ flowchart TD B -- "是" --> C["查询 M09 当前未读数"] C --> D["按需查询最近消息或消息列表"] D --> E["校正本标签页角标和已展示消息集合"] - E --> F["保持实时连接"] + E --> F["保持实时连接;收到提示时回到 C 查询权威未读数"] F --> G{"发生非主动断线?"} G -- "否" --> F - G -- "是" --> H["进入有限退避重连,不阻塞页面其他功能"] - H --> I{"重连成功?"} - I -- "否,仍在阈值内" --> H - I -- "否,持续断线" --> J["显示简短连接状态
告知消息中心仍可查询"] + G -- "是" --> H["依次立即、2秒、5秒、10秒重连
不阻塞页面其他功能"] + H --> I{"四次内重连成功?"} + I -- "否" --> J["显示简短连接状态
告知实时能力已降级"] J --> W["暂停自动重连
等待网络恢复或用户手动重试"] W -->|"网络恢复或用户重试"| H I -- "是" --> C @@ -129,9 +133,11 @@ flowchart TD 补查规则: - 初次连接和每次重连成功后至少校正未读数,并按需查询消息列表;不假设服务端会无限重放断线期间实时事件。 +- 每次收到实时提示也重新查询权威未读数;角标不按收到的消息数递增,避免重复、乱序、跨标签页已读和断线补偿造成漂移。 - 多标签页各自接收实时提示,但已读状态统一写回 M09;任一标签页成功标记已读后,其他标签页通过补查得到一致结果。 - 页面不可见或被浏览器节流时,不把客户端收到时间当作业务发生时间。 -- 持续断线只影响实时性,不应阻塞商品浏览、订单查询或消息中心的普通 HTTP 查询。 +- 持续断线只影响实时性,不阻塞公开商品浏览;受保护的订单、消息 HTTP 查询只有在 Identity 能安全确认凭证状态时才继续,否则失败关闭。 +- WebSocket 不可用时使用 M09 HTTP 查询或定期补查作为功能补偿,不切换到 SSE 或长轮询。 ## 六、多实例、故障与责任 @@ -142,7 +148,8 @@ flowchart TD | 网络抖动造成重复连接或重复推送 | 允许连接恢复,轻提示按消息标识去重 | 不重复生成消息或改变业务状态 | | Redis Backplane 暂时不可用 | 实时能力降级并记录指标 | M09 数据事实仍保留;只有 Identity 仍能安全完成鉴权时,受保护的 M09 HTTP 查询才能继续,否则按失败关闭策略处理 | | 当前连接所在 API 停止 | 客户端进入重连并切换到存活实例 | 不承诺连接无中断,但消息不丢失 | -| 用户令牌过期、撤销或账号禁用 | 拒绝新连接;已有连接失效边界待 Identity 评审 | 不允许用客户端参数绕过认证 | +| 用户主动退出、令牌到期、手机号修改或账号禁用 | 关闭相关既有连接并拒绝旧凭证重连;无法确认失效事实时失败关闭 | 启用账号不恢复旧凭证,不允许用客户端参数绕过认证 | +| WebSocket 升级不可用 | 完成固定四次有限重连后暂停,回退 M09 HTTP 查询或定期补查 | 不启用 SSE 或长轮询,不改变消息事实 | | 推送载荷处理失败 | 不显示或转为普通消息入口补查 | 不使用错误载荷改变订单状态 | | 用户主动退出 | 关闭当前连接并清理本地实时状态 | 不删除 M09 历史消息 | | 被动断网、连接超时或实例中断 | 服务端清理已断开的连接状态,客户端按有限退避重连 | 不把单实例内存连接表当作跨实例唯一在线事实 | @@ -153,9 +160,9 @@ SignalR 连接和服务端事件不是 HTTP 接口,不占用 Axxx,也不得 | 流程能力 | 当前派生契约 | 事实来源 | 当前状态 | |---|---|---|---| -| 买家或商家建立本人实时连接 | 接口设计 4.3.7“Hub 连接” | M01 认证上下文 | 待交叉评审 | -| M09 消息提交后向全部在线连接发送 | 接口设计 4.3.8“MessageCreated” | M09 已持久化消息 | 待交叉评审 | -| 初次连接或重连后校正未读数 | A503 | M09/PostgreSQL | 待交叉评审 | +| 买家或商家以 WebSockets 跳过协商建立本人实时连接 | 接口设计 4.3.7“Hub 连接” | M01 认证上下文 | 待补齐固定传输和失败关闭契约 | +| M09 消息提交后向全部有效在线连接发送提示 | 接口设计 4.3.8“MessageCreated” | M09 已持久化消息 | 待补齐提示语义与最小载荷 | +| 初次连接、重连和收到提示后校正未读数 | A503 | M09/PostgreSQL | 待补齐权威补查时机 | | 补查断线期间消息 | A501;查看详情时使用 A502 | M09/PostgreSQL | 待交叉评审 | | 任一标签页标记已读并校正 | A504、A505,随后复用 A503 | M09/PostgreSQL | 待交叉评审 | | 两个 API 实例共享实时通道 | 接口设计 1.16、4.3.7 | Redis Backplane,不登记 DBxxx | 待 C10 部署验证 | @@ -166,26 +173,30 @@ SignalR 连接和服务端事件不是 HTTP 接口,不占用 Axxx,也不得 - 系统架构 7.13“C10 容器化部署与负载均衡”:Nginx WebSocket Upgrade、双实例与故障切换; - 系统架构 8“安全设计”:JWT、账号状态、令牌撤销与资源隔离。 -## 八、待交叉评审项 +## 八、下游设计与验证约束 -1. 与 Identity 确认令牌撤销、账号禁用和退出后,现有跨标签页连接何时关闭及失败时的安全策略。 -2. 确定有限退避的次数、间隔、抖动和“持续断线”提示阈值;这些参数只影响交互,不改变消息补查边界。 -3. 与 C10 确认 Nginx WebSocket 转发、事件触发实例和单实例停止的可重复证据方式,并冻结 SignalR 协商请求与连接升级的实例落点策略;可评审会话亲和或经验证的 WebSockets 跳过协商方案,不能假设 Redis Backplane 已解决该问题。 -4. 确认 Redis Backplane 故障和恢复的监控指标、日志及告警,不承诺恢复后重放全部实时事件。 -5. 由 Ordering、Payment、AfterSales 确认所有来源都先经过 M09 持久化,禁止业务模块直接向客户端广播。 -6. 真实 OpenAPI、Hub 集成测试和多标签页端到端测试尚未建立,本流程不得标记为已实现或已验证。 +1. Hub 契约必须固定 WebSockets、跳过协商、服务端认证用户通道和失败关闭;不得引入客户端任意加组或其他传输回退。 +2. Identity 必须向现有连接传播主动退出、到期、手机号修改、账号禁用和全部旧凭证失效结果;无法确认状态时连接关闭。 +3. C10 必须提供 Nginx WebSocket Upgrade、双 API、Redis Backplane、连接实例停止和恢复就绪的可重复部署证据。 +4. 客户端重连数组固定为立即、2 秒、5 秒、10 秒;四次失败后只有网络恢复或用户手动操作重新启动一轮。 +5. 客户端收到 `MessageCreated` 只去重提示并调用 A503,按需调用 A501/A502;不得通过推送次数累计角标。 +6. Redis Backplane 故障和恢复要记录指标、日志和告警;不承诺恢复后重放实时事件,由 M09 持久化查询补偿。 +7. 真实 Hub 集成测试、多标签页端到端测试和部署验证尚未建立,因此“流程完整”不等于“已实现或已验证”。 ## 九、验收证据清单 - [ ] 买家在线时触发发货等已提交事实,无需刷新即可收到一条轻提示,消息中心存在同一消息。 - [ ] 断网期间触发消息,恢复后自动重连,并通过未读数和消息列表补查。 +- [ ] 重连严格按立即、2 秒、5 秒、10 秒执行,四次失败后暂停,网络恢复或手动重试才开始新一轮。 - [ ] 同一账号至少两个标签页均能收到通知;任一标签页标记已读后,其他标签页可校正为一致状态。 - [ ] 用户 A、用户 B 同时在线时,只有明确接收人获得私人消息。 - [ ] 买家、指定商家、无关商家和管理员同时在线时,推送范围符合身份和接收账号约束。 - [ ] 连接落在实例 1、事件由实例 2 触发时能够通过共享实时通道送达,并保存实例与 Trace 证据。 -- [ ] 按已冻结的连接落点策略验证初次协商、WebSocket 升级和重连,证明请求跨两个 API 分发时不会因落点不一致失败。 +- [ ] 验证当前 PC Web 只使用 WebSockets 并跳过协商;WebSocket 失败后只回退 M09 HTTP 查询或定期补查,不启用 SSE 或长轮询。 - [ ] 停止当前连接所在 API 后,客户端可重连到存活实例,消息和未读状态完整。 - [ ] 主动退出、被动断网、连接超时和实例中断后,服务端均能清理断开连接状态,且无需依赖单实例内存保存唯一在线状态。 +- [ ] 主动退出、JWT 到期、手机号修改和账号禁用都会关闭相关既有连接,旧凭证不能重连;失效事实无法确认时失败关闭。 - [ ] 暂停 Redis/实时推送后,来源业务和 M09 消息事实仍成功;Identity 可安全鉴权时用户可通过列表补查,否则受保护请求按失败关闭策略处理。 - [ ] 连续消息和短暂断线不使用阻塞弹窗打断当前表单或支付操作。 +- [ ] 重复或乱序推送、跨标签页已读和断线新增消息均通过 A503 校正角标,不出现客户端 `+1` 漂移。 - [ ] 保存连接/重连时间、消息标识、实例标识、`traceId`、M09 查询结果和故障恢复日志。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/M09-\347\253\231\345\206\205\346\266\210\346\201\257\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/M09-\347\253\231\345\206\205\346\266\210\346\201\257\346\265\201\347\250\213.md" index 67ca38a..ff719ae 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/M09-\347\253\231\345\206\205\346\266\210\346\201\257\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/M09-\347\253\231\345\206\205\346\266\210\346\201\257\346\265\201\347\250\213.md" @@ -2,9 +2,9 @@ > 负责人:罗皓晨 > 覆盖:M09、X03 -> 基础核心流程:F08、F09、F10、F12;鉴权依赖 F02、F13;X04 可追加售后来源 +> 基础核心流程:F08、F09、F10、F12;鉴权依赖 F02、F13;接入 X04 售后来源 > 直接协作:韦乾强(M04 Ordering)、张海洋(M05 Payment、M10 AfterSales)、唐宇昊(M01 Identity) -> 文档状态:初稿,待罗皓晨自审及 Ordering、Payment、AfterSales、Identity 交叉评审 +> 文档状态:完整定义;业务语义已按需求冻结,待接口、数据库、实现和测试承接 > 需求事实源:[需求规格说明书 M09](../../../01-需求文档/需求规格说明书.md) 的“M09 站内消息通知(X03)”完整七节 ## 一、范围与事实来源 @@ -15,9 +15,9 @@ M09 负责把订单、支付和售后模块已经提交的业务事实转换为 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| -| M09/X03 需求 | 完整定义,待需求冻结 | 作为角色、规则、异常和验收事实源 | -| 本文业务流程 | 初稿 | 明确事件入口、消息状态、异常、模块出口和不可变结果 | -| A501~A505、接口设计 4.3 | 部分定义、待交叉评审 | 由流程派生并做契约映射,不作为流程输入 | +| M09/X03 需求 | 完整定义 | 作为角色、规则、异常和验收事实源 | +| 本文业务流程 | 完整定义 | 冻结事件入口、固定接收人、整事件原子性、消息状态、异常和模块出口 | +| A501~A505、接口设计 4.3 | 部分定义,待按本文补齐 | 由流程派生并做契约映射,不作为流程输入 | | DB101~DB120 | 模板/占位,未冻结 | 不发明消息、Inbox 或 Outbox 的具体 DBxxx、字段、约束和索引 | | C06 实时推送 | 独立挑战流程 | 只在消息提交成功后接入,不承担消息持久化 | @@ -26,9 +26,9 @@ M09 负责把订单、支付和售后模块已经提交的业务事实转换为 ```mermaid flowchart LR ID["M01 Identity
已认证用户、角色、账号状态"] -->|"允许买家或商家访问本人消息"| MSG["M09 Messaging
消息生成、查询与已读"] - ORD["M04 Ordering
已提交的创建、取消、发货、完成事实"] -->|"事件标识、业务标识、明确接收人"| MSG - PAY["M05 Payment
已提交且幂等确定的支付结果"] -->|"支付事实与明确接收人"| MSG - AFTER["M10 AfterSales
已提交的申请、审核、寄回或退款事实"] -->|"售后事实与明确接收人"| MSG + ORD["M04 Ordering
已提交的创建、取消、发货、完成事实"] -->|"事件标识、业务标识和订单归属"| MSG + PAY["M05 Payment
已提交且幂等确定的支付成功事实"] -->|"支付事实和订单归属"| MSG + AFTER["M10 AfterSales
已提交的申请、审核、寄回或退款确定事实"] -->|"售后事实和申请归属"| MSG MSG -->|"本人消息列表、详情、未读数与已读结果"| BUYER["买家消息中心"] MSG -->|"本人经营消息与安全操作入口"| MERCHANT["指定商家消息中心"] @@ -46,8 +46,9 @@ flowchart LR 边界约束: -- 直接入口必须是来源模块已经提交的业务事实,并包含稳定事件标识、发生时间、业务标识和明确接收账号。 -- 接收人归属以用户 ID 为准;角色只决定文案、入口和允许的操作,不允许按“全部买家”或“全部商家”广播私人业务事实。 +- 直接入口必须是来源模块已经提交的业务事实,并包含稳定事件标识、发生时间、业务标识,以及固定接收人矩阵所需的买家和 `assignedMerchantUserId` 归属。 +- 接收人由第六章固定矩阵从业务归属派生,不能由调用方自由指定;角色只决定文案、入口和允许的操作,不允许按“全部买家”或“全部商家”广播私人业务事实。 +- 同一事件的全部必需接收人先整体校验、后整批提交;任一必需账号缺失、角色不符或业务归属不一致时,整事件零消息并告警。 - 确定出口是 PostgreSQL 中可查询的消息、未读数和首次已读结果;实时提示、前端角标和 Redis 均不是消息事实来源。 - 消息中的安全操作入口只描述目标业务对象。进入目标页面时仍由 M04、M05 或 M10 重新校验身份、归属和当前状态。 @@ -55,33 +56,34 @@ flowchart LR ```mermaid flowchart TD - UP["M04/M05/M10:业务事务提交成功"] --> A["提交事件标识、事实类型、发生时间、业务标识和明确接收人"] - A --> B{"事件字段、类型和接收人是否完整合法?"} - B -- "否" --> X["记录失败和 traceId
不生成半完整消息"] - B -- "是" --> C["按接收账号逐项处理"] - C --> D{"该事件、接收人和消息类型是否已有确定结果?"} - D -- "是" --> E["返回既有处理结果
不重复新增或增加未读数"] - D -- "否" --> F{"接收身份和数据范围是否匹配?"} - F -- "否" --> Y["停止该接收项并记录原因
整事件/部分成功边界待评审"] - F -- "是" --> G["按接收身份生成标题、摘要、正文和安全操作入口"] - G --> H["保存历史文案快照并设为未读"] - H --> I["同时保存本次消费的幂等结果"] - I --> J{"消息事务是否提交成功?"} - J -- "否" --> Z["本次整体不生效
等待来源可靠事实重试"] - J -- "是" --> K["M09 确定出口:消息可查询且未读数增加一次"] - K -. "提交后旁路" .-> L["交给 C06 尝试实时推送"] - L --> M{"实时推送是否成功?"} - M -- "是" --> N["在线用户收到轻提示"] + UP["M04/M05/M10:业务事务提交成功"] --> A["提交事件标识、事实类型、发生时间、业务标识和业务归属"] + A --> B{"事实类型是否属于固定消息矩阵?"} + B -- "否,明确为无消息事实" --> W["记录消费结果
不生成 M09 消息"] + B -- "否,未知或非法" --> X["记录失败和 traceId
告警且不生成消息"] + B -- "是" --> C["按固定矩阵解析全部必需接收人"] + C --> D{"全部账号、角色和业务归属都有效?"} + D -- "否" --> Y["整事件零消息并告警
不允许部分成功"] + D -- "是" --> E{"该事件是否已有确定处理结果?"} + E -- "是" --> F["返回整事件既有结果
不重复新增或增加未读数"] + E -- "否" --> G["按各接收身份生成标题、摘要、正文和安全操作入口"] + G --> H["在一个提交边界内保存全部消息和幂等结果"] + H --> I{"整批事务是否提交成功?"} + I -- "否" --> Z["本次整体不生效
等待来源可靠事实重试"] + I -- "是" --> K["M09 确定出口:全部消息可查询且各自未读一次"] + K -. "提交后旁路" .-> L["逐接收用户交给 C06 尝试实时提示"] + L --> M{"实时提示是否成功?"} + M -- "是" --> N["在线用户补查权威未读数"] M -- "否" --> O["保留消息事实
用户稍后通过消息中心补查"] ``` 关键规则: - 被回滚、仍在处理或结果不确定的业务操作不得生成“成功”消息。 -- 同一业务事实可按不同接收身份生成不同文案,但同一事件、接收账号和消息类型只能得到一份对应消息。 +- 同一业务事实可按不同接收身份生成不同文案,但同一事件、接收账号和消息类型只能得到一份对应消息;整事件幂等结果包含全部必需接收人。 - 消息正文、摘要和创建时间是生成时的历史快照,不因商品名称、订单展示文本或用户昵称后来变化而重写。 - 事件重复投递只能返回既有处理结果,不能重复生成消息、重复增加未读数或重复触发相同业务通知。 -- 消息事务失败时不得留下只有消费记录或只有消息正文的部分结果。 +- 消息事务失败时不得留下只有消费记录、部分接收人的消息或只有消息正文的部分结果。 +- 已禁用但身份和业务归属仍有效的账号仍属于有效接收人,消息照常保存;禁用只阻断查询、已读操作和实时推送,不删除历史。 ## 四、消息查询、详情与已读 @@ -97,8 +99,8 @@ flowchart TD E --> H{"消息属于本人且存在?"} H -- "否" --> X["按不存在处理,不泄露接收人和正文"] H -- "是" --> I["返回历史正文与当前可用的安全操作入口"] - I --> J{"关联资源仍存在且当前身份仍有权访问?"} - J -- "否" --> K["正文仍可查看,操作入口为空"] + I --> J{"关联资源存在且当前身份权限可确认?"} + J -- "否" --> K["正文仍可查看,操作入口为空
显示“目标暂不可用”"] J -- "是" --> L["允许进入目标模块并再次校验"] I --> M{"用户是否选择标记该消息已读?"} A --> N["用户选择全部已读"] @@ -106,9 +108,9 @@ flowchart TD M -- "是" --> O{"当前消息仍为未读?"} O -- "是" --> P["记录首次已读时间"] O -- "否" --> Q["返回原首次已读结果"] - N --> R["服务端记录本次操作开始时间"] - R --> S["只更新本人且创建时间不晚于该时间的未读消息"] - S --> T["并发到达的新消息保持未读"] + N --> R["服务端捕获本人已提交消息的稳定高水位"] + R --> S["只更新本人且不高于该高水位的未读消息"] + S --> T["高水位之后提交的新消息保持未读"] P --> U["返回最新已读结果并校正角标"] Q --> U T --> U @@ -119,6 +121,7 @@ flowchart TD - 列表、详情、未读数和写操作都必须包含当前认证用户范围,不能先读取任意消息再由客户端过滤。 - 买家和商家可以复用消息能力,但不能跨身份或跨账号查看、标记、跳转到他人资源。 - 管理员身份不自动获得查看任意用户私人消息的权限。 +- 账号禁用时,列表、详情、未读数、单条已读和全部已读全部拒绝;重新启用后可继续查询本人既有及禁用期间形成的消息,但旧凭证不恢复。 - 列表按页码分页;新消息导致后续页位移属于当前已知边界,本期不提前引入游标分页。 - 全站提供容易发现但不过度突出的消息入口和未读角标;列表加载时显示与页面结构一致的占位,空数据、请求失败、失败重试和分页加载都给出明确反馈。 - 标记已读成功后立即更新当前列表项与角标;请求失败时恢复操作前状态并提供就地重试,不能把前端乐观状态当作数据库已读事实。 @@ -137,13 +140,13 @@ stateDiagram-v2 状态约束: - 首次已读时间由服务端生成并持久化;重复或并发标记不得覆盖首次时间。 -- “全部已读”只覆盖操作开始时已经存在的当前用户未读集合,不影响操作期间新到达的消息。 +- “全部已读”先捕获本人已提交消息的稳定高水位,只覆盖不高于该水位的当前未读集合;不得使用客户端时间或墙上时钟作为截止依据。 - 本期不提供物理删除消息流程。后续确需清理时,必须先定义保留期、归档和验收追踪规则。 - Redis 或前端角标可以加速展示,但未读状态始终以 PostgreSQL 查询结果为准。 ## 六、来源事实、接收人和业务出口 -下表只登记业务语义。具体集成事件名称、字段和 Routing Key 由接口设计 4.3 承接,并仍需来源模块交叉评审。 +下表只登记已经冻结的业务语义。具体集成事件名称、字段和 Routing Key 由接口设计 4.3 承接;契约必须服从此矩阵。 | 已提交业务事实 | 直接来源 | 目标接收人 | 消息业务出口 | 当前边界 | |---|---|---|---|---| @@ -152,24 +155,26 @@ stateDiagram-v2 | 支付成功 | M05 Payment | 订单买家、订单指定处理商家 | 买家支付/订单详情、商家待发货订单 | 接收商家必须由订单事实明确给出 | | 订单发货 | M04 Ordering | 订单买家 | 买家订单详情 | 只接受唯一 `Shipped` 结果 | | 订单完成 | M04 Ordering | 订单买家 | 买家订单详情 | 买家确认与自动完成只通知唯一胜出结果 | -| 售后申请提交 | M10 AfterSales | 申请买家、订单指定处理商家 | 买家售后详情、商家审核入口 | X04 未确认前保持待交叉评审 | +| 售后申请提交 | M10 AfterSales | 订单指定处理商家 | 商家审核入口 | 买家已在当前操作中得到申请结果,不重复给本人发站内消息 | | 售后审核或待寄回 | M10 AfterSales | 申请买家 | 买家售后详情 | 文案必须反映已提交审核结果 | | 买家提交寄回信息 | M10 AfterSales | 订单指定处理商家 | 商家售后详情 | 不按全部商家广播 | -| 退款成功或失败 | M05 Payment / M10 AfterSales | 申请买家;接口设计 4.3.6 草案对退款失败另列订单指定商家 | 售后详情 | 主需求只明确买家,当前存在契约冲突,待 Payment/AfterSales/Identity 评审;不用消息反向修改退款状态 | +| 退款成功或确定失败 | M05 Payment / M10 AfterSales | 申请买家 | 售后详情 | 不通知商家;消息不反向修改退款状态 | +| 支付失败、回调 `Ignored`、回调 `Difference`、对账差异发现或管理员对账处置 | M05 Payment / C08 | 无 | 无 | 记录在支付/对账事实和审计中,不生成 M09 消息 | ## 七、异常、补偿与责任 | 场景 | M09 处理 | 最终状态与责任 | |---|---|---| | 来源事务回滚或事实未确定 | 不生成成功消息 | 来源模块继续拥有业务状态 | -| 事件字段、类型或接收人非法 | 记录失败、`traceId` 和安全原因 | 不生成半完整消息,不自行猜接收人 | +| 事件字段、类型或任一必需接收人非法 | 整事件零消息,记录失败、`traceId` 和安全原因并告警 | 不自行猜接收人,不允许部分接收人先成功 | | 同一事件重复到达 | 返回既有处理结果 | 不新增消息、不增加未读数 | | 可靠消息通道暂时不可用 | 由来源模块保留待发布事实并重试 | 已提交业务结果不回滚 | | 消息事务失败 | 整体不生成,允许可靠重试 | 不留下消息/消费记录的部分结果 | | SignalR 或 Redis 不可用 | M09 数据事实仍保留并记录实时推送失败;Identity 仍能安全鉴权时可继续查询 | 若令牌撤销状态无法确认,受保护请求按 Identity 失败关闭策略处理 | | 查询他人消息 | 与不存在统一处理 | 不泄露消息是否存在、接收人或正文 | -| 关联资源被归档或失去权限 | 返回历史消息正文,移除操作入口 | 目标模块状态不被消息覆盖 | -| 全部已读期间新消息到达 | 新消息保持未读 | 批量结果只覆盖操作开始时集合 | +| 关联资源不存在、目标模块不可用或权限无法确认 | 返回历史消息正文,操作入口为空并显示“目标暂不可用” | 目标模块恢复后重新查询详情,不用消息覆盖目标状态 | +| 账号禁用 | 仍按有效业务归属保存消息,拒绝查询、已读和推送 | 启用后可查历史;旧凭证不恢复 | +| 全部已读期间新消息到达 | 高水位后提交的消息保持未读 | 批量结果只覆盖稳定高水位内集合 | | 列表首次加载、空数据或分页请求失败 | 保留消息中心页面结构,分别显示加载占位、空状态或就地重试 | 不把加载失败显示成“没有消息” | | 单条或全部已读请求失败 | 恢复操作前的列表项和角标,提示用户重试 | 数据库未提交时不得保留虚假已读状态 | @@ -179,12 +184,12 @@ stateDiagram-v2 | 流程能力 | 当前派生契约 | 数据设计 | 当前状态 | |---|---|---|---| -| 接收已提交事实并按接收人幂等生成消息 | 接口设计 4.3.6“业务模块到 Messaging 的集成事件” | DB101~DB120 尚未分配具体表 | 来源模块待交叉评审;`RefundFailedIntegrationEvent` 的商家接收人范围与主需求存在契约冲突 | +| 接收已提交事实并按固定矩阵整事件幂等生成消息 | 接口设计 4.3.6“业务模块到 Messaging 的集成事件” | DB101~DB120 尚未分配具体表 | 待按固定矩阵、整事件原子性和无消息事实补齐非 HTTP 契约 | | 查询本人消息列表 | A501 | 待 `database-lhc.md` 和数据库主文档确认 | 待交叉评审 | | 查询本人消息详情与安全操作入口 | A502 | 待确认 | 待交叉评审 | | 查询本人未读数 | A503 | 待确认 | 待交叉评审 | | 首次标记单条消息已读 | A504 | 待确认 | 待交叉评审 | -| 将操作开始前的本人当前消息全部已读 | A505 | 待确认 | 待交叉评审 | +| 将稳定高水位内的本人当前消息全部已读 | A505 | 待确认 | 待补齐服务端高水位返回与并发约束 | | 消息提交后实时推送 | 接口设计 4.3.7“Hub 连接”、4.3.8“MessageCreated” | 复用已持久化消息事实 | 转入 C06,待部署与测试评审 | 架构承接章节: @@ -193,27 +198,26 @@ stateDiagram-v2 - 系统架构 7.6“四项选做功能”:消息接收账号、持久化后推送和数据库已读事实; - 系统架构 7.10“C06 实时消息推送”:消息提交后的实时到达旁路。 -## 九、待交叉评审项 +## 九、下游设计与验证约束 -1. Ordering、Payment、AfterSales 与 Identity 共同确认商家运营账号的精确接收范围,禁止由 Messaging 自行按角色扩散。 -2. 各来源模块确认事件触发时机、明确接收人和最小业务快照;X04 未确认前,其售后来源只作为待评审接入点。 -3. 数据库设计需在罗皓晨的 DB101~DB120 区间明确消息、Inbox 及必要可靠事件表,并定义唯一约束、索引和删除行为。 -4. A501~A505 仍需生成真实 OpenAPI,并完成 HTTP 契约交叉评审。 -5. 接口设计 4.3 的来源事件、接收人、SignalR Hub 与载荷仍需来源模块评审和非 HTTP 契约测试,不属于 OpenAPI 接口。 -6. 同一来源事件包含多个接收项时,单个接收项无效应使整事件失败还是允许已明确的其他接收项成功,尚需来源模块共同确认。 -7. 主需求只明确退款成功和失败通知申请买家,但接口设计 4.3.6 的 `RefundFailedIntegrationEvent` 草案另列订单 `assignedMerchantUserId`;Payment、AfterSales 与 Identity 必须先解决该契约冲突并确认精确接收账号。 -8. 消息安全操作入口需由目标模块确认当前身份与资源归属的重新校验方式。 -9. “全部已读”的服务端截止时间与并发新消息边界需要进入数据库约束和契约测试。 +1. A501~A505 必须承接禁用账号拒绝访问、本人范围、目标暂不可用和稳定高水位,不得用现有草案改变流程。 +2. 接口设计 4.3 的来源事件必须承接固定接收人矩阵、整事件零或全、稳定幂等键和明确无消息事实;非 HTTP 契约不进入 OpenAPI,但必须形成可测试定义。 +3. 数据库设计需明确消息、可靠消费及必要 Outbox/Inbox 事实,保证整事件原子提交、每个接收人的唯一性、首次已读时间和高水位范围。 +4. 来源模块只发布已提交事实及业务归属,不自由指定广播范围;Messaging 不反向修改订单、支付或售后状态。 +5. 目标模块负责在用户打开操作入口时重新校验当前身份、资源归属和状态;无法确认时返回目标暂不可用。 ## 十、验收证据清单 - [ ] 订单创建、取消、支付、发货、完成和售后事实只在来源事务提交后生成消息。 - [ ] 买家与指定商家收到符合身份的内容,游客、管理员和无关账号不收到私人消息。 +- [ ] 支付成功仅通知买家和指定商家;售后申请/寄回仅通知指定商家;审核和退款确定结果仅通知买家;明确无消息事实不生成消息。 +- [ ] 任一必需接收人无效时整事件零消息并告警,不出现部分接收人成功。 - [ ] 同一事件重复投递至少两次,只形成一份对应接收人的消息。 - [ ] 本人列表、详情、筛选、分页和未读数正确,越权请求不泄露消息内容。 -- [ ] 单条已读、重复已读、并发已读和全部已读满足首次时间及集合边界。 +- [ ] 单条已读、重复已读、并发已读和全部已读满足首次时间及稳定高水位边界,高水位后消息保持未读。 - [ ] 消息入口和列表分别验证加载占位、空状态、请求失败重试与分页加载反馈。 - [ ] 标记已读成功时列表与角标立即更新;模拟失败时恢复原状态并提供重试。 - [ ] 可靠消息或实时推送故障时,来源业务结果不回滚,消息能够按既定责任恢复或补查。 -- [ ] 关联资源不可访问时,历史正文仍可查看,但不返回无效或越权操作入口。 +- [ ] 关联资源不存在、不可访问或权限无法确认时,历史正文仍可查看,操作入口为空并显示“目标暂不可用”。 +- [ ] 禁用账号仍按有效归属保存消息但不能查询、已读或接收推送;启用后可查历史且旧凭证不恢复。 - [ ] 保留事件标识、消息标识、接收用户、`traceId`、数据库结果和重试结果的脱敏证据。 -- Gitee From e96b58533581d478214c5804da22e012ebacfbd5 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 21:39:32 +0800 Subject: [PATCH 102/118] =?UTF-8?q?docs(process):=20=E5=86=BB=E7=BB=93=20C?= =?UTF-8?q?07=20=E7=BC=93=E5=AD=98=E6=B5=81=E7=A8=8B=EF=BC=9B=E9=97=AD?= =?UTF-8?q?=E5=90=88=E8=B7=A8=E6=A8=A1=E5=9D=97=E5=BA=93=E5=AD=98=E5=A4=B1?= =?UTF-8?q?=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 50 ++--- ...06\345\223\201\346\265\201\347\250\213.md" | 12 +- ...41\347\220\206\346\265\201\347\250\213.md" | 6 +- ...23\345\255\230\346\265\201\347\250\213.md" | 172 ++++++++++-------- ...05\346\227\266\346\265\201\347\250\213.md" | 4 +- ...42\345\215\225\346\265\201\347\250\213.md" | 5 +- ...22\346\235\200\346\265\201\347\250\213.md" | 2 +- ...56\345\220\216\346\265\201\347\250\213.md" | 7 +- 8 files changed, 145 insertions(+), 113 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 38b829d..ea695b0 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -1,6 +1,6 @@ # 电子商城需求规格说明书 -> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.10 +> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.11 > > 文档状态:内容和格式已完成内部统一,待全组与指导教师评审后冻结 > 截止:第 1 周周三提交初稿 @@ -19,6 +19,7 @@ | v0.8 | 2026-07-24 | 罗皓晨 | 冻结 F13 买家与商家分流、非默认商家责任清单、禁用竞争顺序和全部旧凭证失效边界 | | v0.9 | 2026-07-24 | 罗皓晨 | 冻结 X02 收藏幂等、浏览历史默认开启与最近 200 条上限,明确关闭记录不隐藏旧历史并取消清空历史能力 | | v0.10 | 2026-07-24 | 罗皓晨 | 冻结 X03 事件接收人、整事件消息原子性和全部已读水位,并统一 C06 WebSocket、凭证失效、角标补查和固定重连边界 | +| v0.11 | 2026-07-24 | 罗皓晨 | 冻结 C07 固定首页、详情缓存、TTL、跨实例填充、二次失效和普通/秒杀库存失效矩阵 | ## 业务流程设计入口 @@ -1877,7 +1878,7 @@ C01 要求在面向买家的高并发秒杀场景下保证库存“只减不超 - 幂等与拒绝顺序:完成认证和固定字段校验后,先按 PostgreSQL 幂等记录处理相同 Key;同指纹直接重放首次结果,不再受当前限流、时间、库存或限购变化影响,不同指纹返回 409。只有全新 Key 才依次进入限流、时间窗口、售罄、单用户限购和其他业务校验;前端不把 5xx 误判为“已售罄”。 - 数据隔离:秒杀活动接口、订单接口和库存接口在读写上都必须按活动 ID、买家 ID 双重过滤;活动维度数据不暴露他人抢购明细,只返回当前请求可见信息。 - 日志脱敏:秒杀日志记录买家 ID、活动 ID、行为结果和 traceId;不输出完整 Token、密码或支付卡号;截图和答辩材料中订单金额、库存数据按需脱敏。 -- 与 C03、C08、C10、C07 的衔接:超时取消使用秒杀库存回补通道;支付回调幂等(C08)也作用于秒杀订单;多 API 实例(C10)下秒杀入口由任一实例受理,最终一致性仍以数据库为准;C07 只用于固定首页商品摘要和商品详情,秒杀活动列表、活动状态和秒杀库存均不缓存。 +- 与 C03、C08、C10、C07 的衔接:超时取消使用秒杀库存回补通道;支付回调幂等(C08)也作用于秒杀订单;多 API 实例(C10)下秒杀入口由任一实例受理,最终一致性仍以数据库为准;C01 发布时划拨普通库存会触发 C07 失效固定首页和商品详情,其后活动抢购、取消及售后原活动回补不触发 C07,秒杀活动列表、活动状态和秒杀库存均不缓存。 #### 6. 异常与边界场景 @@ -2178,18 +2179,20 @@ C07 使用 Redis 优化首页商品摘要和商品详情两个高频只读场景 | 编号 | 功能 | 详细要求 | |---|---|---| -| C07-FR01 | 缓存对象 | 本期只缓存固定首页商品摘要和商品详情中的商品自身公开字段;分类、普通商品列表、关键词搜索、组合筛选、秒杀活动、M07 评价汇总和评价列表均不缓存。缓存内容不含管理员字段、连接信息或用户敏感数据。 | -| C07-FR02 | Cache-Aside 读取 | 查询先读取 Redis;命中时返回缓存值,未命中时查询 PostgreSQL、生成与原接口一致的响应并写入有限 TTL 的缓存。 | +| C07-FR01 | 缓存对象 | 本期只缓存固定首页商品摘要和 A103 商品详情中的商品自身公开字段。固定首页没有用户筛选参数,只取 `OnSale` 商品,按 `createdAt DESC, productId DESC` 稳定排序并固定返回前 12 条;普通库存为零的商品仍展示并标记售罄。分类、A102 普通商品列表、关键词搜索、组合筛选、秒杀活动、M07 评价汇总和评价列表均不缓存。缓存内容不含管理员字段、连接信息或用户敏感数据。 | +| C07-FR02 | Cache-Aside 读取 | 查询先读取 Redis;命中时返回缓存值,未命中时查询 PostgreSQL、生成与原接口一致的响应并写入缓存。正常值 TTL 固定为 60 秒;请求取得回填资格后,数据库查询到缓存写入决定的有效回填窗口最多 2 秒,超时仍可按接口规则返回数据库结果,但不得再回填该次可能过旧的值。 | | C07-FR03 | Key 隔离 | Key 必须包含环境、模块、资源类型、资源 ID 或稳定查询标识及必要版本信息,避免不同环境、不同查询条件和不同数据结构互相污染。 | -| C07-FR04 | 写后失效 | 商品改价、库存调整、上下架、名称/图片/描述变更的数据库事务提交后,删除受影响的详情和首页缓存;事务回滚时不得提前删除并生成错误的新值。 | -| C07-FR05 | 最终一致窗口 | 明确每类缓存 TTL、主动失效时机和删除失败后的重试方式;文档和验收报告必须给出理论最迟生效时间,不能只描述“最终会一致”。 | -| C07-FR06 | 空值保护 | 对不存在或不可售商品的重复查询采用短时空值、受控校验或等价方式降低缓存穿透;空值有效期必须短于正常数据且不能掩盖新上架商品。 | -| C07-FR07 | 热点保护 | 同一热点 Key 并发失效时使用受控的请求合并、短期互斥或等价策略减少数据库瞬时冲击;等待失败的请求必须有超时和回退,不得无限阻塞。 | +| C07-FR04 | 写后失效 | 商品、分类展示或库存事实的数据库事务提交后立即执行首次失效,3 秒后执行一次二次失效。名称、价格、普通库存、描述、分类展示、图片新增/删除/排序/主图、销售状态和删除等已提交变化必须失效受影响的 A103 详情;影响首页展示字段、成员资格或稳定顺序时同时失效固定首页。事务回滚时不得发布成功失效。 | +| C07-FR05 | 最终一致窗口 | 正常值 TTL 为 60 秒,空值 TTL 为 10 秒;首次失效在事务提交后立即触发,二次失效在提交后 3 秒触发。提交前已经开始的旧查询最多在提交后 2 秒内回填,因此两次删除都失败时,正常旧值最坏在提交后 62 秒到期,旧空值最坏在提交后 12 秒到期;二次失效用于通常更早清除旧回填,不能替代 TTL 上限。 | +| C07-FR06 | 空值保护 | A103 对不存在或不可公开商品写入 10 秒空值;上架、恢复公开或创建同标识资源后立即失效对应空值。固定首页空结果也只保存 10 秒,不得用空值掩盖新上架商品。 | +| C07-FR07 | 热点保护 | 同一热点 Key 未命中时,全系统只允许一个跨实例填充者查询并回填;其他请求最多等待 500 毫秒,仍未得到缓存结果时直接查询 PostgreSQL 并返回,不继续争抢填充资格,也不得无限阻塞。 | | C07-FR08 | 故障降级 | Redis 不可用时,允许首页和商品详情回退到 PostgreSQL 并记录降级指标;不得返回无法判断新旧的缓存副本冒充数据库结果。 | | C07-FR09 | 多实例一致使用 | 两个 API 实例共享同一 Redis 和 Key 约定;任一实例完成商品变更后,其他实例后续读取应遵守同一失效结果。 | | C07-FR10 | 可观测性 | 记录命中、未命中、写入、失效、错误和降级次数,以及缓存读取耗时;日志携带资源标识和 `traceId`,但不记录完整缓存值中的敏感信息。 | | C07-FR11 | 用户感知性能 | 首页和商品详情请求期间展示与页面结构一致的加载占位,避免内容突然跳动;请求失败提供就地重试。Redis 降级到数据库时不向用户暴露技术错误,只有数据库查询也失败时才展示统一错误反馈。 | | C07-FR12 | 身份与数据隔离 | 公共商品缓存只能存放游客和买家均可见的数据;个人字段、商家管理字段和管理员字段使用独立查询且默认不进入本期缓存,防止不同身份共享 Key 导致越权泄露。 | +| C07-FR13 | 库存通道失效 | 普通订单扣减、普通订单取消回补和 M10 普通库存售后回补均失效详情及受影响的固定首页;C01 发布时从普通库存原子划拨到独立秒杀库存,也触发同样失效。秒杀活动内部抢购,以及秒杀订单取消或售后回补到原活动库存,只改变独立秒杀库存,不触发 C07。 | +| C07-FR14 | 评价隔离 | M07 评价提交、图片变化和评分汇总不进入 A103 商品自身公开缓存,也不触发 C07 失效;评价事实由 M07 查询链路独立提供。 | #### 4. 主流程 @@ -2197,22 +2200,24 @@ C07 使用 Redis 优化首页商品摘要和商品详情两个高频只读场景 1. 服务端完成参数、资源范围和公开可见性校验,构造稳定缓存 Key。 2. 查询 Redis;命中且反序列化成功时返回缓存响应,并记录命中指标。 -3. 未命中时查询 PostgreSQL;资源不存在时按约定写入短期空值或直接返回不存在。 -4. 查询成功后以有限 TTL 写入 Redis;缓存写入失败只影响性能,不改变本次数据库查询结果。 +3. 未命中时竞争跨实例填充资格;唯一填充者在最长 2 秒回填窗口内查询 PostgreSQL,其他请求最多等待 500 毫秒,超时后直接查询 PostgreSQL且不争抢回填。 +4. 正常结果以 60 秒 TTL 写入,不存在或不可公开结果以 10 秒 TTL 写入空值;缓存写入失败只影响性能,不改变本次数据库查询结果。 **商品变更:** -1. 商品模块在 PostgreSQL 事务内完成改价、库存、上下架或内容更新。 -2. 事务成功后触发缓存失效;删除商品详情 Key,并删除或版本化受影响的首页列表 Key。 -3. 删除失败时记录待重试信息;在重试完成前由较短 TTL 限制旧值最长存在时间。 -4. 下一次读取未命中后从 PostgreSQL 回填新值,所有 API 实例共享更新结果。 +1. 商品、订单、秒杀或售后模块通过所属公开能力在 PostgreSQL 事务内完成商品展示事实或普通库存变化。 +2. 事务提交后立即删除受影响的商品详情 Key;若首页展示字段、成员资格或稳定顺序受影响,同时删除唯一固定首页 Key。 +3. 提交后 3 秒再次删除同一组 Key,清理可能由提交前旧查询在首次失效后回填的旧值;失效失败记录告警和重试证据。 +4. 下一次有效读取未命中后从 PostgreSQL 回填新值,所有 API 实例共享更新结果;即使两次删除都失败,旧值也不得超过提交后 62 秒。 #### 5. 业务规则与权限 - 价格、库存和上下架状态以 PostgreSQL 当前值为准;下单流程必须重新校验数据库,不能相信首页或详情缓存中的库存和价格。 - 主动失效与有限 TTL 必须同时存在:主动失效缩短正常更新窗口,TTL 负责约束删除失败或漏删后的最长旧值时间。 -- 如果采用延迟二次失效,其目的仅是缩短“并发旧查询回填旧值”的窗口,不能替代事务提交后的首次失效和 TTL。 -- 首页存在分类、排序、分页等多种组合时,只缓存已明确纳入验收的固定首页摘要,不为任意查询参数生成无限数量 Key。 +- 延迟 3 秒的二次失效只用于缩短“提交前旧查询回填旧值”的窗口,不能替代事务提交后的首次失效和 60 秒 TTL。 +- 固定首页没有分类、关键词、分页或用户筛选参数;只缓存最新 `OnSale` 商品前 12 条的唯一摘要 Key,不为任意查询参数生成缓存 Key。 +- A103 缓存只包含商品自身公开字段,不包含 M07 评价、评分汇总、收藏、购物车或任何身份化字段。 +- 秒杀库存与普通库存是两个通道。只有改变普通库存或公开商品字段的已提交动作触发 C07;活动内部秒杀库存变化不触发。 - 缓存数据结构发生不兼容变化时通过版本化 Key 或受控清理处理,不直接尝试把旧结构反序列化为新结构。 - 缓存 Key 不得包含用户密码、完整 Token、手机号或收货地址;公开商品缓存不得混入当前登录用户的个性化字段。 @@ -2220,9 +2225,10 @@ C07 使用 Redis 优化首页商品摘要和商品详情两个高频只读场景 - Redis 完全不可用:接口回退数据库并保持结果正确,健康状态和日志能够反映缓存降级。 - 缓存中存在损坏或旧版本数据:视为未命中并删除异常 Key,不向客户端返回反序列化异常或错误结构。 -- 商品刚下架时发生并发读取:事务提交后的失效流程启动,购物端最终不再展示商品;即使命中旧详情,下单仍通过数据库校验拒绝不可售商品。 -- 热点 Key 同时过期:数据库请求量受到控制,等待请求不会无限阻塞;保护机制失败时仍以正确响应或可解释错误结束。 -- 多实例环境中由实例 1 修改商品、实例 2 查询:实例 2 在约定一致性窗口内读取到新值。 +- 商品刚下架时发生并发读取:提交后立即失效并在 3 秒后再次失效;即使两次删除失败,旧详情最迟在提交后 62 秒失效,下单始终通过 PostgreSQL 拒绝不可售商品。 +- 热点 Key 同时过期:仅一个跨实例填充者回填,其余请求等待最多 500 毫秒后直查 PostgreSQL,不无限等待或继续争抢填充。 +- 多实例环境中由实例 1 修改商品、实例 2 查询:实例 2 共享同一 Redis、填充资格和失效结果,最坏在提交后 62 秒内读取到新值。 +- C01 发布改变普通库存时触发失效;秒杀活动内部扣减、取消回补和售后回补只返回原活动库存,不清理与该通道无关的 C07 Key。 - 游客和会员读取同一公开商品时可共享公开缓存,但会员个人字段由独立接口返回;商家修改商品后,游客和会员均在一致性窗口内看到新值,商家管理页始终显示其有权查看的完整字段。 #### 7. 验收标准与证据 @@ -2244,7 +2250,9 @@ C07 使用 Redis 优化首页商品摘要和商品详情两个高频只读场景 - 保存压测脚本、数据初始化方式、软硬件环境、Commit SHA、配置、测试时间、原始输出和汇总表。 - 保存商品改价、库存变化、上下架、多实例读取、Redis 故障和热点 Key 过期的操作步骤与结果。 -- 能解释 Cache-Aside 的读写流程、数据库为何仍是事实来源、TTL 与主动失效的分工,以及旧值窗口的来源和理论上限。 +- 保存固定首页排序和 12 条边界、售罄展示、A102 不缓存、A103 不含评价、普通/秒杀库存失效矩阵的验证结果。 +- 能解释 Cache-Aside 的读写流程、数据库为何仍是事实来源、60/10 秒 TTL、提交后立即及 3 秒二次失效的分工,以及 62 秒最坏旧值窗口的来源。 +- 能证明同一热点只有一个跨实例填充者,其他请求最多等待 500 毫秒并可回退 PostgreSQL。 - 能区分缓存穿透、击穿和雪崩,并说明本项目实际处理了哪些场景、没有实现哪些高级方案及原因。 ### C08 支付回调幂等与对账 — 张海洋 @@ -2549,7 +2557,7 @@ Electron 后续原则上复用 PC Web 原型,只补充窗口、桌面导航和 | C03 | 订单超时取消—韦乾强 | 订单倒计时、Worker 验收脚本 | 复用 A304;超时扫描为 Worker 内部契约 | 待挑战测试登记 | Worker 扫描、取消复用和原库存通道回补已定义,待数据库与测试评审 | | C04 | 商品搜索进阶—顾欣月 | 商品搜索页、性能对比报告 | A102 | 待挑战测试登记 | 接口草案已汇总,待交叉评审 | | C06 | 实时消息推送—罗皓晨 | 实时通知、连接状态、消息中心 | SignalR 契约(无 Axxx),补查复用 A501~A505 | 待挑战测试登记 | WebSockets 跳过协商、固定重连、凭证失效与权威角标补查已冻结,待接口、部署与测试承接 | -| C07 | 缓存与性能优化—罗皓晨,顾欣月协作 | 首页、商品详情、压测报告 | 复用 A102 的固定首页摘要调用、A103 商品详情;不缓存普通列表与搜索 | 待挑战测试登记 | 不新增业务 HTTP,缓存约定待确认 | +| C07 | 缓存与性能优化—罗皓晨,顾欣月协作 | 首页、商品详情、压测报告 | 复用 A102 的固定首页摘要调用、A103 商品详情;不缓存普通列表与搜索 | 待挑战测试登记 | 60/10 秒 TTL、500 毫秒跨实例填充等待、提交后立即/3 秒双删和 62 秒兜底已冻结,待接口、实现与压测承接 | | C08 | 支付回调幂等与对账—张海洋 | 回调重放脚本、管理端对账 | A421~A425、A432~A433;退款入账使用 Payment 应用契约 | 待挑战测试登记 | A431 为取消历史号,待数据库、OpenAPI 与交叉评审 | | C10 | 容器化与负载均衡—罗皓晨 | 统一访问入口、健康与实例验证 | A506、A507 | 待部署验收登记 | 健康检查草案已汇总,待交叉评审 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" index 6f3da02..ea9ecf0 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" @@ -21,7 +21,7 @@ | 本文业务流程 | 完整定义、待交叉评审 | 已确认角色、状态、分支和模块出入口 | | 分类与商品接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | | DBxxx 分类/商品表 | 部分定义 | 本文不发明表字段、状态码和索引 | -| C07 缓存协作 | 待细化(罗皓晨主责、顾欣月协作失效规则) | 只登记接入点,不混入 F04~F06 核心浏览口径;具体 TTL 与失效策略由缓存主责确认 | +| C07 缓存协作 | 完整定义 | 只接入固定首页摘要和 A103 商品自身详情;60/10 秒 TTL、3 秒二次失效和 62 秒兜底已冻结 | | C04 中文搜索进阶 | 待细化(顾欣月主责) | 在 M02-01 基础模糊查询入口上增强,业务口径与本文保持一致 | ## 二、模块直接出入口 @@ -63,7 +63,7 @@ flowchart LR - 购物端的价格和库存只能作为浏览口径,下单与购物车写入必须由服务端在 M03、M04 中重新校验。 - 商品模块不直接访问用户私有数据;买家专属操作(收藏、加购、购买)由 M08、M03、M04 提供,商品模块只提供事实输入和入口。 - 评价(X01)汇总来自 M07,商品详情只做公开读取,不修改评价事实。 -- C07 本期只缓存预定义且不可由用户任意组合查询参数的固定首页商品摘要,以及商品详情。除该固定首页调用外,M02 分类、普通列表、关键词搜索和组合筛选不得接入 C07,每次都按 PostgreSQL 已提交事实执行。首页摘要和详情缓存只允许在 C07 已确认的一致性窗口内短暂返回旧公开值,缓存失效或不可用时回退 PostgreSQL。 +- C07 本期只缓存无用户筛选、只取 `OnSale`、按 `createdAt DESC, productId DESC` 稳定排序的前 12 条固定首页摘要,以及 A103 商品自身公开详情;库存为零仍显示售罄。除该固定首页调用外,M02 分类、普通列表、关键词搜索和组合筛选不得接入 C07,每次都按 PostgreSQL 已提交事实执行。A103 不含 M07 评价或评分;正常旧值最坏不超过事务提交后 62 秒,旧空值不超过 12 秒,缓存不可用时回退 PostgreSQL。 - C04 在 M02-01 列表入口上替换底层搜索实现,对外参数白名单、已上架过滤和返回口径与本文保持一致;接口与数据待评审项在第十章集中登记。 - 扩展完成后回到的核心结果:F04 公开列表仍只返回已上架商品、F05 关键词查询仍按统一搜索契约返回、F06 详情仍遵守已上架、售罄和有限一致性窗口口径。扩展失败不能改变上述核心结果。 @@ -193,15 +193,15 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| | 查询启用分类 | A101 Catalog 分类 | 只返回购物端筛选入口使用的启用分类;分类停用不改变其下商品销售状态 | 待交叉评审 | -| 查询固定首页摘要、普通列表与组合筛选/关键词搜索 | A102 Catalog 列表/搜索 | 固定首页摘要调用可由 C07 缓存;所有用户控制的普通列表、关键词和组合筛选每次按 PostgreSQL 强制已上架过滤、分页、白名单排序并正常返回空结果;库存为零时标记售罄,所属分类停用时仍可通过全部商品和关键词命中;F05 与 C04 共用同一契约 | 待交叉评审 | -| 查询商品详情 | A103 Catalog 详情 | 仅公开已上架商品;允许库存为零并标记售罄,允许所属分类停用;C07 旧值受一致性窗口约束 | 待交叉评审 | +| 查询固定首页摘要、普通列表与组合筛选/关键词搜索 | A102 Catalog 列表/搜索 | 只有无筛选、`OnSale`、`createdAt DESC, productId DESC`、前 12 条的固定首页摘要可由 C07 缓存;所有用户控制的普通列表、关键词和组合筛选每次按 PostgreSQL 强制已上架过滤、分页、白名单排序并正常返回空结果;库存为零时标记售罄,所属分类停用时仍可通过全部商品和关键词命中;F05 与 C04 共用同一契约 | 待重建详细契约 | +| 查询商品详情 | A103 Catalog 详情 | 仅公开已上架商品;允许库存为零并标记售罄,允许所属分类停用;缓存只含商品自身公开字段,不含 M07 评价/评分或个人字段;旧值最坏不超过提交后 62 秒 | 待重建详细契约 | | 公开评价汇总与列表(X01 衔接) | 由 M07 的公开读取能力派生,编号待 M07 流程确认后映射 | 商品详情只读取公开评价和评分汇总,不混入上传、提交或资格判断能力 | 待交叉评审 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 ## 九、扩展接入边界 -- C07 缓存:A102 中只有预定义且不可由用户任意组合参数的固定首页摘要调用可以接入缓存,A103 商品详情也可接入;A101 分类及 A102 的普通列表、关键词搜索和组合筛选不进入本期缓存。M06-01 提交商品事务后只失效受影响的首页摘要和目标商品详情,TTL、一致性窗口与主动失效策略由 C07 主责确认。 +- C07 缓存:A102 中只有无筛选、`OnSale`、稳定倒序、前 12 条的固定首页摘要调用可以接入缓存,A103 只缓存商品自身公开字段;A101 分类及 A102 普通列表、关键词搜索和组合筛选不缓存。正常值 60 秒、空值 10 秒,事务提交后立即及 3 秒执行两次失效,提交前旧查询的有效回填窗口最多 2 秒,双删失败时正常旧值最坏 62 秒、旧空值最坏 12 秒。 - C04 中文搜索:在 M02-01 列表查询入口上替换底层搜索实现,返回口径与基础模糊查询一致;公开浏览口径、参数白名单和已上架过滤不变。 - M03 购物车:只接收本模块输出的已上架商品与实时价格库存;下架或库存归零由 M03 标记失效,不反向修改商品状态。 - M04 订单:下单时由 M04 重读本模块的最新事实进行条件扣减,不信任购物端传入的金额和库存。 @@ -217,7 +217,7 @@ flowchart TD 6. 评价公开汇总字段(平均分、总条数的计算时机)由 M07 派生;本期不得把它混入 C07 商品详情缓存,避免评价变更扩大商品缓存失效范围。 7. C04 进阶搜索替换 F05 基础模糊查询时,需要保留 A102 商品列表/搜索接口的请求字段和返回口径,避免前端按字段名硬编码查询逻辑。 8. DBxxx 分类/商品表字段尚未形成可实施的完整定义,数据库字段、约束、索引和 GIN/pg_trgm 维护策略必须由数据库设计任务另行确认。 -9. C07 的 TTL 与主动失效上限需要在本流程评审前完成,避免缓存值与商品最新事实长期不一致。 +9. 接口和测试必须承接 C07 已冻结的固定首页语义、A103 字段隔离、60/10 秒 TTL、跨实例单填充、3 秒二次失效及 62 秒最坏窗口,不得继续保留“缓存参数待确认”的旧口径。 ## 十一、验收证据清单 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" index b1696ba..5b63e68 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -22,7 +22,7 @@ | 本文业务流程 | 完整定义、待交叉评审 | 已确认角色、状态、分支和模块出入口 | | 商家端写操作接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | | DBxxx 分类/商品表 | 部分定义 | 本文不发明表字段、状态码和索引 | -| C07 缓存失效协作 | 待细化 | 只登记接入点,不混入商家写操作核心结果 | +| C07 缓存失效协作 | 完整定义 | 商品事实提交后按冻结矩阵立即失效并在 3 秒后二次失效,不混入商家写操作核心结果 | | C04 搜索索引更新 | 待细化 | 仅约束 PostgreSQL 同步维护,不建设独立索引任务 | ## 二、模块直接出入口 @@ -236,7 +236,7 @@ flowchart TD ## 十、扩展接入边界 -- C07 缓存:商品事务提交后失效受影响的固定首页摘要和目标商品详情;分类、后台商品列表及用户控制的购物端普通列表和搜索本期不缓存。缓存不可用时不影响商品事务,由 C07 重试失效动作并以 TTL 约束旧首页摘要和详情窗口。 +- C07 缓存:商品事务提交后立即失效目标详情;名称、价格、库存、分类展示、图片/主图、销售状态或首页成员资格变化时同步失效唯一固定首页,并在提交后 3 秒对同一 Key 二次失效。分类、后台商品列表及购物端普通列表和搜索不缓存。缓存不可用不影响商品事务;正常值 60 秒、空值 10 秒,双删失败时正常旧值最坏不超过提交后 62 秒、旧空值不超过 12 秒。 - C04 搜索:商品创建、编辑、上下架、删除或关联分类名称修改时,检索文本、销售状态与 PostgreSQL 数据库索引同步维护;本模块不建设独立索引同步任务,事务回滚时检索事实同样回滚;进阶搜索不可用时由 C04 回退基础模糊查询。 - M03 购物车:商品下架、库存归零或被禁用后由购物车模块按 M03 规则标记失效,不反向写入商品状态。 - M04 订单:商品事务不修改历史订单的地址或商品快照;价格或上下架变更不影响已有订单。 @@ -248,7 +248,7 @@ flowchart TD 2. 商品物理删除必须按全量历史关联判断,不按订单状态排除已取消订单;订单、购物车、收藏、浏览、评价和秒杀等任何历史引用均阻止删除,后续由数据库设计落实约束。 3. 停用分类下已有已上架商品继续公开已冻结;接口需要确保 A101 移除分类筛选入口时,A102/A103 不按分类启停状态额外隐藏商品。 4. 图片上传顺序和替换规则的接口字段(主图上传后是否自动替换旧主图)尚未定义,需在接口设计前与命名规范统一。 -5. 缓存失效失败的处理需要记录可追踪错误并由 C07 重试;商品写接口按商品事务结果响应,不能把缓存失效失败伪装成商品保存失败。 +5. 商品写接口必须在事务提交后向 C07 提供受影响商品及首页范围,支持立即和 3 秒二次失效;失效失败记录可追踪错误,但不能把缓存失败伪装成商品保存失败。 6. 搜索索引异常时的降级语义需要在接口契约和 C04 搜索实现之间达成一致;搜索不接入 C07,商家端不感知底层使用哪种索引实现。 7. DBxxx 分类/商品表字段尚未形成可实施的完整定义,数据库字段、约束、索引和 GIN/pg_trgm 维护策略必须由数据库设计任务另行确认。 8. 商品图片上传接口与对象存储的兼容边界需要与系统架构设计同步,避免不同商家端入口使用不同的上传契约。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" index 925ca27..56c6560 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" @@ -2,46 +2,48 @@ > 负责人:罗皓晨 > 覆盖:C07 -> 基础核心流程:F04、F06、F11;F08/F09 通过 Catalog 改变库存时触发失效,F08 最终仍重读 PostgreSQL -> 直接协作:顾欣月(M02 Catalog 与商品失效规则)、韦乾强(M04 Ordering 库存扣减/回补入口)、M00/C10 公共 Redis 与多实例环境 -> 文档状态:初稿,待罗皓晨自审及 Catalog、Ordering 交叉评审;TTL 和失效上限未冻结 +> 基础核心流程:F04 固定首页、F06 商品详情、F11 商品管理;F08/F09、C01、X04 仅在改变普通库存时触发失效 +> 直接协作:顾欣月(M02 Catalog)、韦乾强(M04 Ordering)、朱惠惠(C01 Seckill)、张海洋(M10 AfterSales)、M00/C10 公共 Redis 与多实例环境 +> 文档状态:完整定义;缓存范围、参数和失效矩阵已冻结,待接口、架构、实现和压测承接 > 需求事实源:[需求规格说明书 C07](../../../01-需求文档/需求规格说明书.md) 的“C07 缓存与性能优化”完整七节 ## 一、范围、职责与事实来源 C07 使用 Redis 优化本期固定首页商品摘要和购物端商品详情两个高频公开只读场景。游客和买家可以共享只含公开字段的缓存;个人字段、商家管理字段、管理员字段及购物车、订单、支付、售后写操作不进入本期缓存。 -PostgreSQL 始终是价格、库存、上下架状态和商品内容的事实来源。缓存命中与未命中的 A102/A103 响应必须保持同一业务口径;Redis 故障可以使查询变慢,但不能改变公开范围、权限或结果正确性。 +PostgreSQL 始终是价格、库存、上下架状态和商品内容的事实来源。缓存命中与未命中的响应必须保持同一业务口径;Redis 故障可以使查询变慢,但不能改变公开范围、权限或结果正确性。A102 只有无筛选、固定排序、第一页 12 条的首页摘要请求进入缓存,其他普通列表、分类、关键词和组合筛选全部直读 PostgreSQL;A103 只缓存商品自身公开字段,不含 M07 评价或评分。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| -| C07 需求与教师验收 | 完整定义,待需求冻结 | 作为 Cache-Aside、写后失效、降级和压测边界 | -| 本文业务流程 | 初稿 | 明确读取、事务后失效、多实例和错误出口 | -| A102、A103 与 Catalog 写接口 | 部分定义、待交叉评审 | 由流程映射,不改变原接口响应 | +| C07 需求与教师验收 | 完整定义 | 作为 Cache-Aside、写后失效、降级和压测边界 | +| 本文业务流程 | 完整定义 | 冻结缓存对象、读取参数、事务后双删、跨实例填充、库存通道和错误出口 | +| A102、A103 与 Catalog 写接口 | 部分定义,待按本文补齐 | 由流程映射,不改变原接口响应 | | DB022、DB023 | 仅为接口文档引用,数据库主文档未确认 | 不把接口引用写成已冻结表设计 | -| Redis Key、TTL 与热点保护 | 部分定义 | 只确认必须有界,具体参数进入待评审项 | +| Redis Key、TTL 与热点保护 | 流程参数已冻结 | 正常值 60 秒、空值 10 秒、回填窗口 2 秒、二次失效 3 秒、等待 500 毫秒 | 职责边界: - 罗皓晨负责 Redis 接入、统一序列化、稳定 Key 约定、故障降级、多实例共享和压测环境。 -- 顾欣月负责确认商品创建、编辑、图片、上架、下架、删除及库存变化分别影响哪些详情和首页缓存。 +- 顾欣月负责由 Catalog 在商品创建、编辑、分类展示、图片、上架、下架、删除及普通库存变化提交后发布对应失效事实。 - Ordering 通过 Catalog 公开能力扣减或回补库存;Catalog 仍负责对外暴露最新商品事实和触发相应缓存失效。 +- C01 发布时从普通库存划拨到独立秒杀库存,因此触发一次商品失效;活动内部库存扣减与原通道回补不触发 C07。M10 只有回补普通库存时触发 C07。 - C07 不反向修改商品、库存或订单数据,也不以 Redis 替代下单事务和库存条件更新。 ## 二、模块直接出入口 ```mermaid flowchart LR - Q1["F04:公开商品列表/固定首页摘要查询"] --> CACHE["C07 Cache-Aside"] - Q2["F06:公开商品详情查询"] --> CACHE + Q1["F04:首页固定摘要
OnSale,createdAt/productId 倒序,前 12 条"] --> CACHE["C07 Cache-Aside"] + Q2["F06:A103 商品自身公开详情"] --> CACHE + LIST["A102 其他列表、分类、关键词和组合筛选"] -->|"不进入缓存"| CAT["M02 Catalog / PostgreSQL"] CACHE -->|"命中且内容有效"| WEB["游客/买家公开响应"] - CACHE -->|"未命中、损坏或 Redis 降级"| CAT["M02 Catalog / PostgreSQL"] + CACHE -->|"未命中、损坏或 Redis 降级"| CAT CAT -->|"已上架商品的公开事实直接返回"| WEB - CAT -. "使用有限 TTL 尽力回填;写入失败不阻塞响应" .-> CACHE + CAT -. "60 秒正常值/10 秒空值尽力回填;写入失败不阻塞响应" .-> CACHE - WRITE["F11:商品编辑、图片、上下架或删除
F08/F09:通过 Catalog 扣减或回补库存"] -->|"Catalog 事务提交成功"| INVALIDATE["C07 失效入口"] + WRITE["F11:商品/分类/图片/销售状态
普通订单扣减或回补、普通库存售后回补
C01 发布划拨普通库存"] -->|"所属事实事务提交成功"| INVALIDATE["C07 失效入口"] INVALIDATE -->|"删除详情和受影响的固定首页缓存"| CACHE - INVALIDATE -->|"失败记录与受控重试"| RETRY["有限 TTL 约束最长旧值窗口"] + INVALIDATE -->|"失败记录、3 秒二次失效与告警"| RETRY["60/10 秒 TTL 约束最长旧值窗口"] ORDER["F08 提交订单"] -->|"重新读取销售状态、价格和库存"| CAT CACHE -. "不得作为下单事实" .-> ORDER @@ -49,7 +51,9 @@ flowchart LR 不可变结果: -- A102/A103 只返回已上架且允许公开的商品信息;任何身份都不能通过缓存命中看到草稿、下架、删除或管理字段。 +- 固定首页 A102 语义和 A103 只返回已上架且允许公开的商品信息;任何身份都不能通过缓存命中看到草稿、下架、删除或管理字段。 +- 固定首页只包含最新 `OnSale` 商品前 12 条,按 `createdAt DESC, productId DESC` 稳定排序;普通库存为零仍保留并标记售罄。 +- A103 缓存不包含 M07 评价、评分、收藏或购物车字段;M07 变化不触发 C07。 - F08 下单始终从 PostgreSQL 事实重新校验销售状态、价格和库存,不接受页面或 Redis 中的旧值作为交易依据。 - Redis 写入、删除或重试失败只影响性能和约定的一致性窗口,不得让数据库事务回滚或返回无法判断新旧的副本。 - 两个 API 实例共享同一 Redis 和 Key 规范,不能使用单实例内存保存跨实例唯一缓存事实。 @@ -60,32 +64,40 @@ flowchart LR flowchart TD A["页面显示结构一致的加载占位
发起固定首页摘要或商品详情查询"] --> B{"参数、公开范围和身份字段是否符合原接口?"} B -- "否" --> X["按公开商品查询契约拒绝或返回不可用"] - B -- "是" --> C["构造包含环境、模块、资源、查询标识和版本的稳定 Key"] + B -- "是,属于固定首页或 A103" --> C["构造包含环境、模块、资源、固定查询标识和版本的稳定 Key"] + B -- "是,但属于其他 A102 查询" --> R["直接查询 PostgreSQL
不读取或写入 C07"] C --> D{"Redis 是否可用?"} D -- "否" --> DB["记录降级并查询 PostgreSQL"] D -- "是" --> E{"Key 是否命中且可正常反序列化?"} E -- "是" --> F["返回与原接口一致的公开响应并记录命中"] - E -- "否,未命中" --> DB + E -- "否,未命中" --> LOCK{"是否取得该 Key 的跨实例填充资格?"} + LOCK -- "是" --> DB + LOCK -- "否" --> WAIT["最多等待 500 毫秒后重读缓存"] + WAIT --> HIT{"已出现有效缓存?"} + HIT -- "是" --> F + HIT -- "否" --> DIRECT["直查 PostgreSQL 并返回
不争抢填充、不回填"] E -- "否,损坏或旧版本" --> G["删除异常 Key 并按未命中处理"] - G --> DB + G --> LOCK DB --> H{"数据库查询结果?"} H -- "已上架商品/摘要" --> I["生成原接口公开响应"] H -- "不存在或不可公开" --> J["按原公开查询规则返回空结果或稳定不可用结果"] H -- "查询失败" --> P["保留页面结构并显示统一错误反馈
提供就地重试入口"] - I --> K{"Redis 当前可写?"} - K -- "是" --> L["使用有限 TTL 回填"] + I --> K{"从取得填充资格起是否未超过 2 秒且 Redis 可写?"} + K -- "是" --> L["以 60 秒 TTL 回填"] K -- "否" --> M["仅记录写入失败"] L --> N["返回本次数据库结果"] M --> N - J --> O["按待评审的短空值或受控直查策略处理"] + J --> O["在 2 秒回填窗口内以 10 秒 TTL 写入空值"] O --> Q["返回原查询结果并结束加载状态"] ``` 读取规则: -- 缓存 Key 必须区分环境、资源、稳定查询条件和结构版本;不得为任意查询参数无限生成 Key。 -- 固定首页摘要的具体查询条件、页大小和排序组合尚待 Catalog 确认,未确认前不冻结 Key 集合。 -- 正常值使用有限 TTL;不存在/不可公开结果是否使用短时空值,以及空值 TTL,须在第八章确认。 +- 缓存 Key 必须区分环境、资源、固定查询条件和结构版本;只有一个固定首页 Key,以及按商品 ID 区分的 A103 详情 Key,不为其他 A102 查询参数生成 Key。 +- 固定首页强制 `OnSale`、无用户筛选、`createdAt DESC, productId DESC`,固定前 12 条;库存为零仍返回并标记售罄。 +- 正常值 TTL 为 60 秒;A103 不存在/不可公开结果和固定首页空结果的空值 TTL 为 10 秒。上架、恢复公开或创建同标识资源时主动失效对应空值。 +- 同一 Key 只允许一个跨实例填充者。其他请求等待最多 500 毫秒;超时后直查 PostgreSQL 并返回,但不继续争抢填充资格或回填。 +- 取得填充资格后,数据库查询到缓存写入决定的有效回填窗口最多 2 秒;超时结果仍按接口规则处理,但不再写缓存,防止迟到旧查询延长旧值窗口。 - 缓存写入失败时,本次 PostgreSQL 查询结果仍正常返回;不得把缓存错误暴露为商品业务错误。 - 商家管理查询和个人字段默认直接走原授权接口,不复用公共商品缓存。 - 每次缓存读取记录命中/未命中、读取耗时和错误;回填、主动失效与降级分别记录写入、失效、错误和降级次数,并携带资源标识与 `traceId`,不得记录完整缓存值。 @@ -94,42 +106,45 @@ flowchart TD ```mermaid flowchart TD - A["Catalog 接收商品或库存变更"] --> B["在 PostgreSQL 事务内校验并写入最新事实"] + A["所属模块接收商品、分类或普通库存变更"] --> B["通过公开能力在 PostgreSQL 事务内校验并写入最新事实"] B --> C{"事务是否提交成功?"} C -- "否" --> X["保持原数据库事实
不发布成功失效结果"] - C -- "是" --> D["确认受影响的商品详情和固定首页摘要范围"] - D --> E["删除商品详情 Key"] - D --> F["删除或版本化受影响的固定首页 Key"] - E --> G{"全部失效动作成功?"} - F --> G - G -- "是" --> H["本次失效动作完成"] - G -- "否" --> I["记录资源标识、失败范围和 traceId"] - I --> J["登记受控重试"] - J --> K{"重试是否在约定期限内成功?"} - K -- "是" --> H - K -- "否" --> L["由有限 TTL 约束旧值最长存在时间
并保留告警和证据"] - H --> M{"是否存在并发旧查询
在失效后回填旧值?"} - M -- "否" --> N["后续查询未命中并从 PostgreSQL 回填"] - M -- "是" --> O["按待确认的二次失效、版本或等价最小机制纠正"] - O --> P["选定机制生效前由有限 TTL 约束并记录证据"] - N --> Q["所有 API 实例共享同一 Redis 和失效结果"] - P --> Q + C -- "是" --> D["按冻结矩阵确定详情 Key
及是否影响唯一固定首页 Key"] + D --> E["提交后立即执行首次删除"] + E --> F{"首次删除是否成功?"} + F -- "否" --> G["记录失败范围、traceId 和告警
保留受控重试证据"] + F -- "是" --> H["等待提交后第 3 秒"] + G --> H + H --> I["对同一组 Key 执行二次删除"] + I --> J{"二次删除是否成功?"} + J -- "否" --> K["记录失败和告警
由 60/10 秒 TTL 兜底"] + J -- "是" --> L["清除可能的并发旧回填"] + K --> M["理论最坏窗口:正常旧值 62 秒
旧空值 12 秒"] + L --> N["后续查询从 PostgreSQL 回填新值"] + M --> N + N --> Q["所有 API 实例共享同一 Redis、填充资格和失效结果"] ``` -触发范围初稿: +冻结失效矩阵: | 已提交变更 | 详情缓存 | 固定首页摘要 | 说明 | |---|---:|---:|---| -| 新建草稿商品 | 通常无公开缓存 | 通常无公开缓存 | 未上架商品不得进入公开结果 | -| 名称、价格、库存、描述、分类变更 | 失效 | 若摘要字段或筛选结果受影响则失效 | 精确首页范围待 Catalog 确认 | -| 商品图片新增或删除 | 失效 | 若摘要缩略图受影响则失效 | A127/A128 尚未登记失效规则,待 Catalog 补齐 | -| 图片顺序或主图变化 | 待 Catalog 确认 | 待 Catalog 确认 | 当前未登记对应接口,不能写成已确认触发动作 | +| 新建草稿商品 | 清理同标识短空值 | 不失效 | 未上架商品不得进入公开结果 | +| 名称、价格、普通库存、描述变化 | 失效 | 若字段出现在首页摘要或库存售罄标记变化则失效 | 普通库存变化包括下列普通订单与划拨动作 | +| 分类展示信息或商品分类关系变化 | 失效受影响商品 | 若首页展示字段或 `OnSale` 成员资格受影响则失效 | 分类、普通列表本身不进入 C07 | +| 商品图片新增、删除、排序或主图变化 | 失效 | 若首页缩略图变化则失效 | 接口设计必须补齐所有图片写动作的失效后置条件 | | 上架 | 清理短空值/旧详情 | 失效 | 上架后下一次查询方可公开 | | 下架 | 失效 | 失效 | 购物端旧链接不再允许购买 | -| 满足约束后删除 | 失效 | 失效 | A124 尚未登记失效规则,待 Catalog 补齐;不影响历史订单快照 | -| F08 库存扣减、F09 库存回补 | 失效 | 若摘要展示库存状态则失效 | 由 Catalog 公开库存能力触发 | +| 满足约束后删除 | 失效 | 失效 | 不影响历史订单快照 | +| 普通订单提交扣减普通库存 | 失效 | 失效 | 首页保留库存为零商品但售罄标记必须更新 | +| 普通订单取消回补普通库存 | 失效 | 失效 | 包含主动取消和 C03 超时取消 | +| M10 普通库存售后回补 | 失效 | 失效 | 只在退款原子结果实际回补普通库存后触发 | +| C01 发布时从普通库存划入活动库存 | 失效 | 失效 | 发布事务提交后普通库存已减少 | +| 秒杀活动内部抢购 | 不失效 | 不失效 | 只改变独立活动库存 | +| 秒杀订单取消或售后回补原活动库存 | 不失效 | 不失效 | 库存严格回到原活动通道 | +| M07 评价、评价图或评分变化 | 不失效 | 不失效 | 评价链路不在 A103 商品自身缓存内 | -上表是流程级触发提案,不是已经冻结的 Key 清单。C01 秒杀库存划拨是否影响普通商品公开库存及其缓存,需在 C01 库存口径确认后追加评审。 +所有“失效”均表示提交后立即首次删除、提交后 3 秒二次删除;影响多个商品时按确定的商品集合执行,不把任意筛选列表扩大为缓存对象。 ## 五、缓存运行状态与一致性窗口 @@ -148,10 +163,10 @@ stateDiagram-v2 一致性边界: -- 主动失效用于缩短正常更新后的旧值窗口,有限 TTL 用于约束漏删、删除失败或重试失败时的最长旧值时间。 -- 若采用延迟二次失效,只用于减少“并发旧查询回填旧值”的窗口,不能替代事务提交后的首次失效和有限 TTL。 -- 理论最迟生效时间必须根据最终选定的首次失效、失败重试、剩余 TTL 和并发旧值保护机制分别确定计时起点后推导并验证,不能在参数未冻结时预设固定相加公式。 -- 在具体 TTL、重试次数和失效范围确认前,本流程不能标记为已确认或冻结。 +- 正常缓存 TTL 固定为 60 秒,空值 TTL 固定为 10 秒;主动失效缩短正常更新窗口,TTL 约束漏删或删除失败时的最长旧值时间。 +- 事务提交后立即首次失效,提交后 3 秒二次失效。二次失效只用于清除提交前旧查询可能在首次删除后回填的旧值,不能替代首次删除和 TTL。 +- 取得填充资格后的有效回填窗口最多 2 秒。最不利情况下,提交前开始的旧查询在提交后第 2 秒写回,因此两次删除均失败时,正常旧值最迟在提交后第 62 秒到期,旧空值最迟在提交后第 12 秒到期。 +- 3 秒二次失效正常成功时会更早清除上述回填;“62 秒”是失败兜底上限,不是系统故意等待时间。下单、取消和售后事务始终重读 PostgreSQL,不等待缓存一致。 ## 六、故障、并发与身份隔离 @@ -162,9 +177,9 @@ stateDiagram-v2 | 缓存值损坏或结构版本旧 | 视为未命中并删除异常 Key | 不向客户端返回错误结构 | | 缓存写入失败 | 返回本次数据库结果 | 不改变接口业务结果 | | 商品事务回滚 | 不产生成功失效动作 | 原缓存仍对应原数据库事实 | -| 事务提交后删除失败 | 记录范围并受控重试 | 有限 TTL 约束旧值窗口 | -| 事务前旧查询在失效后回填旧值 | 记录并按待确认的二次失效、版本或等价最小机制纠正 | 机制未冻结前由有限 TTL 约束,F08 仍重读数据库 | -| 热点 Key 同时过期 | 使用待确认的请求合并、短期互斥或等价最小方案 | 等待有超时与回退,不能无限阻塞 | +| 事务提交后首次删除失败 | 记录范围、告警并在提交后第 3 秒再次删除 | 二次失效成功则提前收敛;否则由 60/10 秒 TTL 兜底 | +| 事务前旧查询在首次失效后回填旧值 | 提交后第 3 秒删除同一组 Key | 二次删除也失败时,正常旧值上限为 2+60=62 秒,旧空值上限为 2+10=12 秒 | +| 热点 Key 同时过期 | 仅一个跨实例填充者回填,其余等待最多 500 毫秒 | 未等到有效值则直查 PostgreSQL,不争抢填充、不无限阻塞 | | 商品下架时并发旧读 | 失效并最终不再公开;F08 始终重读数据库 | 旧展示不能绕过下单校验 | | 实例 1 完成变更、实例 2 查询 | 共享 Redis 与 Key 规范 | 在约定窗口内读取新值 | | 游客与买家共享公开缓存 | 只保存双方共同可见字段 | 收藏、购物车等个人字段独立查询 | @@ -172,17 +187,16 @@ stateDiagram-v2 ## 七、由流程派生的契约映射 -缓存是 A102/A103 的服务端实现能力,不新增业务 HTTP 接口,也不改变成功响应、失败响应、鉴权和公开范围。 +缓存是 A102 固定首页语义和 A103 的服务端实现能力,不新增业务 HTTP 接口,也不改变成功响应、失败响应、鉴权和公开范围。 | 流程能力 | 当前派生契约 | 数据设计 | 当前状态 | |---|---|---|---| -| 固定首页/商品列表公开查询 | A102 | 接口文档引用 DB022/DB023,数据库主文档未确认 | 待 Catalog 交叉评审 | -| 购物端商品详情查询 | A103 | 接口文档引用 DB022/DB023,数据库主文档未确认 | 待 Catalog 交叉评审 | -| 商品内容、价格和库存编辑后失效 | A123 提交后的内部协作 | 目标表设计待数据库汇总 | 待 Catalog 交叉评审 | -| 上架、下架后失效 | A125、A126 提交后的内部协作 | 目标表设计待数据库汇总 | 接口已登记失效,待 Catalog 交叉评审 | -| 删除、商品图片新增/删除后失效 | A124、A127、A128 提交后的内部协作 | 目标表设计待数据库汇总 | 接口契约缺口:尚未登记缓存失效,待 Catalog 补齐 | -| F08/F09 库存扣减或回补后失效 | Catalog 公开库存应用能力,不新增 Axxx | 库存事实与索引待数据库评审 | 待 Catalog/Ordering 评审 | -| Redis 故障回退 PostgreSQL | 继续复用 A102/A103 响应口径 | Redis 不登记 DBxxx | 降级细节待架构和测试确认 | +| 固定首页公开查询 | A102 的无筛选固定首页语义;其他 A102 请求不缓存 | 数据库设计待汇总 | 待补齐固定排序、12 条和售罄字段 | +| 购物端商品自身公开详情 | A103 | 数据库设计待汇总 | 待明确排除 M07 评价、评分和个人字段 | +| 商品、分类展示和图片写入后失效 | A123~A128 提交后的内部协作 | 目标表设计待数据库汇总 | 待为名称、价格、描述、分类、图片、主图、状态和删除逐项补齐后置条件 | +| 普通订单与普通库存售后回补后失效 | Catalog 公开库存应用能力,不新增 Axxx | 库存事实待数据库汇总 | 待在 M04/M10 下游契约承接原库存通道 | +| C01 发布划拨后失效;活动内部库存不失效 | C01 与 Catalog 的内部协作 | 普通库存与活动库存设计待汇总 | 待接口/事件契约承接冻结矩阵 | +| Redis 故障回退 PostgreSQL | 继续复用固定首页/A103 响应口径 | Redis 不登记 DBxxx | 待架构和测试承接 | 架构承接章节: @@ -191,16 +205,15 @@ stateDiagram-v2 - 系统架构 7.1“下单事务”:F08 重新校验商品、价格和库存; - 系统架构 7.13“C10 容器化部署与负载均衡”:两个 API 共享 Redis。 -## 八、待交叉评审项 +## 八、下游设计与验证约束 -1. 与 Catalog 确认固定首页摘要的查询范围,以及编辑、图片、分类、上架、下架、删除和库存变化分别影响的 Key 集合。 -2. 确定详情、固定首页摘要和空值的 TTL;空值 TTL 必须短于正常值,且不能掩盖新上架商品。 -3. 确定首次失效失败后的重试机制、最大重试期限和理论最长旧值窗口,不能只写“最终一致”。 -4. 选择用于纠正并发旧查询回填的二次失效、版本或等价最小机制,并据此单独推导一致性窗口。 -5. 在请求合并、短期互斥或等价方案中选择满足当前规模的最小热点保护方式,并为等待设置超时和数据库回退。 -6. 与 Ordering 确认 F08 扣减、F09 回补的失效触发方式;C01 库存划拨待秒杀库存口径确认后再补。 -7. 确定压测的固定请求集合、并发参数、预热/冷缓存轮次和环境资源,确保开关缓存时可公平比较。 -8. DB022/DB023 仅为接口文档引用,须等待数据库主文档汇总确认;当前不得据此宣称表设计已完成。 +1. A102 必须可表达唯一固定首页语义:无用户筛选、只取 `OnSale`、`createdAt DESC, productId DESC`、前 12 条、库存为零仍返回并标记售罄;其他 A102 查询不得写入 C07。 +2. A103 只承载商品自身公开字段,M07 评价与评分、收藏、购物车和任何身份化字段走独立查询。 +3. 接口、事件或应用协作必须为失效矩阵中的每个已提交动作提供明确后置失效入口;不得在数据库事务提交前发布“成功失效”。 +4. Redis 实现必须提供跨实例单填充者、500 毫秒等待、2 秒有效回填窗口、60/10 秒 TTL、提交后立即及 3 秒二次失效,并记录可验证指标。 +5. 普通库存与秒杀活动库存必须携带原通道语义:普通扣减/回补触发 C07,活动内部扣减和原活动回补不触发。 +6. 数据库表、索引及 OpenAPI 仍待后续汇总;流程完整不代表实现或压测已经完成。 +7. 压测固定请求集合、并发参数、预热/冷缓存轮次和资源必须在测试计划中登记,确保开关缓存公平比较。 ## 九、压测与验收证据清单 @@ -209,10 +222,13 @@ stateDiagram-v2 - [ ] 每组包含预热、稳定采样和冷缓存轮次,预热数据不混入正式统计。 - [ ] 记录请求数、成功率、吞吐量、平均耗时、P50、P95、P99、缓存命中/未命中次数、读取耗时、写入/失效次数、错误/降级次数、Redis 错误和 PostgreSQL 查询次数。 - [ ] 缓存开关前后 A102/A103 业务字段、公开范围和错误结果一致。 -- [ ] 改价、库存、图片、上架、下架及多实例读取在约定一致性窗口内得到正确结果。 +- [ ] 固定首页只返回最新 `OnSale` 前 12 条且排序稳定,库存为零仍显示售罄;其他 A102 查询、M07 评价与评分均不进入缓存。 +- [ ] 改价、普通库存、描述、分类展示、图片/主图、上架、下架、删除及多实例读取在提交后立即/3 秒失效链路,以及正常旧值 62 秒、旧空值 12 秒失败兜底内得到正确结果。 - [ ] Redis 故障时可回退数据库;恢复后能够重新回填并产生正常命中。 - [ ] Redis 与 PostgreSQL 同时失败时保留页面结构,展示统一错误反馈和就地重试,不暴露技术异常。 -- [ ] 构造事务前旧查询在首次失效后回填旧值的并发场景,按选定机制或有限 TTL 证明旧值窗口有界。 -- [ ] 热点 Key 过期时等待有界,不出现无限阻塞或无法解释的数据库冲击。 +- [ ] 构造提交前旧查询在首次失效后第 2 秒内回填旧值的场景,证明 3 秒二次失效通常清除,双删失败时正常值 60 秒 TTL 保证提交后 62 秒、空值 10 秒 TTL 保证提交后 12 秒上限。 +- [ ] 热点 Key 过期时只有一个跨实例填充者;其他请求最多等待 500 毫秒后直查 PostgreSQL,不出现无限阻塞。 +- [ ] C01 发布划拨普通库存会失效详情与首页;秒杀活动内部扣减、秒杀订单取消和售后原活动回补均不触发 C07。 +- [ ] 普通订单扣减、主动/超时取消回补以及 M10 普通库存售后回补均触发正确失效。 - [ ] F08 在旧页面或旧缓存条件下仍按 PostgreSQL 最新状态、价格和库存决定下单结果。 - [ ] 保存环境、Commit SHA、初始化方式、配置、原始压测输出、失效日志和数据库查询证据。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" index ca964b6..402dec0 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" @@ -3,7 +3,7 @@ > 负责人:韦乾强 > 覆盖:C03 > 基础核心流程:F08、F09、F10 -> 直接协作:张海洋(M05 Payment)、朱惠惠(C01 Seckill)、罗皓晨(M09 Messaging、Worker 与 C10 多实例基础设施) +> 直接协作:张海洋(M05 Payment)、朱惠惠(C01 Seckill)、罗皓晨(M09 Messaging、C07 Cache、Worker 与 C10 多实例基础设施) > 文档状态:已按需求和 M04 / M05 截止时间边界重构,可作为接口与 Worker 设计输入 > 需求事实源:[需求规格说明书 C03](../../../01-需求文档/需求规格说明书.md) 的“C03 订单超时自动取消”完整七节 @@ -137,6 +137,7 @@ flowchart TD - 商品或活动的原库存来源事实必须保留到所有待支付订单的取消责任结束,不能因后台删除导致历史订单无法回补。 - 一个订单包含多项时,任一项回补失败都使整笔过期取消不成立,不能出现部分库存已回补。 - 回补成功后再次扫描、Worker 重启或消息重投都不重复增加库存或释放限购数量。 +- 普通库存回补的取消结果提交后触发 C07 失效商品详情与固定首页;秒杀原活动库存回补不触发 C07。缓存失效失败不回滚取消。 ## 七、失败恢复与可观察结果 @@ -184,6 +185,7 @@ C03 不派生新的公开 HTTP 接口。它依赖 M04 的 Ordering 内部应用 - **M04**:拥有固定支付截止时间、订单状态和统一取消;C03 不复制取消逻辑。 - **M05 / C08**:任何支付通道在到期后都拒绝。M05 可立即触发同一过期取消,C08 迟到成功不得覆盖取消终态。 - **C01 / M02**:分别接受秒杀和普通库存原路回补;活动结束、取消或商品下架不消灭历史回补责任。 +- **C07**:普通库存回补提交后失效商品详情与固定首页;秒杀原活动回补不失效普通商品缓存。 - **M09**:只在取消完整结果提交后通知当前买家,通知失败自行重试。 - **C10**:多 API / Worker 实例共享同一订单事实,重复扫描不会形成重复业务结果。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" index b7dc3de..c0a9a14 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" @@ -3,7 +3,7 @@ > 负责人:韦乾强 > 覆盖:M04-01、M04-02、M04-03、M04-04、F08、F09 > 基础核心流程:F03、F04、F05、F06、F07、F10、F11、F12 -> 直接协作:唐宇昊(M01 Identity)、顾欣月(M02 Catalog)、朱惠惠(M03 Cart、C01 Seckill)、张海洋(M05 Payment、M10 AfterSales)、罗皓晨(M09 Messaging、Worker 基础设施) +> 直接协作:唐宇昊(M01 Identity)、顾欣月(M02 Catalog)、朱惠惠(M03 Cart、C01 Seckill)、张海洋(M05 Payment、M10 AfterSales)、罗皓晨(M09 Messaging、C07 Cache、Worker 基础设施) > 文档状态:已按需求重构,可作为接口设计输入;待 Cart、Payment、AfterSales、Messaging 交叉评审 > 需求事实源:[需求规格说明书 M04](../../../01-需求文档/需求规格说明书.md) 的 M04-01~M04-04 完整七节 @@ -127,6 +127,7 @@ flowchart TD - 处理商家固定为提交时唯一启用的默认商家,保存为 `assignedMerchantUserId`。不存在、重复或不可用时整单失败,不能创建无人处理订单。 - 默认商家禁用与下单必须形成确定顺序:下单先被接受时禁用复核应发现新责任;禁用先生效时下单不能再分配给该账号。 - 普通购物车订单由 M04 协调 Catalog 库存扣减;秒杀入口由 C01 协调独立活动库存与限购,并在同一原子边界调用 M04 的统一订单创建能力。两条入口都由 M04 生成共享订单、指定商家、快照和固定支付截止时间,订单项库存来源不得混用。 +- 普通库存扣减完整结果提交后触发 C07 失效目标详情和固定首页;秒杀抢购只改变活动独立库存,不触发 C07。缓存失效失败不回滚订单。 - 任一条目不可售、数量非法、库存不足、地址无效、默认商家不可用、购物车清理失败或可靠创建事实失败时,整单不成立。 - 同一买家、同一幂等标识、同一内容只形成一张订单。第一次结果未知时先查询原结果,不能用相同动作再扣一次库存。 - 支付截止时间在订单创建时按当时可追踪配置固定;正式口径为创建后 30 分钟,演示参数只能缩短演示等待,不改变正式规则。 @@ -215,6 +216,7 @@ flowchart TD - 只有 `PendingPayment` 可以首次取消。`Cancelled` 重放幂等成功;`Paid`、`Shipped`、`Completed` 明确拒绝。 - 状态、取消时间与原因、普通 / 秒杀库存、秒杀限购数量和可靠通知事实必须同时成功或同时失败。 - 普通订单回补 Catalog;秒杀订单回补原活动独立库存并释放该买家本次订单占用的限购数量,不得增加普通库存。 +- 普通库存回补完整结果提交后触发 C07 失效目标详情和固定首页;秒杀原活动回补不触发 C07。 - 秒杀活动已经结束或取消时仍必须接受合法历史订单的取消回补;回补数量留在原活动且不重新开放购买。 - 商品下架不阻止回补;商品或活动的来源事实不能在所有待支付取消与售后责任结束前被破坏性删除。 - 取消失败时订单可能仍是已经过期的 `PendingPayment`,但 M05 / C08 仍必须按截止时间拒绝支付;C03 后续继续取消,不得因为 Worker 延迟重开支付窗口。 @@ -331,6 +333,7 @@ flowchart TD - **M01**:地址归属与默认商家是下单权威输入;默认商家禁用和新订单分配不能同时成功。 - **M02 / M03**:M04 读取选中购物车条目和实时商品事实;订单成功才清理选中条目,失败保持购物车。普通库存只由 Catalog 扣减和回补。 +- **C07**:普通订单扣减和取消回补提交后失效商品详情与固定首页;秒杀活动库存扣减和原活动回补不触发。缓存失败不改变订单原子结果。 - **C01**:秒杀绕过购物车,但不建立第二套订单创建器。C01 负责活动、独立库存和限购,并在同一原子边界调用 M04 生成共享订单、指定商家、快照和固定支付截止时间;取消只回补活动独立库存。 - **M05 / C08 / C03**:三者共同遵守支付截止时间。到期后支付无条件拒绝,统一过期取消可由支付请求或 Worker 触发。 - **M06-02 / M10**:订单指定商家是唯一履约范围;发货前读取售后阻断与已退款数量并串行复核。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" index b85b701..3874f2b 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" @@ -236,7 +236,7 @@ flowchart TD - **M05 Payment / C08 回调**:秒杀订单沿用统一支付和幂等回写;支付成功与取消竞争由订单状态条件推进裁决。 - **C03 超时取消**:只触发 M04 公开取消入口,由订单来源决定回补普通库存还是原秒杀库存,不得直接改写 C01 数据。 - **M06-02 履约、M09 消息、M10 售后**:分别沿用发货、可靠消息和售后流程,不为秒杀建立第二套通道。 -- **C07 缓存**:只复用固定首页商品摘要和商品详情缓存;秒杀活动列表、活动状态、权威剩余库存、已售数量和售罄结果全部从数据库事实判定。 +- **C07 缓存**:活动发布事务从普通库存划拨到独立活动库存后,失效目标商品详情和固定首页;其后抢购、秒杀订单取消与售后回补只改变原活动库存,不触发 C07。C07 只服务固定首页商品摘要和 A103 商品自身详情;秒杀活动列表、状态、权威剩余库存、已售数量和售罄结果全部从数据库事实判定。 - **C10 多实例**:任一实例都可受理秒杀请求,最终结果只由共享数据库事务决定,不能依赖进程内状态。 ## 九、由流程派生的接口契约映射 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" index 590b926..0fcfbec 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" @@ -3,7 +3,7 @@ > 负责人:张海洋 > 覆盖:M10、X04 > 基础核心流程:F08、F10、F11、F12 -> 直接协作:韦乾强(M04 Ordering、M06-02 履约)、顾欣月(M02 Catalog)、朱惠惠(C01 Seckill)、罗皓晨(M09 Messaging) +> 直接协作:韦乾强(M04 Ordering、M06-02 履约)、顾欣月(M02 Catalog)、朱惠惠(C01 Seckill)、罗皓晨(M09 Messaging、C07 Cache) > 文档状态:已按需求重构,可作为接口设计输入;待 Ordering、Payment、Catalog、Messaging 交叉评审 > 需求事实源:[需求规格说明书 M10](../../../01-需求文档/需求规格说明书.md) 的“M10 售后流程(X04)”完整七节 @@ -18,7 +18,7 @@ M10 负责订单项售后资格、申请数量占用、商家审核、退货说 | M10 / X04 需求 | 完整定义 | 作为业务语义事实源 | | M04 / M06-02 订单与履约边界 | 已定义、待统一整合 | 冻结订单状态、指定商家、可履约数量和发货竞争 | | M05 退款能力 | 已定义、待由本流程补齐退款契约 | 只承接幂等退款,不决定售后资格与库存 | -| M02 / C01 库存通道 | 已定义、待统一整合 | 售后成功时按订单项原库存来源回补 | +| M02 / C01 / C07 库存通道 | 完整定义 | 售后成功按原来源回补;普通库存触发 C07,秒杀原活动库存不触发 | | M09 通知 | 已定义、待按本流程校准接收人 | 只消费已经提交的售后事实 | | 本文业务流程 | 已重构、待交叉评审 | 作为接口与后续数据设计输入 | @@ -298,6 +298,8 @@ flowchart TD 任一步失败时,不能留下“余额增加但售后仍失败”“库存回补但余额未增加”或“售后显示成功但没有退款操作”的部分结果。 +库存回补属于上述成功原子结果的一部分:原来源为普通库存时,完整结果提交后触发 C07 失效目标详情和固定首页;原来源为秒杀活动时只回补原活动库存,不触发 C07。缓存失效失败不回滚已提交退款。 + ### 8.3 失败重试 ```mermaid @@ -421,6 +423,7 @@ flowchart TD - **M04 / M06-02**:订单提供状态、指定商家、实付与来源快照;履约在发货前读取非终态售后和已退款数量。售后与发货必须串行复核。 - **M05**:对一个售后申请形成一个退款操作;钱包入账、退款成功和必要库存回补必须是完整结果。结果未知时先核实,不盲目重试。 - **M02 / C01**:只接收已经满足回补条件的原通道数量;秒杀活动结束或取消后回补数量仍留在原活动,不恢复抢购。 +- **C07**:普通库存售后回补提交后失效商品详情与固定首页;秒杀原活动回补不失效普通商品缓存。 - **M09**:申请提交通知指定商家;审核、待退货、退款成功和失败通知申请买家;退货说明通知指定商家。退款失败不额外通知商家。 - **C08**:只把 M10 `Refunded`、成功退款操作和本人钱包入账纳入成功三方对账;`RefundFailed` 和结果未知的 `Refunding` 不伪造成功记录。 - **M06-03**:存在任一非终态售后申请时不得禁用负责处理的商家;禁用买家不能发起主动动作,但系统退款恢复可继续。 -- Gitee From b9a6d0a5df9f8fd0180e7914eb16aa4c2be582a3 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 21:46:24 +0800 Subject: [PATCH 103/118] =?UTF-8?q?docs(process):=20=E5=86=BB=E7=BB=93=20C?= =?UTF-8?q?10=20=E8=BF=90=E8=A1=8C=E6=B5=81=E7=A8=8B=EF=BC=9B=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E8=BF=81=E7=A7=BB=E9=99=8D=E7=BA=A7=E4=B8=8E=E4=BC=98?= =?UTF-8?q?=E9=9B=85=E5=81=9C=E6=AD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 49 +++--- ...57\347\224\250\346\265\201\347\250\213.md" | 149 ++++++++++++------ ...00\345\207\272\346\265\201\347\250\213.md" | 4 +- ...41\347\220\206\346\265\201\347\250\213.md" | 3 +- 4 files changed, 133 insertions(+), 72 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index ea695b0..04dffbe 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -1,6 +1,6 @@ # 电子商城需求规格说明书 -> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.11 +> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.12 > > 文档状态:内容和格式已完成内部统一,待全组与指导教师评审后冻结 > 截止:第 1 周周三提交初稿 @@ -20,6 +20,7 @@ | v0.9 | 2026-07-24 | 罗皓晨 | 冻结 X02 收藏幂等、浏览历史默认开启与最近 200 条上限,明确关闭记录不隐藏旧历史并取消清空历史能力 | | v0.10 | 2026-07-24 | 罗皓晨 | 冻结 X03 事件接收人、整事件消息原子性和全部已读水位,并统一 C06 WebSocket、凭证失效、角标补查和固定重连边界 | | v0.11 | 2026-07-24 | 罗皓晨 | 冻结 C07 固定首页、详情缓存、TTL、跨实例填充、二次失效和普通/秒杀库存失效矩阵 | +| v0.12 | 2026-07-24 | 罗皓晨 | 冻结 C10 一次性 Migrator、全局就绪与能力降级、Redis 安全恢复、WebSocket 落点和优雅停止边界 | ## 业务流程设计入口 @@ -232,7 +233,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, 1. 各模块负责人先依据 OpenAPI 和模块边界完成本模块的注册入口、端点、数据库迁移及必要事件定义。 2. M00 将模块注册到统一 API/Worker 组合根,并检查依赖方向、配置项和启动顺序。 3. 开发人员通过 Aspire 启动当前任务需要的 API、Worker、PostgreSQL、Redis、RabbitMQ 和对象存储;未使用的可选依赖可以不启动。 -4. API 启动后提供 Swagger UI、存活检查和就绪检查,开发人员据此确认应用和当前启用的关键依赖可用。 +4. API 启动后提供 Swagger UI、存活检查和就绪检查;就绪结果区分配置/版本/Migration/PostgreSQL 全局门槛与 Redis、RabbitMQ、对象存储能力状态。 5. 联调时按照“接口文档先行”原则调用公开接口或集成事件,不允许通过跨模块 DbContext、内部仓储或直接改表完成协作。 6. 合入 `dev` 前记录实际运行命令、配置要求、Migration 状态、联调结果和剩余问题,由至少一名其他成员交叉审查。 @@ -243,7 +244,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, - PostgreSQL 是业务事实来源;Redis、RabbitMQ 和进程内存不得保存无法从事实数据恢复的唯一业务状态。 - RabbitMQ 只用于跨进程集成事件,不替代下单、扣库存、支付等核心数据库事务。 - API 和 Worker 必须支持优雅停止;后台任务收到停止信号后不再领取新任务,并安全完成或释放当前任务。 -- 可选依赖仅在对应功能启用时进入就绪检查;未启用的 Redis、RabbitMQ 或对象存储不得导致基础 API 永久不就绪。 +- 配置完整、版本兼容、Migration 匹配和 PostgreSQL 决定全局就绪;Redis、RabbitMQ 或对象存储即使已启用也按能力级降级,未启用或单项故障不得错误导致全部公开读取永久不就绪。 - 所有公共约定必须提供最小使用示例或说明,但不得为了未来可能出现的需求增加复杂基类、通用仓储或无业务价值的事件层。 #### 6. 异常与边界场景 @@ -257,7 +258,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, #### 7. 验收标准与证据 - 新成员按照 README 和示例配置,能够在不修改源代码中的密钥或地址的情况下启动开发环境。 -- API、Worker 及当前启用的依赖能够启动;Swagger UI、存活检查和就绪检查返回符合预期的状态。 +- API、Worker 及当前启用的依赖能够启动;Swagger UI、存活检查和就绪检查能区分全局门槛与能力级状态。 - F01~F13 的模块均有明确注册入口,并能通过公开 API、应用接口或集成事件完成规定协作。 - 任取一条跨模块链路,能够通过结构化日志和 `traceId` 定位请求、数据库访问及消息处理过程。 - 关闭 RabbitMQ 后产生的待发布事件不会丢失;恢复 RabbitMQ 后 Worker 能够继续投递且消费者不会重复产生业务结果。 @@ -2360,46 +2361,53 @@ M00 提供服务装配和公共健康检查,C06依赖 WebSocket 转发与 Redi | C10-FR03 | 双实例 API | Compose 默认启动至少两个 API 实例,二者均能独立处理无状态 HTTP 请求并访问共享 PostgreSQL、Redis、RabbitMQ 和对象存储。 | | C10-FR04 | Nginx 入口 | 浏览器只通过 Nginx 暴露的统一入口访问前端、API 和 WebSocket;后端容器端口默认不直接暴露给公网。 | | C10-FR05 | 负载均衡 | Nginx 将 API 请求分发到两个健康实例,并正确转发客户端 IP、协议、Host、请求 ID 及 WebSocket Upgrade 所需请求头。 | -| C10-FR06 | 健康检查 | API 提供存活和就绪检查;Nginx/Compose 能识别不可用实例,停止或未就绪实例不应持续接收新请求。 | -| C10-FR07 | 登录态共享 | 身份使用由两个实例共同验证的 JWT;必要的令牌失效记录和共享状态存放 Redis,不依赖单实例内存 Session,因此请求切换实例后仍可鉴权。 | -| C10-FR08 | 实时连接 | Nginx 支持 SignalR WebSocket Upgrade;两个 API 通过 Redis Backplane 共享实时消息通道,单实例停止后客户端可以重连到存活实例。 | +| C10-FR06 | 健康检查 | API 提供存活和就绪检查。全局就绪门槛固定为安全配置完整、运行版本兼容、目标 Migration 版本匹配和 PostgreSQL 可用;Redis、RabbitMQ、SeaweedFS 以能力级状态反映,不因单项故障错误阻断全部公开读取。Nginx/Compose 不向停止或全局未就绪实例持续分发新请求。 | +| C10-FR07 | 登录态共享 | 身份使用由两个实例共同验证的 JWT;必要的令牌失效记录和共享状态不依赖单实例内存 Session。Redis 不可用或无法确认撤销、账号禁用、手机号修改及全部旧凭证失效事实时,相关受保护 HTTP 和 Hub 连接失败关闭;Redis 恢复后必须先恢复有效期内的撤销事实并通过安全健康检查,才能恢复这些受保护能力。 | +| C10-FR08 | 实时连接 | Nginx 支持 SignalR WebSocket Upgrade;当前 PC Web 只使用 WebSockets 并跳过协商,两个 API 通过 Redis Backplane 共享实时消息通道。单实例停止后客户端可重连到存活实例;WebSocket 持续不可用时回退 M09 HTTP 查询或定期补查,不启用 SSE 或长轮询。 | | C10-FR09 | Worker 单独运行 | Worker Service 使用独立容器运行 Outbox、超时取消或对账任务,不随某个 API 实例停止;同一任务的并发与幂等规则由对应业务模块保证。 | | C10-FR10 | 数据持久化 | PostgreSQL、Redis(需要保留的运行数据)、RabbitMQ 和 SeaweedFS 使用明确持久卷;重建应用容器不得删除数据库和对象文件。 | | C10-FR11 | 配置与 Secret | 环境差异通过环境变量或受控文件注入;仓库提供不含真实秘密的示例配置,不提交真实密码、Token、证书私钥或生产连接信息。 | | C10-FR12 | 日志与追踪 | 每个容器日志包含服务名和实例标识;API 响应或日志可用于证明请求落点,关键请求能够用 `traceId` 跨 Nginx、API、数据库和消息处理追踪。 | | C10-FR13 | 连续访问体验 | 前端对短暂网络或实例切换提供有限次数自动重试,仅对安全的查询请求自动重试;提交类请求不得盲目重放。持续失败时展示统一维护/服务不可用页面、可理解提示和手动重试入口。 | | C10-FR14 | 跨实例身份一致性 | 两个 API 实例必须使用一致的 JWT、Policy 和账号状态校验配置;对同一 Token、同一资源和同一请求应给出一致授权结果,禁止因实例差异出现偶发越权或错误拒绝。 | +| C10-FR15 | 唯一迁移门禁 | Compose 使用与应用同版本的一次性 `Migrator` 服务。PostgreSQL 就绪后只运行该服务;迁移成功退出且数据库版本匹配后,两个 API 与 Worker 才允许启动或进入就绪。迁移失败时阻止业务流量,API 启动过程不得自行并发执行 Migration。 | +| C10-FR16 | 能力级降级 | Redis 故障时固定首页和商品详情回退 PostgreSQL、C06 实时推送关闭;RabbitMQ 故障时已提交 Outbox 保留且实时投递暂停;SeaweedFS 故障时上传及依赖对象写入失败,其他不依赖对象写入的能力继续。任何降级都不得伪造成功或绕过安全校验。 | +| C10-FR17 | 优雅停止 | 停止单实例时先从 Nginx 摘除并停止新请求,再有界等待在途请求;Worker 先停止领取新任务,再完成或安全释放当前任务。整套环境停止时刷新日志与遥测后停止应用和共享依赖,日常停止始终保留数据卷。 | #### 4. 主流程 1. 部署人员准备 Docker/Compose、复制示例环境变量并填写演示环境值,确认端口和持久卷目录可用。 2. 使用版本化镜像或从指定 Commit 构建前端、API、Worker 和 Nginx 镜像。 -3. Compose 先创建网络和持久卷,再启动 PostgreSQL、Redis、RabbitMQ、SeaweedFS 等依赖。 -4. 依赖达到可用状态后启动两个 API、Worker 和前端/Nginx;数据库迁移采用明确且只执行一次的受控步骤,不允许两个 API 无约束并发迁移。 -5. 部署人员通过统一入口检查首页、Swagger(若演示环境开放)、存活端点、就绪端点和一条核心业务 API。 +3. Compose 先创建网络和持久卷,启动 PostgreSQL,并同时启动 Redis、RabbitMQ、SeaweedFS 等能力依赖。 +4. PostgreSQL 就绪后运行同版本一次性 `Migrator`;只有迁移成功退出且数据库版本匹配,两个 API 和 Worker 才启动或进入就绪,失败时不接收业务流量。 +5. 启动 PC Web 与 Nginx;部署人员通过统一入口检查首页、Swagger(若演示环境开放)、存活端点、全局就绪与各能力状态,并执行一条核心业务 API。 6. 使用实例标识端点、响应头或结构化日志连续发送请求,确认两个 API 实例均收到流量。 -7. 停止其中一个 API 实例,继续执行商品查询和已登录访问,确认 Nginx 将新请求转发到存活实例。 -8. 恢复被停止实例,确认其就绪后重新参与请求处理,且数据库、消息和对象数据未丢失。 +7. 停止其中一个 API 实例前先摘除新流量并有界等待在途请求;继续执行商品查询和已登录访问,确认 Nginx 只转发到存活实例。 +8. 恢复被停止实例;其版本、配置、Migration 版本和 PostgreSQL 就绪全部通过后重新参与请求处理,且数据库、消息和对象数据未丢失。 #### 5. 业务规则与权限 - 只公开演示必需端口;PostgreSQL、Redis、RabbitMQ 管理端和 SeaweedFS 管理端默认限制在 Compose 网络或受控管理网络。 - 两个 API 使用一致的 JWT Issuer、Audience 和签名配置;若签名配置不同,请求切换实例将导致登录态失效,验收视为失败。 -- Nginx 不负责保存用户 Session;登录连续性来自可由任一 API 验证的 JWT 和 Redis 中的共享失效/通道数据。 +- Nginx 不负责保存用户 Session;登录连续性来自可由任一 API 验证的 JWT 和共享失效事实。Redis 无法安全提供撤销事实时,相关受保护能力失败关闭,不以放行旧 Token 换取表面可用。 - 容器不得依赖开发机绝对路径、IDE 启动配置或人工复制 DLL 才能运行。 -- 健康检查不得只验证进程端口打开;就绪状态至少反映 PostgreSQL 和当前阶段必需依赖是否可用。 +- 健康检查不得只验证进程端口打开。全局就绪必须确认安全配置、版本、Migration 和 PostgreSQL;Redis、RabbitMQ、SeaweedFS 另行反映能力降级,不把一项外围依赖故障误判成所有公开读取都不可用。 +- API 与 Worker 不执行启动时自动 Migration;唯一迁移责任属于同版本一次性 `Migrator`。 +- 当前 PC Web 的 SignalR 连接固定使用 WebSockets 并跳过协商,Nginx 不依赖会话亲和解决协商与升级落点。 - 日志不得输出环境变量中的密码、完整 JWT 或连接字符串;演示截图和报告同样需要脱敏。 - 数据卷删除属于破坏性运维操作,不包含在日常停止和重启命令中;清空演示数据必须使用单独、明确并经过确认的步骤。 #### 6. 异常与边界场景 - **单 API 停止**:Nginx 将新请求转发到存活实例;短暂失败应受连接重试/健康摘除控制,已登录用户无需重新登录。 -- **API 恢复**:恢复实例通过就绪检查后重新加入服务,版本和配置与存活实例一致。 +- **API 恢复**:恢复实例只有在版本一致、配置校验通过、Migration 版本匹配且 PostgreSQL 就绪后重新加入服务,不能仅凭容器 `running`。 - **Worker 停止**:普通同步查询仍可使用;Outbox 或后台任务保留待处理数据,Worker 恢复后继续处理且不重复产生业务结果。 -- **Redis 停止**:依赖 C06/C07 的实时和缓存能力允许降级,健康状态和日志清晰;使用 Redis 的 Token 失效策略必须按安全设计处理,不能静默绕过失效校验。 -- **RabbitMQ 停止**:业务事务与 Outbox 事实保留,恢复后继续投递;不得因容器重启丢失已持久化消息。 +- **Redis 停止**:C07 公开查询回退 PostgreSQL,C06 实时推送关闭;任何需要确认撤销、账号禁用或全部旧凭证失效事实的受保护 HTTP 与 Hub 连接失败关闭。恢复后先恢复有效期内撤销事实并通过安全健康检查,再恢复受保护能力。 +- **RabbitMQ 停止**:业务事务与 PostgreSQL Outbox 事实保留,实时投递暂停;恢复后继续投递,不得因容器重启丢失已提交事实。 +- **SeaweedFS 停止**:上传及依赖对象写入的动作明确失败,不提交无效对象引用;不依赖对象写入的业务继续。 - **数据库停止**:API 就绪检查失败,数据库业务不可继续伪装为成功;数据库恢复后实例能够重新就绪。 - **Nginx 停止**:统一入口不可用,应能通过容器状态和日志快速定位;Nginx 恢复后无需重建业务数据容器。 +- **受控停止**:先停止新流量,再有界等待 API 在途请求;Worker 停止领取新任务并完成或安全释放当前任务;刷新日志与遥测后停止应用和依赖,日常停止不删除数据卷。 #### 7. 验收标准与证据 @@ -2416,6 +2424,8 @@ M00 提供服务装配和公共健康检查,C06依赖 WebSocket 转发与 Redi | 7 | 重启应用容器但保留数据卷 | 用户、商品、消息和对象文件仍存在,证明数据未存于临时容器层。 | | 8 | 模拟短暂后端不可用和持续不可用 | 短暂故障恢复后查询可继续且登录态保留;持续故障显示友好页面和重试入口,不出现无限加载或浏览器原始错误。 | | 9 | 分别以游客、会员、商家和管理员连续请求并切换 API 实例 | 各身份在两个实例上的菜单入口、接口授权和数据范围一致;跨身份请求始终被拒绝,合法用户不会因实例切换退出登录。 | +| 10 | 分别停止 Redis、RabbitMQ 和 SeaweedFS | 公开商品读取、事件投递、上传及受保护访问分别按冻结能力矩阵降级,不扩大成伪成功或越权。 | +| 11 | 执行单实例摘除和整套日常停止 | 新流量先停止,API/Worker 有界收尾,日志刷新,数据卷保留;再次启动后数据仍存在。 | **验收证据与答辩要求:** @@ -2423,6 +2433,9 @@ M00 提供服务装配和公共健康检查,C06依赖 WebSocket 转发与 Redi - 保存两实例请求分布、健康检查、停止/恢复实例、登录态连续性、WebSocket 转发和数据持久化的原始日志或录屏。 - 能解释 Nginx 反向代理与负载均衡、健康检查、WebSocket Upgrade、容器网络和持久卷的作用。 - 能解释 JWT 为什么可在多实例验证、Redis 保存哪些共享能力、为什么应用不能依赖单实例内存 Session。 +- 能证明唯一 `Migrator` 成功是 API/Worker 准入门槛,迁移失败不会由多个 API 并发补跑。 +- 能解释全局就绪与 Redis/RabbitMQ/SeaweedFS 能力级降级的区别,以及 Redis 恢复前为什么必须先恢复有效撤销事实。 +- 能证明 WebSockets 跳过协商、不使用 SSE/长轮询,并按优雅停止顺序保留在途结果和数据卷。 - 能说明 Aspire 与 Docker Compose 的使用边界,以及为什么演示部署必须使用同一版本镜像和受控 Secret。 ### 4.1 挑战验收证据 @@ -2559,7 +2572,7 @@ Electron 后续原则上复用 PC Web 原型,只补充窗口、桌面导航和 | C06 | 实时消息推送—罗皓晨 | 实时通知、连接状态、消息中心 | SignalR 契约(无 Axxx),补查复用 A501~A505 | 待挑战测试登记 | WebSockets 跳过协商、固定重连、凭证失效与权威角标补查已冻结,待接口、部署与测试承接 | | C07 | 缓存与性能优化—罗皓晨,顾欣月协作 | 首页、商品详情、压测报告 | 复用 A102 的固定首页摘要调用、A103 商品详情;不缓存普通列表与搜索 | 待挑战测试登记 | 60/10 秒 TTL、500 毫秒跨实例填充等待、提交后立即/3 秒双删和 62 秒兜底已冻结,待接口、实现与压测承接 | | C08 | 支付回调幂等与对账—张海洋 | 回调重放脚本、管理端对账 | A421~A425、A432~A433;退款入账使用 Payment 应用契约 | 待挑战测试登记 | A431 为取消历史号,待数据库、OpenAPI 与交叉评审 | -| C10 | 容器化与负载均衡—罗皓晨 | 统一访问入口、健康与实例验证 | A506、A507 | 待部署验收登记 | 健康检查草案已汇总,待交叉评审 | +| C10 | 容器化与负载均衡—罗皓晨 | 统一访问入口、健康与实例验证 | A506、A507 | 待部署验收登记 | Migrator、全局就绪/能力降级、Redis 安全恢复、WebSocket 与优雅停止已冻结,待接口、部署和验收承接 | 负责人补齐缺少接口并完成交叉评审后,应把对应状态更新为“已确认”;生成真实 OpenAPI 后再补充 `operationId` 校验结果。测试计划完成后,应替换为真实测试用例编号,并随需求变更同步维护状态。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C10-\351\253\230\345\217\257\347\224\250\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C10-\351\253\230\345\217\257\347\224\250\346\265\201\347\250\213.md" index 6b67bbc..463a1ee 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C10-\351\253\230\345\217\257\347\224\250\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C10-\351\253\230\345\217\257\347\224\250\346\265\201\347\250\213.md" @@ -4,7 +4,7 @@ > 覆盖:C10 > 基础核心流程:横切 F01~F13,并支撑 C06/C07 及 Worker 后台运行 > 直接协作:M00 公共装配、M01 Identity、全部业务模块、C06 SignalR、C07 Redis -> 文档状态:初稿,待罗皓晨自审及全组交叉评审 +> 文档状态:完整定义;迁移、就绪、降级、恢复和停止边界已冻结,待部署资产、实现和验收承接 > 需求事实源:[需求规格说明书 C10](../../../01-需求文档/需求规格说明书.md) 的“C10 容器化部署与负载均衡”完整七节 ## 一、验收范围与事实来源 @@ -15,9 +15,9 @@ C10 使用 Docker Compose 从同一版本启动 Nginx、PC Web、两个 API 实 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| -| C10 需求与教师验收 | 完整定义,待需求冻结 | 作为 Compose、双 API、单实例故障和登录态验收边界 | -| 本文部署流程 | 初稿 | 明确启动、流量准入、故障切换、恢复和不可变业务结果 | -| A506、A507 | 部分定义、待交叉评审 | 承接存活和就绪检查,不单独证明业务连续性 | +| C10 需求与教师验收 | 完整定义 | 作为 Compose、双 API、单实例故障和登录态验收边界 | +| 本文部署流程 | 完整定义 | 冻结 Migrator 门禁、流量准入、能力降级、故障切换、安全恢复和优雅停止 | +| A506、A507 | 部分定义,待按本文补齐 | 承接存活、全局就绪和能力状态,不单独证明业务连续性 | | Compose/Nginx/镜像/Secret | 设计阶段,尚无真实资产证据 | 不写成已部署或已验证 | | 各业务模块幂等与数据一致性 | 由各模块负责 | C10 不代替订单、支付、库存等业务规则 | @@ -41,8 +41,9 @@ flowchart LR WORKER["Mall.Worker 独立容器"] --> PG WORKER --> MQ - API1 -->|"存活/就绪事实"| HEALTH["存活与就绪检查"] - API2 -->|"存活/就绪事实"| HEALTH + MIGRATOR["一次性 Migrator
同版本、成功后退出"] -->|"目标 Migration 版本"| PG + API1 -->|"存活/全局就绪/能力状态"| HEALTH["存活与就绪检查"] + API2 -->|"存活/全局就绪/能力状态"| HEALTH ``` 边界约束: @@ -51,11 +52,12 @@ flowchart LR - 后端容器端口默认只在内部网络可见,不直接暴露给公网;现场验收不得绕过 Nginx 访问业务接口。 - Nginx 必须正确转发客户端 IP、协议、Host、请求 ID 和 WebSocket Upgrade 所需请求头;受控实例标识只用于脱敏验收证据。 - Hub 握手路径中的 `access_token` Query 必须在 Nginx、ASP.NET Core、Serilog 和 Trace 中完整脱敏,集成、演示和发布环境只使用 HTTPS/WSS。 -- Redis Backplane 只传播跨实例 Hub 消息;SignalR 协商请求与 WebSocket 连接升级的实例落点必须另行冻结会话亲和或经验证的跳过协商策略。 +- Redis Backplane 只传播跨实例 Hub 消息;当前 PC Web 固定只使用 WebSockets 并跳过 SignalR 协商,Nginx 负责 Upgrade,不依赖协商请求与升级请求之间的会话亲和。 - 两个 API 使用同一 Commit SHA/版本 Tag 构建的同一镜像和等价业务配置;只允许实例标识等运行信息不同。 - JWT Issuer、Audience、签名、Policy、账号状态和令牌失效语义在两个实例上必须一致。 - PostgreSQL 保存业务事实;Redis、RabbitMQ、容器内存和前端状态不得成为无法恢复的唯一业务事实。 - Worker 独立于任一 API 实例运行;后台任务的领取、幂等和状态规则仍由对应业务负责人定义。 +- API 与 Worker 不执行启动时自动 Migration;同版本一次性 `Migrator` 是唯一迁移执行者。 ## 三、Compose 启动与受控迁移 @@ -63,15 +65,15 @@ flowchart LR flowchart TD A["部署人员确认 Docker/Compose、端口、持久卷和受控 Secret 可用"] --> B["选择同一 Commit SHA/版本 Tag 的镜像"] B --> C["创建内部网络和持久卷"] - C --> D["启动 PostgreSQL、Redis、RabbitMQ 和 SeaweedFS"] - D --> E{"当前阶段必需依赖是否就绪?"} - E -- "否" --> X["保持应用未就绪
显示故障依赖并停止继续验收"] - E -- "是" --> F["执行一次受控数据库迁移"] - F --> G{"迁移是否唯一执行且成功?"} - G -- "否" --> Y["停止业务流量准入
保留日志并修复迁移问题"] + C --> D["启动 PostgreSQL
并启动 Redis、RabbitMQ、SeaweedFS"] + D --> E{"PostgreSQL 是否就绪?"} + E -- "否" --> X["不运行 Migrator
API/Worker 不接收业务"] + E -- "是" --> F["运行同版本一次性 Migrator"] + F --> G{"迁移是否成功退出且目标版本匹配?"} + G -- "否" --> Y["阻止 API/Worker 启动或就绪
保留日志并修复迁移问题"] G -- "是" --> H["启动两个 API、Worker、PC Web 与 Nginx"] - H --> I["分别检查两个 API 的存活和就绪结果"] - I --> J{"两个 API 均可接收流量?"} + H --> I["检查配置、版本、Migration、PostgreSQL
并分别报告能力级依赖状态"] + I --> J{"两个 API 是否都满足全局就绪?"} J -- "否" --> Z["仅允许符合就绪准入规则的实例用于诊断
C10 双实例启动与现场验收不通过"] J -- "是" --> K["通过统一入口检查首页、健康与一条核心查询"] K --> L["连续请求并用受控实例标识或日志证明双实例分发"] @@ -80,9 +82,9 @@ flowchart TD 启动规则: - Compose 提供明确的一条启动命令和一条日常停止命令;日常停止不得删除数据卷。 -- 数据库迁移必须是独立、受控且只执行一次的步骤,不能让两个 API 无约束并发迁移。 -- A506 只说明进程能够响应;A507 才表达 PostgreSQL 和当前必需依赖是否允许实例接收业务流量。 -- 未启用的可选依赖不得错误阻塞 API 就绪;当前环境哪些依赖属于“必需”仍需在第十章确认。 +- 数据库迁移由 Compose 中与应用同版本的一次性 `Migrator` 执行;PostgreSQL 就绪后运行,成功退出后两个 API 与 Worker 才启动或进入就绪。失败时阻止业务流量,不允许 API 自行并发补跑。 +- A506 只说明进程能够响应;A507 的全局就绪固定检查安全配置、运行版本、目标 Migration 版本和 PostgreSQL。 +- Redis、RabbitMQ、SeaweedFS 的异常通过 A507 能力状态与指标反映,并按第七章降级;单项异常不错误阻断所有公开读取。 - Aspire 只用于本地开发编排,C10 现场验收统一使用 Docker Compose 和 Nginx。 ## 四、实例状态与流量准入 @@ -93,10 +95,11 @@ flowchart TD stateDiagram-v2 [*] --> Stopped: 尚未启动 Stopped --> Starting: 容器启动 - Starting --> NotReady: 进程存活但必需依赖未满足 - Starting --> Ready: 存活且就绪检查通过 - NotReady --> Ready: 依赖恢复并重新检查通过 - Ready --> NotReady: 必需依赖失败 + Starting --> NotReady: 配置/版本/Migration/PostgreSQL 未满足 + Starting --> Ready: 全局就绪检查通过 + NotReady --> Ready: 全局门槛恢复并重新检查通过 + Ready --> NotReady: 任一全局门槛失败 + Ready --> Ready: Redis/RabbitMQ/SeaweedFS 能力降级或恢复 Ready --> Stopped: 实例停止 NotReady --> Stopped: 实例停止 Ready --> Recovering: 实例重启或版本恢复 @@ -107,9 +110,10 @@ stateDiagram-v2 流量规则: - 只有达到 `Ready` 的实例才允许接收新业务流量;`Stopped`、`Starting` 和 `NotReady` 实例不得持续接收新请求。 -- 恢复实例必须先通过 A507,再重新参与负载均衡,不能仅凭容器“running”状态加入。 +- `Ready` 的固定门槛是安全配置完整、运行版本兼容、Migration 版本匹配和 PostgreSQL 可用;Redis、RabbitMQ、SeaweedFS 的单项异常改变能力状态,不直接把整个 API 置为 `NotReady`。 +- 恢复实例必须同时通过版本、配置、Migration 版本、PostgreSQL 和 A507 检查,再重新参与负载均衡,不能仅凭容器 `running` 状态加入。 - 存活与就绪响应不得包含连接字符串、主机、端口、异常堆栈、凭据或其他敏感配置。 -- Nginx/Compose 如何使用探针、阈值和超时实现摘除及重新加入,当前仍是部署待评审项。 +- Nginx/Compose 必须以 A507 和有界探针阈值摘除、恢复实例;具体秒数属于部署配置,但不得绕过上述准入条件。 ## 五、单 API 实例停止与恢复 @@ -128,12 +132,12 @@ flowchart TD I -- "否" --> K["所属流程已定义幂等标识时复用原标识
否则使用已确认的业务查询动作确认结果"] G --> L["实例 2 使用同一 JWT/Policy 校验并返回同一数据范围"] K --> L - D --> M["C06 连接进入重连"] - M --> N["连接转移到实例 2 并通过 M09 补查"] + D --> M["C06 WebSocket 连接进入固定有限重连"] + M --> N["以 WebSockets 跳过协商连接实例 2
并通过 M09 补查权威事实"] L --> O["用户保持登录,已提交业务事实不丢失、不重复"] N --> O O --> P["恢复实例 1"] - P --> Q["实例 1 通过存活、就绪与版本配置检查"] + P --> Q["实例 1 通过版本、配置、Migration、PostgreSQL
存活与全局就绪检查"] Q --> S["重新加入流量并继续共享 PostgreSQL、Redis、RabbitMQ 和对象数据"] ``` @@ -144,6 +148,7 @@ flowchart TD - 结果未知时回到所属模块:业务流程已定义幂等标识时复用原标识,否则使用已确认的结果查询入口;C10 不自行假设所有提交都具备幂等标识。 - 单实例停止不能删除 PostgreSQL、RabbitMQ、Redis 或对象存储的持久化数据,也不能停止独立 Worker 容器。 - SignalR 连接允许短暂中断,但 M09 消息事实必须保留,重连后通过列表和未读数补偿。 +- 当前 PC Web 只以 WebSockets 跳过协商重连;持续失败时使用 M09 HTTP 查询或定期补查,不增加 SSE 或长轮询。 - 安全查询超过有限重试后必须进入统一维护/服务不可用页面,保留可理解提示和手动重试入口;不得停留在 Nginx 默认错误页、白屏或无限加载。 ## 六、身份连续性与安全失败 @@ -155,7 +160,7 @@ flowchart TD C --> D{"认证和当前账号状态是否可确定?"} D -- "有效" --> E["继续执行 Policy、资源归属和业务状态校验"] D -- "无效" --> X["拒绝访问并按登录失效处理"] - D -- "关键撤销状态无法安全确认" --> Y["拒绝受保护请求
具体安全失败策略待 Identity 评审"] + D -- "撤销或账号状态无法安全确认" --> Y["拒绝受保护 HTTP / Hub
失败关闭,不继续业务"] E --> F{"请求切换到另一实例?"} F -- "否" --> G["返回当前业务结果"] F -- "是" --> H["另一实例使用相同配置和共享状态重新校验"] @@ -164,8 +169,9 @@ flowchart TD 安全边界: -- Nginx 不保存业务 Session;登录连续性来自任一实例可验证的 JWT 和经 Identity 评审后的共享令牌失效状态。 -- 不得为了 Redis 故障时“保持可用”而静默绕过令牌撤销、账号禁用或资源归属校验。 +- Nginx 不保存业务 Session;登录连续性来自任一实例可验证的 JWT 和可安全确认的共享令牌失效事实。 +- 任何需要确认主动退出、JWT 撤销、手机号修改、账号禁用或全部旧凭证失效事实的受保护 HTTP 与 Hub 连接,在 Redis 不可用或事实无法确认时都失败关闭;不得只限制 M09,也不得为可用性静默放行。 +- Redis 恢复后,先从受控持久化或可重建事实源恢复有效期内的撤销事实,并通过安全健康检查;完成前相关受保护能力继续失败关闭,防止旧 Token 复活。 - 前端隐藏菜单、缓存身份或记录上一次成功实例都不能作为服务端授权依据。 - 实例标识只用于 C10 请求分布证据,不进入业务判断,也不暴露主机名、IP 或内部网络信息。 @@ -175,15 +181,50 @@ flowchart TD |---|---|---|---| | 单个 API | 存活实例继续处理查询和受控业务请求 | 当前连接短暂中断 | 恢复实例就绪后重新加入 | | Worker | 普通同步 API 可继续 | Outbox 投递和后台任务暂缓 | 恢复后按各模块幂等规则继续,不重复业务结果 | -| Redis | C07 公开查询可回退 PostgreSQL;M09 消息数据事实仍保留 | C06 实时跨实例和缓存性能降级;若 Identity 无法确认令牌撤销状态,受保护的 M09 HTTP 请求必须拒绝或返回服务不可用 | 恢复后重新连接;令牌失效策略需 Identity 评审 | -| RabbitMQ | 已提交业务事务和同步查询可保留 | 集成事件实时传输暂缓 | Outbox 保留待发布事实,恢复后重投并由消费者防重 | -| PostgreSQL | 存活端点仍可反映进程 | 数据库业务请求不得伪装成功;实例应未就绪 | 恢复后重新检查,不能用缓存冒充完整事实 | -| SeaweedFS | 与对象无关的业务可按契约继续 | 新上传和依赖对象内容的操作按所属模块失败/占位规则处理 | 对象恢复后继续,不写入无效对象引用 | +| Redis | C07 固定首页与 A103 回退 PostgreSQL;公开列表等原本直读能力继续;M09 已持久化事实保留 | C06 实时推送关闭;任何需要确认撤销、账号禁用或全部旧凭证失效事实的受保护 HTTP/Hub 失败关闭 | Redis 基础健康后可恢复公开缓存;先恢复有效期内撤销事实并通过安全健康检查,才恢复受保护能力和实时推送 | +| RabbitMQ | 同步事务与 PostgreSQL Outbox 事实继续按所属契约提交 | 集成事件实时投递暂停 | Outbox 保留待发布事实,恢复后重投并由消费者防重 | +| PostgreSQL | 存活端点仍可反映进程 | 全部数据库业务请求不得伪装成功;实例全局未就绪 | 恢复并确认 Migration 版本后重新检查,不能用缓存冒充完整事实 | +| SeaweedFS | 与对象写入无关的业务可按契约继续;已有对象读取失败时使用所属页面占位/重试 | 新上传和依赖对象写入的操作明确失败 | 对象恢复后继续,不写入无效对象引用 | | Nginx | 内部容器可用于诊断 | 用户统一入口不可用 | 恢复入口不应要求重建业务数据 | 本表定义正确失败边界,不表示这些共享依赖已经具备冗余高可用。演示时不得把“能看到容器状态”写成依赖故障已经自动切换。 -## 八、数据卷、版本与运行责任 +全局就绪与能力状态是两层结论:配置、版本、Migration 或 PostgreSQL 失败会把实例置为 `NotReady`;Redis、RabbitMQ、SeaweedFS 失败时实例仍可为全局 `Ready`,但必须按本表关闭或降级对应能力,健康响应和指标不得伪装为全部正常。 + +## 八、优雅停止、数据卷与运行责任 + +### 8.1 单实例与整套环境停止顺序 + +```mermaid +flowchart TD + A["发起停止"] --> B{"停止范围?"} + B -- "单个 API 实例" --> C1["Nginx 停止向目标实例分发新请求"] + C1 --> D1["目标 API 有界等待在途请求"] + D1 --> E1["关闭目标实例 Hub 连接
刷新目标实例日志与遥测"] + E1 --> F1["只停止目标 API
Worker 和共享依赖继续"] + + B -- "整套环境" --> C2["Nginx 停止接收全部新业务请求"] + C2 --> D2["全部 API 有界等待在途请求"] + D2 --> E2["关闭 Hub 连接
客户端转为不可用提示/后续补查"] + E2 --> F2["Worker 停止领取新任务"] + F2 --> G2{"当前任务能否在期限内安全完成?"} + G2 -- "是" --> H2["提交确定结果和检查点"] + G2 -- "否" --> I2["安全释放领取权
保留可重试事实"] + H2 --> J2["刷新全部结构化日志、Trace 和 Metric"] + I2 --> J2 + J2 --> K2["停止 API、Worker、PC Web 与 Nginx"] + K2 --> L2["再停止共享依赖
保留全部数据卷"] +``` + +停止约束: + +- 单个 API 维护只摘除并停止目标实例,不停止 Worker 或共享依赖;存活实例继续服务。 +- API 在有界排空期间不得接受新提交;在途提交必须得到确定结果,或由所属流程保留原幂等标识/可查询事实,不能静默丢失后让客户端盲目重放。 +- Worker 收到停止信号后不再领取新任务;当前任务要么完成提交,要么安全释放并保留可重试事实,不留下永久“处理中”假状态。 +- Hub 连接关闭只影响实时性;客户端按 C06 固定重连,持续不可用时回到 M09 HTTP 查询或定期补查。 +- 整套环境日常停止先完成应用排空和遥测刷新,再停止 PostgreSQL、Redis、RabbitMQ、SeaweedFS;不删除数据卷。 + +### 8.2 数据卷、版本与配置 - PostgreSQL、需要持久化的 Redis 数据、RabbitMQ 和 SeaweedFS 使用明确持久卷;应用容器重建不删除业务数据和对象文件。 - 日常停止、单实例重启和应用升级不得隐式删除数据卷;清空演示数据必须使用独立、明确且经确认的破坏性步骤。 @@ -196,11 +237,12 @@ flowchart TD | 流程能力 | 当前派生契约 | 数据设计 | 当前状态 | |---|---|---|---| -| 判断 API 进程能否响应 | A506 `/health/live` | 无 DBxxx | 待交叉评审 | -| 判断 PostgreSQL 和当前必需依赖是否可用 | A507 `/health/ready` | 无 DBxxx | 必需依赖矩阵待确认 | -| 证明两个实例分别响应 | A506/A507 的受控 `instanceId` 或结构化日志 | 无 DBxxx | 证据方案待部署评审 | -| Nginx 转发 SignalR WebSocket | 接口设计 4.3.7“Hub 连接” | Redis Backplane 不登记 DBxxx | 待 C06/C10 联合验证 | -| 实例切换后保持认证授权 | 接口设计 1.6;系统架构 8 | 令牌失效数据设计待 Identity/数据库确认 | 待交叉评审 | +| 判断 API 进程能否响应 | A506 `/health/live` | 无 DBxxx | 待补齐最小脱敏响应 | +| 判断全局就绪与能力状态 | A507 `/health/ready` | 无 DBxxx | 待补齐配置/版本/Migration/PostgreSQL 门槛及 Redis/RabbitMQ/SeaweedFS 能力状态 | +| 证明两个实例分别响应 | A506/A507 的受控 `instanceId` 或结构化日志 | 无 DBxxx | 待部署资产承接 | +| Nginx 转发 WebSockets 跳过协商的 SignalR 连接 | 接口设计 4.3.7“Hub 连接” | Redis Backplane 不登记 DBxxx | 待 Nginx/双实例验证 | +| 实例切换后保持认证授权并失败关闭 | 接口设计 1.6;系统架构 8 | 撤销事实的持久化/可重建来源待数据库设计承接 | 待补齐 Redis 故障与安全恢复契约 | +| 唯一 Migrator 门禁 | Compose 服务依赖与镜像版本约定,不新增 Axxx | Migration 历史由数据库设计承接 | 待部署资产与失败演练 | | Worker、Outbox 与依赖恢复 | 系统架构 7.4 及对应业务 Worker 契约 | 相关 DBxxx 尚未冻结 | 各模块分别负责 | 架构承接章节: @@ -211,34 +253,37 @@ flowchart TD - 系统架构 12“测试策略”:请求分布、WebSocket、单实例故障和登录态演示; - 系统架构 15“分阶段实施”:C10 属于第四阶段联合验收,不阻塞前期核心闭环。 -## 十、待交叉评审项 +## 十、下游设计与验证约束 -1. 确定数据库 Migration 唯一执行机制、失败回滚和版本不兼容时的停止条件。 -2. 冻结演示环境 A507 的必需依赖矩阵;可选依赖未启用时不得错误阻塞就绪。 -3. 确定 Nginx/Compose 使用存活或就绪事实的方式、失败阈值、超时、摘除和恢复实例重新加入机制。 -4. 与 C06 冻结 SignalR 协商请求与 WebSocket 连接升级的实例落点策略;可评审会话亲和或经验证的 WebSockets 跳过协商方案,Redis Backplane 本身不能替代该决策。 -5. 与 Identity 确认 Redis 中令牌失效数据的持久化范围,以及 Redis 不可用时受保护请求的安全失败策略。 -6. 与各业务负责人确认提交类请求的幂等/结果查询入口,实例故障时不得由前端统一盲目重试。 -7. 确定 PC Web 安全查询的重试上限,以及统一维护/服务不可用页面与 Nginx 默认错误页的具体替换方式;友好失败出口本身是必达结果。 -8. 确定实例标识、请求分布、连接落点和恢复实例重新入池的脱敏证据方式。 -9. 当前尚无真实 Compose、Nginx、镜像、环境配置或运行结果,本流程不得标记为已部署或已验证。 +1. Compose 必须提供同版本一次性 `Migrator`,并以其成功退出作为两个 API 和 Worker 的启动/就绪门禁;API 与 Worker 不得自行执行 Migration。 +2. A507 必须区分全局就绪和能力状态:配置、版本、Migration、PostgreSQL 决定流量准入;Redis、RabbitMQ、SeaweedFS 分别按第七章降级。 +3. Nginx/Compose 要使用有界探针阈值完成摘除和重新加入;恢复实例必须重新验证版本、配置、Migration 和 PostgreSQL,不能只看 `running`。 +4. Hub 与 Nginx 只承接 WebSockets 跳过协商;不设计 SSE、长轮询或依赖会话亲和的第二条连接路径。 +5. Identity 与数据设计必须给出有效期内撤销事实的受控持久化或可重建来源;Redis 故障时相关受保护 HTTP/Hub 失败关闭,恢复事实前不能重新开放。 +6. 各业务提交继续使用所属流程的幂等标识或结果查询;C10 只允许安全查询有限重试,不为所有写请求发明统一重放。 +7. 部署资产需提供优雅停止钩子、统一维护页面、实例标识、请求分布、WebSocket 落点、能力降级和重新入池的脱敏证据。 +8. 当前尚无真实 Compose、Nginx、镜像、环境配置或运行结果,因此“流程完整”不等于“已部署或已验证”。 ## 十一、现场验收证据清单 - [ ] 从停止状态执行一条 Compose 启动命令,必需容器、内部网络和持久卷状态清晰。 - [ ] 两个 API 使用同一版本镜像和等价业务配置,迁移仅由受控步骤执行一次。 +- [ ] PostgreSQL 就绪后只有同版本一次性 Migrator 运行;迁移失败时两个 API 和 Worker 不启动或不就绪,API 不并发补跑。 - [ ] 浏览器只通过 Nginx 完成登录和业务访问,后端容器端口默认不直接暴露公网。 - [ ] Nginx 正确转发客户端 IP、协议、Host、请求 ID 和 WebSocket Upgrade 请求头,并保留脱敏追踪证据。 -- [ ] Hub 连接按已冻结的落点策略完成协商、WebSocket 升级和重连;Nginx、ASP.NET Core、Serilog 与 Trace 证据均不出现完整 `access_token` Query。 +- [ ] Hub 连接只使用 WebSockets 并跳过协商,完成 Upgrade 和重连;持续失败回退 M09 HTTP 查询,不启用 SSE/长轮询,日志不出现完整 `access_token` Query。 - [ ] 连续请求通过实例标识或日志证明至少到达两个就绪 API。 - [ ] 任一 API 未能就绪时明确判定 C10 双实例启动与现场验收不通过,不以单实例运行冒充达标。 - [ ] 停止 API 实例 1 后,商品查询、本人消息查询和已登录访问由实例 2 继续处理。 - [ ] 实例切换前后 JWT、Policy、账号状态、资源归属和数据范围一致。 - [ ] 触发一条实时消息,证明 Nginx WebSocket 转发和跨实例实时通道;停止连接实例后可重连补查。 -- [ ] 恢复实例 1 后,只有通过就绪检查才重新接收请求。 +- [ ] 恢复实例 1 后,只有版本、配置、Migration、PostgreSQL 和 A507 全部通过才重新接收请求。 - [ ] 重启应用容器但保留数据卷后,用户、商品、消息和对象文件仍存在。 - [ ] 分别演示 Worker、Redis、RabbitMQ、PostgreSQL 或 Nginx 故障时的正确停止、降级或恢复边界,不夸大为共享依赖高可用。 +- [ ] Redis 故障时公开商品查询回退 PostgreSQL、实时关闭且受保护请求失败关闭;恢复有效期内撤销事实并通过安全健康检查后才恢复受保护能力。 +- [ ] RabbitMQ 故障时 Outbox 保留且投递暂停;SeaweedFS 故障时上传/对象写入失败,其他能力不被错误全部阻断。 - [ ] 短暂故障仅对安全查询有限重试;持续故障展示统一维护/服务不可用页面、可理解提示和手动重试,不出现 Nginx 默认错误页、白屏或无限加载。 - [ ] 提交类请求在结果未知时,仅在所属流程已定义时复用原幂等标识,否则使用已确认的业务查询确认,不因自动重放产生重复写。 +- [ ] 单实例和整套环境按“停止新流量—API 有界排空—Worker 停领并完成/释放—刷新遥测—停止应用/依赖”执行,日常停止保留全部数据卷。 - [ ] 游客、会员、商家和管理员分别在两个实例上验证菜单入口、接口授权和数据范围一致;跨身份请求均被拒绝,合法登录态不因实例切换丢失。 - [ ] 保存 Compose 配置、示例环境、Commit SHA/镜像 Tag、容器清单、网络/卷说明、健康结果、请求分布、故障恢复和脱敏日志。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" index d41233b..eb3e3bc 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" @@ -22,7 +22,7 @@ A002~A004 由本流程派生;历史清单中的 A005 刷新凭证没有业 | A002~A004 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | | A005 刷新凭证 | 无需求来源 | 取消,不进入实现 | | DBxxx 账号/令牌表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | -| C10 多实例认证 | 部分定义 | 只登记接入点;多实例令牌验证规则由 C10 评审 | +| C10 多实例认证 | 完整定义 | 任一实例一致验证;共享失效事实不可确认时失败关闭,恢复安全事实后才重新开放 | ## 二、模块直接出入口 @@ -176,6 +176,7 @@ flowchart TD - 下游模块只接收登录态解析后的身份和角色,不接受客户端自行声明的接收人。 - 退出、手机号修改或账号禁用只有在所有 API 实例都能一致拒绝相应旧凭证后才能返回成功;无法确认一致失效时不得返回成功,依赖该失效事实的受保护请求必须失败关闭。 +- Redis 等共享失效能力恢复后,C10 必须先恢复有效期内的撤销事实并通过安全健康检查,再恢复相关受保护 HTTP 与 Hub;不得让恢复过程使旧凭证短暂复活。 - 本期仅验收 PC Web 登录入口与角色路由;Electron 和 Android 作为后续客户端规划,不进入当前流程或验收证据。 ## 八、由流程派生的接口契约映射 @@ -196,6 +197,7 @@ flowchart TD 3. A004 必须同时校验 JWT 有效期、签名、当前账号状态和失效事实;任何必需事实无法确认时按服务暂不可用失败关闭。 4. 登录失败次数限制、密码修改和账号锁定均不属于本期业务范围;不得因实现便利写入契约。 5. 多实例必须共享一致的签名配置、账号安全变化和凭证失效判断,但具体存储机制不进入业务接口。 +6. C10 与数据设计必须提供有效期内撤销事实的受控持久化或可重建来源;共享能力不可用或恢复尚未完成时,相关受保护 HTTP 与 Hub 继续失败关闭。 ## 十、验收证据清单 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" index 3547ff4..10035c4 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -21,7 +21,7 @@ A015~A017 由本流程派生,仅在流程评审通过后用于契约映射 | 本文业务流程 | 完整定义 | 已确认角色分流、责任阻断、竞争结果、状态与模块出入口 | | A015~A017 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | | DBxxx 账号/操作记录表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | -| C10 多实例凭证校验 | 部分定义 | 只登记接入点;所有实例必须一致遵守 Identity 的账号状态与失效事实 | +| C10 多实例凭证校验 | 完整定义 | 所有实例一致遵守账号状态与失效事实;不可确认及安全恢复完成前失败关闭 | ## 二、模块直接出入口 @@ -196,6 +196,7 @@ flowchart TD - 商家禁用由 M06-03 发起责任复核;Ordering、AfterSales、Seckill 在接收新责任时仍必须重新校验商家当前状态并参与唯一先后判定。 - 禁用或启用结果必须被所有实例一致执行;无法确认 Identity 安全事实时,受保护请求失败关闭,不允许设置“稍后才失效”的成功窗口。 +- Redis 等共享能力恢复时,必须先恢复有效期内的撤销事实并通过安全健康检查,再开放相关受保护 HTTP 与 Hub;不得让禁用前凭证在恢复窗口复活。 - 管理员账号治理不直接修改订单、支付或售后事实;只通过账号状态影响后续业务受理。 - 业务归属复核必须使用目标模块的公开应用契约,不能直接读取目标模块内部表。 -- Gitee From 13ce89905bc6a31c30efc8b8b98b74215fcfb205 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 22:01:05 +0800 Subject: [PATCH 104/118] =?UTF-8?q?docs(process):=20=E8=A1=A5=E9=BD=90=20C?= =?UTF-8?q?01=20=E9=BB=98=E8=AE=A4=E5=95=86=E5=AE=B6=E5=88=86=E9=85=8D?= =?UTF-8?q?=EF=BC=9B=E7=BB=9F=E4=B8=80=E7=A7=92=E6=9D=80=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E5=B1=A5=E7=BA=A6=E5=BD=92=E5=B1=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...22\346\235\200\346\265\201\347\250\213.md" | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" index 3874f2b..d43aa52 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" @@ -3,7 +3,7 @@ > 负责人:朱惠惠 > 覆盖:C01-01、M03-01 与 M04-01 的秒杀衔接、X04 不参与秒杀取消回补 > 基础核心流程:F11(商品上下架)、F04/F06(活动浏览)、F08(下单)、F10(支付)、F09/F12(取消 / 发货) -> 直接协作:顾欣月(M02 Catalog 与 M06-01 活动维护)、韦乾强(M04 Ordering 与 C03 超时取消)、张海洋(M05 Payment 与 C08 回调) +> 直接协作:顾欣月(M02 Catalog 与 M06-01 活动维护)、唐宇昊(M01 默认商家与账号状态)、韦乾强(M04 Ordering 与 C03 超时取消)、张海洋(M05 Payment 与 C08 回调) > 文档状态:已按需求校准,可作为接口与数据库设计输入;待 Catalog/Ordering 交叉评审 > 需求事实源:[需求规格说明书 C01](../../../01-需求文档/需求规格说明书.md) 的“C01 秒杀与防超卖”完整七节 @@ -147,9 +147,9 @@ flowchart TD C -- "同标识不同请求" --> Y["拒绝复用标识,不产生副作用"] C -- "全新请求" --> D{"当前流量是否在可承载上限内?"} D -- "否" --> Z["形成过载的确定业务结果;普通商品入口继续可用"] - D -- "是" --> E["读取活动、商品、买家当前限购占用和地址归属"] - E --> F{"活动为 Ongoing、权威时间在窗口内、商品匹配、地址归本人且请求未超限?"} - F -- "否" --> G["形成未开始 / 已结束 / 已取消 / 超限 / 地址无效等确定业务结果"] + D -- "是" --> E["读取活动、商品、买家当前限购占用、地址归属和唯一启用的默认商家"] + E --> F{"活动为 Ongoing、权威时间在窗口内、商品匹配、地址归本人、默认商家唯一可用且请求未超限?"} + F -- "否" --> G["形成未开始 / 已结束 / 已取消 / 超限 / 地址无效 / 默认商家不可用等确定业务结果"] F -- "是" --> H["开启短事务"] H --> I["以活动、状态、时间窗口和剩余量为条件原子扣减独立秒杀库存"] I --> J{"扣减是否成功?"} @@ -158,7 +158,7 @@ flowchart TD L --> M{"限购占用是否成功?"} M -- "否" --> K M -- "是" --> N["在同一原子边界调用 M04 统一订单创建能力"] - N --> N1["M04 生成共享待支付订单、指定商家、固定支付截止时间、地址与订单项快照,并记录秒杀来源、活动和成交价"] + N --> N1["M04 生成共享待支付订单,以唯一启用的默认商家写入 assignedMerchantUserId,保存固定支付截止时间、地址与订单项快照,并记录秒杀来源、活动和成交价"] N1 --> O["可靠记录订单已创建事实,并绑定本次稳定请求结果"] O --> P{"库存、限购、订单、快照、可靠事实与请求结果是否整体提交?"} P -- "否" --> T["整体回滚;属于未形成确定结果的瞬态失败,原标识可重试"] @@ -174,9 +174,10 @@ flowchart TD 不可变核心事实: - 秒杀库存扣减必须使用数据库条件更新,一次同时约束目标活动、`Ongoing` 状态、权威时间窗口与剩余量;未命中就失败,禁止在应用层“先读取、后递减”。 -- 秒杀库存扣减、买家限购占用、M04 共享订单、指定商家、固定支付截止时间、地址与订单项快照、可靠订单事实和幂等结果属于同一个原子成功结果;任一步失败全部回滚,不产生支付前置记录。 +- 秒杀库存扣减、买家限购占用、M04 共享订单、唯一启用的默认 `assignedMerchantUserId`、固定支付截止时间、地址与订单项快照、可靠订单事实和幂等结果属于同一个原子成功结果;任一步失败全部回滚,不产生支付前置记录。 +- 活动创建人只决定活动管理范围,不决定订单履约归属。普通订单和秒杀订单都由 M04 在提交时解析同一唯一启用默认商家;默认商家缺失、重复、禁用或与下单并发禁用时,按唯一顺序拒绝或成立,不得创建无人负责订单。 - 完成身份与固定输入校验后,必须先查询稳定请求结果,再进入限流、时间、库存和限购判断。同标识同请求重放首次确定结果,不得因当前活动、库存、限购或流量变化重新裁决。 -- 成功、未开始、已结束、已取消、售罄、超限、地址无效和已正式返回的过载结果都属于可重放的确定业务结果,必须先与稳定请求标识持久绑定再返回。数据库连接中断、事务提交失败等无法确认结果的瞬态故障不得伪装成确定业务失败,也不得固化;客户端使用原标识重试。 +- 成功、未开始、已结束、已取消、售罄、超限、地址无效、默认商家不可用和已正式返回的过载结果都属于可重放的确定业务结果,必须先与稳定请求标识持久绑定再返回。数据库连接中断、事务提交失败等无法确认结果的瞬态故障不得伪装成确定业务失败,也不得固化;客户端使用原标识重试。 - 买家限购以“同一活动下当前有效占用量”作为唯一并发事实;待支付与已支付订单都占用名额,只有取消成功才释放。 - 事务保持短小,只处理单个活动和本次订单;事务内不调用外部 HTTP、不等待用户输入、不发送即时消息、不做长计算或全表扫描。 - 秒杀价、商品归属、订单金额和快照都由服务端重读并计算;客户端价格只能用于展示,不能决定成交金额。 @@ -232,7 +233,7 @@ flowchart TD - **M02 Catalog / M06-01 商家运营**:提供商品归属、销售状态、普通库存和当前价格;发布时完成普通库存到秒杀库存的原子划拨,之后两个通道互不混用。 - **M03 Cart**:秒杀立即抢购绕过购物车,成功、失败、取消和回补均不读写购物车条目。 -- **M04 Ordering**:C01 在同一原子边界调用其统一订单创建能力;M04 生成共享 `PendingPayment` 订单、唯一启用的默认指定商家、固定支付截止时间、地址与订单项快照,并保留秒杀来源、活动和成交价,供查询、取消与追溯。C01 不建立第二套订单创建路径。 +- **M01 Identity / M04 Ordering**:C01 在同一原子边界调用 M04 统一订单创建能力;M04 从 M01 解析唯一启用的默认商家并写入 `assignedMerchantUserId`,生成共享 `PendingPayment` 订单、固定支付截止时间、地址与订单项快照,并保留秒杀来源、活动和成交价,供查询、取消与追溯。活动创建人不替代履约商家,C01 不建立第二套订单创建路径。 - **M05 Payment / C08 回调**:秒杀订单沿用统一支付和幂等回写;支付成功与取消竞争由订单状态条件推进裁决。 - **C03 超时取消**:只触发 M04 公开取消入口,由订单来源决定回补普通库存还是原秒杀库存,不得直接改写 C01 数据。 - **M06-02 履约、M09 消息、M10 售后**:分别沿用发货、可靠消息和售后流程,不为秒杀建立第二套通道。 @@ -253,7 +254,7 @@ flowchart TD | 商家活动详情 | A225 | 返回本人活动、库存与订单汇总;越权不泄露存在性 | 待重建详细契约 | | 买家公开活动列表 | A226 | 返回 `Published` / `Ongoing` 活动及需求规定的完整展示信息 | 待重建详细契约 | | 买家公开活动详情 | A227 | 返回权威倒计时、剩余库存、已售数量及当前买家限购提示 | 待重建详细契约 | -| 买家秒杀下单 | A228 | 稳定请求重放、过载保护、条件扣减、限购占用,并由 M04 统一生成共享订单、指定商家、固定支付截止时间与快照 | 待重建详细契约 | +| 买家秒杀下单 | A228 | 稳定请求重放、过载保护、条件扣减、限购占用,并由 M04 统一生成共享订单、唯一启用的默认 `assignedMerchantUserId`、固定支付截止时间与快照 | 待重建详细契约 | | 秒杀订单查询 | 复用 A302 / A303 | 按买家归属查询共享订单和秒杀追溯信息 | 由 M04 契约承载 | | 取消与秒杀回补 | 复用 M04 公开取消契约 | 首次成功取消时按原通道回补并释放限购 | 由 M04 / C03 契约承载 | @@ -267,7 +268,7 @@ HTTP 路径、方法、请求响应字段、状态码、错误码、幂等标识 4. 数据库必须表达活动状态、计划量、已划拨总量、剩余量、已售量、每名买家当前占用量、共享订单追溯信息、稳定请求结果和取消是否已回补;具体表名与字段在统一数据库设计中确定。 5. 活动期间必须满足“剩余量 + 已售量 = 已划拨总量”;本期不引入冻结量。取消成功时剩余量增加、已售量减少,二者仍保持恒等。 6. 同一活动的买家当前占用量不得超过单用户限购;同一订单最多释放一次,同一稳定请求最多形成一个确定订单结果。 -7. 共享订单必须由 M04 统一生成指定商家、固定支付截止时间和快照,能区分普通购买与秒杀购买,并能追溯原活动、成交价和原库存通道;取消时不得依赖客户端告诉系统回补到哪里。 +7. 共享订单必须由 M04 统一解析唯一启用的默认 `assignedMerchantUserId`、生成固定支付截止时间和快照,能区分普通购买与秒杀购买,并能追溯原活动、成交价和原库存通道;活动创建人不决定履约归属,取消时不得依赖客户端告诉系统回补到哪里。 8. 活动结束或取消后的剩余量继续归属原活动且不可售,不自动并回普通库存;数据库设计不能把这部分库存丢失或误计为普通可售。 9. 公开库存展示必须从权威库存事实派生;接口需提供足够的结果顺序或版本信息,保证旧刷新结果不能覆盖新结果。 10. 接口与数据库完成后,必须回到本文逐项验证动作、状态、异常和原子结果;若实现成本暴露设计缺口,记录缺口并修正下游设计,不能擅自改变已确认业务语义。 -- Gitee From 9466181b906062c6c04caae40e20789001ad2915 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 22:09:24 +0800 Subject: [PATCH 105/118] =?UTF-8?q?docs(process):=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E5=85=A8=E9=87=8F=E6=B5=81=E7=A8=8B=E7=BB=9F=E7=A8=BF=EF=BC=9B?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E8=B7=A8=E6=A8=A1=E5=9D=97=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E4=B8=8E=E6=9E=B6=E6=9E=84=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../process/README.md" | 40 ++- ...01\347\250\213\350\256\276\350\256\241.md" | 338 ++++++++++++------ ...66\346\236\204\350\256\276\350\256\241.md" | 72 ++-- 3 files changed, 297 insertions(+), 153 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/README.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/README.md" index e2d10e1..6dad5ff 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/README.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/README.md" @@ -21,8 +21,8 @@ process/ 维护边界: - [`业务流程设计.md`](业务流程设计.md) 只保留全局核心链路、公共状态、跨模块直接交接、文档索引和成熟度,不长期重复保存个人模块的完整细节。 -- 每位负责人只在本人目录维护文档;一个业务模块或挑战项对应一份文档。 -- 统稿人只建立目录、文件名、空模板、核心基线和交接约束,不代替负责人填写模块主流程、状态转换或异常细节。 +- 每位负责人日常只在本人目录维护文档;一个业务模块或挑战项对应一份文档。 +- 统稿人负责跨模块整合、一致性审计和总基线维护;发现会阻断全链路的缺漏或冲突时,可以在保留负责人和修订痕迹的前提下修正模块文档,再同步根文档和下游设计。 - 个人模块文档完成并通过交叉评审后,根文档中的同类详细图改为链接;迁移期间不得同时修改两份相同流程。 - 文件名使用“稳定模块编号 + 中文名称 + 流程”,例如 `M04-订单流程.md`、`C03-订单超时流程.md`。 @@ -63,14 +63,14 @@ C08 退款对账 → X04 售后退款 → F09/F10 的订单项和支付事实 | 负责人 | 目录 | 核心模块文档 | 扩展/挑战文档 | 主要联调人 | |---|---|---|---|---| -| 唐宇昊 | `tyh/` | [`M01-01-用户注册流程.md`](tyh/M01-01-用户注册流程.md)、[`M01-02-用户登录与退出流程.md`](tyh/M01-02-用户登录与退出流程.md)、[`M01-03-个人信息与收货地址流程.md`](tyh/M01-03-个人信息与收货地址流程.md)、`M06-03-后台用户管理流程.md` | [`M08-商品收藏与浏览历史流程.md`](tyh/M08-商品收藏与浏览历史流程.md) | 顾欣月、罗皓晨 | -| 顾欣月 | `gxy/` | `M02-分类与商品流程.md`、`M06-01-后台商品管理流程.md` | `M07-商品评价流程.md`、`C04-中文搜索流程.md` | 朱惠惠、韦乾强、罗皓晨 | -| 朱惠惠 | `zhh/` | `M03-购物车流程.md` | `C01-秒杀流程.md` | 顾欣月、韦乾强、张海洋 | -| 韦乾强 | `wqq/` | `M04-订单流程.md`、`M06-02-商家履约流程.md` | `C03-订单超时流程.md` | 朱惠惠、张海洋、罗皓晨 | -| 张海洋 | `zhy/` | [`M05-支付流程.md`](zhy/M05-支付流程.md)、`M10-售后流程.md` | `C08-支付回调与对账流程.md` | 韦乾强、罗皓晨 | +| 唐宇昊 | `tyh/` | [`M01-01-用户注册流程.md`](tyh/M01-01-用户注册流程.md)、[`M01-02-用户登录与退出流程.md`](tyh/M01-02-用户登录与退出流程.md)、[`M01-03-个人信息与收货地址流程.md`](tyh/M01-03-个人信息与收货地址流程.md)、[`M06-03-后台用户管理流程.md`](tyh/M06-03-后台用户管理流程.md) | [`M08-商品收藏与浏览历史流程.md`](tyh/M08-商品收藏与浏览历史流程.md) | 顾欣月、罗皓晨 | +| 顾欣月 | `gxy/` | [`M02-分类与商品流程.md`](gxy/M02-分类与商品流程.md)、[`M06-01-后台商品管理流程.md`](gxy/M06-01-后台商品管理流程.md) | [`M07-商品评价流程.md`](gxy/M07-商品评价流程.md)、[`C04-中文搜索流程.md`](gxy/C04-中文搜索流程.md) | 朱惠惠、韦乾强、罗皓晨 | +| 朱惠惠 | `zhh/` | [`M03-购物车流程.md`](zhh/M03-购物车流程.md) | [`C01-秒杀流程.md`](zhh/C01-秒杀流程.md) | 顾欣月、韦乾强、张海洋 | +| 韦乾强 | `wqq/` | [`M04-订单流程.md`](wqq/M04-订单流程.md)、[`M06-02-商家履约流程.md`](wqq/M06-02-商家履约流程.md) | [`C03-订单超时流程.md`](wqq/C03-订单超时流程.md) | 朱惠惠、张海洋、罗皓晨 | +| 张海洋 | `zhy/` | [`M05-支付流程.md`](zhy/M05-支付流程.md)、[`M10-售后流程.md`](zhy/M10-售后流程.md) | [`C08-支付回调与对账流程.md`](zhy/C08-支付回调与对账流程.md) | 韦乾强、罗皓晨 | | 罗皓晨 | `lhc/` | [`M09-站内消息流程.md`](lhc/M09-站内消息流程.md) | [`C06-实时推送流程.md`](lhc/C06-实时推送流程.md)、[`C07-缓存流程.md`](lhc/C07-缓存流程.md)、[`C10-高可用流程.md`](lhc/C10-高可用流程.md) | 各相关业务负责人 | -以上只规定目录、文件名、负责人和联调关系,不代表统稿人已经替负责人完成流程内容。跨模块流程不能由单方标记为“已确认”。 +以上 21 份模块流程均已形成完整定义并纳入 [`业务流程设计.md`](业务流程设计.md) v1.0 统稿基线;负责人和联调关系不因此改变。“完整定义/已校准”表示文档已可供下游设计使用,不等于全体成员已经完成正式确认,也不等于接口、数据库、实现或测试已经完成。 ## 五、每张流程图必须包含什么 @@ -171,15 +171,27 @@ stateDiagram-v2 ## 七、成熟度怎么填写 +流程文档同时记录“内容完整度”和“评审/交付状态”,不能用一个词混淆两件事。 + +内容完整度: + +| 状态 | 使用条件 | +|---|---| +| 缺失 | 尚无对应模块流程文档 | +| 模板/占位 | 只有标题、模板或登记项,不能派生完整下游契约 | +| 部分定义 | 已有主流程,但缺关键状态、异常、原子结果或模块交接 | +| 完整定义 | 主流程、异常、状态、权限、原子结果、直接上下游和不可变事实齐全,可作为接口、数据库和架构设计输入 | + +评审/交付状态: + | 状态 | 使用条件 | |---|---| -| 待细化 | 只有登记项,还没有完整流程图 | -| 初稿 | 已覆盖主流程和主要异常,但负责人尚未完成自查 | -| 待交叉评审 | 负责人已对照需求自查,等待关联模块确认边界 | -| 已确认 | 主责人、直接协作人和所依赖的核心 F 负责人均已确认,需求文字、状态和出入口一致 | +| 待自审 | 负责人尚未完成需求反查 | +| 待交叉评审 | 已完成自审,等待直接上下游确认边界 | +| 已校准 | 已按需求、教师基线和关联流程完成统稿一致性审计,可进入下游设计;不冒充正式团队签字 | +| 已确认/已冻结 | 用户或主责人与直接协作人已经明确确认,且没有未关闭的业务语义阻断项 | -成熟度只表示“流程文档的确认程度”,不代表接口、代码或测试已经完成。 -基础 F 未确认时,依赖它的扩展流程不得标记为“已确认”。 +成熟度只描述流程文档,不代表接口、数据库、代码或测试已经完成。基础 F 未形成完整定义时,依赖它的扩展流程不得越级标记为“已确认/已冻结”;下游交付状态必须在各自事实源中单独记录。 ## 八、提交前检查 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" index 30c96b4..703cc09 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" @@ -2,9 +2,9 @@ > 组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 > -> 编写日期:2026-07-24 版本:v0.2 +> 编写日期:2026-07-24 版本:v1.0 > -> 当前状态:部分定义;F01~F13 已对照总需求和教师验收完成基线校准,仍待各主责人交叉评审;X03、C06、C07、C10 已形成个人流程初稿,仍待主责自审与直接协作人交叉评审;唐宇昊(tyh)新增 F01/F02/F03/F13/X02 个人流程初稿,待主责自审与 M00/Ordering/Catalog 评审 +> 当前状态:完整定义,已完成统稿校准;F01~F13、X01~X04 及已选 C01/C03/C04/C06/C07/C08/C10 均有模块流程承接,可作为接口、数据库和架构设计输入;正式团队确认、实现和测试状态分别在对应事实源中记录 ## 修订记录 @@ -13,6 +13,7 @@ | v0.1 | 2026-07-24 | 全体成员(罗皓晨统稿) | 建立集中式业务流程设计,覆盖核心主链路,并对照 F01~F13 需求与验收校准状态、模块交接、X/C 扩展点和核心结果保护规则 | | v0.2 | 2026-07-24 | 罗皓晨 | 补充 M09、C06、C07、C10 个人流程入口,新增消息、缓存和单 API 实例故障的直接交接图,并更新扩展流程成熟度 | | v0.3 | 2026-07-24 | 唐宇昊 | 在 tyh/ 新增 M01-01、M01-02、M01-03、M06-03、M08 五份个人流程文档,登记 F01/F02/F03/F13 和 X02 追踪矩阵链接 | +| v1.0 | 2026-07-24 | 全体成员(罗皓晨统稿) | 汇总 21 份模块流程,统一唯一履约商家、固定支付截止时间、售后履约竞争、消息接收人、缓存一致性、支付回调对账和 C10 运行边界,关闭流程层阻断项 | ## 一、文档定位与事实来源 @@ -168,11 +169,15 @@ flowchart LR M03 -->|"本人选中条目与数量
不传最终金额"| M04 M04 -->|"订单号、归属、应付金额
状态 PendingPayment"| M05 M05 -->|"确定支付记录
条件推进 PendingPayment → Paid"| M04 - M04 -->|"本期平台经营范围内 Paid 订单与履约快照"| M06O["M06-02 商家履约入口"] + M04 -->|"assignedMerchantUserId 匹配的 Paid 订单与履约快照"| M06O["M06-02 商家履约入口"] M06O -->|"条件推进 Paid → Shipped"| M04 + M04 -->|"订单项、实付快照与履约状态"| M10["M10 AfterSales"] + M10 -->|"处理中申请、已退款数量与剩余可履约数量"| M06O + M10 -->|"幂等退款命令"| M05 - M04 -. "事务提交后的订单事实" .-> EXT["X03/C03 等扩展入口"] + M04 -. "事务提交后的订单事实" .-> EXT["M09/C03 等扩展入口"] M05 -. "事务提交后的支付事实" .-> EXT + M10 -. "事务提交后的售后事实" .-> EXT ``` | 来源模块 | 直接入口数据或命令 | 目标模块 | 直接出口结果 | 边界约束 | @@ -184,7 +189,10 @@ flowchart LR | M03 Cart | 本人选中条目 ID 与数量 | M04 Ordering | 下单成功后清理结果;失败时原状保留 | 购物车金额仅供预览,M04 必须重读商品事实并重新计价 | | M04 Ordering | 本人订单号、归属、持久化应付金额、`PendingPayment` | M05 Payment | 支付准入或当前最终订单状态 | M05 不接受客户端传入最终金额 | | M05 Payment | 幂等支付命令和确定支付事实 | M04 Ordering | `PendingPayment → Paid` 的唯一条件更新结果 | 扣款、记录和状态必须形成一个原子结果 | -| M04 Ordering | 本期平台统一经营范围内的 `Paid` 订单和必要履约快照 | M06-02 | `Paid → Shipped` 结果 | 本期不按店铺/商家拆单;商家不能修改金额、支付事实、地址或订单项快照 | +| M04 Ordering | `assignedMerchantUserId` 等于当前商家的 `Paid` 订单和必要履约快照 | M06-02 | 经售后快照复核后的 `Paid → Shipped` 结果 | 本期不建设店铺、拆单或结算,但每单仍唯一归属一个履约商家;商家不能修改金额、支付事实、地址或订单项快照 | +| M04 Ordering | 订单项归属、实付快照、履约状态与完成时间 | M10 AfterSales | 售后资格、申请占用与退款结果 | M10 不覆盖订单核心状态,也不直接修改订单、支付或库存内部数据 | +| M10 AfterSales | 非终态申请、已退款数量和剩余可履约数量 | M06-02 | 允许、部分允许或阻断发货 | 发货与售后在同一订单事实边界串行化,不能由页面缓存决定 | +| M10 AfterSales | 已确认退款金额、买家和幂等标识 | M05 Payment | 唯一退款流水与钱包入账结果 | M10 不直接修改钱包;退款失败保留可重试状态 | | M06-01 | 分类、商品、上下架维护命令 | M02 Catalog | 最新商品销售状态 | 后台入口不拥有第二份商品事实 | | M06-03 | 买家/商家账号禁用或启用命令 | M01 Identity | 最新账号状态和令牌失效结果 | 不允许修改角色或管理员账号 | @@ -348,9 +356,11 @@ flowchart TD - 新建或编辑商品不会自动上架;只有商家主动上架且完整性校验通过后,状态才进入“已上架”。 - 公开列表和搜索只返回已上架商品;下架商品的旧链接只能显示不可售状态。 - 已上架但库存为 0 的商品仍可展示详情,但必须标记售罄并禁用购买。 +- 分类停用只使该分类退出购物端分类筛选入口,不自动下架或隐藏其既有已上架商品;商品仍按自身销售状态公开和参与购买校验。 - 商品下架不删除历史订单快照、购物车、收藏或浏览历史中的关联记录,但购买入口必须失效。 - 有历史订单关联的商品不得进行破坏性删除,应使用下架表达停售。 - 本期不引入多商家商品归属模型;后台以商家 Policy 控制入口,不在流程图中自行增加店铺或租户边界。 +- 商品公开字段、销售状态或普通库存变更提交后,按 C07 已确认的失效矩阵处理固定首页和商品详情缓存;缓存不参与写入判定。 ### 3.4 购物车结算与提交订单 @@ -378,15 +388,19 @@ flowchart TD I -- "否" --> J["再次校验身份、地址、购物车、商品、库存和正数总额"] J --> K{"最终校验通过?"} K -- "否" --> KX["拒绝提交并保留购物车条目"] - K -- "是" --> L["开启事务,逐项条件扣减库存"] + K -- "是" --> K1["M01 解析唯一启用的默认履约商家"] + K1 --> K2{"是否得到唯一 assignedMerchantUserId?"} + K2 -- "否" --> KX + K2 -- "是" --> L["开启事务,逐项条件扣减库存"] L --> M{"全部库存扣减成功?"} M -- "否" --> R["整体回滚:库存、订单、待发布订单创建事实和购物车均恢复原状"] - M -- "是" --> N["写唯一订单、地址和商品快照、服务端总额"] + M -- "是" --> N["写唯一订单、地址和商品快照、服务端总额、assignedMerchantUserId 与固定 paymentDeadline"] N --> O["记录待发布的订单创建事实"] O --> P["删除本次已结算购物车条目"] P --> Q["提交事务,订单状态为 PendingPayment"] - Q --> S["M04 直接输出:订单号、应付金额和 PendingPayment"] + Q --> S["M04 直接输出:订单号、应付金额、paymentDeadline 和 PendingPayment"] S --> T["进入 M05 收银台"] + Q -. "普通库存已改变" .-> CACHE["按 C07 失效受影响的固定首页和商品详情缓存"] N -. "写入失败" .-> R O -. "订单创建事实记录失败" .-> R P -. "清理失败" .-> R @@ -396,9 +410,11 @@ flowchart TD 关键说明: - 前端显示的价格和库存不能作为下单事实,提交时必须由服务端重新校验。 -- 商品价格变化只刷新服务端计价,不自动把条目标为失效;商品未上架、资源归属错误或库存不足才阻止结算。分类停用是否影响既有已上架商品购买,须由商品主责确认后再进入核心规则。 +- 商品价格变化只刷新服务端计价,不自动把条目标为失效;商品未上架、资源归属错误或库存不足才阻止结算。分类停用不影响既有已上架商品继续公开和购买。 - 购物车条目的“可结算/不可结算”是实时派生结果,提交瞬间必须再次校验。 -- 库存扣减、订单和快照、待发布订单创建事实、已结算购物车清理属于一个原子业务结果。 +- 普通订单创建时由 M01 解析唯一启用的默认商家并保存 `assignedMerchantUserId`;无法得到唯一结果时整次下单失败,不产生无人负责或多商家竞争的订单。 +- 订单创建时按当时生效的正式配置写入固定 `paymentDeadline`(本期正式值为创建后 30 分钟);后续配置变化不重算历史订单截止时间。 +- 库存扣减、订单和快照、履约商家与支付截止时间、待发布订单创建事实、已结算购物车清理属于一个原子业务结果。 - 重复提交同一幂等请求只能返回首次结果,不能重复扣库存或生成订单。 - 事务提交后的消息发布失败由架构确定的可靠机制重试,不回滚已经提交的订单。 @@ -406,7 +422,7 @@ flowchart TD > 主责:张海洋;订单协作:韦乾强 > 模块文档:[`zhy/M05-支付流程.md`](zhy/M05-支付流程.md) -> 当前成熟度:初稿,待主责自审及 Ordering 交叉评审 +> 当前成熟度:完整定义,已完成统稿校准,可作为接口设计输入 根文档只保留 F10 核心基线: @@ -414,6 +430,8 @@ flowchart TD - 原子结果:钱包扣款、钱包流水、支付记录、首次处理结果、`PendingPayment → Paid` 和待发布支付成功事实同一事务提交。 - 直接出口:M04 获得唯一 `Paid` 结果,M06-02 可查询待发货订单,M09 消费事务后的支付事实。 - F10 不新增“支付中”订单状态;支付与主动/超时取消只能有一个条件更新胜出。 +- F10 默认使用小金库 `Wallet` 同步支付;C08 使用不扣小金库的受控 `SimulatedChannel` 回调支付。一个订单只能选择一个通道,两条路径都必须在固定 `paymentDeadline` 前确认并竞争同一 `PendingPayment` 状态,不能互相补写成功。 +- 迟到、重复或乱序的 C08 回调由 C08 记录已处理结果或对账差异,不得把 `Cancelled` 或已经由另一通道支付成功的订单改成 `Paid`。 - 详细充值、支付、查询、异常、回滚、流程派生接口映射和待评审项统一在模块文档维护。 ### 3.6 订单取消、发货与完成 @@ -428,8 +446,8 @@ stateDiagram-v2 [*] --> PendingPayment: F08 下单事务提交成功 PendingPayment --> Paid: F10 本人钱包支付事务成功 PendingPayment --> Cancelled: F09 本人主动取消事务成功 - PendingPayment --> Cancelled: C03 创建满30分钟且系统自动取消成功 - Paid --> Shipped: F12 平台运营商家发货事务成功 + PendingPayment --> Cancelled: C03 到达固定 paymentDeadline 且系统自动取消成功 + Paid --> Shipped: F12 assignedMerchantUserId 对应商家发货事务成功 Shipped --> Completed: F09 订单所属买家确认收货 Shipped --> Completed: F09 发货满7天且系统自动完成 Cancelled --> [*] @@ -465,7 +483,7 @@ flowchart TD A["订单状态为 PendingPayment"] --> B{"触发来源?"} B -- "买家支付" --> P["M05 按 3.5 执行支付事务"] B -- "买家主动取消" --> C{"已登录买家且订单属于本人?"} - B -- "C03 系统超时检查" --> D{"创建满30分钟且仍为 PendingPayment?"} + B -- "C03 系统超时检查" --> D{"数据库当前时间已到固定 paymentDeadline 且仍为 PendingPayment?"} C -- "否" --> X["拒绝操作"] C -- "是" --> E["开启取消事务并条件推进 PendingPayment → Cancelled"] D -- "否" --> Y["跳过本次任务"] @@ -486,14 +504,20 @@ flowchart TD ```mermaid flowchart TD - ID["M01 直接输入:已认证且状态正常的商家"] --> A["M06-02:分页查询本期平台统一经营范围内订单并查看详情"] - ORD["M04 直接输入:Paid 订单与必要履约快照"] --> A - A --> B{"角色、授权范围和订单状态均允许发货?"} + ID["M01 直接输入:已认证且状态正常的商家"] --> A["M06-02:只查询 assignedMerchantUserId 等于本人的订单"] + ORD["M04 直接输入:订单归属、Paid 状态与必要履约快照"] --> A + A --> B{"当前商家是唯一履约责任人且订单仍为 Paid?"} B -- "否" --> X["拒绝发货,不允许修改金额、支付事实或快照"] - B -- "是且状态为 Paid" --> C["事务内条件推进 Paid → Shipped"] + B -- "是" --> LOCK["在同一订单事实边界锁定并复核最新状态"] + LOCK --> AS["通过 M10 公开能力取得售后履约快照"] + AS --> AS1{"是否存在非终态售后申请?"} + AS1 -- "是" --> X1["阻断整单发货,返回当前售后状态"] + AS1 -- "否" --> AS2{"扣除已退款数量后是否仍有可履约数量?"} + AS2 -- "否" --> X2["拒绝发货:订单已全部退款"] + AS2 -- "是" --> C["按剩余可履约数量条件推进 Paid → Shipped"] C --> C1{"状态条件更新成功?"} C1 -- "否" --> Y["返回订单当前状态,不重复发货"] - C1 -- "是" --> D["记录发货时间和待发布订单发货事实"] + C1 -- "是" --> D["记录实际发货数量、发货时间和待发布订单发货事实"] D --> E{"事务提交成功?"} E -- "否" --> Y2["整体回滚,保持原状态并允许安全重试"] E -- "是" --> F["M04 直接输出:订单状态为 Shipped,买家可查看结果"] @@ -515,11 +539,12 @@ flowchart TD 关键说明: -- 买家列表和详情只返回本人订单;本期平台运营商家共享统一经营订单的履约范围,不按店铺或商家账号拆单。 +- 买家列表和详情只返回本人订单;商家列表、详情和发货只返回 `assignedMerchantUserId` 等于当前商家的订单。本期虽不建设店铺和拆单,但不能把全部订单授权给所有商家。 - 买家支付、主动取消和 C03 超时取消竞争 `PendingPayment`,数据库状态条件决定唯一胜出结果。 - 取消、原库存通道回补和待发布订单取消事实处于同一事务,重复取消不能重复回补。 +- 发货前必须读取 M10 权威履约快照:任一非终态售后申请阻断整单发货;已退款数量从可履约数量中扣除,部分退款只发剩余数量,全部退款不允许发货。发货与售后提交在同一订单事实边界串行化。 - 商家只能条件推进 `Paid → Shipped`;买家确认与系统自动完成只能竞争 `Shipped → Completed`。 -- 正式自动完成期限为发货满 7 天;C03 的正式超时取消期限为订单创建满 30 分钟。 +- 正式自动完成期限为发货满 7 天;C03 按订单创建时已固化的 `paymentDeadline` 判断到期,正式配置值为创建后 30 分钟。 - X01 评价和 X04 售后只能从已确认的订单状态接入,不得反向覆盖核心订单状态。 ### 3.7 后台角色与操作边界 @@ -545,10 +570,18 @@ flowchart TD I --> J{"目标账号和操作是否合法?"} J -- "否" --> Z["拒绝管理员账号、角色修改或其他越权操作"] J -- "是" --> K{"禁用还是启用?"} - K -- "禁用" --> L["M01 将账号状态改为禁用并撤销旧令牌"] - K -- "启用" --> M["M01 将账号状态恢复正常"] - L --> N["目标无法登录,禁用前令牌不能访问受保护资源"] - M --> O["禁用前令牌不恢复,目标必须重新登录"] + K -- "启用" --> M["M01 幂等恢复正常;旧凭证仍保持失效"] + K -- "禁用" --> L{"目标角色?"} + L -- "买家" --> L1["不检查订单、支付或售后事实"] + L -- "商家" --> L2{"是否为唯一默认商家?"} + L2 -- "是" --> Z1["拒绝禁用默认商家"] + L2 -- "否" --> L3["查询待支付、履约、售后窗口/申请和秒杀责任"] + L3 --> L4{"存在任一未结束责任?"} + L4 -- "是" --> Z2["拒绝禁用并返回稳定阻断原因"] + L4 -- "否" --> L5["M01 原子禁用、撤销全部旧凭证并记录追踪结果"] + L1 --> L5 + L5 --> N["目标无法登录,禁用前凭证不能访问受保护资源"] + M --> O["目标必须主动重新登录"] ``` 关键说明: @@ -558,6 +591,10 @@ flowchart TD - 管理员账号治理不提供角色修改、提权或普通用户私人资料编辑。 - 后台页面隐藏入口不能替代服务端 Policy 和资源归属校验。 - 重复禁用或启用按当前状态幂等返回;令牌失效结果无法确认时,禁用不得返回虚假成功。 +- 买家禁用只改变账号和凭证可用性,不检查、取消或改写其既有订单、支付、售后、消息等业务事实。 +- 唯一默认商家始终不能禁用。非默认商家存在以下任一责任时也不能禁用:`PendingPayment` 订单;仍有可履约数量的 `Paid` 订单;`Shipped` 订单;处于完成后 7 天售后窗口的 `Completed` 订单;任一非终态售后申请;任一未结束秒杀活动。 +- `Paid` 订单若已全部退款、没有剩余可履约数量且没有非终态售后申请,不构成永久阻断;商家禁用检查和新责任写入必须通过唯一订单事实串行化,不能在检查后写入新的责任。 +- 禁用成功必须形成“账号状态、全部旧凭证失效、追踪记录和幂等结果”的一个确定结果;重新启用不会恢复旧凭证。 - 本期不定义管理员创建商家或修改角色流程,商家账号继续由已确认的受控初始化方式提供。 ### 3.8 核心模块直接出入口详图 @@ -585,12 +622,12 @@ flowchart LR ```mermaid flowchart LR A["M03 Cart
本人选中条目"] -->|"选中条目与数量"| B["M04 Ordering
服务端重读并计价"] - B -->|"事务成功"| C["订单 PendingPayment
库存已扣、快照已保存、购物车已清理"] + B -->|"事务成功"| C["订单 PendingPayment
库存已扣、快照/assignedMerchantUserId/paymentDeadline 已保存、购物车已清理"] B -->|"事务失败"| X["库存与购物车保持原结果
不产生部分订单"] - C -->|"订单号、归属、应付金额"| D["M05 Payment"] + C -->|"订单号、归属、应付金额、支付通道与固定截止时间"| D["M05 Payment"] D -->|"支付事务成功"| E["支付记录已落库
订单 Paid"] D -->|"余额不足或状态竞争失败"| F["订单保持当前最终状态
钱包不产生部分扣款"] - E -->|"Paid 订单"| G["M06-02 待发货入口"] + E -->|"assignedMerchantUserId 对应的 Paid 订单"| G["M06-02 待发货入口"] C -->|"买家主动取消"| H["M04 取消事务"] H -->|"取消成功"| I["订单 Cancelled
原库存通道已回补"] H -->|"状态竞争或事务失败"| J["返回当前订单状态
库存不产生部分回补"] @@ -600,9 +637,11 @@ flowchart LR ```mermaid flowchart LR - A["M04 Ordering
Paid 订单与履约快照"] -->|"本期平台统一经营范围内查询"| B["M06-02 商家履约"] - B -->|"合法发货命令"| C["M04 条件推进 Paid → Shipped"] - B -->|"越权或状态非法"| X["拒绝发货并返回当前状态"] + A["M04 Ordering
Paid 订单、assignedMerchantUserId 与履约快照"] -->|"只允许责任商家查询"| B["M06-02 商家履约"] + B -->|"锁定同一订单事实并请求售后快照"| AS["M10 AfterSales"] + AS -->|"存在非终态申请或已全部退款"| X["拒绝发货并返回稳定原因"] + AS -->|"无非终态申请且仍有剩余可履约数量"| C["M04 按剩余数量条件推进 Paid → Shipped"] + B -->|"越权或状态非法"| X C -->|"状态竞争或事务失败"| X C -->|"Shipped 详情"| D["订单所属买家"] D -->|"主动确认收货"| E["M04 条件推进 Shipped → Completed"] @@ -626,6 +665,9 @@ flowchart LR C -->|"最新销售状态"| F["F04~F06 购物端"] D -->|"Shipped/Completed"| G["F09 买家订单"] E -->|"正常/禁用"| H["F02 登录与全部受保护入口"] + E -->|"禁用非默认商家前查询责任"| R["M04/M10/C01
订单履约、售后窗口/申请与未结束活动"] + R -->|"存在责任"| J["拒绝禁用"] + R -->|"无责任且未并发写入新责任"| E ``` #### 3.8.5 核心业务事实、M09 与 C06 的交接 @@ -633,47 +675,64 @@ flowchart LR ```mermaid flowchart LR ID["M01 Identity
买家/商家身份和账号状态"] -->|"本人消息查询与已读操作鉴权"| MSG["M09 Messaging"] - ID -->|"实时连接鉴权"| RT["C06 实时推送"] - ORD["M04 Ordering
F08/F09/F12 已提交事实"] -->|"事件标识、业务标识和明确接收人"| MSG - PAY["M05 Payment
F10 已提交支付事实"] -->|"买家与指定商家接收人"| MSG - AFTER["M10 AfterSales
X04 已提交事实(可选来源)"] -. "待 X04 交叉评审" .-> MSG - - MSG -->|"首次处理"| STORED["本人未读消息已持久化"] - MSG -->|"重复事件"| EXISTING["返回既有结果
不新增消息或未读数"] - MSG -->|"事件非法"| REJECT["拒绝并记录/告警
不自动重试无效事件"] - MSG -->|"临时处理或事务失败"| RETRY["记录失败并等待可靠重试
不改变来源业务结果"] - STORED -. "提交后旁路" .-> RT - RT -->|"在线连接可用"| ONLINE["本人全部在线连接收到轻提示"] - RT -->|"断线或推送失败"| QUERY["通过 M09 列表与未读数补查"] + ID -->|"WebSocket 建连及存续期间鉴权"| RT["C06 实时推送"] + ORD["M04 Ordering
F08/F09/F12 已提交事实"] --> MATRIX["按事件类型解析固定接收人集合"] + PAY["M05 Payment
F10 已提交支付事实"] --> MATRIX + AFTER["M10 AfterSales
X04 已提交事实"] --> MATRIX + + MATRIX --> VALID{"全部必需接收人都存在、角色正确且归属匹配?"} + VALID -- "否" --> REJECT["整事件零消息并告警
不得只写部分接收人"] + VALID -- "是" --> MSG + MSG -->|"首次处理"| STORED["Inbox 结果与全部接收人消息同事务提交"] + MSG -->|"重复事件"| EXISTING["返回既有整事件结果
不新增消息或未读数"] + MSG -->|"临时处理或事务失败"| RETRY["可靠重试
不改变来源业务结果"] + STORED -. "提交后轻提示" .-> RT + RT -->|"在线连接可用"| ONLINE["本人全部在线连接收到消息标识"] + RT -->|"断线、Redis 降级或推送失败"| QUERY["通过 M09 权威未读数、列表与详情补查"] ONLINE --> QUERY STORED -->|"用户按需打开安全操作入口"| TARGET["M04/M05/M10 重新校验资源权限"] ``` +固定接收人矩阵: + +| 已提交业务事实 | 必需接收人 | +|---|---| +| 订单创建、取消、发货、完成 | 订单买家 | +| 支付成功 | 订单买家 + 订单 `assignedMerchantUserId` | +| 售后申请、买家提交退货说明 | 订单 `assignedMerchantUserId` | +| 售后审核结果、退款成功、确定退款失败 | 订单买家 | +| 支付失败、被忽略回调、回调差异、对账差异及管理员对账处置 | 不生成 M09 消息 | + 交接约束: -- Ordering、Payment 和 AfterSales 只能提交已经完成事务的确定事实,并明确买家或订单指定处理商家;不得按角色全量广播私人消息。 -- M09 完成校验、幂等判断和消息持久化后才能进入 C06;来源模块不得直接向客户端广播未落库的成功事实。 -- C06 失败只影响实时到达,M09 消息、未读状态和来源核心事务均不改变。 -- 消息中的业务入口不继承永久权限,进入目标模块时必须重新校验当前身份、归属和状态。 +- Ordering、Payment 和 AfterSales 只能提交已经完成事务的确定事实;事件类型决定固定接收人,调用方不能把 `recipients[]` 当作任意广播名单。 +- 任一必需接收人不存在、角色错误或订单归属不匹配时,整事件零消息并告警;账号已禁用但身份与归属仍有效时消息仍持久化,查询、已读和实时连接在禁用期间被拒绝,重新启用后可见。 +- M09 完成整事件校验、幂等判断和全部消息持久化后才能进入 C06;来源模块不得直接向客户端广播未落库的成功事实。 +- C06 当前 PC Web 只使用 WebSockets 并跳过协商,不启用 SSE 或长轮询。重连顺序固定为立即、2 秒、5 秒、10 秒,四次失败后暂停并保留 HTTP 补查。 +- C06 事件只是“有新消息”的轻提示;客户端收到后以 M09 权威未读数校正角标,按需查询列表或详情,不能直接执行 `badge + 1`。 +- “全部标记已读”在事务开始捕获本人已提交消息的稳定高水位,只更新该高水位及以前的未读消息;并发新消息保持未读,不使用客户端时间或墙上时钟界定批次。 +- 消息历史正文始终可读;目标资源已删除、不可用或当前权限无法确认时,操作入口为空并显示“目标暂不可用”。进入目标模块时仍须重新校验当前身份、归属和状态。 +- 主动退出、JWT 到期、手机号修改、账号禁用或无法确认撤销/账号状态时,既有实时连接必须关闭;Redis 恢复不重放历史推送,由 M09 查询补偿。 #### 3.8.6 Catalog、C07 与购物端读取的交接 ```mermaid flowchart LR - READ["F04/F06 公开商品查询"] --> CACHE["C07 Cache-Aside"] + HOME["A102 固定首页
无筛选、第一页12条、createdAt/productId 倒序"] --> CACHE["C07 Cache-Aside"] + DETAIL["A103 商品自身公开详情"] --> CACHE + DIRECT["其他列表/分类/搜索/筛选、M07 评分评价、秒杀活动事实"] --> CAT["M02 Catalog / PostgreSQL"] CACHE -->|"有效命中"| RESPONSE["返回原公开商品响应"] - CACHE -->|"未命中、损坏或 Redis 降级"| CAT["M02 Catalog / PostgreSQL"] + CACHE -->|"未命中或损坏"| FILL{"取得跨实例唯一回填资格?"} + FILL -- "是" --> CAT + FILL -- "否" --> WAIT["最多等待 500 ms"] + WAIT -->|"仍未命中"| CAT + CACHE -->|"Redis 降级"| CAT CAT -->|"已上架商品事实"| RESPONSE - CAT -. "有限 TTL 回填;写入失败只影响性能" .-> CACHE + CAT -. "仅唯一回填者且查询在2秒窗口内完成时回填
正常60秒,空结果10秒" .-> CACHE - WRITE["F11 商品变更"] -->|"Catalog 事务提交成功"| INVALIDATE["失效详情和受影响的固定首页缓存"] - STOCK["F08 扣减 / F09 回补"] -->|"通过 Catalog 改变库存事实"| INVALIDATE + WRITE["商品公开字段、销售状态或普通库存变更"] -->|"事务提交成功"| INVALIDATE["立即失效详情和受影响的固定首页缓存"] + INVALIDATE --> DELAY["提交后第3秒再次失效相同 Key"] WRITE -->|"事务回滚"| KEEP["不产生成功失效结果"] - INVALIDATE --> RESULT{"本次失效是否成功?"} - RESULT -->|"删除失败"| RETRY["受控重试并由有限 TTL 约束旧值窗口"] - RESULT -->|"成功"| WINDOW{"是否存在并发旧查询
在失效后回填?"} - WINDOW -->|"否"| MISS["后续读取未命中并从 PostgreSQL 回填"] - WINDOW -->|"是"| PROTECT["按待确认的二次失效、版本或等价最小机制纠正"] ORDER["F08 提交订单"] -->|"重读销售状态、价格和库存"| CAT CACHE -. "不得作为交易事实" .-> ORDER @@ -681,36 +740,96 @@ flowchart LR 交接约束: -- C07 只缓存游客与买家共同可见的公开字段;个人、商家管理和管理员字段不复用公共 Key。 -- 商品事务提交后才触发失效;事务回滚不得生成新的成功失效结果。 -- F08 扣减、F09 回补及后续 C01 库存划拨的具体失效范围由 Catalog、Ordering 和相关负责人交叉评审。 -- Redis 故障回退 PostgreSQL;缓存只能影响性能和约定的一致性窗口,不能改变权限、价格、库存或上下架事实。 +- 固定首页仅指 A102 无用户筛选、第一页 12 条、`OnSale`、`createdAt DESC, productId DESC` 的公开摘要;普通库存为 0 的商品仍展示为售罄。A103 只缓存商品自身公开字段,不缓存 M07 评分或评价。 +- 其他 A102 列表、分类、关键词、价格/库存筛选、排序,M07 评分评价和 C01 秒杀活动事实全部直读 PostgreSQL,不得以接口实现方便反向扩大缓存范围。 +- 跨实例只允许一个回填者;其他请求最多等待 500 ms,仍未命中就直读 PostgreSQL且不竞争回填。取得资格的查询只有在 2 秒有效窗口内完成才可回填。 +- 正常结果 TTL 为 60 秒,空结果 TTL 为 10 秒;事务提交后立即删除并在第 3 秒二次删除。若两次删除都失败,计入最长 2 秒有效回填窗口后,正常旧值最迟 62 秒、空结果最迟 12 秒自然消失。 +- 触发详情和受影响固定首页失效的事实包括:名称、价格、普通库存、描述、分类展示、图片/主图/排序、销售状态和删除;普通订单扣减/取消回补;M10 普通库存售后回补;C01 发布时普通库存划转为秒杀配额。 +- C01 活动内部秒杀库存变化、原活动库存取消/售后回补以及 M07 评价和评分变化不触发 C07;它们本来不属于本期缓存响应。 +- 事务提交后才触发失效,回滚不触发;Redis 故障时所有公开读取回退 PostgreSQL。F08、C01、M10 等交易动作始终重读并修改 PostgreSQL 事实,缓存只影响性能。 #### 3.8.7 C10 统一入口与单 API 实例故障交接 ```mermaid -flowchart LR - WEB["PC Web 浏览器"] -->|"唯一公开地址"| NGINX["Nginx 统一入口"] - NGINX -->|"只向可接收流量的实例转发"| API1["同版本 API 实例 1"] - NGINX -->|"只向可接收流量的实例转发"| API2["同版本 API 实例 2"] - API1 --> SHARED["共享 PostgreSQL、Redis、RabbitMQ 和对象存储"] - API2 --> SHARED - WORKER["独立 Worker"] --> SHARED - - API1 -->|"实例停止或未就绪"| HEALTH["Nginx/健康判断识别不可接收流量"] - HEALTH -->|"摘除实例 1"| SURVIVE["新请求转到就绪实例 2"] - SURVIVE -->|"安全查询"| RETRY["有限重试"] - SURVIVE -->|"提交结果未知"| VERIFY["已定义时复用原幂等标识
否则使用已确认的业务查询确认结果"] - API1 -->|"C06 连接中断"| RECONNECT["重连到存活实例并由 M09 补查"] - RECOVER["实例 1 恢复"] -->|"就绪检查通过后"| NGINX +flowchart TD + PG["PostgreSQL 可连接"] --> MIG["一次性 Migrator
唯一 Migration 执行者"] + MIG -->|"目标 Migration 成功"| APPS["同版本 API 1、API 2 与 Worker 启动"] + MIG -->|"失败或版本不兼容"| STOP["API/Worker 不进入就绪"] + APPS --> READY{"安全配置、运行版本、Migration 与 PostgreSQL 均通过?"} + READY -- "否" --> STOP + READY -- "是" --> NGINX["Nginx 只向就绪 API 转发"] + WEB["PC Web"] --> NGINX + + NGINX --> API1["API 实例 1"] + NGINX --> API2["API 实例 2"] + API1 -->|"单实例停止或摘除"| API2 + API1 --> REDIS{"Redis 能力状态"} + API2 --> REDIS + REDIS -- "正常且安全事实已恢复"| CAP["缓存、实时与受保护鉴权能力按约定工作"] + REDIS -- "故障"| DEG["公开缓存回退 PostgreSQL;实时关闭;无法确认撤销事实的受保护请求失败关闭"] + + RABBIT["RabbitMQ 故障"] --> OUTBOX["Outbox 保留,投递暂停"] + OBJECT["SeaweedFS 故障"] --> UPLOAD["上传/对象写入失败,其他能力继续"] + API1 -->|"WebSocket 中断"| RECONNECT["按 C06 固定节奏重连 API 2,并由 M09 补查"] ``` 交接约束: -- 两个 API 使用一致的 JWT、Policy、账号状态和令牌失效规则;无法安全确认撤销状态时不得静默放行。 -- 查询请求可以有限重试,提交类请求不得盲目重放;结果未知时回到所属业务流程确认。 -- C06 经共享实时通道跨实例送达并允许重连;C07 在 Redis 故障时回退 PostgreSQL。 -- 本节只承诺 C10 验收中的单 API 实例故障能力,不代表 Nginx、PostgreSQL、Redis 或 RabbitMQ 已实现全链路高可用。 +- 同一版本只启动一次一次性 Migrator;API 和 Worker 不并发自动迁移。启动顺序固定为 PostgreSQL 就绪 → Migrator 成功 → API/Worker 启动并通过就绪检查。 +- 全局就绪只由安全配置、运行版本兼容、目标 Migration 匹配和 PostgreSQL 决定。Redis、RabbitMQ 和 SeaweedFS 是能力级状态,单项故障不把整个 API 误判为不可用。 +- Redis 故障时 C07 公开读取回退 PostgreSQL,C06 实时能力关闭;任何需要令牌撤销、账号禁用、手机号变更或旧凭证失效事实的受保护 HTTP/Hub 请求无法确认时均失败关闭。 +- Redis 基础连通恢复后可恢复公开缓存;受保护请求和实时连接只有在撤销事实重建完成且安全健康检查通过后才能恢复。恢复过程不得短暂放行未知旧凭证。 +- RabbitMQ 故障时已提交业务事实和 Outbox 保留、投递暂停;SeaweedFS 故障时上传和对象写入失败,其他不依赖对象写入的能力继续。 +- 查询请求可以有限重试;提交类请求不得盲目重放,结果未知时复用原幂等标识或回到所属业务权威查询确认。C06 仅使用 WebSockets 并跳过协商,不依赖会话亲和。 +- 停止单个 API 时,Nginx 先停止向目标实例转发,目标实例在有界时间内排空请求、关闭本实例 Hub、刷新日志后退出;Worker 和共享依赖继续运行。 +- 停止整个系统时,先停止全部新流量,再排空 API、关闭 Hub、让 Worker 停止领取并完成或安全释放任务、刷新遥测,最后停止应用和共享依赖;PostgreSQL、Redis、RabbitMQ 和对象存储卷必须保留。 +- 本节只承诺 C10 的双 API 分发和单 API 实例故障能力,不宣称 Nginx、PostgreSQL、Redis、RabbitMQ 或对象存储具备集群高可用或跨机房容灾。 + +#### 3.8.8 C01 下单、支付、取消、履约与缓存交接 + +```mermaid +flowchart LR + CATALOG["M02/M06-01
已上架商品与普通库存"] -->|"发布活动时原子划转独立秒杀配额"| C01["C01 Seckill"] + C01 -->|"条件扣减配额、占用限购并调用共享订单创建"| ORDER["M04 PendingPayment
保存唯一启用默认商家为 assignedMerchantUserId"] + ORDER --> PAY["M05 Wallet 或 C08 SimulatedChannel
竞争固定 paymentDeadline"] + ORDER --> CANCEL["M04 主动取消 / C03 到期取消"] + CANCEL -->|"仅回补原活动库存并释放限购"| C01 + PAY -->|"唯一 Paid"| SHIP["M06-02 责任商家履约"] + SHIP --> AFTER["M10 售后"] + CATALOG -. "发布活动的普通库存划转" .-> C07["C07 失效"] + C01 -. "活动内部库存变化不进入 C07" .-> C07 +``` + +#### 3.8.9 M10 售后、履约、退款、库存与消息交接 + +```mermaid +flowchart LR + ORDER["M04 订单项、实付快照与履约状态"] --> AFTER["M10 售后资格与独立状态机"] + AFTER -->|"非终态申请/已退款数量"| SHIP["M06-02 发货准入"] + AFTER -->|"稳定退款操作"| PAY["M05 幂等退回买家钱包"] + PAY -->|"成功"| STOCK{"原库存来源?"} + STOCK -- "普通库存" --> CAT["M02 原子回补并触发 C07 失效"] + STOCK -- "秒杀库存" --> SEC["C01 回补原活动库存
不混回普通库存"] + AFTER -. "申请、审核、退货、退款确定结果" .-> MSG["M09 固定接收人消息"] + PAY -. "退款资金事实" .-> C08["C08 每日退款对账"] +``` + +#### 3.8.10 C08 回调、订单、支付、对账与消息交接 + +```mermaid +flowchart LR + CHANNEL["受控 SimulatedChannel"] -->|"HMAC 回调、callbackId 与请求指纹"| C08["C08 幂等/乱序处理"] + C08 -->|"截止前且仍为 PendingPayment"| ATOMIC["原子写模拟通道支付事实、Paid、回调终态和 Outbox"] + C08 -->|"失败回调或成功后的迟到失败"| IGNORE["ProcessedFailure / Ignored"] + C08 -->|"金额/币种/关联不符,或迟到成功"| DIFF["Difference 来源事实"] + ATOMIC --> ORDER["M04 唯一 Paid"] + ATOMIC -. "支付成功" .-> MSG["M09 买家 + assignedMerchantUserId"] + IGNORE -. "不生成 M09" .-> MSG + DIFF -. "不生成 M09" .-> MSG + WORKER["每日对账批次"] -->|"固定 UTC 范围与水位"| RECON["聚合支付、订单、钱包退款与售后事实"] + DIFF --> RECON + RECON --> ADMIN["管理员领取、举证、受控处置并重新核验后关闭"] +``` 直接交接约束: @@ -723,20 +842,19 @@ flowchart LR | 教师编号 | 核心状态或确定结果 | 直接入口 → 直接出口 | 本文流程 | 主责人 | 当前成熟度 | |---|---|---|---|---|---| -| F01 | 创建“正常”买家账号 | 游客注册 → F02 登录 | [M01-01 用户注册流程](tyh/M01-01-用户注册流程.md)、3.1 | 唐宇昊 | 初稿,待主责自审及 M00 协作评审 | -| F02 | 有效令牌 + 服务端角色;退出后当前令牌失效 | M01 → M03/M04/M05/M06 | [M01-02 用户登录与退出流程](tyh/M01-02-用户登录与退出流程.md)、3.1、3.8.1 | 唐宇昊 | 初稿,待主责自审及 M00/C10 评审 | -| F03 | 本人资料与地址;敏感修改后令牌状态明确 | M01 Address → M04 地址快照 | [M01-03 个人信息与收货地址流程](tyh/M01-03-个人信息与收货地址流程.md)、3.2、3.8.1 | 唐宇昊 | 初稿,待主责自审及 Ordering 评审 | -| F13 | 买家/商家账号“正常 ↔ 禁用”,旧令牌结果明确 | M06-03 → M01 → 全部受保护入口 | [M06-03 后台用户管理流程](tyh/M06-03-后台用户管理流程.md)、3.1、3.7、3.8.4 | 唐宇昊 | 初稿,待主责自审及 M00/C10 评审 | -| F04 | 只返回已上架商品的分页列表 | M02 → 购物端列表 | 3.3 | 顾欣月 | 基线已校准,待主责确认 | -| F05 | 安全的关键词/组合查询结果 | 查询条件 → M02 → F04 列表 | 3.3 | 顾欣月 | 基线已校准,待主责确认 | -| F06 | 公开详情、最新价格库存和明确可售状态 | F04 列表 → M02 详情 → M03 | 3.3、3.8.1 | 顾欣月 | 基线已校准,待主责确认 | -| F07 | 本人购物车;条目可结算状态实时派生 | M01/M02 → M03 → M04 | 3.4、3.8.1 | 朱惠惠 | 基线已校准,待 Cart/Ordering 评审 | -| F08 | 唯一 `PendingPayment` 订单、快照、扣减库存和清理结果 | M01/M02/M03 → M04 → M05 | 3.4、3.8.2 | 韦乾强 | 基线已校准,待 Cart/Catalog 评审 | -| F09 | 本人订单可查;合法到达 `Cancelled` 或 `Completed` | M04/M05/系统定时任务 → M04 | 3.6、3.8.2~3.8.3 | 韦乾强 | 基线已校准,待 Payment 评审 | -| F10 | 确定支付记录且订单进入 `Paid` | M04 → M05 → M04/M06-02 | [M05 支付流程](zhy/M05-支付流程.md)、3.8.2 | 张海洋 | 初稿,待主责自审及 Ordering 交叉评审;C08 接入语义待决 | -| F11 | 商品处于草稿/未上架、已上架、已下架,或满足约束后完成删除 | M06-01 → M02 → F04~F06 | 3.3、3.7、3.8.4 | 顾欣月 | 基线已校准,待主责确认 | -| F12 | 合法 `Paid → Shipped` 并记录发货事实 | M04 → M06-02 → M04 | 3.6、3.7、3.8.3 | 韦乾强 | 基线已校准,待主责确认 | -| F13 | 买家/商家账号“正常 ↔ 禁用”,旧令牌结果明确 | M06-03 → M01 → 全部受保护入口 | 3.1、3.7、3.8.4 | 唐宇昊 | 基线已校准,待主责确认 | +| F01 | 创建 `Normal` 买家账号,不自动登录 | 游客注册 → F02 登录 | [M01-01 用户注册流程](tyh/M01-01-用户注册流程.md)、3.1 | 唐宇昊 | 完整定义,已校准;待接口、数据库、实现和测试承接 | +| F02 | 有效 JWT + 服务端角色;退出后当前 JWT 失效 | M01 → 全部受保护入口 | [M01-02 用户登录与退出流程](tyh/M01-02-用户登录与退出流程.md)、3.1、3.8.1 | 唐宇昊 | 完整定义,已校准;待接口、数据库、实现和测试承接 | +| F03 | 本人资料与地址;敏感修改后全部旧凭证失效 | M01 Address → M04 地址快照 | [M01-03 个人信息与收货地址流程](tyh/M01-03-个人信息与收货地址流程.md)、3.2、3.8.1 | 唐宇昊 | 完整定义,已校准;待接口、数据库、实现和测试承接 | +| F04 | 只返回 `OnSale` 商品的稳定分页列表 | M02 → 购物端列表 | [M02 分类与商品流程](gxy/M02-分类与商品流程.md)、3.3 | 顾欣月 | 完整定义,已校准;待接口、数据库、实现和测试承接 | +| F05 | 强制公开过滤下的安全关键词与组合查询 | 查询条件 → M02/C04 → F04 列表 | [M02 分类与商品流程](gxy/M02-分类与商品流程.md)、[C04 中文搜索流程](gxy/C04-中文搜索流程.md)、3.3 | 顾欣月 | 完整定义,已校准;待接口、数据库、实现和测试承接 | +| F06 | 商品自身公开详情、最新价格库存和明确可售状态 | F04 → M02 详情 → M03/M07/M08 | [M02 分类与商品流程](gxy/M02-分类与商品流程.md)、3.3、3.8.1 | 顾欣月 | 完整定义,已校准;待接口、数据库、实现和测试承接 | +| F07 | 本人购物车;选中与可结算状态由服务端实时派生 | M01/M02 → M03 → M04 | [M03 购物车流程](zhh/M03-购物车流程.md)、3.4、3.8.1 | 朱惠惠 | 完整定义,已校准;待接口、数据库、实现和测试承接 | +| F08 | 唯一 `PendingPayment` 订单、快照、默认商家、固定截止时间、库存扣减与购物车清理 | M01/M02/M03 → M04 → M05 | [M04 订单流程](wqq/M04-订单流程.md)、3.4、3.8.2 | 韦乾强 | 完整定义,已校准;待接口、数据库、实现和测试承接 | +| F09 | 本人订单可查;合法到达 `Cancelled` 或 `Completed` | M04/M05/系统任务 → M04 | [M04 订单流程](wqq/M04-订单流程.md)、[C03 订单超时流程](wqq/C03-订单超时流程.md)、3.6、3.8.2~3.8.3 | 韦乾强 | 完整定义,已校准;待接口、数据库、实现和测试承接 | +| F10 | `Wallet` 或 `SimulatedChannel` 形成唯一确定支付事实并推进 `Paid` | M04 → M05/C08 → M04/M06-02 | [M05 支付流程](zhy/M05-支付流程.md)、[C08 支付回调与对账流程](zhy/C08-支付回调与对账流程.md)、3.5、3.8.2、3.8.10 | 张海洋 | 完整定义,已校准;待接口、数据库、实现和测试承接 | +| F11 | 商品处于 `Draft`、`OnSale`、`OffSale`,或满足约束后完成删除 | M06-01 → M02 → F04~F06/C07 | [M06-01 后台商品管理流程](gxy/M06-01-后台商品管理流程.md)、3.3、3.7、3.8.4 | 顾欣月 | 完整定义,已校准;待接口、数据库、实现和测试承接 | +| F12 | 责任商家经售后快照复核后合法 `Paid → Shipped` | M04/M10 → M06-02 → M04 | [M06-02 商家履约流程](wqq/M06-02-商家履约流程.md)、3.6、3.7、3.8.3 | 韦乾强 | 完整定义,已校准;待接口、数据库、实现和测试承接 | +| F13 | 买家/商家账号 `Normal ↔ Disabled`,旧凭证和商家责任结果明确 | M06-03 → M01/M04/M10/C01 → 全部受保护入口 | [M06-03 后台用户管理流程](tyh/M06-03-后台用户管理流程.md)、3.1、3.7、3.8.4 | 唐宇昊 | 完整定义,已校准;待接口、数据库、实现和测试承接 | ## 五、选做与挑战流程登记 @@ -744,23 +862,23 @@ flowchart LR | 编号 | 基础核心流程 | 直接扩展入口 → 出口 | 不可变核心结果 | 主责人 | 当前状态 | |---|---|---|---|---|---| -| X01 | F09、F06 | `Completed` 订单项 → 评价记录 → F06 公开评价 | 订单保持 `Completed`;快照、商品状态、价格和库存不变;同一订单项最多一条评价 | 顾欣月 | 待细化 | -| X02 | F02、F06,展示衔接 F04 | 已登录买家查看详情 → 本人收藏/浏览记录 | 不修改商品事实;游客不产生个人记录;严格按买家隔离 | 唐宇昊 | [M08 商品收藏与浏览历史流程](tyh/M08-商品收藏与浏览历史流程.md):初稿,待主责自审及 Catalog 评审 | -| X03 | F02、F13;F08、F09、F10、F12;X04 可追加来源 | 核心事务提交事件 → 消息落库/查询/已读/离线补查 | 消息失败不回滚核心事务,也不能反向修改订单、支付或售后状态 | 罗皓晨 | [M09 站内消息流程](lhc/M09-站内消息流程.md):初稿,待主责自审;来源事件、精确商家接收账号及核心流程边界待 Ordering、Payment、AfterSales、Identity 交叉评审 | -| X04 | F09、F10、F12 | 本人 `Paid/Shipped/Completed` 订单项 → 独立售后状态 → 幂等退款 | 订单核心状态和快照不被“已退款”覆盖;退款不超实付且不重复入账 | 张海洋 | 待细化 | -| C01 | F11 + F04/F06 → F08 → F10/F09/F12 | F11 商家创建/发布活动;F06 买家进入秒杀入口 → 独立库存条件扣减 → 汇入 `PendingPayment` | 后续复用核心支付和履约;取消只回补原秒杀库存;支付成功不得回补 | 朱惠惠 | 待细化;库存划拨口径待确认 | -| C03 | F08、F10、F09 | `PendingPayment` 创建满 30 分钟 → 系统定时任务复用取消流程 → `Cancelled` | 与支付只能一个胜出;取消和原库存通道回补原子且幂等 | 韦乾强 | 部分定义 | -| C04 | F04、F05、F06 | 替换 F05 查询实现 → 返回同口径 F04 列表 → F06 详情 | 只公开已上架商品;权限、筛选口径和下单重校验不变 | 顾欣月 | 待细化 | -| C06 | F02、F13;经 X03 接入 F08/F09/F10/F12,X04 可追加来源 | X03 消息成功落库 → 实时推送/重连 → X03 补查 | 推送失败不改变消息事实和核心事务;客户端不得仅凭推送改状态 | 罗皓晨 | [C06 实时推送流程](lhc/C06-实时推送流程.md):初稿,待主责自审;连接失效规则及 C10 多实例/WebSocket 边界待评审 | -| C07 | F04、F06、F11;F08/F09 通过 Catalog 改变库存时触发失效,F08 始终重读 PostgreSQL | F04/F06 数据库读取前 Cache-Aside;商品或库存事务提交后失效 | PostgreSQL 仍是事实源;Redis 故障只影响性能;F08 不接受缓存作为交易事实 | 罗皓晨、顾欣月 | [C07 缓存流程](lhc/C07-缓存流程.md):初稿,待主责自审;TTL、主动失效、删除失败重试和库存变更触发范围待 Catalog/Ordering 评审 | -| C08 | F10、F09;退款对账关联 X04 | F10 支付确认阶段 → 回调幂等/乱序 → 稳定结果与每日对账 | 不重复扣款;`Cancelled` 收到迟到成功不得变为 `Paid`,只登记差异 | 张海洋 | 待细化;与同步 F10 的替换边界待确认 | -| C10 | F01~F13 全部横切;支撑 C06/C07 与 Worker 后台运行 | 统一入口 → 双实例分发/单 API 实例故障切换 → 同一业务结果 | API、鉴权、权限、状态机和数据库结果不变;实例切换不得重复写或越权 | 罗皓晨 | [C10 高可用流程](lhc/C10-高可用流程.md):初稿,待主责自审;当前只定义单 API 实例故障边界,鉴权一致性、各模块幂等和依赖就绪规则待全组评审 | - -当前不得直接冻结的三项: - -1. C01 必须先确认秒杀库存从普通库存划拨或冻结的口径,避免两个库存通道共同超用总库存。 -2. C08 必须确认其替换 F10 的哪个支付确认步骤,不能让同步支付和异步回调同时成为最终支付事实。 -3. C07 必须在设计与验收前确定 TTL、主动失效和失败重试的最长旧值窗口。 +| X01 | F09、F06 | 本人 `Completed` 订单项 → 提交时资格重检 → 唯一公开评价 | 订单保持 `Completed`;不修改商品状态、价格、库存,不触发 C07 或 M09 | 顾欣月 | [M07 商品评价流程](gxy/M07-商品评价流程.md):完整定义,已校准;待下游承接 | +| X02 | F02、F06,展示衔接 F04 | 已登录买家查看详情 → 本人收藏/最近 200 条浏览记录 | 不修改商品事实;游客不产生个人记录;关闭历史只阻止未来写入 | 唐宇昊 | [M08 商品收藏与浏览历史流程](tyh/M08-商品收藏与浏览历史流程.md):完整定义,已校准;待下游承接 | +| X03 | F02、F13;F08、F09、F10、F12、X04 | 固定来源事实 → 整事件消息落库 → 查询/高水位已读/离线补查 | 消息和推送失败不回滚核心事务;接收人不能由调用方任意扩张 | 罗皓晨 | [M09 站内消息流程](lhc/M09-站内消息流程.md):完整定义,接收矩阵与原子性已冻结;待下游承接 | +| X04 | F09、F10、F12 | 本人合格订单项 → 独立售后状态 → 稳定退款操作 → 原库存通道回补 | 不覆盖订单核心状态和快照;退款不超实付且不重复入账/回补 | 张海洋 | [M10 售后流程](zhy/M10-售后流程.md):完整定义,履约竞争和退款闭环已校准;待下游承接 | +| C01 | F11 + F04/F06 → F08 → F10/F09/F12 | 发布时普通库存原子划转 → 独立库存扣减 → M04 统一 `PendingPayment` | 后续复用核心支付和履约;取消/售后只回原活动库存;活动创建人不决定履约商家 | 朱惠惠 | [C01 秒杀流程](zhh/C01-秒杀流程.md):完整定义,库存归属和默认商家已冻结;待下游承接 | +| C03 | F08、F10、F09 | 到达固定 `paymentDeadline` → Worker 复用 M04 统一取消 → `Cancelled` | 与 Wallet/C08 只能一个胜出;原库存通道回补原子且幂等 | 韦乾强 | [C03 订单超时流程](wqq/C03-订单超时流程.md):完整定义,固定截止时间已冻结;待下游承接 | +| C04 | F04、F05、F06 | 同一查询入口选择高级搜索 → 失败时安全回退基础搜索 | 只公开 `OnSale`;权限、强制筛选、下单重校验和性能口径不变 | 顾欣月 | [C04 中文搜索流程](gxy/C04-中文搜索流程.md):完整定义,降级与验收口径已校准;待下游承接 | +| C06 | F02、F13;经 X03 接入核心与售后事实 | M09 消息提交 → WebSocket 轻提示/固定重连 → M09 权威补查 | 推送失败不改变消息与业务事实;未知撤销状态必须关闭连接 | 罗皓晨 | [C06 实时推送流程](lhc/C06-实时推送流程.md):完整定义,传输与连接边界已冻结;待下游承接 | +| C07 | F04、F06、F11;普通库存变更扩展至 F08/F09/C01/X04 | 固定首页/A103 Cache-Aside;提交后立即及第 3 秒失效 | PostgreSQL 是事实源;范围、60/10 秒 TTL、2 秒回填窗、500 ms 等待和 62/12 秒旧值上限固定 | 罗皓晨、顾欣月 | [C07 缓存流程](lhc/C07-缓存流程.md):完整定义,范围、参数与失效矩阵已冻结;待下游承接 | +| C08 | F10、F09;退款对账关联 X04 | 受控回调 → 四种终态 → 每日固定范围对账 → 领取/举证/复核闭环 | 不扣 Wallet;迟到成功只形成 Difference;不重复支付、退款或关闭差异 | 张海洋 | [C08 支付回调与对账流程](zhy/C08-支付回调与对账流程.md):完整定义,通道竞争与差异闭环已冻结;待下游承接 | +| C10 | F01~F13 全部横切;支撑 C06/C07 与 Worker | Migrator → 全局就绪 → 能力级降级/恢复 → 有序停止 | 状态机和数据库结果不变;未知安全事实失败关闭;单实例切换不重复写或越权 | 罗皓晨 | [C10 高可用流程](lhc/C10-高可用流程.md):完整定义,迁移、就绪、降级、恢复和停止边界已冻结;待下游承接 | + +流程层已关闭的原阻断项: + +1. C01 已确认发布时从普通库存原子划转为独立秒杀配额,取消和售后回补保留在原活动,普通与秒杀库存不会共同超用。 +2. C08 已确认是独立于 Wallet 的受控 `SimulatedChannel`,两种通道与取消竞争同一固定支付截止时间和 `PendingPayment`,不会出现双重最终支付事实。 +3. C07 已确认缓存范围、TTL、单回填者、双失效和最长旧值窗口;下游接口、架构与实现不得扩大或改写这些业务边界。 ## 六、维护与评审规则 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" index 847c3d2..1f7e58f 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" @@ -1,8 +1,8 @@ # 系统架构设计 -> 组别:24级1班第7组 编写人:罗皓晨 编写日期:2026-07-22 版本:v0.1 +> 组别:24级1班第7组 编写人:罗皓晨 编写日期:2026-07-22 版本:v0.2 > -> 文档状态:内容和需求边界已完成内部统一,待全组技术评审后冻结 +> 文档状态:已按业务流程 v1.0 完成架构承接校准,待接口、数据库、实现和全组技术评审后冻结 > 截止:第 1 周周五 ## 修订记录 @@ -10,6 +10,7 @@ | 版本 | 日期 | 修改人 | 修改说明 | |---|---|---|---| | v0.1 | 2026-07-24 | 罗皓晨 | 形成并完善系统架构,明确技术选型、分层依赖、模块边界、角色权限、事件、Worker、库存与售后履约协作及六人纵向职责 | +| v0.2 | 2026-07-24 | 罗皓晨 | 按流程 v1.0 统一默认履约商家、固定支付截止时间、售后竞争、消息矩阵、C07 参数、C08 通道和 C10 Migrator/降级/停止边界 | ## 一、架构目标与约束 @@ -319,15 +320,15 @@ sequenceDiagram participant DB as PostgreSQL FE->>API: POST /api/orders + Idempotency-Key API->>DB: 开启事务 - API->>DB: 校验地址、商品、价格和库存 + API->>DB: 校验地址、商品、价格、库存和唯一启用默认商家 API->>DB: 条件扣减库存 - API->>DB: 写订单、订单项快照和 Outbox(启用时) + API->>DB: 写订单、订单项快照、assignedMerchantUserId、固定 paymentDeadline 和 Outbox(启用时) API->>DB: 删除已结算购物车条目 API->>DB: 提交事务 API-->>FE: 201 + 订单号 ``` -任一步失败时回滚整个事务,不产生部分订单。库存扣减使用数据库条件更新或等价并发控制,不能只依赖前端库存值。 +任一步失败时回滚整个事务,不产生部分订单。库存扣减使用数据库条件更新或等价并发控制,不能只依赖前端库存值。普通订单和秒杀订单都解析同一个唯一启用默认商家;活动创建人只决定秒杀活动管理范围,不决定订单履约归属。正式支付期限在创建时按配置固化为 `paymentDeadline`(本期正式值 30 分钟),后续配置变化不重算历史订单。 ### 7.2 支付幂等 @@ -335,14 +336,16 @@ sequenceDiagram - 模拟充值写入钱包流水并原子增加余额;充值请求使用幂等键,重复请求不重复到账。 - 支付请求使用唯一支付流水号和幂等键。 - 只有待支付订单允许支付。 +- 支付还必须满足服务端权威时间早于订单固定 `paymentDeadline`;Wallet 支付、C08 模拟通道回调、买家取消和 C03 到期取消竞争同一 `PendingPayment` 条件,最多一个成功。 - 钱包余额条件扣减、钱包流水、支付记录、订单状态和 Outbox 在同一数据库事务内更新,余额不得为负。 - 重复成功请求返回原成功结果,不重复写入或重复发布事件。 +- 默认 F10 使用 `Wallet` 同步支付;C08 使用不扣钱包的受控 `SimulatedChannel`。订单选择的支付来源必须持久化并出现在支付查询中,不能由另一通道补写成功。 - 售后退款通过 Payment 的公开应用能力幂等退回原买家的小金库,并写入退款钱包流水;AfterSales 不直接修改钱包数据。 ### 7.3 订单履约与完成 - 商家只能把已支付订单推进为已发货,并记录发货时间;其他状态的发货请求必须拒绝。 -- 本期为单店 B2C,不建设多商户商品归属、拆单或结算;Ordering 为每张普通订单保存 Identity 解析的默认 `assignedMerchantUserId`,秒杀订单保存活动创建人,商家查询、发货、售后和消息接收均按该账号精确过滤。 +- 本期为单店 B2C,不建设多商户商品归属、拆单或结算;Ordering 为每张普通订单和秒杀订单都保存 Identity 解析的唯一启用默认 `assignedMerchantUserId`,活动创建人只用于活动管理。商家查询、发货、售后和消息接收均按订单指定账号精确过滤。 - Identity 对默认商家标记建立唯一约束,并由启动配置/种子数据保证存在一个启用账号;A016 不允许直接禁用默认商家,也不允许禁用仍有待履约订单、售后窗口/申请或未结束秒杀活动的其他商家。本期不做自动重新分配。 - A307 发货与 A412 提交售后都先通过 Ordering 公开应用契约在当前 PostgreSQL 事务中锁定同一 `orders` 行并复核最新履约状态,锁保持到业务写入提交;随后 A307 通过 AfterSales 公开应用契约取得售后快照。处理中申请阻断发货,已退款数量从可履约数量中扣除,部分退款只发剩余数量,全部退款不再发货;模块之间不得直接读取对方内部表。 - 买家只能确认本人已发货订单,确认成功后以状态条件把订单推进为已完成,并记录完成时间和“买家确认”方式。 @@ -358,6 +361,8 @@ sequenceDiagram - 消费者在业务事务内写入 Inbox/消费处理记录,并结合消息 ID 唯一约束或业务唯一约束防止重复处理。 - Outbox 负责可靠发布,Inbox 负责可靠消费;二者均不能替代订单、支付、消息等业务表上的最终唯一约束和状态条件。 - 第一条集成事件链确定为 `OrderPaidIntegrationEvent`:Payment 在支付事务成功后通过 Outbox 发布,Messaging 幂等消费并生成买家支付成功、商家待发货通知;不为普通 CRUD 广泛发布事件。 +- Messaging 的接收人不是来源模块可任意填写的广播数组,而是由事件类型固定解析:订单创建/取消/发货/完成给买家,支付成功给买家和 `assignedMerchantUserId`,售后申请/退货说明给指定商家,售后审核/退款成功/确定退款失败给买家。支付失败、忽略回调、回调差异、对账差异及处置不生成站内消息。 +- Messaging 在同一事务中写 Inbox 处理结果和该事件全部必需接收人的消息;任一接收人不存在、角色或归属不符时整事件零消息并告警,不能部分成功。 ### 7.5 图片存储 @@ -370,10 +375,10 @@ sequenceDiagram ### 7.6 四项选做功能 -- **评价晒图**:评价必须校验当前用户已完成订单项;创建时通过 Identity 公开应用契约取得安全展示名并保存脱敏快照,公开列表不逐条跨模块查询用户资料;评价记录与图片元数据分离,图片存 SeaweedFS。 +- **评价晒图**:评价必须在上传和正式提交时分别校验当前用户的本人 `Completed` 订单项且尚未评价;创建时通过 Identity 公开应用契约取得自动用户名的安全脱敏快照,评价、图片关联和唯一资格原子提交并立即公开。评价不触发 C07 缓存失效或 M09 消息;公开列表不逐条跨模块查询用户资料。 - **收藏/历史**:按用户隔离;收藏使用唯一约束防重,浏览历史对同一用户和商品更新最近时间。 -- **站内消息**:来源模块在事件中明确列出买家或订单处理商家接收账号,Messaging 不按角色全量广播;按消息 ID、接收用户和消息类型去重,消息先落 PostgreSQL,再由 SignalR 推送;已读状态以数据库为准,WebSocket 只负责实时性。 -- **售后流程**:建立独立售后状态机,按订单项数量计算退款;已支付、已发货或完成后 7 天内允许申请,模拟退款通过 Payment 幂等退回小金库并纳入 C08 对账,订单核心状态不因部分退款被覆盖。 +- **站内消息**:来源模块只提交已确认事件类型和业务标识,Messaging 按固定矩阵解析买家和订单指定商家;按事件 ID 做整事件幂等,全部消息同事务落 PostgreSQL 后再由 SignalR 发送轻提示。已读以消息稳定序列高水位和数据库状态为准,WebSocket 不累加权威未读数。 +- **售后流程**:建立独立售后状态机,按订单项数量占用申请资格并按实付金额退款;`Paid`、`Shipped` 或完成后 7 天内允许申请。发货前读取非终态申请和已退款数量,退款经 Payment 的稳定退款操作幂等退回小金库并纳入 C08 对账;普通库存回补触发 C07,秒杀库存回原活动,订单核心状态不因售后被覆盖。 ### 7.7 C01 秒杀与防超卖 @@ -393,13 +398,14 @@ WHERE activity_id = @id - `Mall.Worker` 按数据库时间幂等推进 `Published → Ongoing → Ended`;秒杀下单同时校验状态和时间窗口。 - 单用户限购由 `(activity_id, buyer_id)` 唯一配额事实和条件更新保证,取消成功按订单幂等释放,不以 Redis 或普通聚合查询承担并发正确性。 - 秒杀库存扣减、限购占用、Ordering 共享订单创建和必要 Outbox 写入处于同一受控事务;Seckill 不建立第二套订单状态机。 +- Ordering 在秒杀共享订单创建中仍由 Identity 解析唯一启用默认商家并保存 `assignedMerchantUserId`;活动创建人不替代订单履约商家。 - Redis 可用于活动热点读取和入口削峰,但不能成为唯一库存事实来源。 - 压测固定记录并发数、库存、成功/失败数、数据库最终库存和有效订单数,验证不超卖、不少卖。 ### 7.8 C03 订单超时自动取消 -- 创建订单时写入 `expires_at = created_at + 30 分钟`。 -- `Mall.Worker` 周期扫描已到期的待支付订单;演示环境只缩短配置值,不改变规则。 +- 创建订单时写入固定 `paymentDeadline = createdAt + 当时配置期限`,正式配置为 30 分钟;历史订单不因后续配置变化重新计算。 +- `Mall.Worker` 使用 PostgreSQL 权威时间周期扫描已到固定 `paymentDeadline` 的待支付订单;演示环境只缩短新订单的配置值,不改变规则。 - 多 Worker 使用批量领取/跳过已锁定记录或等价机制,取消时执行带 `PendingPayment` 条件的状态更新。 - 状态更新和库存回补同事务;普通订单回补 Catalog,秒杀订单回补原 Seckill 活动库存并释放限购额度。支付也必须带待支付状态条件,因此支付与取消竞争只能一方成功。 - Worker 重试安全,重复扫描不会重复回补库存。 @@ -414,41 +420,49 @@ WHERE activity_id = @id ### 7.10 C06 实时消息推送 -- ASP.NET Core SignalR 提供 WebSocket 通道,JWT 用于连接身份认证。 +- ASP.NET Core SignalR 只提供 PC Web WebSocket 通道并跳过协商,不启用 SSE、长轮询或会话亲和;JWT 用于连接身份认证。 - PostgreSQL 站内消息表保存通知事实;SignalR 推送失败不回滚订单业务,也不丢失可查询消息。 - Redis Backplane 在两个 API 实例之间传播 Hub 消息,保证用户连接落在不同实例时仍能接收。 -- 前端实现自动重连,重连后查询未读消息补偿;多标签页各自维持连接,但已读状态共享。 +- 前端重连节奏固定为立即、2 秒、5 秒、10 秒,四次失败后暂停;重连后先查询权威未读数,再按需查询列表/详情。多标签页各自维持连接,但消息和已读状态共享。 +- SignalR 只发送消息标识和必要轻提示,客户端不得用每条推送直接 `badge + 1`;A503/M09 权威未读数负责校正,Redis 恢复也不重放历史推送。 +- 建连、重连和连接存续期间都校验账号状态与撤销事实。退出、JWT 到期、手机号修改、账号禁用、全部旧凭证失效或安全事实无法确认时,关闭既有连接。 ### 7.11 C07 缓存与性能优化 -- 首页商品摘要和商品详情使用 Cache-Aside;Key 包含稳定业务版本,设置有限 TTL。 -- 读取未命中时查询 PostgreSQL 并回填 Redis;数据库始终为事实来源。 -- 商品改价、库存或上下架事务提交后删除相关缓存,并通过重试/事务后事件处理删除失败。 -- 为降低更新与回填竞争导致的旧值窗口,可结合短 TTL 和延迟二次失效;文档须说明理论最迟生效时间。 +- 只有固定首页商品摘要和商品自身公开详情使用 Cache-Aside。固定首页严格为 A102 无筛选、第一页 12 条、`OnSale`、`createdAt DESC, productId DESC`;A103 不含 M07 评价/评分。普通列表、分类、搜索/筛选、评价和秒杀活动事实全部直读 PostgreSQL。 +- 正常结果 TTL 60 秒、空结果 TTL 10 秒。跨实例只允许一个回填者;其他请求最多等待 500 ms,仍未命中就直读 PostgreSQL且不竞争回填;唯一回填者只有在 2 秒有效窗口内完成查询才可写入 Redis。 +- 商品公开字段、销售状态或普通库存事务提交后立即删除目标详情和受影响固定首页缓存,并在第 3 秒二次删除。两次删除都失败时,计入 2 秒回填窗后的最长旧值窗口为正常结果 62 秒、空结果 12 秒。 +- 失效来源固定为商品名称、价格、普通库存、描述、分类展示、图片/主图/排序、销售状态和删除,以及普通订单扣减/取消回补、C01 发布时普通库存划转、M10 普通库存回补。C01 活动内部库存与原活动回补、M07 评价变化不触发 C07。 +- Redis 故障时公开查询直接回退 PostgreSQL;数据库始终为价格、库存、上下架和商品内容事实来源,F08/C01/M10 不读缓存做交易判断。 - 压测报告对比缓存启用前后 P50/P95、吞吐量、命中率和数据库查询次数。 ### 7.12 C08 支付回调幂等与对账 -- 回调包含全局唯一回调 ID、支付流水号、订单号、结果和时间;回调 ID/流水号建立唯一约束。 -- 订单状态机拒绝迟到或逆序更新;已取消订单不会因迟到成功回调直接变为已支付,而是进入对账差异。 -- 支付记录、订单状态、Inbox/处理记录和 Outbox 在一致事务边界内处理;重复回调读取并返回已处理结果。 -- `Mall.Worker` 生成每日对账批次,对比支付记录和订单状态,输出匹配、差异和处理状态。 -- 同一对账批次核对售后退款成功、退款流水和小金库入账,识别缺失、重复和金额不一致。 +- C08 是不扣小金库的受控 `SimulatedChannel`,不新增买家前台支付入口。回调使用 HMAC 验证来源,幂等身份为全局唯一 `callbackId + 请求指纹`;同一支付流水允许多个不同回调以表达先失败后成功或先成功后失败。 +- 回调终态固定为 `ProcessedSuccess`、`ProcessedFailure`、`Ignored`、`Difference`。只有服务端处理时间早于 `paymentDeadline` 且订单仍为 `PendingPayment` 的成功回调,才原子写模拟通道支付事实、订单 `Paid`、回调终态和 Outbox;不扣 Wallet。 +- 订单状态机拒绝迟到或逆序更新;金额、币种、订单关联不一致、订单不存在,以及 `Cancelled` 或其他通道成功后的迟到成功都保存为完整 `Difference` 来源事实,不改订单也不伪造成功支付记录。 +- 回调接收只保存来源终态;`Mall.Worker` 按固定 UTC 范围和稳定水位生成每日对账批次,统一聚合支付、订单、售后退款与钱包入账事实,输出 `Matched`、`HasDifferences` 或 `Resolved`。 +- 差异必须可稳定分页和查看完整证据,由管理员领取、释放/接管、选择受控处置、引用证据,并在关闭前重新读取权威事实、重跑原比较规则。只有当前有效领取人可关闭;最后一条差异和批次 `Resolved` 原子提交,仅填写说明不能关闭。 - 验收脚本随机重复并打乱回调顺序,验证最终状态和对账差异。 ### 7.13 C10 容器化部署与负载均衡 -- Docker Compose 定义 Nginx、Vue 静态站点、2 个 Mall.Api、Mall.Worker、PostgreSQL、Redis、RabbitMQ 和 SeaweedFS。 +- Docker Compose 定义 Nginx、Vue 静态站点、2 个 Mall.Api、一次性 Migrator、Mall.Worker、PostgreSQL、Redis、RabbitMQ 和 SeaweedFS。 +- 同一版本只允许 Migrator 执行 Migration:PostgreSQL 就绪后运行 Migrator,成功后才启动 API/Worker;API 和 Worker 禁止并发自动迁移。目标 Migration 失败或运行版本不兼容时,应用不得进入就绪。 - 两个 API 镜像和配置一致,不使用本地内存 Session;JWT 验签配置一致,失效记录和 SignalR Backplane 共享 Redis。 -- Nginx 负责 API 负载均衡和 WebSocket Upgrade;Health Check 不通过的实例不应继续接收新请求。 +- Nginx 负责 API 负载均衡和 WebSocket Upgrade;Health Check 不通过的实例不应继续接收新请求。SignalR 使用 WebSockets-only + skip negotiation,不依赖会话亲和。 +- 全局就绪只检查安全配置、运行版本兼容、目标 Migration 匹配和 PostgreSQL;Redis、RabbitMQ、SeaweedFS 作为能力级状态暴露,单项故障不直接把整个 API 判为未就绪。 +- Redis 故障时公开缓存回退 PostgreSQL、实时推送关闭、无法确认令牌撤销/账号禁用/手机号变更/旧凭证失效的受保护 HTTP 与 Hub 请求失败关闭;只有撤销事实重建且安全健康通过后才恢复受保护能力。RabbitMQ 故障时 Outbox 保留且投递暂停;SeaweedFS 故障时对象写入失败,其他能力继续。 - 镜像使用 Commit SHA/版本 Tag,不只使用 `latest`;Secret 通过环境变量或受控文件注入。 - 验收演示包含请求分布证明、停止一个 API 实例后的可用性和登录态连续性。 +- 单 API 停止顺序为:Nginx 停止向目标实例转发 → 目标实例有界排空 → 关闭该实例 Hub 并刷新日志 → 仅目标 API 停止;Worker 和共享依赖继续。整套停止时再依次停止全部新流量、排空 API、关闭 Hub、停止 Worker 领取并完成或安全释放任务、刷新遥测、停止应用和共享依赖,始终保留数据卷。 ## 八、安全设计 - 密码采用 ASP.NET Core PasswordHasher 或等价可靠算法,不自行实现加密。 - JWT 包含用户 ID、角色、`jti` 和过期时间,不包含密码或敏感资料;退出时把 `jti` 写入 Redis 至令牌自然过期。 - 用户禁用或修改手机号后,原有登录凭证立即失效且各 API 实例结果一致;重新启用账号不会恢复旧凭证。 +- 令牌撤销、账号禁用、手机号变更或全部旧凭证失效事实无法安全确认时,受保护 HTTP 请求和 Hub 连接必须失败关闭;Redis 仅恢复连通但安全事实尚未重建时仍不得放行。 - Policy 至少包括 `BuyerOnly`、`MerchantOnly`、`AdminOnly`。 - 所有资源查询同时校验资源归属,防止水平越权。 - EF Core 参数化查询,禁止拼接 SQL;手写统计 SQL 也必须参数化。 @@ -478,20 +492,20 @@ WHERE activity_id = @id | 端点 | 用途 | 检查内容 | |---|---|---| | `/health/live` | 存活检查 | 进程能够响应 | -| `/health/ready` | 就绪检查 | PostgreSQL 及当前启用的关键依赖可用 | +| `/health/ready` | 全局就绪检查 | 安全配置有效、运行版本兼容、目标 Migration 匹配、PostgreSQL 可用 | -Redis、RabbitMQ 或对象存储未被当前阶段启用时,不应错误地阻塞 API 就绪状态。 +Redis、RabbitMQ 和 SeaweedFS 不决定全局 200/503,而在响应中按能力报告:C07 `fallback`、C06 `disabled`、受保护鉴权 `failClosed`、Outbox 投递 `paused`、对象写入 `disabled`。Redis 基础连通恢复与撤销事实重建/安全健康恢复必须分开表达;后者完成前受保护能力和实时连接不能恢复。 ## 十一、环境与部署 | 环境 | 用途 | 规划 | |---|---|---| | 本地开发 | 单人开发和调试 | Aspire 启动 API、Worker 和所需基础设施;前端由 Vite 启动 | -| 集成验证 | `dev` 分支集成 | Docker Compose 单实例优先,执行接口和 Playwright 测试;使用可重复初始化的演示种子数据 | -| 演示/发布 | `master` 稳定版本 | Docker Compose + Nginx;C10 必须启动至少 2 个 API 实例,并预置不少于 30 个商品、3 个分类及三类登录账号 | +| 集成验证 | `dev` 分支集成 | Docker Compose 单实例优先;按 PostgreSQL → Migrator → API/Worker 顺序启动,执行接口和 Playwright 测试;使用可重复初始化的演示种子数据 | +| 演示/发布 | `master` 稳定版本 | Docker Compose + Nginx;C10 必须启动一次性 Migrator、至少 2 个同版本 API 实例和独立 Worker,并预置不少于 30 个商品、3 个分类及三类登录账号 | | 后续客户端验证 | PC Web 稳定后的独立阶段 | Electron 和 Android 分别验证统一 API 接入、鉴权、SignalR、平台交互和构建产物,不纳入当前四周验收结论 | -`dev` 和 `master` 表示代码成熟度,不等同于具体服务器环境。发布使用同一 Commit 构建的版本化镜像。 +`dev` 和 `master` 表示代码成熟度,不等同于具体服务器环境。发布使用同一 Commit 构建的版本化镜像;API、Worker 和 Migrator 必须来自同一版本,不允许滚动期间由多个进程竞争 Migration。 ## 十二、测试策略 -- Gitee From a8abd6fad5049881f7668fd748338639586ec2cf Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 22:12:01 +0800 Subject: [PATCH 106/118] =?UTF-8?q?docs(process):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E7=BB=9F=E7=A8=BF=E5=A4=8D=E6=A0=B8=E7=BC=BA=E5=8F=A3=EF=BC=9B?= =?UTF-8?q?=E9=97=AD=E5=90=88=E6=94=AF=E4=BB=98=E9=80=9A=E9=81=93=E4=B8=8E?= =?UTF-8?q?=E5=8F=AF=E9=9D=A0=E4=BA=8B=E5=AE=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\216\345\225\206\345\223\201\346\265\201\347\250\213.md" | 2 +- ...\201\347\256\241\347\220\206\346\265\201\347\250\213.md" | 2 +- ...\255\347\211\251\350\275\246\346\265\201\347\250\213.md" | 2 +- ...M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" | 4 ++-- ...M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" | 6 +++--- ...\241\346\265\201\347\250\213\350\256\276\350\256\241.md" | 4 ++-- ...\237\346\236\266\346\236\204\350\256\276\350\256\241.md" | 6 +++--- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" index ea9ecf0..724322e 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" @@ -22,7 +22,7 @@ | 分类与商品接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | | DBxxx 分类/商品表 | 部分定义 | 本文不发明表字段、状态码和索引 | | C07 缓存协作 | 完整定义 | 只接入固定首页摘要和 A103 商品自身详情;60/10 秒 TTL、3 秒二次失效和 62 秒兜底已冻结 | -| C04 中文搜索进阶 | 待细化(顾欣月主责) | 在 M02-01 基础模糊查询入口上增强,业务口径与本文保持一致 | +| C04 中文搜索进阶 | 完整定义,已完成统稿校准 | 在 M02-01 同一查询入口增强,强制过滤、同步索引、降级和性能口径与本文一致 | ## 二、模块直接出入口 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" index 5b63e68..f9a29fb 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -23,7 +23,7 @@ | 商家端写操作接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | | DBxxx 分类/商品表 | 部分定义 | 本文不发明表字段、状态码和索引 | | C07 缓存失效协作 | 完整定义 | 商品事实提交后按冻结矩阵立即失效并在 3 秒后二次失效,不混入商家写操作核心结果 | -| C04 搜索索引更新 | 待细化 | 仅约束 PostgreSQL 同步维护,不建设独立索引任务 | +| C04 搜索索引更新 | 完整定义,已完成统稿校准 | PostgreSQL 同步维护索引,失败安全回退基础搜索,不建设独立索引任务 | ## 二、模块直接出入口 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" index 6f997ed..d8dc73e 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" @@ -16,7 +16,7 @@ | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M03-01/F07 需求 | 完整定义 | 作为购物车业务语义事实源 | -| 本文业务流程 | 初稿 | 先确认参与者、上游输入、状态派生、原子结果和模块出入口 | +| 本文业务流程 | 完整定义,已完成统稿校准 | 参与者、上游输入、状态派生、原子结果和模块出入口已闭合,可作为下游设计输入 | | A2xx 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | | 购物车相关表(条目、幂等记录) | 模板/占位 | 本文不发明表名、字段、约束或索引 | | X02 收藏与浏览历史 | 独立扩展 | 仅登记边界,不混入 F07 主流程 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" index a06149b..e97bd32 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" @@ -16,7 +16,7 @@ | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M05-01/F10 需求 | 完整定义 | 作为业务语义事实源 | -| 本文业务流程 | 初稿 | 先确认角色、状态、分支、事务边界和模块出入口 | +| 本文业务流程 | 完整定义,已完成统稿校准 | 角色、状态、分支、事务边界和模块出入口已闭合,可作为下游设计输入 | | A401~A408 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | | DB081~DB100 数据库设计 | 模板/占位 | 本文不发明表字段、枚举编码和索引 | | X04/C08 | 独立扩展 | 只登记接入点,不混入 F10 核心状态机 | @@ -166,7 +166,7 @@ flowchart TD B -- "钱包余额" --> C["返回本人实时余额"] B -- "充值记录" --> D["分页返回本人充值记录"] B -- "收银台" --> E["返回本人订单金额、状态、余额、截止时间和按权威时间派生的可支付性"] - B -- "订单支付结果" --> F["返回已确定支付结果
无记录时的响应语义待评审"] + B -- "订单支付结果" --> F["只返回本人已确定支付事实;没有记录时明确返回 paymentResult=None,不按订单状态猜测支付成功"] B -- "支付记录/详情" --> G["仅返回本人支付记录"] C --> H["页面展示确定状态和下一步"] D --> H diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" index 0fcfbec..1e46c73 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" @@ -16,10 +16,10 @@ M10 负责订单项售后资格、申请数量占用、商家审核、退货说 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M10 / X04 需求 | 完整定义 | 作为业务语义事实源 | -| M04 / M06-02 订单与履约边界 | 已定义、待统一整合 | 冻结订单状态、指定商家、可履约数量和发货竞争 | -| M05 退款能力 | 已定义、待由本流程补齐退款契约 | 只承接幂等退款,不决定售后资格与库存 | +| M04 / M06-02 订单与履约边界 | 完整定义,已完成统稿校准 | 已冻结订单状态、指定商家、可履约数量和发货竞争 | +| M05 退款能力 | 业务边界完整,待接口契约承接 | 只承接稳定退款操作,不决定售后资格与库存 | | M02 / C01 / C07 库存通道 | 完整定义 | 售后成功按原来源回补;普通库存触发 C07,秒杀原活动库存不触发 | -| M09 通知 | 已定义、待按本流程校准接收人 | 只消费已经提交的售后事实 | +| M09 通知 | 完整定义,固定接收矩阵已校准 | 只消费已经提交的售后事实 | | 本文业务流程 | 已重构、待交叉评审 | 作为接口与后续数据设计输入 | ## 二、参与者、事实归属与直接出入口 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" index 703cc09..fedf0b5 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" @@ -438,13 +438,13 @@ flowchart TD > 覆盖:M04-02、M04-03、M04-04、M06-02;F09、F12 > 主责:韦乾强;支付协作:张海洋;定时任务实现协作:罗皓晨 -> 直接入口:M05 返回 `Paid`;M06-02 提交发货;买家或系统定时任务提交取消/完成触发 +> 直接入口:M05 Wallet 或 C08 SimulatedChannel 返回唯一 `Paid`;M06-02 提交发货;买家或系统定时任务提交取消/完成触发 > 直接出口:M04 返回唯一最终状态、状态时间线和必要快照;事务提交后的事实可供 X03/C03 消费 ```mermaid stateDiagram-v2 [*] --> PendingPayment: F08 下单事务提交成功 - PendingPayment --> Paid: F10 本人钱包支付事务成功 + PendingPayment --> Paid: F10 Wallet 或 C08 SimulatedChannel 在 paymentDeadline 前原子成功 PendingPayment --> Cancelled: F09 本人主动取消事务成功 PendingPayment --> Cancelled: C03 到达固定 paymentDeadline 且系统自动取消成功 Paid --> Shipped: F12 assignedMerchantUserId 对应商家发货事务成功 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" index 1f7e58f..058d6db 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" @@ -322,7 +322,7 @@ sequenceDiagram API->>DB: 开启事务 API->>DB: 校验地址、商品、价格、库存和唯一启用默认商家 API->>DB: 条件扣减库存 - API->>DB: 写订单、订单项快照、assignedMerchantUserId、固定 paymentDeadline 和 Outbox(启用时) + API->>DB: 写订单、订单项快照、assignedMerchantUserId、固定 paymentDeadline 和待发布事实 API->>DB: 删除已结算购物车条目 API->>DB: 提交事务 API-->>FE: 201 + 订单号 @@ -346,7 +346,7 @@ sequenceDiagram - 商家只能把已支付订单推进为已发货,并记录发货时间;其他状态的发货请求必须拒绝。 - 本期为单店 B2C,不建设多商户商品归属、拆单或结算;Ordering 为每张普通订单和秒杀订单都保存 Identity 解析的唯一启用默认 `assignedMerchantUserId`,活动创建人只用于活动管理。商家查询、发货、售后和消息接收均按订单指定账号精确过滤。 -- Identity 对默认商家标记建立唯一约束,并由启动配置/种子数据保证存在一个启用账号;A016 不允许直接禁用默认商家,也不允许禁用仍有待履约订单、售后窗口/申请或未结束秒杀活动的其他商家。本期不做自动重新分配。 +- Identity 对默认商家标记建立唯一约束,并由启动配置/种子数据保证存在一个启用账号;A016 始终拒绝禁用默认商家。非默认商家存在 `PendingPayment`、仍有可履约数量的 `Paid`、`Shipped`、完成后 7 天窗口内的 `Completed`、任一非终态售后申请或未结束秒杀活动时也拒绝禁用;已全量退款、无剩余可履约数量且无非终态售后的 `Paid` 不再单独阻断。本期不做自动重新分配。 - A307 发货与 A412 提交售后都先通过 Ordering 公开应用契约在当前 PostgreSQL 事务中锁定同一 `orders` 行并复核最新履约状态,锁保持到业务写入提交;随后 A307 通过 AfterSales 公开应用契约取得售后快照。处理中申请阻断发货,已退款数量从可履约数量中扣除,部分退款只发剩余数量,全部退款不再发货;模块之间不得直接读取对方内部表。 - 买家只能确认本人已发货订单,确认成功后以状态条件把订单推进为已完成,并记录完成时间和“买家确认”方式。 - `Mall.Worker` 扫描发货满 7 天且仍为已发货的订单,以同一状态条件推进为已完成,并记录“自动完成”方式;演示环境可以缩短配置,但不改变正式规则。 @@ -357,7 +357,7 @@ sequenceDiagram - 领域事件在同一进程内表达领域事实,例如 `OrderPaidDomainEvent`。 - 只有跨进程需求才转换为集成事件,例如 `OrderPaidIntegrationEvent`。 -- 启用 RabbitMQ 时,业务事务同时写入 Outbox;Worker 成功发布后标记已处理。 +- 所有进入 M09 固定矩阵的成功业务事实都在来源事务内写入待发布事实;RabbitMQ 只是事务提交后的传输方式。Worker 成功发布后标记已处理,RabbitMQ 故障时 Outbox 保留并暂停投递,不能因消息中间件未启用而省略可靠事实。 - 消费者在业务事务内写入 Inbox/消费处理记录,并结合消息 ID 唯一约束或业务唯一约束防止重复处理。 - Outbox 负责可靠发布,Inbox 负责可靠消费;二者均不能替代订单、支付、消息等业务表上的最终唯一约束和状态条件。 - 第一条集成事件链确定为 `OrderPaidIntegrationEvent`:Payment 在支付事务成功后通过 Outbox 发布,Messaging 幂等消费并生成买家支付成功、商家待发货通知;不为普通 CRUD 广泛发布事件。 -- Gitee From ee5e879341d508801a6ba5f1b38e636b85cf26d8 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 22:13:41 +0800 Subject: [PATCH 107/118] =?UTF-8?q?docs(process):=20=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E6=88=90=E5=91=98=E6=B5=81=E7=A8=8B=E6=88=90=E7=86=9F=E5=BA=A6?= =?UTF-8?q?=EF=BC=9B=E6=B6=88=E9=99=A4=E7=BB=9F=E7=A8=BF=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...47\210\345\216\206\345\217\262\346\265\201\347\250\213.md" | 2 +- .../M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" | 2 +- ...56\266\345\261\245\347\272\246\346\265\201\347\250\213.md" | 2 +- .../C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" | 4 ++-- ...70\216\345\257\271\350\264\246\346\265\201\347\250\213.md" | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" index aa3e4d6..a7aafa7 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" @@ -22,7 +22,7 @@ A018~A022、A024、A025 由本流程派生;历史清单中的 A023 清空历 | A018~A022、A024、A025 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | | A023 清空浏览历史 | 无需求来源 | 取消,不进入实现 | | DBxxx 收藏/浏览表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | -| M02 商品事实 | 部分定义 | 只登记接入点;价格、库存与销售状态由 Catalog 评审 | +| M02 商品事实 | 完整定义,已完成统稿校准 | 只登记接入点;价格、库存与销售状态由 Catalog 权威流程提供 | ## 二、模块直接出入口 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" index c0a9a14..a520e04 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" @@ -16,7 +16,7 @@ M04 拥有订单创建、买家订单查询、待支付订单取消、订单核 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M04-01~M04-04 需求 | 完整定义 | 作为订单语义事实源 | -| M03 购物车与 M01 地址 | 已校准或待整合 | 提供本人购物车条目和地址事实 | +| M03 购物车与 M01 地址 | 完整定义,已完成统稿校准 | 提供本人购物车条目和地址事实 | | M02 / C01 库存来源 | 已定义或已校准 | 下单扣减、取消与售后按原通道回补 | | M05 / C08 支付 | 已校准 | 只在截止时间前竞争待支付状态 | | M06-02 商家履约 | 已补齐、待交叉评审 | 使用指定商家、售后履约快照与订单状态公开能力 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M06-02-\345\225\206\345\256\266\345\261\245\347\272\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M06-02-\345\225\206\345\256\266\345\261\245\347\272\246\346\265\201\347\250\213.md" index e6d2dee..997efae 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M06-02-\345\225\206\345\256\266\345\261\245\347\272\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M06-02-\345\225\206\345\256\266\345\261\245\347\272\246\346\265\201\347\250\213.md" @@ -18,7 +18,7 @@ M06-02 为商家提供授权订单列表、状态筛选、履约详情和发货 | M06-02 / F12 需求 | 完整定义 | 作为商家履约事实源 | | M04 订单状态与指定商家 | 已重构 | 提供权威订单、快照和 `Paid → Shipped` 动作 | | M10 售后履约快照 | 已重构 | 提供非终态阻断与已退款数量 | -| M09 发货通知 | 已定义、待整合 | 只消费已提交发货事实 | +| M09 发货通知 | 完整定义,固定接收矩阵已校准 | 只消费已提交发货事实 | | 本文业务流程 | 新增并已校准、待交叉评审 | 补齐此前缺失的 M06-02 设计 | ## 二、参与者、事实归属与权限边界 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" index d43aa52..d67114e 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" @@ -17,8 +17,8 @@ |---|---|---| | C01 需求与教师 C01 验收项 | 完整定义 | 作为业务语义和硬指标事实源 | | 本文业务流程 | 已校准、待交叉评审 | 明确状态、动作、原子结果、异常与模块边界 | -| M04/M05/M06-02/M09/M10 核心流程 | 部分已定义 | 复用其公开业务出入口,不建立第二套订单链路 | -| C03/C07/C08/C10 挑战流程 | 部分定义 | 仅登记与秒杀直接相交的责任 | +| M04/M05/M06-02/M09/M10 核心流程 | 完整定义,已完成统稿校准 | 复用其公开业务出入口,不建立第二套订单链路 | +| C03/C07/C08/C10 挑战流程 | 完整定义,相交边界已冻结 | 只承接与秒杀直接相交的责任 | | A220~A228 接口 | 部分定义、未冻结 | 待按本文第九章重新派生和补齐 | | 秒杀相关数据设计 | 模板/占位 | 待全部流程完成后从零统一设计 | | X04 售后退款 | 独立扩展 | 不并入“待支付订单取消回补”流程 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" index d0d9268..62a7952 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" @@ -19,7 +19,7 @@ F10 的默认买家路径仍为 M05 小金库同步支付。C08 不扣买家小 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | C08 需求与教师挑战目标 | 完整定义 | 作为回调、对账和差异闭环事实源 | -| F10/F09/M10 流程 | 已定义或校准中 | 作为支付、取消和退款协作边界 | +| F10/F09/M10 流程 | 完整定义,已完成统稿校准 | 作为支付、取消和退款协作边界 | | 本文业务流程 | 已校准、待交叉评审 | 冻结回调终态、原子结果、截止时间与对账闭环 | | A421~A425 接口 | 部分定义、未冻结 | 待按第十一章重新派生 | | 回调、支付、对账数据设计 | 模板/占位 | 流程完成后统一派生,不在业务图中预设字段 | -- Gitee From e0b533824efdc23b4ddab066647b34a0d485a366 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Fri, 24 Jul 2026 23:18:17 +0800 Subject: [PATCH 108/118] =?UTF-8?q?docs(interface):=20=E6=8C=89=E5=85=A8?= =?UTF-8?q?=E9=87=8F=E6=B5=81=E7=A8=8B=E9=87=8D=E5=BB=BA=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=A5=91=E7=BA=A6=EF=BC=9B=E9=97=AD=E5=90=88=E5=9B=9E=E8=B0=83?= =?UTF-8?q?=E9=80=80=E6=AC=BE=E6=B6=88=E6=81=AF=E4=B8=8E=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 16 +- ...06\345\223\201\346\265\201\347\250\213.md" | 4 +- ...41\347\220\206\346\265\201\347\250\213.md" | 3 +- ...23\345\255\230\346\265\201\347\250\213.md" | 2 +- ...71\350\264\246\346\265\201\347\250\213.md" | 11 +- ...45\345\217\243\350\256\276\350\256\241.md" | 2148 +++++++++-------- 6 files changed, 1157 insertions(+), 1027 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 04dffbe..e16521e 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -2545,13 +2545,13 @@ Electron 后续原则上复用 PC Web 原型,只补充窗口、桌面导航和 ### 9.1 需求追踪矩阵 -> 本表用于把教师验收编号落实到负责人、页面、接口契约和测试用例。接口列引用《接口设计》中的 Axxx;当前保留 107 个追踪编号,其中 103 个为有效 HTTP 契约,均仍处于汇总或交叉评审阶段,不得据此宣称已经实现、冻结或验证。 +> 本表用于把教师验收编号落实到负责人、页面、接口契约和测试用例。接口列引用《接口设计》中的 Axxx;当前保留 109 个追踪编号,其中 99 个为活动 HTTP 契约、10 个为历史取消编号,均仍处于汇总或交叉评审阶段,不得据此宣称已经实现、冻结或验证。 | 教师编号 | 模块与负责人 | 页面或操作入口 | 接口契约(Axxx) | 测试用例 | 当前状态 | |---|---|---|---|---|---| | F01 | M01-01 用户注册—唐宇昊 | PC Web 注册页 | A001 | 待测试计划登记 | 注册后显式登录、角色拒绝和原子账号创建已统一,待接口同步 | -| F02 | M01-02 登录与退出—唐宇昊 | PC Web 统一登录页、退出入口 | A002~A004;A005 无业务来源,接口整合时取消 | 待测试计划登记 | 单一 JWT、退出和全部旧凭证失效已统一,待接口同步 | -| F03 | M01-03 个人信息与地址—唐宇昊 | PC Web 买家个人中心、地址管理 | A006~A008、A010~A014;A009 无业务来源,接口整合时取消 | 待测试计划登记 | 资料字段、并发修改和独立默认地址切换已统一,待接口同步 | +| F02 | M01-02 登录与退出—唐宇昊 | PC Web 统一登录页、退出入口 | A002~A004;A005 为历史取消编号 | 待测试计划登记 | 单一 JWT、退出和全部旧凭证失效已统一,待接口同步 | +| F03 | M01-03 个人信息与地址—唐宇昊 | PC Web 买家个人中心、地址管理 | A006~A008、A010~A014;A009 为历史取消编号 | 待测试计划登记 | 资料字段、并发修改和独立默认地址切换已统一,待接口同步 | | F04 | M02-01 商品列表与分类—顾欣月 | 购物端商品列表、分类入口 | A101、A102 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | | F05 | M02-01 商品搜索—顾欣月 | 购物端搜索框、商品列表 | A102 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | | F06 | M02-02 商品详情—顾欣月 | 商品详情页 | A103 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | @@ -2559,19 +2559,19 @@ Electron 后续原则上复用 PC Web 原型,只补充窗口、桌面导航和 | F08 | M04-01 提交订单—韦乾强 | 购物车结算、提交订单页 | A301 | 待测试计划登记 | 幂等、超时和商家归属已统一,待数据库、OpenAPI 与交叉评审 | | F09 | M04-02/M04-03/M04-04 订单查询、取消与完成—韦乾强 | 买家订单列表、订单详情、确认收货 | A302~A304、A308 | 待测试计划登记 | 接口草案已闭合,待数据库、OpenAPI 与交叉评审 | | F10 | M05-01 模拟支付—张海洋 | 小金库、统一收银台、支付记录 | A401~A408 | 待测试计划登记 | 重复支付与幂等语义已统一,待数据库、OpenAPI 与交叉评审 | -| F11 | M06-01 后台商品管理—顾欣月 | 商家分类管理、商品管理 | A110~A114、A120~A128 | 待测试计划登记 | 创建、图片顺序与同步索引边界已统一,待数据库、OpenAPI 与交叉评审 | +| F11 | M06-01 后台商品管理—顾欣月 | 商家分类管理、商品管理 | A110~A115、A120~A128 | 待测试计划登记 | 创建、受约束分类删除、图片顺序与同步索引边界已统一,待数据库、OpenAPI 与交叉评审 | | F12 | M06-02 后台订单管理—韦乾强 | 商家订单列表、详情和发货入口 | A305~A307 | 待测试计划登记 | Ordering 命名、状态与商家归属已统一,待数据库、OpenAPI 与交叉评审 | | F13 | M06-03 后台用户管理—唐宇昊 | 管理端用户管理 | A015~A017 | 待测试计划登记 | 买家/商家分流、固定责任清单、禁用竞争和全部旧凭证失效已统一,待接口同步 | -| X01 | M07 商品评价与晒图—顾欣月 | 订单评价入口、商品评价区 | A140~A143;A144 无业务来源,接口整合时取消 | 待测试计划登记 | 订单入口、提交重检、唯一评价、公开计分和图片边界已统一,待数据库、OpenAPI 与交叉评审 | -| X02 | M08 收藏与浏览历史—唐宇昊 | 商品收藏、收藏页、浏览历史页 | A018~A022、A024、A025;A023 无业务来源,接口整合时取消 | 待测试计划登记 | 收藏幂等、默认开启、最近 200 条及关闭后旧历史可见已统一,待接口同步 | +| X01 | M07 商品评价与晒图—顾欣月 | 订单评价入口、商品评价区 | A140~A143;A144 为历史取消编号 | 待测试计划登记 | 订单入口、提交重检、唯一评价、公开计分和图片边界已统一,待数据库、OpenAPI 与交叉评审 | +| X02 | M08 收藏与浏览历史—唐宇昊 | 商品收藏、收藏页、浏览历史页 | A018~A022、A024、A025;A023 为历史取消编号 | 待测试计划登记 | 收藏幂等、默认开启、最近 200 条及关闭后旧历史可见已统一,待接口同步 | | X03 | M09 站内消息—罗皓晨 | 买家/商家消息中心 | A501~A505 | 待测试计划登记 | 固定接收人、整事件原子性和稳定已读水位已冻结,待数据库、接口与实现承接 | -| X04 | M10 售后流程—张海洋 | 买家售后申请、商家售后审核 | A411~A417、A419、A432~A434;退款入账使用 Payment 应用契约 | 待测试计划登记 | A418/A431 为取消历史号,待数据库、OpenAPI 与交叉评审 | +| X04 | M10 售后流程—张海洋 | 买家售后申请、商家售后审核 | A411~A417、A419、A434;退款入账使用 Payment 应用契约 | 待测试计划登记 | A418/A431/A432/A433 为取消历史号,待数据库、OpenAPI 与交叉评审 | | C01 | 秒杀与防超卖—朱惠惠 | 秒杀活动页、商家活动管理、压测脚本 | A220~A228;订单查询复用 A302/A303;生命周期为 Worker 内部契约 | 待挑战测试登记 | 原子限购、库存通道与状态推进已定义,待数据库、OpenAPI 与跨模块联调 | | C03 | 订单超时取消—韦乾强 | 订单倒计时、Worker 验收脚本 | 复用 A304;超时扫描为 Worker 内部契约 | 待挑战测试登记 | Worker 扫描、取消复用和原库存通道回补已定义,待数据库与测试评审 | | C04 | 商品搜索进阶—顾欣月 | 商品搜索页、性能对比报告 | A102 | 待挑战测试登记 | 接口草案已汇总,待交叉评审 | | C06 | 实时消息推送—罗皓晨 | 实时通知、连接状态、消息中心 | SignalR 契约(无 Axxx),补查复用 A501~A505 | 待挑战测试登记 | WebSockets 跳过协商、固定重连、凭证失效与权威角标补查已冻结,待接口、部署与测试承接 | | C07 | 缓存与性能优化—罗皓晨,顾欣月协作 | 首页、商品详情、压测报告 | 复用 A102 的固定首页摘要调用、A103 商品详情;不缓存普通列表与搜索 | 待挑战测试登记 | 60/10 秒 TTL、500 毫秒跨实例填充等待、提交后立即/3 秒双删和 62 秒兜底已冻结,待接口、实现与压测承接 | -| C08 | 支付回调幂等与对账—张海洋 | 回调重放脚本、管理端对账 | A421~A425、A432~A433;退款入账使用 Payment 应用契约 | 待挑战测试登记 | A431 为取消历史号,待数据库、OpenAPI 与交叉评审 | +| C08 | 支付回调幂等与对账—张海洋 | 回调重放脚本、管理端对账 | A421~A426;退款入账使用 Payment 应用契约 | 待挑战测试登记 | A431/A432/A433 为取消历史号,待数据库、OpenAPI 与交叉评审 | | C10 | 容器化与负载均衡—罗皓晨 | 统一访问入口、健康与实例验证 | A506、A507 | 待部署验收登记 | Migrator、全局就绪/能力降级、Redis 安全恢复、WebSocket 与优雅停止已冻结,待接口、部署和验收承接 | 负责人补齐缺少接口并完成交叉评审后,应把对应状态更新为“已确认”;生成真实 OpenAPI 后再补充 `operationId` 校验结果。测试计划完成后,应替换为真实测试用例编号,并随需求变更同步维护状态。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" index 724322e..e265758 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" @@ -173,7 +173,7 @@ flowchart TD | 场景 | M02 处理 | 最终状态/责任 | |---|---|---| | 公开浏览携带过期/无效令牌 | 按游客处理并正常返回公开商品数据 | 公开接口不依赖有效令牌 | -| 公开浏览携带账号禁用令牌 | 按游客处理并正常返回公开商品数据;保护写操作时返回 401 | 公开接口与受保护接口分开校验 | +| 公开浏览携带账号禁用令牌 | 按游客处理并正常返回公开商品数据;凭据仍有效但账号为 `Disabled` 时,受保护写操作返回 403 `AUTH.ACCOUNT_DISABLED`;凭据已撤销、过期或版本失效时返回 401 | 公开接口与受保护接口分开校验 | | 商家或管理员在购物端尝试买家专属操作 | 服务端按角色拒绝;前端隐藏入口不替代后端 | 由收藏、购物车或订单模块返回无权限结果 | | 商品 ID 不存在或商品已物理删除 | 返回“商品不存在”,提供返回列表入口 | 不暴露内部异常;删除不是销售状态 | | 商品已下架 | 显示“暂不可售”,禁用购买 | 历史订单快照仍可读 | @@ -195,7 +195,7 @@ flowchart TD | 查询启用分类 | A101 Catalog 分类 | 只返回购物端筛选入口使用的启用分类;分类停用不改变其下商品销售状态 | 待交叉评审 | | 查询固定首页摘要、普通列表与组合筛选/关键词搜索 | A102 Catalog 列表/搜索 | 只有无筛选、`OnSale`、`createdAt DESC, productId DESC`、前 12 条的固定首页摘要可由 C07 缓存;所有用户控制的普通列表、关键词和组合筛选每次按 PostgreSQL 强制已上架过滤、分页、白名单排序并正常返回空结果;库存为零时标记售罄,所属分类停用时仍可通过全部商品和关键词命中;F05 与 C04 共用同一契约 | 待重建详细契约 | | 查询商品详情 | A103 Catalog 详情 | 仅公开已上架商品;允许库存为零并标记售罄,允许所属分类停用;缓存只含商品自身公开字段,不含 M07 评价/评分或个人字段;旧值最坏不超过提交后 62 秒 | 待重建详细契约 | -| 公开评价汇总与列表(X01 衔接) | 由 M07 的公开读取能力派生,编号待 M07 流程确认后映射 | 商品详情只读取公开评价和评分汇总,不混入上传、提交或资格判断能力 | 待交叉评审 | +| 公开评价汇总与列表(X01 衔接) | A140(由 M07 流程派生) | 商品详情只读取公开评价和评分汇总,不混入上传、提交或资格判断能力 | 待交叉评审 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" index f9a29fb..3202e58 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -14,7 +14,7 @@ 本模块不包含多商家数据隔离、批量导入导出、定时上架、复杂审批流、商品操作审计功能或管理员代商家修改商品。 -本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。M06-01 商家写操作接口编号落在 A110~A128 范围(A110~A114 后台分类、A120~A128 后台商品),与 M02 公开浏览 A101~A103、M07 评价 A140~A144 同属 Catalog/Review 编号段(A101~A200);该编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。M06-01 商家写操作接口编号落在 A110~A128 范围(A110~A115 后台分类、A120~A128 后台商品),与 M02 公开浏览 A101~A103、M07 评价 A140~A144 同属 Catalog/Review 编号段(A101~A200);该编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| @@ -229,6 +229,7 @@ flowchart TD | 编辑分类 | A112 编辑分类 | 校验名称、父级与排序后保存;商品或历史引用不能阻止元数据编辑;分类名称变更与关联商品检索事实同步提交 | 待交叉评审 | | 启用分类 | A113 启用分类 | 切换启停状态并校验依赖 | 待交叉评审 | | 停用分类 | A114 停用分类 | 移出 A101 分类筛选入口,但不改变已有商品状态;A102 仍可在全部商品和关键词搜索中返回其下已上架商品 | 待交叉评审 | +| 删除分类 | A115 删除分类 | 仅允许无子分类、无商品和无其他历史引用的分类物理删除;任一引用存在则整体拒绝,并发新增引用与删除只允许一个结果,删除后不保留伪状态 | 待交叉评审 | | 上传商品图片 | A127 商品图片上传 | 按合规要求上传到对象存储,返回主图与附加图标识 | 待交叉评审 | | 删除商品图片 | A128 商品图片删除 | 删除商品图片关联与对象存储对象,保持引用一致 | 待交叉评审 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" index 56c6560..ba35b00 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" @@ -193,7 +193,7 @@ stateDiagram-v2 |---|---|---|---| | 固定首页公开查询 | A102 的无筛选固定首页语义;其他 A102 请求不缓存 | 数据库设计待汇总 | 待补齐固定排序、12 条和售罄字段 | | 购物端商品自身公开详情 | A103 | 数据库设计待汇总 | 待明确排除 M07 评价、评分和个人字段 | -| 商品、分类展示和图片写入后失效 | A123~A128 提交后的内部协作 | 目标表设计待数据库汇总 | 待为名称、价格、描述、分类、图片、主图、状态和删除逐项补齐后置条件 | +| 商品、分类展示和图片写入后失效 | A112、A122~A128 提交后的内部协作 | 目标表设计待数据库汇总 | 待为创建短空值、名称、价格、描述、分类展示、图片、主图、状态和删除逐项补齐后置条件 | | 普通订单与普通库存售后回补后失效 | Catalog 公开库存应用能力,不新增 Axxx | 库存事实待数据库汇总 | 待在 M04/M10 下游契约承接原库存通道 | | C01 发布划拨后失效;活动内部库存不失效 | C01 与 Catalog 的内部协作 | 普通库存与活动库存设计待汇总 | 待接口/事件契约承接冻结矩阵 | | Redis 故障回退 PostgreSQL | 继续复用固定首页/A103 响应口径 | Redis 不登记 DBxxx | 待架构和测试承接 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" index 62a7952..a15e475 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" @@ -14,14 +14,14 @@ C08 负责受控模拟支付通道的回调接收、来源鉴别、幂等、乱 F10 的默认买家路径仍为 M05 小金库同步支付。C08 不扣买家小金库,也不新增第二套买家支付页面;它只接收受控挑战脚本或模拟通道产生的合法回调。同步钱包支付、模拟通道成功回调、买家主动取消和 C03 超时取消共同竞争订单的 `PendingPayment` 状态。模拟成功回调只有在订单仍为待支付且权威时间早于支付截止时间时才能推进为 `Paid`;其他路径先成功后,回调只能重放、忽略或登记差异。 -本文先确认回调结果、订单竞争、事务结果、对账范围和差异闭环,再由这些业务动作派生 A421~A425。具体签名格式、HTTP 字段、表名、索引、Inbox/Outbox 结构和 Worker 参数属于接口、数据库或架构下游设计,不能反向塑造业务流程。 +本文先确认回调结果、订单竞争、事务结果、对账范围和差异闭环,再由这些业务动作派生 A421~A426。具体签名格式、HTTP 字段、表名、索引、Inbox/Outbox 结构和 Worker 参数属于接口、数据库或架构下游设计,不能反向塑造业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | C08 需求与教师挑战目标 | 完整定义 | 作为回调、对账和差异闭环事实源 | | F10/F09/M10 流程 | 完整定义,已完成统稿校准 | 作为支付、取消和退款协作边界 | | 本文业务流程 | 已校准、待交叉评审 | 冻结回调终态、原子结果、截止时间与对账闭环 | -| A421~A425 接口 | 部分定义、未冻结 | 待按第十一章重新派生 | +| A421~A426 接口 | 部分定义、未冻结 | 待按第十一章重新派生 | | 回调、支付、对账数据设计 | 模板/占位 | 流程完成后统一派生,不在业务图中预设字段 | ## 二、参与者与模块直接出入口 @@ -292,10 +292,11 @@ flowchart TD | 接收受控模拟支付回调 | A421 | 来源鉴别、回调标识幂等、支付流水聚合与乱序、截止时间和状态竞争、四种确定终态 | 待重建详细契约 | | 对账批次列表 | A422 | 管理员按日期与状态分页查看 `Matched` / `HasDifferences` / `Resolved` 批次 | 待重建详细契约 | | 对账批次详情 | A423 | 返回范围、总数、匹配数、差异数、类型汇总和当前闭环状态 | 待重建详细契约 | -| 差异列表 / 详情 | A424 | 管理员按批次、类型和状态查询差异事实与时间线 | 待重建详细契约 | +| 差异列表 | A424 | 管理员按批次、类型和状态分页查询差异摘要 | 待重建详细契约 | | 差异领取、转交与解决 | A425 | 条件领取 / 接管、受控处置类型、权威事实复核、原子关闭差异并按需关闭批次 | 待重建详细契约 | +| 差异详情 | A426 | 返回比较规则、期望与实际事实、全部来源证据、领取/转交/处置和复核时间线 | 待重建详细契约 | -A421 以回调标识和请求指纹保证同一次投递幂等,以支付流水聚合一次支付尝试的多个时序信号,不要求客户端另造独立幂等语义;A422~A425 只向管理员开放。具体 HTTP 方法、路径、签名头、请求响应、状态码和错误码在接口阶段由本表派生。 +A421 以回调标识和请求指纹保证同一次投递幂等,以支付流水聚合一次支付尝试的多个时序信号,不要求客户端另造独立幂等语义;A422~A426 只向管理员开放。具体 HTTP 方法、路径、签名头、请求响应、状态码和错误码在接口阶段由本表派生。 ## 十二、跨模块边界 @@ -318,7 +319,7 @@ A421 以回调标识和请求指纹保证同一次投递幂等,以支付流水 8. 支付成功来源必须区分小金库与受控模拟通道;同一订单最多一个成功来源,回调不得生成钱包流水。 9. 管理员差异处理不直接更新业务表;需要纠正时必须引用所属模块的受控操作结果,并由系统重跑原比较规则确认已经一致。 10. 对账归属只使用成功提交时间和固定 UTC 水位;领取转交、处置类型、复核失败和当前领取人权限必须由接口承载。 -11. 接口完成后从本文逐项复核鉴别、幂等、截止时间、乱序、事务、批次和差异状态,不能用现有 A421~A425 草案反向修改流程。 +11. 接口完成后从本文逐项复核鉴别、幂等、截止时间、乱序、事务、批次和差异状态,不能用现有 A421~A426 草案反向修改流程。 ## 十四、验收证据清单 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" index b1e8c83..163f259 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" @@ -1,13 +1,16 @@ # 接口设计文档 -> 组别:24级1班第7组 编写人:罗皓晨 编写日期:2026-07-24 版本:v0.1 +> 组别:24级1班第7组 编写人:罗皓晨 编写日期:2026-07-24 版本:v1.0 > 截止:第 2 周周三(开发过程中持续更新,保持与代码一致) +> +> 当前状态:已按业务流程 v1.0 重建统一契约;共 109 个追踪编号,其中 99 个活动 HTTP 定义、10 个历史取消编号;待 OpenAPI、数据库、实现、测试和正式交叉评审承接 ## 修订记录 | 版本 | 日期 | 修改人 | 修改说明 | |------|------|--------|----------| | v0.1 | 2026-07-24 | 罗皓晨、各模块负责人 | 建立通用约定,综合六份最新个人接口原稿,形成 107 个追踪编号、103 个有效 HTTP 契约及非 HTTP 协作边界,并更新冻结条件 | +| v1.0 | 2026-07-24 | 全体成员(罗皓晨统稿) | 以 21 份已校准业务流程为输入,隔离 10 个历史取消编号、补出 A115/A426,统一 99 个活动 HTTP 定义及消息、缓存、Worker、回调、退款和运行契约 | ## 一、通用约定 @@ -127,7 +130,7 @@ | `Content-Type` | 请求 | 有 Body 时必需 | 声明 JSON、表单或文件上传格式 | | `Accept` | 请求 | 建议 | 声明客户端可接收的响应格式 | | `Authorization` | 请求 | 受保护接口必需 | 携带 Bearer JWT | -| `Idempotency-Key` | 请求 | 指定高风险接口必需 | 防止重复下单、重复支付、重复充值或重复回调处理 | +| `Idempotency-Key` | 请求 | 指定高风险接口必需 | 防止重复下单、重复支付、重复充值、重复状态命令或重复退款;A421 回调按 `callbackId + 请求指纹` 幂等,不使用此 Header | | `traceparent` | 请求 | 可选 | 延续 W3C Trace Context;缺失时服务端创建新 Trace | | `X-Request-Id` | 请求/响应 | 可选 | 客户端请求关联标识;服务端校验格式后回传或生成 | | `Location` | 响应 | 创建资源时建议 | 指向新创建资源 | @@ -144,20 +147,21 @@ - 受保护接口使用 `Authorization: Bearer `。 - JWT 至少包含用户 ID、角色、`jti`、过期时间和账号令牌版本;不得包含密码、余额、完整手机号等敏感业务数据。 - API 依次校验签名、Issuer、Audience、有效期、撤销状态、账号状态和账号令牌版本。 -- Token 缺失、格式错误、签名无效、过期、已撤销、账号被禁用或版本失效时返回 `401 Unauthorized`。 +- Token 缺失、格式错误、签名无效、过期、已撤销或版本失效时返回 `401 Unauthorized`;凭据有效但账号状态为 `Disabled` 时返回 `403 AUTH.ACCOUNT_DISABLED`。 - 退出接口只使当前 Token 失效时,应以 `jti` 为范围执行;是否退出全部设备由专用接口另行定义。 +- 撤销状态、账号状态、手机号变更或全部旧凭证失效事实无法安全确认时返回 `503 AUTH.TOKEN_SERVICE_UNAVAILABLE`,不得先读取或修改受保护资源。 #### 1.6.2 Policy 与资源授权 - 角色策略保持 `BuyerOnly`、`MerchantOnly`、`AdminOnly`;接口表中以 `/` 连接多个策略时表示一个角色授权要求接受其中任一角色,不得在 ASP.NET Core 中叠加多个 Policy 导致逻辑变成 AND。 -- 公开接口在清单中统一标记为 `Anonymous`,对应 OpenAPI `security: []`,不要求 JWT;`Anonymous` 只表示未强制认证,不是持久化角色或授权 Policy。 -- 刷新访问令牌以有效刷新令牌作为认证凭据,不要求访问令牌仍在有效期内;刷新令牌本身必须校验过期、撤销、账号状态和账号令牌版本。 -- 模拟支付回调不使用用户 JWT,通过 `X-Callback-Signature` 的 HMAC 签名认证渠道,请求幂等仍由 `Idempotency-Key` 与 PostgreSQL 唯一约束保证。 +- 公开接口在清单中统一标记为 `Anonymous`,对应 OpenAPI `security: []`,不要求 JWT;`Anonymous` 只表示未强制认证,不是持久化角色或授权 Policy。A101、A102、A103、A140、A226、A227 等公开读取即使收到过期、无效或账号已禁用的 Bearer Token,也按游客口径处理并返回同一公开字段,不把可选凭据错误升级为 401/403。 +- 本期只签发一个 JWT,不提供刷新令牌、会话或刷新入口;A005 保留为历史取消编号。 +- 模拟支付回调不使用用户 JWT,通过时间戳、密钥版本和 `X-Callback-Signature` 的 HMAC 签名认证渠道,请求幂等由 `callbackId + 规范请求指纹` 与 PostgreSQL 唯一约束保证,不接收独立 `Idempotency-Key`。 - 已认证但角色不满足 Policy 时返回 `403 Forbidden`。 - 角色正确不代表可以访问任意资源。订单、地址、购物车、消息、评价、售后等接口还必须校验资源归属和业务数据范围。 - 管理员权限只覆盖已明确的平台治理能力,不自动获得查看任意用户私人订单、地址、钱包或消息的权限。 - 商家只能操作其被授权的数据范围,不得通过修改路径 ID 或请求体访问其他商家数据。 -- 为减少资源枚举风险,涉及私人资源时可根据安全需要统一返回 `404`;具体接口必须在详细定义中固定行为,不能同一接口随机返回 `403/404`。 +- 私人资源统一遵守:角色不符返回 `403`;资源不存在或不属于当前买家/当前 `assignedMerchantUserId` 返回 `404`,避免泄露存在性。详细定义不得改回随机 `403/404`。 - 前端路由守卫和隐藏按钮只改善体验,不能代替服务端 Policy、归属校验和状态校验。 ### 1.7 成功响应格式 @@ -379,7 +383,6 @@ AFTER_SALES.INVALID_STATUS - 提交订单; - 模拟充值; - 模拟支付; -- 支付回调; - 退款入账; - 秒杀下单; - 其他重复执行会造成资金、库存或状态副作用的接口。 @@ -388,10 +391,13 @@ AFTER_SALES.INVALID_STATUS - 客户端生成 UUID 作为幂等键,并在不确定首次请求结果时使用原键重试。 - 服务端幂等范围至少包含“已认证用户或可信调用方 + 接口/业务动作 + 幂等键”。 +- 完成身份、Header 和固定字段格式校验后,必须先查询持久化幂等结果,再读取库存、状态、资格、余额、限购、截止时间等会变化的事实;同键同指纹直接重放首次确定结果。 - 同一范围、同一幂等键、相同请求内容重复提交时,不重复执行副作用,返回首次已确认结果。 - 同一幂等键对应不同请求内容时返回 `409` 和 `IDEMPOTENCY.KEY_REUSED`。 +- 已正式返回的成功和确定业务失败都应按具体接口保存为可重放结果;依赖中断、事务提交未知等瞬态结果不得伪装为确定失败或固化。 - 幂等记录的保留时间、唯一约束和响应重放范围由具体接口定义;资金、订单等关键结果不能只依赖短期内存缓存。 - 客户端按钮置灰、防抖只能改善体验,不能替代服务端幂等、唯一约束和状态条件。 +- A421 是唯一例外:完成 HMAC 与固定字段校验后,以 `callbackId + 规范请求指纹` 查询已处理结果;同 ID 不同指纹拒绝,同一支付流水允许不同 callbackId 表达乱序事实。 #### 1.12.2 并发与状态竞争 @@ -403,7 +409,7 @@ AFTER_SALES.INVALID_STATUS ### 1.13 缓存与条件请求 - PostgreSQL 始终是业务事实来源;Redis 缓存不改变 API 契约和权限规则。 -- 商品列表、商品详情等只读接口可以使用服务端 Cache-Aside,但缓存命中和未命中的响应结构必须一致。 +- 只有 A102 的固定首页形态和 A103 商品自身公开详情可以使用 C07 Cache-Aside:固定首页必须无筛选、`page=1`、`pageSize=12`、`OnSale`、`createdAt desc, productId desc`;A103 不含评分、评价或个人字段。分类、普通列表、搜索/筛选、评价、秒杀及其他接口不得缓存。 - 用户私人数据、钱包、支付结果和敏感管理数据默认不得被共享 HTTP 缓存。 - 鉴权响应默认建议使用 `Cache-Control: no-store`;公开资源是否允许浏览器/CDN缓存由具体接口和部署方案决定。 - 商品改价、库存或上下架后,即使客户端仍持有旧展示数据,提交订单时也必须以服务端数据库最新校验为准。 @@ -433,20 +439,21 @@ AFTER_SALES.INVALID_STATUS ### 1.16 SignalR 与实时消息 - SignalR 只提高消息到达速度,PostgreSQL 中的站内消息和业务状态才是事实来源。 -- Hub 路径、连接鉴权、事件名称和载荷结构在消息接口进入详细设计时单独列出。 +- 当前 PC Web 只使用 WebSockets 并跳过协商,不启用 SSE、长轮询或会话亲和;Hub 路径、连接鉴权、事件名称和载荷结构在 4.3 固定。 - 连接身份来自有效 JWT,服务端不接受客户端自行声明接收用户 ID、角色或消息组。 -- 推送载荷应包含消息 ID、类型、创建时间和安全跳转信息;客户端收到推送后按需调用 HTTP API 获取最新详情,不直接据此修改订单最终状态。 -- 客户端断线重连后查询未读数和消息列表补偿,不要求服务端无限重放全部实时事件。 +- 推送载荷只提供消息 ID、类型、创建时间和安全跳转提示;客户端收到后先调用 A503 校正权威未读数,再按需调用 A501/A502,不做 `badge + 1`,也不直接据此修改订单最终状态。 +- 客户端固定按立即、2 秒、5 秒、10 秒重连,四次失败后暂停;重连后查询未读数和消息列表补偿,不要求服务端重放历史实时事件。 - 多 API 实例通过 Redis Backplane 共享实时通道,但 Redis 不保存唯一消息事实。 +- 建连、重连和存续期间都校验账号状态与撤销事实;退出、JWT 到期、手机号修改、账号禁用、全部旧凭证失效或安全事实无法确认时关闭既有连接。 - Hub 错误不得泄漏内部异常;需要用户处理的稳定业务失败通过 HTTP ProblemDetails 表达。 ### 1.17 安全、隐私、CORS 与日志 - 集成、演示和发布环境只通过 HTTPS 暴露 API,不允许明文传输 Token、密码和个人资料。 -- 密码、完整 Token、数据库连接密码、支付敏感内容不得出现在 URL、Query、响应、日志、Trace 或错误详情中。 +- 除 4.3.7 明确限定的 `/hubs/messaging` WebSocket 握手 `access_token` Query 例外外,密码、完整 Token、数据库连接密码、支付敏感内容不得出现在 URL、Query、响应、日志、Trace 或错误详情中;该唯一握手例外也必须在 Nginx、ASP.NET Core、Serilog 和 Trace 中完整脱敏。 - 手机号、地址等个人数据只返回当前页面和身份真正需要的字段;列表摘要不得默认返回完整敏感资料。 - CORS允许源来自环境配置白名单,不使用任意源与凭据的危险组合。 -- 当前 JWT 通过 Authorization Header 传递,不依赖跨站 Cookie;未来若改用 Cookie,必须另行设计 CSRF 防护。 +- 当前 HTTP API 的 JWT 通过 Authorization Header 传递;只有 4.3.7 WebSocket 握手使用受控 Query,不依赖跨站 Cookie;未来若改用 Cookie,必须另行设计 CSRF 防护。 - 请求 DTO 使用字段白名单,输出 DTO 隐藏密码哈希、内部审计字段、删除标记和不应公开的外键。 - 手写 SQL 必须参数化;排序和筛选字段必须使用白名单映射。 - 日志记录时间、Trace、接口、状态码、耗时、模块和允许的业务标识;用户 ID 在合规范围内记录,敏感字段脱敏。 @@ -558,7 +565,7 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 1. 一个有效 HTTP 方法与路径组合占用一个接口编号;同一路径使用不同方法时分别编号。 2. 编号合入 `dev` 后保持稳定。接口重命名但业务含义不变时保留编号;取消、废弃或转为内部契约后仍保留原编号,且不得复用。 -3. SignalR 事件、领域事件、集成事件、内部应用契约、Redis Key、RabbitMQ 资源和对象存储路径不新增 Axxx;A431 已取消并仅保留历史编号。 +3. SignalR 事件、领域事件、集成事件、内部应用契约、Redis Key、RabbitMQ 资源和对象存储路径不新增 Axxx;全部历史取消编号集中在 4.1,永不复用。 4. 不得为了占满区间提前设计无需求依据的接口。 5. 只有本文件中同时具备清单、同编号详细定义且状态为“已确认”的有效 HTTP 接口,才可作为实现与 OpenAPI 的冻结事实源。 @@ -568,11 +575,11 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 | 负责人 | 个人接口文件 | 追踪编号 | 有效 HTTP | 接口编号范围 | |---|---|---:|---:|---| -| 唐宇昊 | [`interface-tyh.md`](interface/interface-tyh.md) | 25 | 25 | `A001~A100` | -| 顾欣月 | [`interface-gxy.md`](interface/interface-gxy.md) | 22 | 22 | `A101~A200` | +| 唐宇昊 | [`interface-tyh.md`](interface/interface-tyh.md) | 25 | 22 | `A001~A100` | +| 顾欣月 | [`interface-gxy.md`](interface/interface-gxy.md) | 23 | 22 | `A101~A200` | | 朱惠惠 | [`interface-zhh.md`](interface/interface-zhh.md) | 19 | 17 | `A201~A300` | | 韦乾强 | [`interface-wqq.md`](interface/interface-wqq.md) | 8 | 8 | `A301~A400` | -| 张海洋 | [`interface-zhy.md`](interface/interface-zhy.md) | 26 | 24 | `A401~A500` | +| 张海洋 | [`interface-zhy.md`](interface/interface-zhy.md) | 27 | 23 | `A401~A500` | | 罗皓晨 | [`interface-lhc.md`](interface/interface-lhc.md) | 7 | 7 | `A501~A600` | 保留与同步规则: @@ -581,7 +588,7 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 2. 本文件是实现、OpenAPI、联调和测试的唯一接口事实源;个人原稿与本文件冲突时,不得直接按个人原稿编码。 3. 负责人修正个人原稿时,必须在同一任务中同步本文件的统一清单和同编号详细定义;只修改个人原稿不构成契约变更完成。 4. 总文档不得掩盖个人原稿中的缺口。尚未确认的字段、状态、跨模块边界或 DBxxx 必须标记为“部分定义”或“待交叉评审”。 -5. 当前共保留 107 个不重复 Axxx 追踪编号,其中 103 个是有效 HTTP 契约;A229、A230、A418、A431 均为已取消历史编号。有效接口的编号、`operationId` 和“方法 + 路径”未发现全局重复。 +5. 当前共保留 109 个不重复 Axxx 追踪编号,其中 99 个是活动 HTTP 定义;A005、A009、A023、A144、A229、A230、A418、A431、A432、A433 为历史取消编号。活动接口的编号、`operationId` 和“方法 + 路径”未发现全局重复;正式冻结数量仍以“已确认”状态另行统计,不能把“未取消”写成“已确认”。 ### 2.3 统一接口登记 @@ -593,11 +600,11 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 | A002 | Identity | F02 | 登录 | POST | `/api/auth/login` | `Identity_Login` | Anonymous | 待交叉评审 | | A003 | Identity | F02 | 退出当前令牌 | POST | `/api/auth/logout` | `Identity_Logout` | BuyerOnly / MerchantOnly / AdminOnly | 待交叉评审 | | A004 | Identity | F02 | 获取当前用户 | GET | `/api/auth/me` | `Identity_GetCurrentUser` | BuyerOnly / MerchantOnly / AdminOnly | 待交叉评审 | -| A005 | Identity | F02 | 刷新访问令牌 | POST | `/api/auth/refresh-token` | `Identity_RefreshToken` | 有效刷新令牌 | 待交叉评审 | +| A005 | Identity | F02 | 刷新访问令牌(取消) | — | — | — | — | 已取消,历史占号 | | A006 | Identity | F03 | 修改手机号 | POST | `/api/users/me/phone` | `Identity_ChangePhone` | BuyerOnly | 待交叉评审 | | A007 | Identity | F03 | 重置用户名 | POST | `/api/users/me/username/reset` | `Identity_ResetUsername` | BuyerOnly | 待交叉评审 | | A008 | Identity | F03 | 获取本人资料 | GET | `/api/users/me` | `Identity_GetMyProfile` | BuyerOnly | 待交叉评审 | -| A009 | Identity | F03 | 修改本人资料 | PATCH | `/api/users/me` | `Identity_UpdateMyProfile` | BuyerOnly | 待交叉评审 | +| A009 | Identity | F03 | 修改本人资料(取消) | — | — | — | — | 已取消,历史占号 | | A010 | Identity | F03 | 我的地址列表 | GET | `/api/users/me/addresses` | `Identity_ListMyAddresses` | BuyerOnly | 待交叉评审 | | A011 | Identity | F03 | 新增地址 | POST | `/api/users/me/addresses` | `Identity_CreateMyAddress` | BuyerOnly | 待交叉评审 | | A012 | Identity | F03 | 编辑地址 | PATCH | `/api/users/me/addresses/{addressId}` | `Identity_UpdateMyAddress` | BuyerOnly | 待交叉评审 | @@ -611,7 +618,7 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 | A020 | Engagement | X02 | 取消收藏 | DELETE | `/api/favorites/{productId}` | `Engagement_RemoveFavorite` | BuyerOnly | 待交叉评审 | | A021 | Engagement | X02 | 浏览历史列表 | GET | `/api/browsing-history` | `Engagement_ListBrowsingHistory` | BuyerOnly | 待交叉评审 | | A022 | Engagement | X02 | 修改浏览记录开关 | PATCH | `/api/browsing-history/settings` | `Engagement_UpdateBrowsingHistorySetting` | BuyerOnly | 待交叉评审 | -| A023 | Engagement | X02 | 清空浏览历史 | DELETE | `/api/browsing-history` | `Engagement_ClearBrowsingHistory` | BuyerOnly | 待交叉评审 | +| A023 | Engagement | X02 | 清空浏览历史(取消) | — | — | — | — | 已取消,历史占号 | | A024 | Engagement | X02 | 记录浏览历史 | POST | `/api/browsing-history/records` | `Engagement_RecordBrowsingHistory` | BuyerOnly | 待交叉评审 | | A025 | Engagement | X02 | 查询浏览记录开关 | GET | `/api/browsing-history/settings` | `Engagement_GetBrowsingHistorySetting` | BuyerOnly | 待交叉评审 | @@ -627,6 +634,7 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 | A112 | Catalog | M06-01-FR02 | 编辑分类 | PUT | `/api/merchant/categories/{categoryId}` | `Catalog_UpdateCategory` | MerchantOnly | 待交叉评审 | | A113 | Catalog | M06-01-FR02 | 启用分类 | POST | `/api/merchant/categories/{categoryId}/enable` | `Catalog_EnableCategory` | MerchantOnly | 待交叉评审 | | A114 | Catalog | M06-01-FR03 | 停用分类 | POST | `/api/merchant/categories/{categoryId}/disable` | `Catalog_DisableCategory` | MerchantOnly | 待交叉评审 | +| A115 | Catalog | M06-01-FR03 | 删除无引用分类 | DELETE | `/api/merchant/categories/{categoryId}` | `Catalog_DeleteCategory` | MerchantOnly | 待交叉评审 | | A120 | Catalog | M06-01-FR04 | 后台商品分页(全状态) | GET | `/api/merchant/products` | `Catalog_ListMerchantProducts` | MerchantOnly | 待交叉评审 | | A121 | Catalog | M06-01-FR04 | 后台商品详情 | GET | `/api/merchant/products/{productId}` | `Catalog_GetMerchantProduct` | MerchantOnly | 待交叉评审 | | A122 | Catalog | M06-01-FR05 | 新建商品 | POST | `/api/merchant/products` | `Catalog_CreateProduct` | MerchantOnly | 待交叉评审 | @@ -640,7 +648,7 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 | A141 | Review | M07-FR03 | 上传评价图片(提交前暂存) | POST | `/api/reviews/images` | `Review_UploadReviewImage` | BuyerOnly | 待交叉评审 | | A142 | Review | M07-FR04、FR05 | 提交商品评价(幂等) | POST | `/api/reviews` | `Review_CreateReview` | BuyerOnly | 待交叉评审 | | A143 | Review | M07-FR01 | 查询订单项评价资格/结果 | GET | `/api/reviews/eligibility` | `Review_GetReviewEligibility` | BuyerOnly | 待交叉评审 | -| A144 | Review | M07-FR06 | 单条公开评价详情查询 | GET | `/api/reviews/{reviewId}` | `Review_GetReview` | Anonymous | 待交叉评审 | +| A144 | Review | M07-FR06 | 单条公开评价详情查询(取消) | — | — | — | — | 已取消,历史占号 | #### 朱惠惠(A201~A300) @@ -697,7 +705,7 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 | A414 | AfterSales | M10-FR04 | 申请详情 | GET | `/api/after-sales/requests/{requestId}` | `AfterSales_GetRequest` | BuyerOnly/MerchantOnly | 待交叉评审 | | A415 | AfterSales | M10-FR10 | 撤销申请 | POST | `/api/after-sales/requests/{requestId}/cancel` | `AfterSales_CancelRequest` | BuyerOnly | 待交叉评审 | | A416 | AfterSales | M10-FR05 | 商家审核 | POST | `/api/after-sales/requests/{requestId}/audit` | `AfterSales_AuditRequest` | MerchantOnly | 待交叉评审 | -| A417 | AfterSales | M10-FR11 | 商家确认退货 | POST | `/api/after-sales/requests/{requestId}/confirm-return` | `AfterSales_ConfirmReturn` | MerchantOnly | 待交叉评审 | +| A417 | AfterSales | M10-FR11 | 商家确认收货 | POST | `/api/after-sales/requests/{requestId}/confirm-receipt` | `AfterSales_ConfirmReceipt` | MerchantOnly | 待交叉评审 | | A418 | AfterSales | M10-FR04 | 已取消:状态时间线并入 A414 | — | — | — | — | 已取消,历史占号 | | A419 | AfterSales | M10-FR07 | 退款失败重试 | POST | `/api/after-sales/requests/{requestId}/retry-refund` | `AfterSales_RetryRefund` | MerchantOnly | 待交叉评审 | | A421 | Payment | C08-FR01~FR05 | 接收支付回调 | POST | `/api/payment/callbacks` | `Payment_ReceiveCallback` | HMAC 签名(模拟渠道) | 待交叉评审 | @@ -705,9 +713,10 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 | A423 | Payment | C08-FR06 | 对账批次详情 | GET | `/api/admin/reconciliation/batches/{batchId}` | `Payment_GetReconciliationBatch` | AdminOnly | 待交叉评审 | | A424 | Payment | C08-FR07/FR08 | 差异列表 | GET | `/api/admin/reconciliation/batches/{batchId}/differences` | `Payment_ListReconciliationDifferences` | AdminOnly | 待交叉评审 | | A425 | Payment | C08-FR08 | 差异处理 | POST | `/api/admin/reconciliation/differences/{differenceId}/process` | `Payment_ProcessReconciliationDifference` | AdminOnly | 待交叉评审 | +| A426 | Payment | C08-FR07/FR08 | 差异详情 | GET | `/api/admin/reconciliation/differences/{differenceId}` | `Payment_GetReconciliationDifference` | AdminOnly | 待交叉评审 | | A431 | Payment | M10-FR07 | 已取消:退款 HTTP 改为 Payment 应用契约 | — | — | — | — | 已取消,历史占号 | -| A432 | Payment | M10-FR04 | 退款详情 | GET | `/api/refunds/{refundId}` | `Payment_GetRefund` | BuyerOnly/MerchantOnly | 待交叉评审 | -| A433 | Payment | M10-FR03 | 退款列表 | GET | `/api/refunds` | `Payment_ListRefunds` | BuyerOnly/MerchantOnly | 待交叉评审 | +| A432 | Payment | M10-FR04 | 退款详情(取消,结果并入 A414) | — | — | — | — | 已取消,历史占号 | +| A433 | Payment | M10-FR03 | 退款列表(取消,资金事实由支付记录承载) | — | — | — | — | 已取消,历史占号 | | A434 | AfterSales | M10-FR11 | 买家提交退货/寄回信息 | POST | `/api/after-sales/requests/{requestId}/return-info` | `AfterSales_SubmitReturnInfo` | BuyerOnly | 待交叉评审 | #### 罗皓晨(A501~A600) @@ -724,7 +733,7 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 ## 三、统一接口详细定义 -本章只收录 103 个有效 HTTP 契约。已取消编号和内部应用契约统一放在第四章,避免被误实现为公开端点。 +本章只收录 99 个活动 HTTP 定义。已取消编号和内部应用契约统一放在第四章,避免被误实现为公开端点;本章“活动”表示未取消,不等于已经完成正式冻结。 > 来源:[`interface-tyh.md`](interface/interface-tyh.md)。F03 已统一为买家专属;A024/A025 已闭合浏览记录写入与设置查询,仍待 DBxxx、OpenAPI 和交叉评审。 @@ -761,7 +770,7 @@ RegisterUserRequest { - `phone` 必须匹配 `^1[3-9]\d{9}$`,不接受 `+86`、`0086`、固话、空格。 - `password` 长度 8~16,必须同时包含字母和数字;不得等于 `phone`、不得等于 `phone` 倒序字符串。 - `confirmPassword` 必须等于 `password`。 - - 服务端忽略请求中任何尝试指定 `role`、`username`、`status` 的字段;公开注册结果固定为 Buyer。 + - 请求只允许 `phone`、`password`、`confirmPassword`;出现 `role`、`username`、`status`、`userId` 等越权字段时整次返回 `400 COMMON.VALIDATION_FAILED`,不得静默接受。 #### 成功响应 @@ -791,10 +800,10 @@ RegisteredUserResponse { #### 业务规则与并发 -- 用户名生成规则:`u_` + 8 位不易混淆字符(去除 0/O/1/I/L),最多重试 3 次;最终不重复。 +- 用户名生成规则:`u_` + 8 位不易混淆字符(去除 0/O/1/I/L),最多重试 3 次;最终不重复。生成后再次校验密码不等于用户名,不满足时拒绝注册。 - 密码使用可靠的自适应哈希保存,具体算法按系统架构与实现统一确定;明文密码、确认密码和哈希结果均不得出现在响应、日志或 ProblemDetails 中。 - 手机号唯一性由 PostgreSQL 唯一约束 `ux_users_phone` 保证;并发注册同一手机号时仅一笔成功,其余返回 `409 / AUTH.PHONE_ALREADY_REGISTERED`。 -- 公开注册固定产出 Buyer;客户端传入的角色字段被忽略,且不被任何后续接口读取。 +- 账号、固定 Buyer 角色、唯一用户名和默认头像必须在同一事务内创建;任一步失败不留部分账号。公开注册不接受客户端角色。 #### 缓存、事件或外部依赖 @@ -807,7 +816,7 @@ RegisteredUserResponse { - 7 位密码、纯字母、纯数字、与手机号相同、与手机号倒序相同 → 400 / `COMMON.VALIDATION_FAILED`。 - 两次密码不一致 → 400 / `COMMON.VALIDATION_FAILED`。 - 已注册手机号 → 409 / `AUTH.PHONE_ALREADY_REGISTERED`,不暴露其他用户资料。 -- 请求体注入 `role=Admin` → 忽略字段,最终账号仍为 Buyer。 +- 请求体注入 `role=Admin` → 400,且不创建账号。 - 并发注册同一手机号 → 仅一笔 201,另一笔 409。 ### A002 登录 @@ -820,7 +829,7 @@ RegisteredUserResponse { - 负责人:唐宇昊 - 关联数据表:DB001、DB004 - 当前状态:待交叉评审 -- 用途:用户使用手机号和密码登录,签发由任一 API 实例可验证的访问令牌与刷新令牌;登录结果在多实例间一致。 +- 用途:用户使用手机号和密码登录,签发一个由任一 API 实例可验证的 JWT;登录结果在多实例间一致。 - 方法与路径:`POST /api/auth/login` - operationId:`Identity_Login` @@ -849,8 +858,6 @@ LoginRequest { LoginResponse { accessToken: string accessTokenExpiresAt: string // UTC ISO 8601 - refreshToken: string - refreshTokenExpiresAt: string // UTC ISO 8601 tokenType: "Bearer" user: CurrentUserResponse } @@ -883,17 +890,17 @@ CurrentUserResponse { - 账号不存在和密码错误统一返回 `401 / AUTH.INVALID_CREDENTIALS`,不泄露账号是否存在。 - 禁用账号返回 `403 / AUTH.ACCOUNT_DISABLED` 并明确说明联系管理员。 - 签发的 JWT 至少包含 `sub`、`role`、`jti`、过期时间与 `tokenVersion`;签名、Issuer、Audience、过期、撤销、账号状态、版本号任一校验失败即拒绝。 -- 访问令牌和刷新令牌都包含独立 `jti`;仅退出、刷新轮换或账号禁用时把相应 `jti` 加入撤销集合,刚签发的有效令牌不得写入撤销集合。 +- JWT 使用独立 `jti`;仅退出、手机号修改、账号禁用或全部旧凭证失效时把相应 `jti`/账号令牌版本加入撤销事实,刚签发的有效令牌不得写入撤销集合。 - 当令牌服务或 Redis 撤销校验不可用时,宁可拒绝登录也不放过无法确认的请求(`503 / AUTH.TOKEN_SERVICE_UNAVAILABLE`)。 #### 缓存、事件或外部依赖 -- 登录成功后登记当前账号的 `tokenVersion` 和刷新令牌会话;`auth:revoked:*` 只保存已经撤销的令牌,不登记新签发令牌。 -- 不发布集成事件;用户级会话不持久化到数据库。 +- 登录成功返回账号当前 `tokenVersion` 对应的 JWT;`auth:revoked:*` 只保存已经撤销的令牌,不登记新签发令牌。 +- 不发布集成事件,不创建刷新令牌或服务端会话。 #### 验证场景 -- 正确买家账号 → 200,访问令牌 + 刷新令牌返回;切换 API 实例后同一令牌仍可通过 `A004` 校验。 +- 正确买家账号 → 200,只返回一个 JWT;切换 API 实例后同一令牌仍可通过 `A004` 校验。 - 错误密码 → 401 / `AUTH.INVALID_CREDENTIALS`。 - 不存在手机号 → 401 / `AUTH.INVALID_CREDENTIALS`,与错误密码文案一致。 - 禁用账号 → 403 / `AUTH.ACCOUNT_DISABLED`。 @@ -901,7 +908,7 @@ CurrentUserResponse { ### A003 退出当前令牌 -- 请求 Schema:无 +- 请求 Schema:无(Route) - 身份与 Policy:BuyerOnly / MerchantOnly / AdminOnly - 模块 / Tag:Identity @@ -909,7 +916,7 @@ CurrentUserResponse { - 负责人:唐宇昊 - 关联数据表:DB001、DB004 - 当前状态:待交叉评审 -- 用途:使当前访问令牌与刷新令牌在自然过期前不可继续使用;只影响本令牌,不影响同一账号其他设备。 +- 用途:使当前 JWT 在自然过期前不可继续使用;只影响本令牌,不影响同一账号其他设备。 - 方法与路径:`POST /api/auth/logout` - operationId:`Identity_Logout` @@ -941,13 +948,14 @@ LogoutResponse { #### 业务规则与并发 -- 当前令牌与刷新令牌均被加入 Redis 撤销集合;过期时间不晚于原令牌过期时间。 +- 当前 JWT 的 `jti` 被写入共享撤销事实,过期时间不晚于原 JWT 过期时间;没有刷新令牌需要处理。 +- 撤销事实无法确认写入时返回 `503`,不得向客户端报告退出成功。 - 同一账号在其他设备的有效令牌不受影响。 - 退出后前端必须清理本地令牌和登录态;后续 `A004` 使用已退出的令牌必须返回 `401 / AUTH.TOKEN_REVOKED`。 #### 缓存、事件或外部依赖 -- Redis Key:`auth:revoked:{jti}`、`auth:revoked:refresh:{jti}`。 +- Redis Key:`auth:revoked:{jti}`。 #### 验证场景 @@ -986,8 +994,9 @@ LogoutResponse { |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少访问令牌 | | 401 | `AUTH.TOKEN_EXPIRED` | 访问令牌已过期 | -| 401 | `AUTH.TOKEN_REVOKED` | 访问令牌已退出、账号已禁用或令牌版本失效 | -| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态校验不可用 | +| 401 | `AUTH.TOKEN_REVOKED` | 访问令牌已退出或令牌版本失效 | +| 403 | `AUTH.ACCOUNT_DISABLED` | 凭据有效但账号已禁用 | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销、账号状态或旧凭证失效事实无法确认 | #### 业务规则与并发 @@ -1004,58 +1013,6 @@ LogoutResponse { - 过期令牌 → 401 / `AUTH.TOKEN_EXPIRED`。 - 已退出令牌 → 401 / `AUTH.TOKEN_REVOKED`。 -### A005 刷新访问令牌 - -- 请求 Schema:RefreshTokenRequest -- 身份与 Policy:有效刷新令牌 - -- 模块 / Tag:Identity -- 需求编号:F02、M01-02 -- 负责人:唐宇昊 -- 关联数据表:DB001、DB004 -- 当前状态:待交叉评审 -- 用途:使用有效刷新令牌换取新的访问令牌和刷新令牌。 -- 方法与路径:`POST /api/auth/refresh-token` -- operationId:`Identity_RefreshToken` - -#### 请求 - -- Body: - -```text -RefreshTokenRequest { - refreshToken: string // 必填 -} -``` - -#### 成功响应 - -- HTTP 状态:`200 OK` -- 响应 Schema:`LoginResponse`(与 A002 一致) - -#### 失败响应 - -| HTTP 状态 | 业务错误码 | 触发条件 | -|---:|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 缺少刷新令牌 | -| 401 | `AUTH.TOKEN_REVOKED` | 刷新令牌已撤销、账号已禁用或令牌版本失效 | -| 401 | `AUTH.TOKEN_EXPIRED` | 刷新令牌已过期 | -| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态共享不可用 | - -#### 业务规则与并发 - -- 旧刷新令牌随新令牌签发一起撤销,避免长期重放。 -- 旧刷新令牌加入撤销集合;新访问令牌和新刷新令牌保持有效,不得误写入撤销集合。 - -#### 缓存、事件或外部依赖 - -- Redis Key:`auth:revoked:refresh:{jti}`。 - -#### 验证场景 - -- 有效刷新令牌 → 200,返回新令牌;旧刷新令牌再次使用返回 401。 -- 过期刷新令牌 → 401 / `AUTH.TOKEN_EXPIRED`。 - ### A006 修改手机号 - 请求 Schema:ChangePhoneRequest @@ -1094,13 +1051,15 @@ ChangePhoneRequest { | 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或新手机号格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 401 | `AUTH.INVALID_CREDENTIALS` | 当前密码错误 | -| 401 | `AUTH.TOKEN_REVOKED` | 账号已禁用或访问令牌版本失效 | +| 401 | `AUTH.TOKEN_REVOKED` | 访问令牌版本失效 | +| 403 | `AUTH.ACCOUNT_DISABLED` | 当前账号已禁用 | | 409 | `AUTH.PHONE_ALREADY_REGISTERED` | 新手机号已被他人使用 | | 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态共享不可用,无法签发新令牌 | #### 业务规则与并发 -- 修改成功后:账号令牌版本号 +1,Redis 中该用户全部未过期令牌记录按版本失效;当前访问令牌立即失效。 +- 使用“当前手机号/账号版本仍等于读取值”的条件更新处理并发;手机号变更、账号令牌版本号 +1 和全部旧凭证失效结果形成一个确定提交。并发请求至多一个成功。 +- 修改成功后当前及其他旧 JWT 立即失效;撤销事实无法可靠建立时整次不提交手机号变更。 - 成功后强制要求重新登录;前端需要清理本地登录态。 - 新手机号唯一性由 PostgreSQL 唯一约束 `ux_users_phone` 保证。 @@ -1159,8 +1118,8 @@ ResetUsernameResponse { #### 业务规则与并发 -- 重置次数记录在 `users.username_reset_count`,重置后置为 1;再次调用返回 `409 / AUTH.USERNAME_RESET_EXHAUSTED`。 -- 用户名生成规则与 A001 一致;并发重置时通过乐观更新保证只成功一次。 +- 重置次数记录在账号事实中;只有新用户名通过唯一约束并成功保存时才把机会消耗为 1,生成或提交失败不能吃掉机会。 +- 用户名生成规则与 A001 一致;并发重置时通过同一条件更新保证最多一个成功。 #### 缓存、事件或外部依赖 @@ -1202,8 +1161,6 @@ MyProfileResponse { username: string phoneMasked: string avatarUrl: string - displayName: string? - bio: string? role: "Buyer" canResetUsername: boolean // 是否仍可自助重置用户名 createdAt: string @@ -1215,7 +1172,8 @@ MyProfileResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 401 | `AUTH.TOKEN_REVOKED` | 账号已禁用或访问令牌版本失效 | +| 401 | `AUTH.TOKEN_REVOKED` | 访问令牌版本失效 | +| 403 | `AUTH.ACCOUNT_DISABLED` | 当前账号已禁用 | | 404 | `RESOURCE.NOT_FOUND` | 当前用户记录不存在 | #### 业务规则与并发 @@ -1233,61 +1191,6 @@ MyProfileResponse { - 已禁用账号的旧令牌 → 401 / `AUTH.TOKEN_REVOKED`。 - 商家或管理员调用 → 403 / `AUTH.FORBIDDEN`。 -### A009 修改本人资料 - -- 请求 Schema:UpdateMyProfileRequest -- 身份与 Policy:BuyerOnly - -- 模块 / Tag:Identity -- 需求编号:F03、M01-03 -- 负责人:唐宇昊 -- 关联数据表:DB001 -- 当前状态:待交叉评审 -- 用途:买家维护本人展示资料;手机号与用户名变更走专门接口,本接口不接受这两类字段。 -- 方法与路径:`PATCH /api/users/me` -- operationId:`Identity_UpdateMyProfile` - -#### 请求 - -- Header:`Authorization: Bearer `(必填) -- Body: - -```text -UpdateMyProfileRequest { - displayName?: string // 可选,昵称或展示名 - bio?: string // 可选,简介,0~200 字 -} -``` - -#### 成功响应 - -- HTTP 状态:`200 OK` -- 响应 Schema:`MyProfileResponse` - -#### 失败响应 - -| HTTP 状态 | 业务错误码 | 触发条件 | -|---:|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 字段长度或格式错误 | -| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 401 | `AUTH.TOKEN_REVOKED` | 账号已禁用或访问令牌版本失效 | -| 400 | `COMMON.VALIDATION_FAILED` | 请求包含不允许修改的 `phone`、`username`、`role`、`status`、`userId` 或 `avatarUrl` | - -#### 业务规则与并发 - -- 不允许修改字段:`phone`、`username`、`role`、`status`、`userId`;这些字段变更必须通过专门接口。 -- `avatarUrl` 仅允许在系统默认范围内设置;本期不支持自定义上传。 - -#### 缓存、事件或外部依赖 - -- 不缓存、不发布事件。 - -#### 验证场景 - -- 修改 `displayName` → 200,返回最新资料。 -- 提交 `phone` 字段 → 409 / `COMMON.VALIDATION_FAILED`,字段被忽略。 -- 提交 `role=Admin` → 409,不修改角色。 - ### A010 我的地址列表 - 请求 Schema:无 @@ -1371,7 +1274,6 @@ CreateAddressRequest { city: string // 必填,城市名称 district: string // 必填,区/县名称 detail: string // 必填,详细地址 5~120 字 - isDefault: boolean? // 可选;true 时将其他默认地址取消 } ``` @@ -1403,12 +1305,10 @@ AddressResponse { | 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | -| 409 | `IDENTITY.ADDRESS_LIMIT_REACHED` | 本人地址数量已达到 20 条上限 | #### 业务规则与并发 -- `isDefault=true` 时在同一事务内将其他地址的 `is_default` 置 false;同一用户最多一个默认地址。 -- 单用户地址上限暂定 20 条;超出时返回 409 / `IDENTITY.ADDRESS_LIMIT_REACHED`。 +- 新增地址固定 `isDefault=false`;默认地址只能通过 A014 显式设置。本期不增加需求外的固定地址数量上限。 #### 缓存、事件或外部依赖 @@ -1416,8 +1316,8 @@ AddressResponse { #### 验证场景 -- 合法地址 + `isDefault=false` → 201。 -- 合法地址 + `isDefault=true` 且已有默认地址 → 201,旧默认地址自动取消。 +- 合法地址 → 201,返回 `isDefault=false`。 +- 请求携带 `isDefault` → 400,不修改既有默认地址。 ### A012 编辑地址 @@ -1437,7 +1337,7 @@ AddressResponse { - Route 参数:`addressId: uuid` - Header:`Authorization: Bearer `(必填) -- Body:与 `CreateAddressRequest` 一致,所有字段可选,但至少传一个。 +- Body:`recipientName`、`phone`、`province`、`city`、`district`、`detail` 均可选但至少传一个;不接受 `isDefault`。 #### 成功响应 @@ -1452,6 +1352,7 @@ AddressResponse { | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `RESOURCE.NOT_FOUND` | 地址不存在或不属于当前用户 | +| 404 | `RESOURCE.NOT_FOUND` | 地址不存在或不属于当前用户 | #### 业务规则与并发 @@ -1501,7 +1402,7 @@ AddressResponse { #### 业务规则与并发 - 删除默认地址后不自动指定其他默认地址;下单时由买家明确确认。 -- 幂等:本人地址不存在、已删除或不属于当前买家时统一返回 204,不泄露地址是否存在或归属;历史订单使用地址快照,不阻止删除当前地址记录。 +- 历史订单使用地址快照,不阻止删除当前地址记录;地址不存在、已删除或不属于当前买家统一返回 404,不泄露归属。 #### 缓存、事件或外部依赖 @@ -1511,7 +1412,7 @@ AddressResponse { - 删除非默认地址 → 204,列表更新。 - 删除默认地址 → 204,列表无默认地址标记。 -- 重复删除或传入他人地址 ID → 204,不泄露资源归属。 +- 重复删除或传入他人地址 ID → 404,不泄露资源归属。 ### A014 设置默认地址 @@ -1579,7 +1480,7 @@ AddressResponse { - `page`(默认 1) - `pageSize`(默认 10,上限 50) - `role`(可选,`Buyer` / `Merchant`;不传表示全部非管理员账号) - - `status`(可选,`Active` / `Disabled`) + - `status`(可选,`Normal` / `Disabled`) - `keyword`(可选,对用户名或手机号做模糊匹配) #### 成功响应 @@ -1611,6 +1512,7 @@ AdminUserListResponse { - 列表响应只返回管理操作所需字段;不返回密码哈希、内部审计、登录态。 - 手机号使用掩码 `138****8888` 形式。 - `AdminUserResponse.isDefaultMerchant` 仅用于说明单店默认运营账号及禁用按钮原因;买家固定为 `false`。 +- 默认按 `createdAt desc, userId desc` 稳定分页;相同注册时间不得导致翻页重复或遗漏。 #### 缓存、事件或外部依赖 @@ -1639,7 +1541,7 @@ AdminUserListResponse { #### 请求 - Route 参数:`userId: uuid` -- Header:`Authorization: Bearer `(必填,角色 Admin) +- Header:`Authorization: Bearer `、`Idempotency-Key: `(均必填,角色 Admin) - Body:无。 #### 成功响应 @@ -1654,7 +1556,7 @@ AdminUserResponse { phoneMasked: string role: "Buyer" | "Merchant" isDefaultMerchant: boolean - status: "Active" | "Disabled" + status: "Normal" | "Disabled" updatedAt: string } ``` @@ -1672,11 +1574,13 @@ AdminUserResponse { #### 业务规则与并发 -- 条件更新:仅当目标为 `Active` 时改为 `Disabled` 并提升 `tokenVersion`;目标已是 `Disabled` 时返回当前禁用结果,不再次提升版本号。 +- 完成身份、路径与幂等键格式校验后先查询持久化结果;同键同目标重放,同键换目标返回 `409 IDEMPOTENCY.KEY_REUSED`。 +- 条件更新:仅当目标为 `Normal` 时改为 `Disabled` 并提升 `tokenVersion`;目标已是 `Disabled` 时返回并绑定当前禁用结果,不再次提升版本号。 - Identity 必须配置且最多只能有一个 `isDefaultMerchant=true` 的启用商家账号;该账号负责普通订单默认归属,本期 A016 不提供默认账号迁移能力,因此直接禁用返回 409。 -- 禁用其他商家前,通过 Ordering、AfterSales、Seckill 公开应用契约确认不存在待支付/待履约订单、仍在售后期限内的订单、未完成售后申请或未结束活动;存在时拒绝禁用,不自动改写历史归属。 -- 禁用成功后通过 `auth:token-version:{userId}` 提升版本号;Redis 中保留的令牌记录按版本失效。 -- 状态变更可追踪:操作人、目标账号、原状态、新状态、时间、`traceId` 写入结构化日志;不写入通用操作审计。 +- 买家禁用不查询订单、支付、售后等业务责任,也不改写这些事实。 +- 禁用非默认商家前,通过 Ordering、AfterSales、Seckill 公开应用契约检查固定清单:`PendingPayment`;仍有可履约数量的 `Paid`;`Shipped`;完成后 7 天窗口内 `Completed`;任一非终态售后;任一未结束秒杀活动。已全量退款、无剩余可履约数量且无非终态售后的 `Paid` 不单独阻断。 +- 商家责任检查与新的普通/秒杀订单、售后责任接收形成唯一提交顺序;禁用后不得并发写入新责任。 +- 账号状态、全部旧凭证失效、追踪信息和幂等结果形成一个确定结果;任一部分无法确认时不返回成功。 #### 缓存、事件或外部依赖 @@ -1708,7 +1612,7 @@ AdminUserResponse { #### 请求 - Route 参数:`userId: uuid` -- Header:`Authorization: Bearer `(必填,角色 Admin) +- Header:`Authorization: Bearer `、`Idempotency-Key: `(均必填,角色 Admin) - Body:无 #### 成功响应 @@ -1726,8 +1630,8 @@ AdminUserResponse { #### 业务规则与并发 -- 条件更新:仅当目标为 `Disabled` 时改为 `Active`;目标已是 `Active` 时返回当前正常结果。 -- 启用不改变 `tokenVersion`;禁用前签发的旧令牌仍不可用,需重新登录。 +- 先查询持久化幂等结果;条件更新仅当目标为 `Disabled` 时改为 `Normal`,目标已是 `Normal` 时返回并绑定当前正常结果。 +- 启用不回退 `tokenVersion`;禁用前签发的旧令牌仍不可用,需重新登录。 #### 缓存、事件或外部依赖 @@ -1784,7 +1688,8 @@ FavoriteListResponse { - 严格按 `user_id = current_user_id` 过滤;不允许跨用户访问。 - 排序白名单仅 `createdAt`,方向 `asc` / `desc`;非法字段返回 400。 -- 收藏商品摘要来自 Catalog 模块;若商品已下架仍展示记录但标记不可购买。 +- 默认按 `createdAt desc, favoriteId desc` 稳定分页。 +- 收藏商品摘要来自 Catalog 模块;商品已下架或防御性缺失时仍返回收藏占位,标记 `isAvailable=false`,不得丢失记录。 #### 缓存、事件或外部依赖 @@ -1844,8 +1749,8 @@ FavoriteResponse { #### 业务规则与并发 -- 使用 `(user_id, product_id)` 唯一约束保证幂等;重复收藏返回已存在记录。 -- 商品不存在时拒绝,不建立记录。 +- 先按 `(user_id, product_id)` 查询本人既有收藏;已存在时直接返回原记录,即使商品后来下架或防御性缺失。 +- 只有新建收藏时才要求商品存在且为 `OnSale`;唯一约束保证并发最多新增一条。 #### 缓存、事件或外部依赖 @@ -1945,8 +1850,8 @@ BrowsingHistoryListResponse { #### 业务规则与并发 -- 按 `user_id = current_user_id` 过滤;上限默认 200 条,超出后由清理任务移除最早记录。 -- 浏览历史开关关闭时返回空列表;调用 A022 可重新开启。 +- 按 `user_id = current_user_id` 过滤并默认使用 `viewedAt desc, browsingHistoryId desc` 稳定分页。 +- 开关关闭只阻止未来写入,不能隐藏或删除已有历史;下架或防御性缺失商品仍以不可用占位返回。 #### 缓存、事件或外部依赖 @@ -1954,7 +1859,7 @@ BrowsingHistoryListResponse { #### 验证场景 -- 关闭开关 → items=[]。 +- 关闭开关 → 既有历史仍正常返回,后续不新增或更新时间。 - 启用开关并访问商品 → 按时间倒序展示。 ### A022 修改浏览记录开关 @@ -2000,6 +1905,7 @@ UpdateBrowsingHistorySettingRequest { #### 业务规则与并发 - 关闭开关不影响已有浏览记录;重新开启后恢复写入。 +- 开关切换与 A024 浏览写入按数据库确定提交顺序竞争:关闭先提交则后续写入返回 `recorded=false`;写入先提交则本次记录保留,随后关闭只影响未来请求。 #### 缓存、事件或外部依赖 @@ -2010,50 +1916,6 @@ UpdateBrowsingHistorySettingRequest { - `enabled=false` → 200,后续访问商品不再写入历史。 - `enabled=true` → 200,重新开启写入。 -### A023 清空浏览历史 - -- 请求 Schema:无 -- 响应 Schema:无(204) -- 身份与 Policy:BuyerOnly - -- 模块 / Tag:Engagement -- 需求编号:X02、M08 -- 负责人:唐宇昊 -- 关联数据表:DB006 -- 当前状态:待交叉评审 -- 用途:买家清空本人浏览历史;不影响浏览记录开关状态。 -- 方法与路径:`DELETE /api/browsing-history` -- operationId:`Engagement_ClearBrowsingHistory` - -#### 请求 - -- Header:`Authorization: Bearer `(必填,角色 Buyer) - -#### 成功响应 - -- HTTP 状态:`204 No Content` - -#### 失败响应 - -| HTTP 状态 | 业务错误码 | 触发条件 | -|---:|---|---| -| 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | - -#### 业务规则与并发 - -- 按 `user_id = current_user_id` 物理删除;只影响当前用户。 -- 清空后再次浏览商品仍按当前开关决定是否写入。 - -#### 缓存、事件或外部依赖 - -- 不缓存。 - -#### 验证场景 - -- 清空本人浏览历史 → 204,后续列表为空。 -- 重复清空 → 204,幂等。 - ### A024 记录浏览历史 - 请求 Schema:RecordBrowsingHistoryRequest @@ -2103,14 +1965,14 @@ BrowsingHistoryResponse { | 401 | `AUTH.UNAUTHENTICATED` | 已登录用户令牌无效 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在或未上架 | -| 429 | `COMMON.RATE_LIMITED` | 同一买家短时间内高频记录浏览 | #### 业务规则与并发 - 浏览记录开关关闭时返回 `200`、`recorded=false`,不写入记录;这属于用户偏好,不是权限错误。 - 游客身份不持久化记录;本接口仅 BuyerOnly;前端在游客访问商品详情时引导登录,登录后再调用本接口。 - 同一买家同一商品只保留一条记录;按 `(user_id, product_id)` 唯一约束写入或更新,`viewedAt` 始终取服务端时间。 -- 默认单买家最多保留 200 条记录;超出时按 `viewed_at` 由小到大移除多余记录,并在 `trimmedCount` 返回本次清理数量。 +- 默认单买家最多保留 200 条记录;同一商品 upsert 与按 `viewedAt, browsingHistoryId` 裁剪最早记录必须在同一事务完成,并在 `trimmedCount` 返回本次清理数量。 +- A022 开关切换与本次写入由数据库提交顺序唯一裁决,不能先读开关后在其已关闭时继续写入。 - 新写入仅接受当前已上架商品;商品后来下架时保留既有历史记录,并由列表标记为不可购买。 #### 缓存、事件或外部依赖 @@ -2153,7 +2015,7 @@ BrowsingHistoryResponse { ```text BrowsingHistorySettingResponse { enabled: boolean - updatedAt: string + updatedAt: string? // 无持久化设置记录时为 null } ``` @@ -2166,7 +2028,7 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 -- 严格按 `user_id = current_user_id` 过滤;不存在记录时按 `enabled=true` 返回(默认开启)。 +- 严格按 `user_id = current_user_id` 过滤;不存在设置记录时直接按 `enabled=true, updatedAt=null` 返回,GET 不得为了默认值写数据库。 - 与 A022 配对:GET 读取当前值,PATCH 修改值;同一资源不重复定义写入入口。 #### 缓存、事件或外部依赖 @@ -2179,7 +2041,7 @@ BrowsingHistorySettingResponse { - 已通过 A022 关闭过 → 200,`enabled=false`。 - 商家账号调用 → 403。 -> 来源:[`interface-gxy.md`](interface/interface-gxy.md)。A144 已补齐单条公开评价读取;商品与评价图片顺序、公开字段和 PostgreSQL 搜索边界已统一,仍待 DBxxx、OpenAPI 和交叉评审。 +> 来源:[`interface-gxy.md`](interface/interface-gxy.md)。商品与评价图片顺序、公开字段和 PostgreSQL 搜索边界已按流程统一;A144 因没有独立业务入口转为历史取消号。 ### A101 购物端有效分类列表 @@ -2191,7 +2053,7 @@ BrowsingHistorySettingResponse { - 用途:为购物端商品筛选提供当前启用的分类,供列表页分类入口使用。 - 方法与路径:`GET /api/categories` - operationId:`Catalog_ListCategories` -- 请求 Schema:无(仅可选 Query) +- 请求 Schema:无 - 响应 Schema:`CategoryTreeResponse` - 身份与 Policy:允许游客访问;无需 JWT。 - 资源归属:公开数据,无归属校验。 @@ -2200,15 +2062,15 @@ BrowsingHistorySettingResponse { #### 请求 - Route 参数:无。 -- Query 参数:`includeEmpty`(boolean,可选,默认 `false`,是否包含暂无在架商品的启用分类)。 +- Query 参数:无。 - Header:`Accept: application/json`。 - Body:无。 -- 校验规则:只返回 `status = enabled` 的分类;停用分类不作为购物端筛选入口。 +- 校验规则:只返回 `status = Enabled` 的分类;停用分类不作为购物端筛选入口,也不改变其下商品的销售状态。 #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`CategoryTreeResponse`,`data.items` 为分类数组,字段含 `categoryId`、`name`、`parentId`(可空)、`sortOrder`、`productCount`。 +- 响应 Schema:`CategoryTreeResponse`,`data.items` 为分类数组,字段含 `categoryId`、`name`、`parentId`(可空)、`sortOrder`、`productCount`;`productCount` 只统计当前 `OnSale` 商品,不决定分类是否返回。 - 示例: ```json @@ -2236,11 +2098,11 @@ BrowsingHistorySettingResponse { #### 缓存、事件或外部依赖 -- 只读,可使用服务端 Cache-Aside;命中与未命中结构一致。商品或分类变更后由 M06-01 事务提交后触发缓存失效(C07 协作)。 +- A101 不进入 C07;每次按 PostgreSQL 已提交分类事实查询。分类停用后无需失效不存在的分类缓存。 #### 验证场景 -- 停用分类不出现在结果中;`includeEmpty=false` 时不返回无在架商品的分类。 +- 停用分类不出现在结果中;已启用但暂无 `OnSale` 商品的分类仍返回,`productCount=0`。 --- @@ -2279,7 +2141,7 @@ BrowsingHistorySettingResponse { - Header:`Accept: application/json`。 - Body:无。 -- 校验规则:`minPrice > maxPrice` 返回 `CATALOG.INVALID_PRICE_RANGE`;`sortBy` 非白名单返回 `CATALOG.INVALID_SORT_FIELD`;`keyword` 参数化处理,禁止拼接 SQL。 +- 校验规则:`minPrice > maxPrice` 返回 `CATALOG.INVALID_PRICE_RANGE`;`sortBy` 非白名单,或无 `keyword` 时请求 `sortBy=relevance`,返回 `CATALOG.INVALID_SORT_FIELD`;`keyword` 参数化处理,禁止拼接 SQL。 #### 成功响应 @@ -2311,14 +2173,16 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 -- 服务端始终附加“已上架(`Published`)”过滤;草稿、下架、已删除商品不得泄露。 +- 服务端始终附加“已上架(`OnSale`)”过滤;`Draft`、`OffSale` 和已物理删除商品不得泄露;不得因商品所属分类后来停用而额外隐藏仍为 `OnSale` 的商品。 - 空结果为正常结果,返回空数组与真实分页元数据。 - 稳定排序:业务排序字段相同时追加 `productId` 作为次级排序,避免翻页重复或遗漏。 - C04:`keyword` 存在时走 `IProductSearch` 分词/倒排实现并支持 `relevance` 排序;进阶不可用时在保证“已上架过滤 + 参数安全”的前提下降级为 `ILIKE` 基础模糊查询并记录降级原因,返回口径不变。 #### 缓存、事件或外部依赖 -- 依赖 Catalog 搜索能力契约 `IProductSearch`(C04)。列表可服务端缓存(C07 协作),商品变更事务提交后失效。 +- 依赖 Catalog 搜索能力契约 `IProductSearch`(C04),基础与进阶搜索都读取同一 PostgreSQL 商品事实。 +- A102 只有固定首页摘要形式可进入 C07:无 `keyword`、`categoryId`、价格和库存筛选,固定 `page=1&pageSize=12&sortBy=createdAt&sortOrder=desc`,并只返回 `OnSale` 商品摘要;其余普通列表、关键词、组合筛选和用户自选排序全部直读 PostgreSQL,不生成参数化缓存 Key。 +- 固定首页正常值 TTL 60 秒、空值 TTL 10 秒;事务提交后立即及 3 秒二次失效,双删失败时正常旧值最坏不超过提交后 62 秒、旧空值不超过 12 秒。缓存不可用时回退 PostgreSQL。 #### 验证场景 @@ -2349,7 +2213,7 @@ BrowsingHistorySettingResponse { - Query 参数:无。 - Header:`Accept: application/json`。 - Body:无。 -- 校验规则:`productId` 格式校验;仅返回已上架(`Published`)商品。 +- 校验规则:`productId` 格式校验;仅返回已上架(`OnSale`)商品;所属分类停用不影响已上架商品公开。 #### 成功响应 @@ -2375,18 +2239,20 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| | 400 | `COMMON.VALIDATION_FAILED` | `productId` 格式非法 | -| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | -| 404 | `CATALOG.PRODUCT_UNAVAILABLE` | 商品已下架、已删除或当前不可公开 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在或已物理删除 | +| 404 | `CATALOG.PRODUCT_UNAVAILABLE` | 商品为 `Draft`、`OffSale` 或当前不可公开 | #### 业务规则与并发 - 价格、库存、状态以服务端最新数据为准,前端缓存不得作为下单依据。 - 商品描述按受控内容返回,不含脚本;不返回内部备注或未公开状态字段。 - 已下架商品旧链接返回 404,不提供购买操作;历史订单快照不受影响(由 Ordering 保存)。 +- `OnSale` 且库存为 0 的商品仍返回 200,`stockStatus=SoldOut`,前端禁用加购和购买入口;库存为 0 不自动下架。 #### 缓存、事件或外部依赖 -- 图片 `url` 由对象存储(S3 兼容 / SeaweedFS)受控访问地址提供;详情可缓存,商品变更后失效。 +- 图片 `url` 由对象存储(S3 兼容 / SeaweedFS)受控访问地址提供。 +- A103 只缓存商品自身公开字段,不包含 M07 评价、评分、收藏、购物车或任何身份化字段;正常值 TTL 60 秒、不可公开空值 TTL 10 秒。相关商品事务提交后立即失效并在 3 秒后对同一 Key 二次失效;双删失败时正常旧值最坏不超过提交后 62 秒、旧空值不超过 12 秒,缓存不可用时回退 PostgreSQL。 #### 验证场景 @@ -2407,7 +2273,7 @@ BrowsingHistorySettingResponse { - 请求 Schema:无(Query) - 响应 Schema:`MerchantCategoryListResponse` - 身份与 Policy:MerchantOnly。 -- 资源归属:本期不做多商家分类隔离;商家可见平台分类。管理员/买家/游客调用返回 403。 +- 资源归属:本期不做多商家分类隔离;商家可见平台分类。游客未认证返回 401,已认证买家或管理员返回 403。 - 幂等要求:只读,天然幂等。 #### 请求 @@ -2434,6 +2300,7 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 - 与购物端 A101 分开:本接口返回全状态分类,购物端只返回启用分类。 +- 排序固定按 `sortOrder asc, categoryId asc`。 #### 缓存、事件或外部依赖 @@ -2466,14 +2333,14 @@ BrowsingHistorySettingResponse { - Route 参数:无。 - Query 参数:无。 - Header:`Authorization`、`Content-Type: application/json`。 -- Body:`CreateCategoryRequest`:`name`(string,必填,1~30,去首尾空白)、`parentId`(uuid,可空,最多一层)、`sortOrder`(integer,可选,默认 0,≥ 0)。 -- 校验规则:同一父级下 `name` 唯一;`parentId` 必须存在且为顶级分类(避免超过一层)。 +- Body:`CreateCategoryRequest`:`name`(string,必填,1~30,去首尾空白)、`parentId`(uuid,可空,最多一层)、`sortOrder`(integer,可选,默认 0,≥ 0)、`status`(string,必填,`Enabled`/`Disabled`)。 +- 校验规则:同一父级下 `name` 唯一;`parentId` 必须存在且为顶级分类(避免超过一层);`status` 只接受已确认的两态。 #### 成功响应 - HTTP 状态:`201 Created` -- 响应 Schema:`MerchantCategoryResponse`;`Location` 指向新分类。 -- 示例:`data` 含新 `categoryId` 与回显字段。 +- 响应 Schema:`MerchantCategoryResponse`;本期没有单条分类读取接口,因此不返回不可解析的 `Location`。 +- 示例:`data` 含新 `categoryId`,并回显 `name`、`parentId`、`sortOrder`、`status`。 #### 失败响应 @@ -2491,7 +2358,7 @@ BrowsingHistorySettingResponse { #### 缓存、事件或外部依赖 -- 提交后触发购物端分类缓存失效。 +- A101 分类列表不缓存;新建分类不触发 C07。分类名称与检索事实不存在旧引用时无需额外传播。 #### 验证场景 @@ -2538,10 +2405,11 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 - 禁止形成环或超过一层层级。 +- 商品或历史引用不阻止分类名称、父级和排序等元数据编辑;`status` 不在 A112 修改,启用/停用分别走 A113/A114。 #### 缓存、事件或外部依赖 -- 提交后失效购物端分类缓存与相关商品列表缓存。 +- A101 和普通商品列表不缓存。分类名称、层级或展示信息变更与关联商品检索文本在同一 PostgreSQL 事实中同步提交;若 A103 或固定首页摘要包含受影响的分类展示字段,事务提交后按 C07 精确失效相应商品详情和固定首页 Key。 #### 验证场景 @@ -2587,7 +2455,7 @@ BrowsingHistorySettingResponse { #### 缓存、事件或外部依赖 -- 状态变更后失效购物端分类缓存。 +- A101 不缓存;启用分类不自动改变其下商品状态,也不触发普通列表/搜索缓存失效。 #### 验证场景 @@ -2630,11 +2498,11 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 - 停用分类不做物理删除,不影响历史引用;停用后 A101/A102 不再以其作为筛选入口。 -- 停用分类不能用于新建或上架商品(见 A122、A125)。 +- 停用不改变其下既有商品销售状态:已有 `OnSale` 商品继续在全部商品、关键词搜索和详情公开,并可修改不改变分类归属的字段;新建、改绑分类,以及 `Draft`/`OffSale` 商品重新上架时才要求分类为 `Enabled`(见 A122、A123、A125)。 #### 缓存、事件或外部依赖 -- 状态变更后失效购物端分类缓存。 +- A101 不缓存;停用分类不自动改变其下商品状态,也不触发普通列表/搜索缓存失效。 #### 验证场景 @@ -2642,6 +2510,57 @@ BrowsingHistorySettingResponse { --- +### A115 删除无引用分类 + +- 模块 / Tag:Catalog +- 需求编号:M06-01-FR03 +- 负责人:顾欣月 +- 关联数据表:DB021 `categories` +- 当前状态:待交叉评审 +- 用途:在分类没有任何商品或历史引用时物理删除分类;存在引用时拒绝并引导停用。 +- 方法与路径:`DELETE /api/merchant/categories/{categoryId}` +- operationId:`Catalog_DeleteCategory` +- 请求 Schema:无(Route) +- 响应 Schema:无(204) +- 身份与 Policy:MerchantOnly。 +- 资源归属:本期为统一经营目录;所有账号正常的商家操作同一分类事实,不按创建人隔离。 +- 幂等要求:DELETE 语义幂等;本接口固定对不存在或已删除分类返回 404。 + +#### 请求 + +- Route 参数:`categoryId`(uuid,必填)。 +- Header:`Authorization: Bearer `。 +- Body:无。 + +#### 成功响应 + +- HTTP 状态:`204 No Content`。 + +#### 失败响应 + +| HTTP 状态 | 业务错误码 | 触发条件 | +|---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `categoryId` 格式非法 | +| 401 | `AUTH.UNAUTHENTICATED` | 未认证 | +| 403 | `AUTH.FORBIDDEN` | 非商家 | +| 404 | `CATALOG.CATEGORY_NOT_FOUND` | 分类不存在或已物理删除 | +| 409 | `CATALOG.CATEGORY_HAS_REFERENCES` | 分类仍被商品或其他历史事实引用 | + +#### 业务规则与并发 + +- 物理删除前重新检查直接子分类、商品及其他历史引用;任一引用存在都返回 409,不通过级联删除或改写引用规避约束。 +- 分类为 `Enabled` 或 `Disabled` 都可进入引用检查;停用不是物理删除的前置条件。并发新增引用与删除通过数据库约束和同一事务竞争,只允许一个结果成立。 + +#### 缓存、事件或外部依赖 + +- A101 不缓存;无引用分类删除后无需失效商品详情、固定首页、普通列表或搜索。历史引用检查只读,不跨模块修改数据。 + +#### 验证场景 + +- 无任何引用的分类删除返回 204;有商品或历史引用返回 409;并发新增商品关联与删除不会产生悬空引用。 + +--- + ### A120 后台商品分页(全状态) - 模块 / Tag:Catalog @@ -2649,25 +2568,25 @@ BrowsingHistorySettingResponse { - 负责人:顾欣月 - 关联数据表:DB022 `products` - 当前状态:待交叉评审 -- 用途:商家按关键词、分类、上下架状态分页查询本方商品,展示价格、库存与状态。 +- 用途:商家按关键词、分类、上下架状态分页查询统一经营目录中的商品,展示价格、库存与状态。 - 方法与路径:`GET /api/merchant/products` - operationId:`Catalog_ListMerchantProducts` - 请求 Schema:无(Query) - 响应 Schema:`MerchantProductListResponse` - 身份与 Policy:MerchantOnly。 -- 资源归属:本期不做多商家隔离;管理员/买家不得调用。 +- 资源归属:本期为单店 B2C 统一经营目录;所有账号正常的商家查看同一套商品,不按创建人或当前操作人过滤;管理员/买家不得调用。 - 幂等要求:只读。 #### 请求 -- Query 参数:`page`、`pageSize`(同通用分页)、`keyword`(可选)、`categoryId`(可选)、`status`(可选,多值:`Draft`/`Published`/`Unpublished`)、`sortBy`(白名单:`createdAt`/`price`/`stock`)、`sortOrder`。 +- Query 参数:`page`、`pageSize`(同通用分页)、`keyword`(可选)、`categoryId`(可选)、`status`(可选,多值:`Draft`/`OnSale`/`OffSale`)、`sortBy`(白名单:`createdAt`/`price`/`stock`,默认 `createdAt`)、`sortOrder`(默认 `desc`)。 - Header:`Authorization`、`Accept`。 - 校验规则:`status`、`sortBy` 白名单。 #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`MerchantProductListResponse`,元素 `MerchantProductSummary` 含 `productId`、`name`、`categoryId`、`price`、`stock`、`status`(`Draft`/`Published`/`Unpublished`)、`primaryImageUrl`、`createdAt`、`updatedAt`。 +- 响应 Schema:`MerchantProductListResponse`,元素 `MerchantProductSummary` 含 `productId`、`name`、`categoryId`、`price`、`stock`、`status`(`Draft`/`OnSale`/`OffSale`)、`primaryImageUrl`、`createdAt`、`updatedAt`。 #### 失败响应 @@ -2680,6 +2599,7 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 - 与购物端 A102 严格区分:本接口可返回草稿、下架商品,不做“已上架”强制过滤。 +- 所有排序都追加 `productId` 作为稳定次序;默认 `createdAt desc, productId desc`。 #### 缓存、事件或外部依赖 @@ -2715,7 +2635,7 @@ BrowsingHistorySettingResponse { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`MerchantProductDetailResponse`,含 A121 全字段:`productId`、`name`、`categoryId`、`description`、`price`、`stock`、`status`(`Draft`/`Published`/`Unpublished`)、`images`、`version`(乐观并发标记)、`createdAt`、`updatedAt`。 +- 响应 Schema:`MerchantProductDetailResponse`,含 A121 全字段:`productId`、`name`、`categoryId`、`description`、`price`、`stock`、`status`(`Draft`/`OnSale`/`OffSale`)、`images`、`version`(乐观并发标记)、`createdAt`、`updatedAt`。 #### 失败响应 @@ -2728,6 +2648,7 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 - 返回 `version` 供 A123 编辑提交做乐观并发校验。 +- 所有账号正常的商家读取同一经营目录;不存在统一返回 404,不按商品创建人或当前操作人过滤。 #### 缓存、事件或外部依赖 @@ -2749,14 +2670,15 @@ BrowsingHistorySettingResponse { - 用途:商家录入商品基础信息,创建为草稿状态。 - 方法与路径:`POST /api/merchant/products` - operationId:`Catalog_CreateProduct` -- 请求 Schema:`CreateProductRequest` +- 请求 Schema:`multipart/form-data`(商品字段 + 首批图片) - 响应 Schema:`MerchantProductDetailResponse` - 身份与 Policy:MerchantOnly。 -- 幂等要求:非幂等;由前端防抖 + 服务端校验控制重复。 +- 幂等要求:必需 `Idempotency-Key`;相同键和请求指纹稳定重放首次确定结果,不重复创建商品或对象。 #### 请求 -- Body:`CreateProductRequest`: +- Header:`Authorization`、`Idempotency-Key`(uuid,必需)、`Content-Type: multipart/form-data`。 +- Body(form-data): | 字段 | 类型 | 必填 | 约束 | |---|---|---|---| @@ -2765,8 +2687,10 @@ BrowsingHistorySettingResponse { | `price` | number | 是 | ≥ 0,最多两位小数 | | `stock` | integer | 是 | ≥ 0 非负整数 | | `description` | string | 否 | ≤ 2000,受控内容 | +| `images` | file[] | 是 | 1~8 张,第一张固定为主图 | +| `altTexts` | string[] | 否 | 与 `images` 同序;每项 ≤ 100 | -- 校验规则:分类须启用;价格非负;库存非负整数;图片数 ≤ 8。 +- 校验规则:分类须启用;价格非负;库存非负整数;必须有 1~8 张图片;每张同时校验扩展名、声明 MIME、实际文件特征、大小和尺寸,仅接受 JPEG、PNG、WebP,单图 ≤ 5 MB,宽高均 400~4096 像素。 #### 成功响应 @@ -2782,14 +2706,19 @@ BrowsingHistorySettingResponse { | 403 | `AUTH.FORBIDDEN` | 非商家 | | 404 | `CATALOG.CATEGORY_NOT_FOUND` | 分类不存在 | | 409 | `CATALOG.CATEGORY_DISABLED` | 分类已停用,不能用于新建 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同请求内容 | +| 413 | `COMMON.PAYLOAD_TOO_LARGE` | 任一图片超过大小限制 | +| 415 | `CATALOG.INVALID_IMAGE` | 图片格式或尺寸不合规 | #### 业务规则与并发 -- 新建默认草稿,成功取得 `productId` 后再通过 A127 上传图片,最后经 A125 完整性校验上架;A122 不接受尚无归属商品的预上传图片 ID。 +- 完成身份、幂等键格式和 multipart 结构校验后,先读取持久化幂等结果,再读取分类等可变事实;同键换内容返回 409。 +- 服务端预生成 `productId`,依次写入受控对象并在一个数据库事务中原子保存 `Draft` 商品、图片记录、第一张主图和确定幂等结果;任一对象上传或数据库写入失败时不得返回成功。 +- 对象已写入但数据库事务失败时立即尝试清理本次全部对象;清理失败登记可追踪补偿。创建成功后可用 A127 增加图片、A128 删除图片,最后经 A125 完整性校验上架。 #### 缓存、事件或外部依赖 -- `pg_trgm`/GIN 索引随 PostgreSQL 商品数据同步维护,不通过 Outbox 或 Worker 复制搜索索引;事务提交后只触发已确认的缓存失效。 +- `pg_trgm`/GIN 索引随 PostgreSQL 商品数据同步维护,不通过 Outbox 或 Worker 复制搜索索引。新商品固定为 `Draft`,不进入固定首页;创建事务提交后仍须立即清理该 `productId` 的 A103 短空值并在第 3 秒二次删除,不失效固定首页。 #### 验证场景 @@ -2810,13 +2739,13 @@ BrowsingHistorySettingResponse { - 请求 Schema:`UpdateProductRequest` - 响应 Schema:`MerchantProductDetailResponse` - 身份与 Policy:MerchantOnly。 -- 幂等要求:PUT 语义幂等(相同 `version` + 相同内容重复提交结果一致)。 +- 幂等要求:无独立幂等键;使用 `version` 条件更新防止重复副作用。首次成功后旧 `version` 再提交返回版本冲突,不承诺重放首次响应。 #### 请求 - Route 参数:`productId`(uuid,必填)。 - Body:`UpdateProductRequest`:字段同 A122(`name`、`categoryId`、`price`、`stock`、`description`),另加必填 `version`(integer,来自 A121);图片新增和删除分别使用 A127、A128。 -- 校验规则:`version` 必填;分类须启用;其余同 A122。 +- 校验规则:`version` 必填;仅当 `categoryId` 相对当前商品发生变化时,目标分类必须已启用。原分类后来停用时,仍允许修改名称、价格、库存、图片和描述等不改变分类归属的字段;其余字段规则同 A122。 #### 成功响应 @@ -2831,16 +2760,18 @@ BrowsingHistorySettingResponse { | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | -| 409 | `CATALOG.CATEGORY_DISABLED` | 目标分类停用 | +| 409 | `CATALOG.CATEGORY_DISABLED` | 本次改绑的目标分类已停用 | | 409 | `CATALOG.PRODUCT_VERSION_CONFLICT` | 提交 `version` 与当前不一致(并发编辑冲突) | #### 业务规则与并发 - 服务端使用条件更新(`WHERE version = @version`)实现乐观并发;冲突时返回 409,前端保留已填写内容并提示刷新确认。 +- 所有正常商家修改同一经营目录;并发保护按商品版本执行,不按商家创建人隔离。 #### 缓存、事件或外部依赖 -- 提交后失效商品详情/列表缓存;`pg_trgm`/GIN 索引由 PostgreSQL 随数据同步维护,不发布“同步搜索索引”事件。 +- 商品事务提交后按受影响字段精确失效 A103 商品详情;名称、价格、普通库存、主图或首页排序/成员资格变化时同时失效唯一固定首页摘要,并在 3 秒后对同一 Key 二次失效。普通列表和搜索不缓存。 +- `pg_trgm`/GIN 索引由 PostgreSQL 随数据同步维护,不发布“同步搜索索引”事件。 #### 验证场景 @@ -2855,7 +2786,7 @@ BrowsingHistorySettingResponse { - 负责人:顾欣月 - 关联数据表:DB022 `products` - 当前状态:待交叉评审 -- 用途:无历史订单关联时删除商品;有关联时禁止破坏性删除并建议下架。 +- 用途:仅在商品为草稿或已下架且不存在任何历史关联时物理删除;其他情况拒绝破坏性删除并建议下架。 - 方法与路径:`DELETE /api/merchant/products/{productId}` - operationId:`Catalog_DeleteProduct` - 请求 Schema:无(Route) @@ -2879,19 +2810,23 @@ BrowsingHistorySettingResponse { | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在或已删除 | -| 409 | `CATALOG.PRODUCT_HAS_ORDERS` | 存在历史订单关联,禁止破坏性删除,建议改为下架 | +| 409 | `CATALOG.PRODUCT_MUST_BE_OFF_SALE` | 商品当前为 `OnSale`,必须先下架 | +| 409 | `CATALOG.PRODUCT_HAS_REFERENCES` | 存在订单、购物车、收藏、浏览、评价、秒杀等任一历史关联,禁止物理删除 | #### 业务规则与并发 -- 存在订单项关联时拒绝物理删除;下架(A126)不删除购物车、收藏、浏览记录与历史订单快照。 +- 仅 `Draft` 或 `OffSale` 商品可进入删除判断;存在订单、购物车、收藏、浏览、评价、秒杀等任何历史关联时均拒绝物理删除,不按关联当前状态排除已取消订单或失效记录。 +- 下架(A126)不删除购物车、收藏、浏览记录、评价、秒杀关联与历史订单快照。 +- 删除资格、全量引用检查和物理删除在一个受控提交序列中复核,并由数据库引用约束兜底;检查后并发产生新引用时,删除失败而不能留下悬空引用。 #### 缓存、事件或外部依赖 -- 是否存在订单关联需查询 Ordering 提供的应用契约/只读视图,不直接跨模块改表。 +- 历史关联检查通过各所属模块的公开应用契约或已确认只读检查能力完成,不跨模块修改内部表。 +- 物理删除事务提交后失效目标 A103 正常值/空值与可能包含该商品的固定首页摘要,并在 3 秒后二次失效;对象清理失败进入可追踪补偿,不把已提交的删除伪装为失败。 #### 验证场景 -- 有订单商品删除返回 409;无关联商品删除返回 204。 +- 存在订单、购物车、收藏、浏览、评价或秒杀任一引用时删除返回 409;仅 `Draft`/`OffSale` 且无任何引用时返回 204。 --- @@ -2905,7 +2840,7 @@ BrowsingHistorySettingResponse { - 用途:完成商品销售前校验并将商品设为已上架。 - 方法与路径:`POST /api/merchant/products/{productId}/publish` - operationId:`Catalog_PublishProduct` -- 请求 Schema:无(Route) +- 请求 Schema:`ChangeProductStatusRequest` - 响应 Schema:`MerchantProductDetailResponse` - 身份与 Policy:MerchantOnly。 - 幂等要求:重复上架保持幂等,返回当前状态。 @@ -2913,29 +2848,33 @@ BrowsingHistorySettingResponse { #### 请求 - Route 参数:`productId`(uuid,必填)。 -- Body:无。 +- Body:`ChangeProductStatusRequest`:`version`(integer,必填,来自 A121/A123 最新响应)。 #### 成功响应 -- HTTP 状态:`200 OK`,返回更新后 `status = Published`。 +- HTTP 状态:`200 OK`,返回更新后 `status = OnSale`。 #### 失败响应 | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 缺少 `version` 或格式非法 | | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | | 409 | `CATALOG.PRODUCT_INCOMPLETE` | 必填项或主图缺失 | | 409 | `CATALOG.CATEGORY_DISABLED` | 商品分类已停用 | +| 409 | `CATALOG.PRODUCT_VERSION_CONFLICT` | 非目标态且提交 `version` 与当前版本不一致 | #### 业务规则与并发 -- 上架前必须满足:名称、有效分类、价格、库存以及至少一张主图。 +- 上架前必须满足:名称、启用分类、价格、库存以及至少一张主图;只允许 `Draft` 或 `OffSale` 进入 `OnSale`。 +- 当前已为 `OnSale` 时直接返回当前结果;否则使用 `productId + version + 当前状态` 条件更新,确保编辑、上架和下架竞争只提交一个基于最新版本的结果。 #### 缓存、事件或外部依赖 -- 上架成功后失效购物端缓存;`pg_trgm`/GIN 索引由 PostgreSQL 随商品数据同步维护。 +- 上架事务提交后立即失效目标 A103 空值和唯一固定首页摘要,并在 3 秒后对同一 Key 二次失效;普通列表与搜索不缓存。 +- `pg_trgm`/GIN 索引由 PostgreSQL 随商品数据同步维护。 #### 验证场景 @@ -2953,7 +2892,7 @@ BrowsingHistorySettingResponse { - 用途:停止商品销售,使购物端列表、详情和搜索不再公开该商品。 - 方法与路径:`POST /api/merchant/products/{productId}/unpublish` - operationId:`Catalog_UnpublishProduct` -- 请求 Schema:无(Route) +- 请求 Schema:`ChangeProductStatusRequest` - 响应 Schema:`MerchantProductDetailResponse` - 身份与 Policy:MerchantOnly。 - 幂等要求:重复下架保持幂等,返回当前状态。 @@ -2961,27 +2900,31 @@ BrowsingHistorySettingResponse { #### 请求 - Route 参数:`productId`(uuid,必填)。 -- Body:无。 +- Body:`ChangeProductStatusRequest`:`version`(integer,必填,来自 A121/A123 最新响应)。 #### 成功响应 -- HTTP 状态:`200 OK`,返回更新后 `status = Unpublished`。 +- HTTP 状态:`200 OK`,返回更新后 `status = OffSale`。 #### 失败响应 | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 缺少 `version` 或格式非法 | | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | +| 409 | `CATALOG.PRODUCT_INVALID_STATUS` | 商品为 `Draft`,尚未上架,不能执行下架 | +| 409 | `CATALOG.PRODUCT_VERSION_CONFLICT` | 非目标态且提交 `version` 与当前版本不一致 | #### 业务规则与并发 -- 下架后 A102/A103 不再返回该商品,旧链接不再允许购买;历史订单快照不受影响。 +- 下架后 A102 不再返回该商品,A103 按不可公开错误返回,旧链接不再允许购买;历史订单、购物车、收藏、浏览、评价和秒杀关联均保留。 +- 当前已为 `OffSale` 时直接返回当前结果;`Draft` 拒绝下架;仅 `OnSale` 使用 `productId + version + status=OnSale` 条件更新为 `OffSale`。销售状态、检索事实和新版本在同一 PostgreSQL 事务提交。 #### 缓存、事件或外部依赖 -- 下架成功后失效购物端缓存;公开查询始终过滤商品状态,PostgreSQL 同步索引不会重新公开下架商品。 +- 下架事务提交后立即失效目标 A103 正常值和唯一固定首页摘要,并在 3 秒后二次失效;普通列表与搜索直读 PostgreSQL,公开查询始终过滤商品状态。 #### 验证场景 @@ -3031,10 +2974,14 @@ BrowsingHistorySettingResponse { - 原始文件名只用于安全展示,不作为对象存储 Key;对象 Key 采用 `products/{productId}/{fileId}.` 格式。 - 对象存储失败时不写入指向不存在对象的成功记录。 +- 商品没有图片时服务端强制首图为主图;已有图片且 `isPrimary=true` 时在同一事务取消旧主图并设新图为主图;`isPrimary=false` 或省略时按当前最大 `sortOrder + 1` 追加。 +- 有图片的商品任一时刻恰有一张主图。图片记录、排序和主图切换必须形成一个一致结果;并发上传第 9 张时由受控计数/约束只允许前 8 张成功,其余稳定返回 409。 +- 对象已写入但数据库事务失败时立即尝试删除对象,删除失败则登记可追踪补偿任务。 #### 缓存、事件或外部依赖 - 依赖 M00 提供的 `IObjectStorage` 公共接口与 SeaweedFS 开发环境。 +- 图片事务提交后立即失效目标 A103;主图或首页摘要字段变化时同时失效唯一固定首页,并在 3 秒后二次失效。缓存失效失败不回滚已提交图片事实。 #### 验证场景 @@ -3072,16 +3019,19 @@ BrowsingHistorySettingResponse { |---|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | -| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品或图片不存在,或图片不属于该商品 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | +| 404 | `CATALOG.PRODUCT_IMAGE_NOT_FOUND` | 图片不存在或不属于该商品 | | 409 | `CATALOG.PRIMARY_IMAGE_REQUIRED` | 已上架商品删除后将无主图 | #### 业务规则与并发 -- 删除主图后需存在其余图片可自动/手动指定新主图;已上架商品不得删至无主图。 +- 删除主图且仍有其他图片时,服务端固定按 `sortOrder asc, imageId asc` 自动提升下一张为主图;本接口不接受客户端临时指定替代主图。 +- `OnSale` 商品不得删除最后一张图片;`Draft`/`OffSale` 商品允许删空,后续上架仍须重新满足主图完整性。 +- 图片关联删除和新主图选择在同一数据库事务内完成;对象存储删除在提交后执行,失败时记录可重试补偿,不能恢复已删除的数据库关联或返回虚假失败。 #### 缓存、事件或外部依赖 -- 删除数据库记录并清理对象存储对象(清理失败记录可追踪错误,不阻断主流程)。 +- 数据库事务提交后立即失效目标 A103;主图或首页摘要字段变化时同时失效唯一固定首页,并在 3 秒后二次失效。对象清理失败记录可追踪错误,不阻断已提交主流程。 #### 验证场景 @@ -3096,7 +3046,7 @@ BrowsingHistorySettingResponse { - 负责人:顾欣月 - 关联数据表:DB024 `reviews`、DB025 `review_images` - 当前状态:待交叉评审 -- 用途:商品详情页分页展示公开评价与评分汇总(总数、平均分、星级分布)。 +- 用途:商品详情页分页展示公开评价与评分汇总(总数、平均分)。 - 方法与路径:`GET /api/products/{productId}/reviews` - operationId:`Review_ListProductReviews` - 请求 Schema:无(Route/Query) @@ -3108,15 +3058,15 @@ BrowsingHistorySettingResponse { #### 请求 - Route 参数:`productId`(uuid,必填)。 -- Query 参数:`page`、`pageSize`(通用分页);`sortBy`(白名单:`createdAt`,默认);`sortOrder`(默认 `desc`)。 +- Query 参数:`page`、`pageSize`(通用分页)。排序固定为 `createdAt desc, reviewId desc`,客户端不得改写。 - Header:`Accept`。 -- 校验规则:分页与白名单校验。 +- 校验规则:仅校验分页字段;排序不可由客户端传入。 #### 成功响应 - HTTP 状态:`200 OK` - 响应 Schema:`ProductReviewListResponse`,`data` 含: - - `summary`:`averageRating`(number,一位小数)、`totalCount`(integer)、`ratingDistribution`(对象:`"5"`…`"1"` 计数)。 + - `summary`:`averageRating`(number,一位小数)、`totalCount`(integer)。 - 分页字段 `items`、`page`、`pageSize`、`total`、`totalPages`;`items` 元素含 `reviewId`、`rating`、`content`、`images`(`url` 数组)、`buyerDisplayName`(脱敏昵称)、`createdAt`。 - 示例: @@ -3125,7 +3075,7 @@ BrowsingHistorySettingResponse { "code": "success", "message": "ok", "data": { - "summary": { "averageRating": 4.6, "totalCount": 128, "ratingDistribution": { "5": 90, "4": 25, "3": 8, "2": 3, "1": 2 } }, + "summary": { "averageRating": 4.6, "totalCount": 128 }, "items": [ { "reviewId": "r1…", "rating": 5, "content": "很好用", "images": ["https://…/r1.jpg"], "buyerDisplayName": "用***月", "createdAt": "2026-07-21T03:00:00Z" } ], "page": 1, "pageSize": 10, "total": 128, "totalPages": 13 } @@ -3141,12 +3091,14 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 -- 只返回有效评价;`buyerDisplayName` 直接读取评价创建时保存的脱敏展示名快照,不在列表查询中逐条调用 Identity;不返回手机号、邮箱、内部用户标识。 -- 评分汇总由有效评价计算;新增评价后最终更新(可接受短暂最终一致)。 +- 本期没有待审核、隐藏或删除状态;全部成功提交的评价立即公开并进入总数与平均分。 +- `buyerDisplayName` 直接读取评价创建时保存的脱敏展示名快照,不在列表查询中逐条调用 Identity;不返回手机号、邮箱、内部用户标识。 +- 列表固定按 `createdAt desc, reviewId desc` 稳定分页;总数和平均分与本次 PostgreSQL 已提交评价事实口径一致。 #### 缓存、事件或外部依赖 -- 汇总可缓存,新增评价后失效。商品是否存在依赖 Catalog(同库读取或应用契约)。 +- A140 的评价列表、图片和评分汇总不进入 C07,也不触发商品详情缓存失效;每次公开读取都以 PostgreSQL 已提交评价事实为准。 +- 商品是否公开依赖 Catalog 的公开商品事实;商品下架后不再通过商品详情入口公开评价,但既有评价事实不被删除。 #### 验证场景 @@ -3172,8 +3124,8 @@ BrowsingHistorySettingResponse { #### 请求 - Header:`Authorization`、`Content-Type: multipart/form-data`。 -- Body(form-data):`file`(图片文件,必填)。 -- 校验规则:仅接受 JPEG、PNG、WebP;单图 ≤ 5 MB;宽高均 200~4096 像素;同时校验扩展名、声明 MIME 与实际特征。 +- Body(form-data):`orderItemId`(uuid,必填)、`file`(图片文件,必填)。 +- 校验规则:先校验订单项属于当前买家、所属订单为 `Completed` 且尚无评价,再校验累计图片数不超过 6;仅接受 JPEG、PNG、WebP;单图 ≤ 5 MB;宽高均 200~4096 像素;同时校验扩展名、声明 MIME 与实际特征。该资格只用于允许上传,A142 正式提交时仍完整重检。 #### 成功响应 @@ -3186,16 +3138,22 @@ BrowsingHistorySettingResponse { |---|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 未登录 | | 403 | `AUTH.FORBIDDEN` | 非买家 | +| 404 | `REVIEW.ORDER_ITEM_NOT_FOUND` | 订单项不存在或不属于当前买家 | +| 409 | `REVIEW.ORDER_NOT_COMPLETED` | 订单未完成 | +| 409 | `REVIEW.ALREADY_REVIEWED` | 订单项已经评价 | +| 409 | `REVIEW.IMAGE_LIMIT_EXCEEDED` | 该订单项累计可提交图片超过 6 张 | | 413 | `COMMON.PAYLOAD_TOO_LARGE` | 文件超限 | | 415 | `REVIEW.INVALID_IMAGE` | 格式或尺寸不符合要求 | #### 业务规则与并发 -- 暂存图片归属当前买家;评价尚未创建时对象 Key 使用 `review-uploads/{buyerId}/{fileId}.`,符合 `//.` 规范。A142 提交成功后只在数据库中关联 `reviewId`,不要求物理搬移对象;未被引用的暂存图片由清理策略回收。 +- 上传结果同时绑定当前买家与 `orderItemId`,只能被同一买家针对同一订单项的 A142 请求引用;不得仅凭 `imageId` 跨买家或跨订单项占用图片。 +- 同一订单项的图片计数必须串行化或由等效数据库约束保护;并发上传第 7 张时只允许一方成功,其余稳定返回 `REVIEW.IMAGE_LIMIT_EXCEEDED`。 +- 对象写入成功但上传结果持久化失败时立即尝试清理对象;清理失败登记可追踪补偿。未被成功评价引用的上传结果不得公开。 #### 缓存、事件或外部依赖 -- 依赖 M00 `IObjectStorage`。 +- 依赖 Ordering 校验当前订单项资格,依赖 M00 `IObjectStorage`;不接入 C07。 #### 验证场景 @@ -3235,8 +3193,9 @@ BrowsingHistorySettingResponse { #### 成功响应 -- HTTP 状态:`201 Created`;`Location` 指向该评价(如 `/api/reviews/{reviewId}`)。 -- 响应 Schema:`ReviewDetailResponse`,含 `reviewId`、`productId`、`orderItemId`、`rating`、`content`、`images`、`createdAt`。 +- 首次创建 HTTP 状态:`201 Created`;不返回指向已取消 A144 的 `Location`。 +- 同一幂等键和请求指纹重放首次完整 `201` 状态与响应;使用不同键但订单项已存在唯一评价时返回 `200 OK` 和既有已评价结果,不新增记录。 +- 响应 Schema:`ReviewDetailResponse`,含 `reviewId`、`productId`、`orderItemId`、`rating`、`content`、`images`、`buyerDisplayName`、`createdAt`、`alreadyReviewed`。 #### 失败响应 @@ -3246,20 +3205,24 @@ BrowsingHistorySettingResponse { | 401 | `AUTH.UNAUTHENTICATED` | 未登录或登录失效 | | 403 | `AUTH.FORBIDDEN` | 非买家,或商家/管理员尝试提交 | | 404 | `REVIEW.ORDER_ITEM_NOT_FOUND` | 订单项不存在或不属于当前买家(不泄露归属) | +| 404 | `REVIEW.IMAGE_NOT_FOUND` | 任一图片不存在、不属于当前买家或未绑定该订单项 | | 409 | `REVIEW.ORDER_NOT_COMPLETED` | 订单未完成 | -| 409 | `REVIEW.ALREADY_REVIEWED` | 该订单项已评价 | +| 409 | `REVIEW.IMAGE_ALREADY_USED` | 任一图片已被其他评价使用 | +| 409 | `REVIEW.IMAGE_LIMIT_EXCEEDED` | 图片数量超过 6 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同请求内容 | #### 业务规则与并发 -- 同一订单项只能形成一条评价:唯一约束 `ux_reviews_order_item_id` 作为最终保障;重复点击/重复请求返回首次已确认结果,不新增记录。 +- 完成固定身份、幂等键格式和请求结构校验后,先按当前买家、接口、幂等键及请求指纹读取持久化结果,再读取订单、商品或图片等可变事实;同键同指纹稳定重放首次确定结果,同键换内容返回 409。 +- 同一订单项只能形成一条评价:唯一约束 `ux_reviews_order_item_id` 作为最终保障;重复点击、不同窗口并发或不同幂等键命中同一订单项时返回既有已评价结果,不新增记录。 - 订单完成状态、订单项归属由 Ordering 提供的应用契约校验,不直接改订单表。 - 创建评价时通过 Identity 公开应用契约读取当前买家的安全展示名并完成脱敏,将结果保存为 `buyerDisplayName` 快照;没有展示名时回退到自动用户名的脱敏值。后续用户资料变化不改写历史评价快照,公开列表和详情不逐条查询 Identity。 -- 提交成功后触发商品评分汇总更新(A140 汇总最终一致)。 +- 正式提交重新校验当前买家、订单项归属、`Completed` 状态、尚未评价、字段,以及全部 `imageIds` 均绑定当前买家和该订单项。评价、脱敏展示名快照、图片关联和确定幂等结果在一个原子事务中提交;任一必要写入失败时整体回滚。 +- 事务提交成功后评价立即公开并进入 A140 的总数与平均分;M04 订单和订单项状态保持 `Completed`,本期不发送站内消息。 #### 缓存、事件或外部依赖 -- 依赖 Ordering 校验订单项,依赖 Identity 提供当前买家的安全展示名快照;依赖 M00 幂等基础设施与对象存储图片关联。 +- 依赖 Ordering 校验订单项,依赖 Identity 提供当前买家的安全展示名快照;依赖 M00 幂等基础设施与对象存储图片关联。M07 不进入 C07,也不发布商品缓存失效。 #### 验证场景 @@ -3292,11 +3255,11 @@ BrowsingHistorySettingResponse { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`ReviewEligibilityResponse`,含 `eligible`(boolean)、`reason`(枚举字符串:`Eligible`/`OrderNotCompleted`/`AlreadyReviewed`)、`existingReviewId`(uuid,可空);订单项不存在或不属于当前买家时统一返回 404,不返回 `NotOwner`。 +- 响应 Schema:`ReviewEligibilityResponse`,只含 `eligible`(boolean)、`reason`(枚举字符串:`Eligible`/`OrderNotCompleted`/`AlreadyReviewed`);订单项不存在或不属于当前买家时统一返回 404,不返回 `NotOwner`,也不提供指向已取消 A144 的详情标识。 - 示例: ```json -{ "code": "success", "message": "ok", "data": { "eligible": true, "reason": "Eligible", "existingReviewId": null } } +{ "code": "success", "message": "ok", "data": { "eligible": true, "reason": "Eligible" } } ``` #### 失败响应 @@ -3318,81 +3281,7 @@ BrowsingHistorySettingResponse { #### 验证场景 -- 已评价返回 `eligible=false, reason=AlreadyReviewed` 且带 `existingReviewId`;未完成返回 `OrderNotCompleted`。 - -### A144 单条公开评价详情查询 - -- 模块 / Tag:Review -- 需求编号:M07-FR06;同时承接 A142 `Location` 与 A143 `existingReviewId` 的可达读取 -- 负责人:顾欣月 -- 关联数据表:DB024 `reviews`、DB025 `review_images` -- 当前状态:待交叉评审 -- 用途:单条评价的对外可寻址读取;服务于 A142 创建响应 `Location: /api/reviews/{reviewId}` 的 REST 约定与 A143 `existingReviewId` 跳转场景,供商品详情、订单详情等位置按需拉取单条评价。 -- 方法与路径:`GET /api/reviews/{reviewId}` -- operationId:`Review_GetReview` -- 请求 Schema:无(仅 Route) -- 响应 Schema:`PublicReviewDetailResponse` -- 身份与 Policy:游客可访问;公开评价与 A140 列表同口径,不返回买家手机号、邮箱、内部用户标识等敏感字段。 -- 资源归属:公开评价;当前买家请求时不附加任何归属校验。 -- 幂等要求:只读,天然幂等。 - -#### 请求 - -- Route 参数:`reviewId`(uuid)。 -- Header:`Accept: application/json`。 -- Body:无。 -- 校验规则:`reviewId` 必须是标准带连字符 UUID 格式;非法格式按 `400 Bad Request`(`COMMON.INVALID_UUID` 通用码)处理;记录不存在按 `404 Not Found`(`REVIEW.NOT_FOUND`)处理,不泄露存在性差异。 - -#### 成功响应 - -- HTTP 状态:`200 OK` -- 响应 Schema:`PublicReviewDetailResponse`,含 `reviewId`、`productId`、`rating`、`content`、`images`(`imageId`、`url`、`sortOrder`)、`buyerDisplayName`(脱敏昵称,与 A140 一致)、`createdAt`;不公开 `orderItemId` 或内部买家标识。 -- 示例: - -```json -{ - "code": "success", - "message": "ok", - "data": { - "reviewId": "9c2f…", - "productId": "6f1d…", - "rating": 5, - "content": "很好用", - "images": [ - { "imageId": "img1…", "url": "https://…/r1.jpg", "sortOrder": 1 } - ], - "buyerDisplayName": "用***月", - "createdAt": "2026-07-21T03:00:00Z" - } -} -``` - -#### 失败响应 - -| HTTP 状态 | 业务错误码 | 触发条件 | -|---|---|---| -| 400 | `COMMON.INVALID_UUID` | `reviewId` 非标准 UUID 格式 | -| 404 | `REVIEW.NOT_FOUND` | 评价不存在 | -| 500 | `COMMON.INTERNAL_ERROR` | 未处理服务端错误 | - -#### 业务规则与并发 - -- 本期评价创建成功后即按 A140 口径公开;本期不提供运营下线、隐藏或评价治理状态。评价不存在时返回 404。 -- 不返回买家手机号、邮箱、内部用户标识;昵称读取评价创建时保存的 `buyerDisplayName` 脱敏快照,与 A140 保持一致。 -- 与 A142 创建响应的 `Location` 头严格对齐:创建成功后客户端可凭 `Location` 直接 GET 本接口获取完整评价。 -- A143 返回 `existingReviewId` 时,前端可经本接口跳转拉取评价详情。 - -#### 缓存、事件或外部依赖 - -- 汇总/详情缓存与 A140 共用同一 Key 前缀;本期仅在 A142 评价创建成功后触发对应缓存失效,不定义尚未提供的评价更新事件。 -- 不依赖 Identity、Ordering 等其他模块的应用契约;仅按 `reviewId` 主键读取 DB024 与 DB025。 - -#### 验证场景 - -- 有效 `reviewId` 返回 200 与 `ReviewDetailResponse`; -- 不存在的 `reviewId` 返回 404(`REVIEW.NOT_FOUND`); -- 非法 UUID 格式返回 400(`COMMON.INVALID_UUID`); -- 不暴露买家敏感字段;与 A140 列表的 `buyerDisplayName` 脱敏结果一致。 +- 已评价返回 `eligible=false, reason=AlreadyReviewed`;未完成返回 `eligible=false, reason=OrderNotCompleted`。A143 只决定订单详情入口提示,A142 仍执行完整资格重检。 > 来源:[`interface-zhh.md`](interface/interface-zhh.md)。A229/A230 已取消并由 A302/A303 承接秒杀订单查询;活动、库存与 Ordering 创建契约仍待数据库设计和联调确认。 @@ -3414,20 +3303,20 @@ BrowsingHistorySettingResponse { - Route 参数:无 - Query 参数:无 -- Header:`Authorization: Bearer `(必填);`Idempotency-Key: `(推荐,防止重复点击) +- Header:`Authorization: Bearer `(必填);`Idempotency-Key: `(可选;提供后启用稳定重放) - Body: ```text AddCartItemRequest { productId: uuid // 必填 - quantity: integer // 必填,1 ≤ quantity ≤ 商品当前实时可售库存 + quantity: integer // 必填,固定格式只要求 quantity ≥ 1 } ``` - 校验规则: - - `quantity` 必须为正整数,1~当前实时可售库存。 + - `quantity` 必须为正整数;超过实时库存属于可变业务冲突,按 409 返回当前最大可设值。 - 服务端忽略请求中任何尝试指定 `userId`、`cartItemId`、`createdAt` 的字段;条目归属固定为当前买家。 - - 商品必须处于已上架状态;库存不足、商品下架或被禁用时拒绝。 + - 商品必须处于 `OnSale`;库存不足、商品下架或被禁用时拒绝。 #### 成功响应 @@ -3456,12 +3345,13 @@ CartItemResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | `quantity` 缺失、非整数、≤0 或超过实时库存 | +| 400 | `COMMON.VALIDATION_FAILED` | `quantity` 缺失、非整数或 ≤ 0 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | -| 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品已下架或被禁用 | +| 409 | `CATALOG.PRODUCT_NOT_ON_SALE` | 商品不是 `OnSale` 或已被禁用 | | 409 | `CART.QUANTITY_EXCEEDS_STOCK` | 累加后数量超过商品实时可售库存,返回当前最大允许值 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同请求内容 | | 429 | `COMMON.RATE_LIMITED` | 触发限流 | | 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 商品服务或数据库暂时不可用 | | 500 | `COMMON.INTERNAL_ERROR` | 未处理的服务端错误 | @@ -3472,7 +3362,8 @@ CartItemResponse { - 累加过程在同一数据库事务内完成:读取已有条目、加锁或条件更新、`quantity = quantity + :newQty`;影响行数为 0 即失败。 - 条目归属固定为当前买家;客户端传入的 `userId`、`cartItemId` 被忽略;越权访问他人条目返回 404。 - 商品不可加时返回明确错误码与 `maxAllowedQuantity`;前端按此截断。 -- 接受 `Idempotency-Key` Header;同一 `(userId, key)` 在约定窗口(默认 5 分钟)内重复提交只生效一次,返回首次已确认结果且不重复累加数量。 +- `Idempotency-Key` 为可选;提供时,在固定身份、键格式和请求结构校验后先按买家、接口、键和请求指纹读取持久化结果,再读取商品、库存和购物车条目。同键同请求重放首次确定结果且不重复累加,同键换内容返回 `IDEMPOTENCY.KEY_REUSED`。 +- 提供幂等键时,条目变更与成功结果在同一 PostgreSQL 事务提交;商品不可售、库存不足等确定业务失败同样持久化后返回。数据库或依赖故障等未形成确定结果的瞬态失败不固化,允许原键重试;不发明固定分钟窗口。 #### 缓存、事件或外部依赖 @@ -3485,8 +3376,8 @@ CartItemResponse { - 已上架商品、合法 `quantity` → 201,返回最新条目。 - 同一商品二次加入 → 200,条目数量累加,库存上限生效。 - 数量 ≤ 0 或超过库存 → 400 / `COMMON.VALIDATION_FAILED`,附 `maxAllowedQuantity`。 -- 商品已下架 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`,不创建条目。 -- 商品被禁用 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 +- 商品已下架 → 409 / `CATALOG.PRODUCT_NOT_ON_SALE`,不创建条目。 +- 商品被禁用 → 409 / `CATALOG.PRODUCT_NOT_ON_SALE`。 - 已存在购物车条目累加后超库存 → 409 / `CART.QUANTITY_EXCEEDS_STOCK`,原条目数量不超上限。 - 同一 `Idempotency-Key` 重复提交 → 仅首次创建/累加,后续返回首次结果且 `quantity` 不再累加。 @@ -3527,9 +3418,9 @@ CartListResponse { pageSize: integer total: integer totalPages: integer - selectedCount: integer // 当前选中条目数量 - selectedTotalAmount: number // 选中条目按实时单价计算的总额 - availableSelectedCount: integer // 选中且可结算的条目数量 + selectedCount: integer // 当前买家全部购物车中已选中条目数,不受本页分页影响 + selectedTotalAmount: number // 全部已选中且可结算条目按实时单价计算的总额 + availableSelectedCount: integer // 当前买家全部购物车中已选中且可结算的条目数 } ``` @@ -3544,7 +3435,7 @@ CartListResponse { #### 业务规则与并发 - 严格按 `buyer_id = current_user_id` 过滤;不允许跨用户查看。 -- 排序默认按 `updatedAt desc`;相同 `updatedAt` 时按 `productId` 稳定排序。 +- 排序固定按 `updatedAt desc, cartItemId desc`,避免同一商品事实变化或同时间戳导致翻页重复、遗漏。 - 商品下架、库存归零或被禁用时,条目仍可见但标记 `isAvailable=false` 并附 `unavailableReason`;不参与 `selectedTotalAmount` 与 `availableSelectedCount` 计算。 - 实时单价与库存来自 Catalog 模块;不接受客户端传入的价格或库存覆盖。 @@ -3558,7 +3449,7 @@ CartListResponse { - 买家购物车 0 条 → `items=[]`,`selectedCount=0`,`selectedTotalAmount=0`。 - 包含已下架商品 → 仍可见,`isAvailable=false`,`selectedTotalAmount` 不计入。 - 包含失效商品但被选中 → `availableSelectedCount` 仅统计可用条目。 -- 跨用户访问 → 403 / `AUTH.FORBIDDEN`,不泄露他人条目。 +- 非买家角色调用 → 403 / `AUTH.FORBIDDEN`;列表永远只按当前买家过滤,不存在传入他人用户标识的入口。 - 翻页查询 → 总数与分页元数据稳定,按 `updatedAt desc` 一致排序。 ### A203 修改购物车条目数量 @@ -3583,7 +3474,7 @@ CartListResponse { ```text UpdateCartItemQuantityRequest { - quantity: integer // 必填,1 ≤ quantity ≤ 商品当前实时可售库存 + quantity: integer // 必填,固定格式只要求 quantity ≥ 1 } ``` @@ -3596,7 +3487,7 @@ UpdateCartItemQuantityRequest { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | `quantity` 缺失、非整数、≤0 或超过实时库存 | +| 400 | `COMMON.VALIDATION_FAILED` | `quantity` 缺失、非整数或 ≤ 0 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `RESOURCE.NOT_FOUND` | 条目不存在或不属于当前用户 | @@ -3606,7 +3497,8 @@ UpdateCartItemQuantityRequest { #### 业务规则与并发 - 严格按 `cart_item_id = :cartItemId AND buyer_id = current_user_id` 条件更新;不存在的条目返回 404。 -- 调小或调到 1 不受实时库存上限约束;商品下架或被禁用时允许调小或删除,但禁止调大或累加。 +- 新数量与旧数量相等时不产生写副作用,只按当前商品状态、价格和库存重新派生响应。 +- 调小不受实时库存上限约束;商品下架或被禁用时允许调小或删除,但保存后条目仍可能保持不可结算。只有调大才要求商品为 `OnSale` 且新数量不超过实时库存。 - 库存上限校验以 Catalog 模块实时库存为准;不允许客户端传入目标库存。 - 服务端不接受修改 `productId`、`isSelected`、`userId` 等字段;选中状态变更走 A206。 @@ -3633,14 +3525,14 @@ UpdateCartItemQuantityRequest { - 负责人:朱惠惠 - 关联数据表:DB041 - 当前状态:待交叉评审 -- 用途:买家单条删除购物车条目;幂等执行,已删除条目再次删除返回 204。 +- 用途:买家按稳定请求标识删除本人单条购物车条目;同一请求重放首次结果,新请求删除不存在或不属于本人的条目返回 404。 - 方法与路径:`DELETE /api/cart/items/{cartItemId}` - operationId:`Cart_RemoveItem` #### 请求 - Route 参数:`cartItemId: uuid` -- Header:`Authorization: Bearer `(必填) +- Header:`Authorization: Bearer `(必填)、`Idempotency-Key: `(必填) - Body:无 #### 成功响应 @@ -3653,12 +3545,15 @@ UpdateCartItemQuantityRequest { |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 404 | `CART.ITEM_NOT_FOUND` | 条目不存在或不属于当前买家 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同请求 | #### 业务规则与并发 -- 删除按 `cart_item_id = :cartItemId AND buyer_id = current_user_id` 过滤;影响行数为 0 时返回 204,保持幂等。 -- 不返回 404,避免暴露条目归属;删除请求仅在鉴权失败时返回 401/403。 -- 默认地址或失效条目也可删除;删除后不自动选择其他默认地址或恢复库存。 +- 固定身份、键格式和路由字段校验后先读取持久化幂等结果;同键同请求重放首次 204,同键换目标返回 409。 +- 新请求按 `cart_item_id = :cartItemId AND buyer_id = current_user_id` 过滤;不存在或不属于本人统一形成 404 的确定结果,不泄露真实归属。 +- 删除副作用、确定成功结果和幂等记录在同一事务提交;确定 404 也先保存再返回,瞬态失败不固化。 +- 失效条目同样允许删除;删除购物车条目不恢复库存,也不修改收藏、浏览历史等其他模块事实。 #### 缓存、事件或外部依赖 @@ -3667,8 +3562,8 @@ UpdateCartItemQuantityRequest { #### 验证场景 - 删除本人条目 → 204,列表更新。 -- 重复删除同一 `cartItemId` → 204,幂等。 -- 删除他人条目 → 204,不报错也不泄露归属。 +- 同一幂等键重复删除同一 `cartItemId` → 重放首次 204。 +- 使用新幂等键删除已删除或他人条目 → 404,不泄露归属。 - 未登录调用 → 401 / `AUTH.UNAUTHENTICATED`。 ### A205 批量删除购物车条目 @@ -3681,13 +3576,13 @@ UpdateCartItemQuantityRequest { - 负责人:朱惠惠 - 关联数据表:DB041 - 当前状态:待交叉评审 -- 用途:买家一次性删除多个购物车条目;不在本人购物车中的条目被忽略,整体请求返回成功。 +- 用途:买家按稳定请求标识一次性删除多个本人购物车条目;全部目标先校验,任一不存在或不归属本人时整批拒绝且零删除。 - 方法与路径:`POST /api/cart/items/batch-delete` - operationId:`Cart_BatchRemoveItems` #### 请求 -- Header:`Authorization: Bearer `(必填) +- Header:`Authorization: Bearer `(必填)、`Idempotency-Key: `(必填) - Body: ```text @@ -3704,7 +3599,6 @@ BatchRemoveCartItemsRequest { ```text BatchRemoveCartItemsResponse { removedCount: integer - skippedCount: integer // 不存在或不属于当前买家的条目数量 } ``` @@ -3715,11 +3609,13 @@ BatchRemoveCartItemsResponse { | 400 | `COMMON.VALIDATION_FAILED` | `cartItemIds` 缺失、为空、超过 100 个或包含非法 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 404 | `CART.ITEM_NOT_FOUND` | 任一条目不存在或不属于当前买家 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同批次 | #### 业务规则与并发 -- 同一数据库事务内按 `cart_item_id IN (:ids) AND buyer_id = current_user_id` 删除;返回实际删除数量。 -- 不在本人购物车中的条目被忽略并计入 `skippedCount`;整体请求不报错。 +- 固定身份、键格式和请求结构校验后先重放幂等结果;新请求一次读取并校验所有去重后的 `cartItemIds` 均存在且属于当前买家,任一无效时返回 404 且一个也不删除。 +- 全部校验通过后在同一数据库事务删除整批条目,并原子保存 `removedCount` 和确定幂等结果;确定失败也保存,瞬态失败不保存。 - 删除成功后 `selectedCount` 与 `selectedTotalAmount` 自动按剩余条目重算。 #### 缓存、事件或外部依赖 @@ -3728,8 +3624,8 @@ BatchRemoveCartItemsResponse { #### 验证场景 -- 选中 3 条有效条目批量删除 → 200,`removedCount=3`,`skippedCount=0`。 -- 提交 1 条他人条目 + 2 条本人条目 → 200,`removedCount=2`,`skippedCount=1`。 +- 选中 3 条有效条目批量删除 → 200,`removedCount=3`。 +- 提交 1 条他人条目 + 2 条本人条目 → 404,整批零删除。 - 提交 0 条或 101 条 `cartItemIds` → 400 / `COMMON.VALIDATION_FAILED`。 ### A206 修改选中状态(全选/反选/单选) @@ -3742,18 +3638,18 @@ BatchRemoveCartItemsResponse { - 负责人:朱惠惠 - 关联数据表:DB041 - 当前状态:待交叉评审 -- 用途:买家设置购物车条目选中状态;支持全选、反选、单条切换;失效条目不允许被选中。 +- 用途:买家按稳定请求标识设置购物车条目选中状态;支持全选、全不选、真正反选和显式单选/多选;失效条目始终保持未选中。 - 方法与路径:`PATCH /api/cart/items/selection` - operationId:`Cart_UpdateSelection` #### 请求 -- Header:`Authorization: Bearer `(必填) +- Header:`Authorization: Bearer `(必填)、`Idempotency-Key: `(必填) - Body: ```text UpdateCartItemSelectionRequest { - mode: "SelectAll" | "DeselectAll" | "SetExplicit" + mode: "SelectAll" | "DeselectAll" | "Invert" | "SetExplicit" cartItemIds: uuid[]? // 仅当 mode = "SetExplicit" 时必填;最多 100 个 isSelected: boolean? // 仅当 mode = "SetExplicit" 时必填 } @@ -3771,13 +3667,16 @@ UpdateCartItemSelectionRequest { | 400 | `COMMON.VALIDATION_FAILED` | `mode` 非法、`cartItemIds` 缺失/超限或 `isSelected` 缺失 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 404 | `CART.ITEM_NOT_FOUND` | `SetExplicit` 中任一目标不存在或不属于当前买家 | | 409 | `CART.ITEM_UNAVAILABLE` | 尝试选中已下架或失效的条目 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同选择请求 | #### 业务规则与并发 -- 全选/反选按 `buyer_id = current_user_id` 过滤;失效条目保持未选中,不被强制选中。 -- `SetExplicit` 仅修改 `cartItemIds` 中属于当前买家的条目;不存在或不属于当前买家的 ID 统一忽略,响应不返回数量或明细,避免暴露资源归属。 -- 单条切换并发安全:服务端使用条件更新 `WHERE cart_item_id = :id AND buyer_id = current_user_id`。 +- 固定身份、键格式和请求结构校验后先读取持久化幂等结果;`Invert` 等非天然幂等动作重试必须重放首次结果,不能二次翻转。 +- `SelectAll` 把当前买家全部可结算条目设为选中、失效条目设为未选中;`DeselectAll` 把全部本人条目设为未选中;`Invert` 只反转可结算条目的当前值并把失效条目保持未选中。 +- `SetExplicit` 先验证全部 `cartItemIds` 均存在且属于当前买家,任一无效时整次 404 且零修改;当 `isSelected=true` 时,任一目标失效则整次 409 且零修改。 +- 状态变更和确定幂等结果在同一事务提交;确定 404/409 同样保存后返回,瞬态失败不固化。 - 选中状态保存在服务端;前端刷新或重新登录后状态保留。 #### 缓存、事件或外部依赖 @@ -3787,10 +3686,11 @@ UpdateCartItemSelectionRequest { #### 验证场景 - 全选 → 200,所有可用条目 `isSelected=true`,失效条目仍 `isSelected=false`。 -- 反选 → 200,所有可用条目 `isSelected=false`。 +- 全不选 → 200,所有本人条目 `isSelected=false`。 +- 反选 → 200,所有可用条目取反、失效条目仍 `isSelected=false`;同键重试不再翻转。 - 单选切换某条目 → 200,仅该条目 `isSelected` 变更。 - 尝试选中失效条目 → 409 / `CART.ITEM_UNAVAILABLE`,不修改状态。 -- 跨用户 ID 提交 → 仅本人条目被修改,其他 ID 被静默忽略且响应不泄露数量或明细。 +- 跨用户或不存在 ID 提交 → 404,整次零修改且不泄露真实归属。 ### A207 清空购物车 @@ -3803,13 +3703,13 @@ UpdateCartItemSelectionRequest { - 负责人:朱惠惠 - 关联数据表:DB041 - 当前状态:待交叉评审 -- 用途:买家一键清空本人购物车的全部条目;幂等执行,重复清空返回 204。 +- 用途:买家按稳定请求标识一键清空本人购物车全部条目;购物车本来为空仍返回成功空结果。 - 方法与路径:`DELETE /api/cart` - operationId:`Cart_Clear` #### 请求 -- Header:`Authorization: Bearer `(必填) +- Header:`Authorization: Bearer `(必填)、`Idempotency-Key: `(必填) - Body:无 #### 成功响应 @@ -3822,11 +3722,12 @@ UpdateCartItemSelectionRequest { |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同清空请求指纹 | #### 业务规则与并发 -- 按 `buyer_id = current_user_id` 物理删除全部条目;只影响当前用户。 -- 重复清空 → 204,幂等。 +- 固定身份、键格式和请求结构校验后先读取持久化幂等结果;同键同请求重放首次 204,同键换内容返回 409。 +- 新请求按 `buyer_id = current_user_id` 物理删除全部本人条目;购物车为空也形成成功空结果。删除副作用与确定结果在同一事务提交,瞬态失败不固化。 - 不影响浏览记录、收藏、消息或默认地址等其他模块数据。 #### 缓存、事件或外部依赖 @@ -3841,7 +3742,7 @@ UpdateCartItemSelectionRequest { ### A208 获取结算预览 -- 请求 Schema:无(Query 可选 `cartItemIds`) +- 请求 Schema:无 - 身份与 Policy:BuyerOnly - 模块 / Tag:Cart @@ -3855,7 +3756,7 @@ UpdateCartItemSelectionRequest { #### 请求 -- Query 参数:`cartItemIds`(可选,多个 UUID;不传则按当前 `isSelected=true` 过滤) +- Query 参数:无;目标固定为服务端保存的当前买家 `isSelected=true` 条目。 - Header:`Authorization: Bearer `(必填) - Body:无 @@ -3866,10 +3767,9 @@ UpdateCartItemSelectionRequest { ```text CheckoutPreviewResponse { - items: CartItemResponse[] // 当前可用于结算的条目 - unavailableItems: CartItemResponse[] // 仅当前买家本次选中的失效条目 - totalAmount: number // 服务端按实时单价计算的总额 - availableForCheckout: boolean // 选中项非空且全部可结算时为 true + items: CartItemResponse[] // 当前买家全部已选中条目,含每条可用性与原因 + totalAmount: number? // 仅在全部选中项有效且非空时返回确定总额 + availableForCheckout: boolean // 选中项非空且全部可结算时为 true } ``` @@ -3877,16 +3777,14 @@ CheckoutPreviewResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | `cartItemIds` 超过 100 个或包含非法 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | #### 业务规则与并发 -- 不传 `cartItemIds` 时按 `isSelected=true AND buyer_id = current_user_id` 过滤。 -- 传入 `cartItemIds` 时先与当前买家购物车取交集;不存在或不属于当前买家的 ID 被忽略且不返回任何明细。只有当前买家的失效条目进入 `unavailableItems`。 -- `totalAmount` 由服务端实时计算并返回;前端不得自行覆盖金额。 -- 只有选中项非空且全部有效时 `availableForCheckout=true`;只要存在失效项就返回 `false`,前端提示取消勾选或删除失效项后重试。 +- 固定按 `isSelected=true AND buyer_id = current_user_id` 读取服务端选中事实,客户端不能传一组 ID 绕过选中状态或让服务端静默取交集。 +- 对全部选中条目重新读取销售状态、实时库存和实时单价;任一条目失效时返回全部目标及问题原因,`availableForCheckout=false`、`totalAmount=null`,不得把可用子集包装成可直接下单结果。 +- 只有选中项非空且全部有效时才由服务端计算 `totalAmount` 并返回 `availableForCheckout=true`;M04 提交订单仍必须再次重读全部事实。 #### 缓存、事件或外部依赖 @@ -3895,9 +3793,9 @@ CheckoutPreviewResponse { #### 验证场景 -- 选中 2 条可用 + 1 条失效 → `items=2`、`unavailableItems=1`、`availableForCheckout=false`。 -- 全部失效 → `items=[]`、`availableForCheckout=false`,前端禁用提交。 -- 传入他人 `cartItemId` → 该 ID 被忽略,不进入任何响应数组,不泄露是否存在或归属。 +- 选中 2 条可用 + 1 条失效 → `items` 返回全部 3 条并标明原因,`totalAmount=null`、`availableForCheckout=false`。 +- 全部失效 → 返回全部已选条目及原因,`totalAmount=null`、`availableForCheckout=false`。 +- 没有选中条目 → `items=[]`、`totalAmount=null`、`availableForCheckout=false`。 ### A220 商家创建秒杀活动 @@ -3909,24 +3807,24 @@ CheckoutPreviewResponse { - 负责人:朱惠惠 - 关联数据表:DB042、DB043 - 当前状态:待交叉评审 -- 用途:商家维护秒杀活动;活动绑定一个已上架商品,保存秒杀价、独立库存总量与单用户限购;保存后状态为 `Draft`。 +- 用途:商家创建秒杀草稿;只保存商品、时间、秒杀价、计划秒杀量与单用户限购,不划拨普通库存,也不产生可抢库存。 - 方法与路径:`POST /api/merchant/seckill-activities` - operationId:`Seckill_CreateActivity` #### 请求 -- Header:`Authorization: Bearer `(必填,角色 Merchant) +- Header:`Authorization: Bearer `(必填,角色 Merchant)、`Idempotency-Key: `(必填) - Body: ```text CreateSeckillActivityRequest { - productId: uuid // 必填,必须是平台内已上架商品 + productId: uuid // 必填,必须是统一经营目录中的 OnSale 商品 activityName: string // 必填,1~50 字 - seckillPrice: number // 必填,>0 且 < 商品当前上架价 - totalStock: integer // 必填,1 ≤ totalStock ≤ 商品当前可售库存 - perBuyerLimit: integer // 必填,1 ≤ perBuyerLimit ≤ totalStock + seckillPrice: number // 必填,>0 + plannedQuantity: integer // 必填,1 ≤ plannedQuantity ≤ 商品当前普通可售库存 + perBuyerLimit: integer // 必填,1 ≤ perBuyerLimit ≤ plannedQuantity startAt: string // 必填,UTC ISO 8601,≥ now() - endAt: string // 必填,UTC ISO 8601,> startAt 且 ≤ startAt + 30d + endAt: string // 必填,UTC ISO 8601,> startAt } ``` @@ -3943,9 +3841,10 @@ SeckillActivityResponse { activityName: string seckillPrice: number originalPrice: number // 商品当前上架价 - totalStock: integer - remainingStock: integer // 创建后等于 totalStock - soldCount: integer // 创建后等于 0 + plannedQuantity: integer + allocatedQuantity: integer? // Draft 为 null;发布成功后才有值 + remainingStock: integer? // Draft 为 null;发布成功后才有值 + soldCount: integer? // Draft 为 null;发布成功后才有值 perBuyerLimit: integer startAt: string endAt: string @@ -3963,31 +3862,27 @@ SeckillActivityResponse { | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是商家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | -| 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品未上架 | -| 409 | `SECKILL.STOCK_EXCEEDS_AVAILABLE` | `totalStock` 超过商品当前可售库存 | +| 409 | `CATALOG.PRODUCT_NOT_ON_SALE` | 商品不是 `OnSale` | +| 409 | `SECKILL.STOCK_EXCEEDS_AVAILABLE` | `plannedQuantity` 超过商品当前普通可售库存 | | 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 应用能力或数据库暂时不可用 | #### 业务规则与并发 -- 同一商品同一时间段(`startAt`、`endAt` 与已存在活动存在重叠)不允许重复创建;重叠返回 `409 / SECKILL.TIME_WINDOW_CONFLICT`。 -- `seckillPrice < originalPrice` 由服务端校验;不接受等于或高于原价的秒杀活动。 -- `startAt ≥ now()`;可以创建立即开始的活动,服务端发布时按当前时间决定进入 `Published` 或直接进入 `Ongoing`。 -- 创建活动时同步在 `seckill_inventory`(DB043)写入计划配额 `remainingStock=totalStock`、`soldCount=0`、`frozenCount=0`;`Draft` 不对买家开放,真正的普通库存划转只在 A222 发布事务中完成。 -- 本项目是单一 B2C 平台,不按商户租户隔离商品;活动记录 `createdByMerchantUserId` 用于操作归属与秒杀订单商家分配。 +- 服务端以数据库权威时间校验 `startAt >= now()`、`endAt > startAt`;秒杀价只要求为正,不强制低于普通售价,也不发明最长 30 天或跨活动时段互斥规则。 +- Draft 只保存 `plannedQuantity`,不写可抢 `remainingStock`、`soldCount` 或冻结量,不扣普通库存;发布前这些字段为 `null`/不适用。 +- 本项目是单店 B2C 统一经营目录,不按商家账号隔离商品。`createdByMerchantUserId` 只决定活动维护权限,绝不决定秒杀订单的 `assignedMerchantUserId`。 #### 缓存、事件或外部依赖 -- 同商品时间窗口冲突在 PostgreSQL 事务内按商品加锁并复核,不能依赖 Redis 锁作为唯一正确性边界。 -- 不缓存、不发布集成事件。 +- 不缓存秒杀活动,不发布库存事件;PostgreSQL Draft 是唯一活动计划事实。 #### 验证场景 -- 合法参数创建 → 201,状态 `Draft`,库存=总量。 -- `totalStock` 超过商品库存 → 409 / `SECKILL.STOCK_EXCEEDS_AVAILABLE`。 -- `seckillPrice ≥ originalPrice` → 400 / `COMMON.VALIDATION_FAILED`。 +- 合法参数创建 → 201,状态 `Draft`,仅有 `plannedQuantity`,没有已划拨/剩余/已售库存。 +- `plannedQuantity` 超过商品普通库存 → 409 / `SECKILL.STOCK_EXCEEDS_AVAILABLE`。 - `startAt < now()` → 400 / `COMMON.VALIDATION_FAILED`。 -- 时间窗口与已存在活动重叠 → 409 / `SECKILL.TIME_WINDOW_CONFLICT`。 -- 尝试绑定未上架商品 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 +- 同商品活动时间重叠不由本期接口额外拒绝。 +- 尝试绑定非 `OnSale` 商品 → 409 / `CATALOG.PRODUCT_NOT_ON_SALE`。 ### A221 商家更新秒杀活动 @@ -3999,7 +3894,7 @@ SeckillActivityResponse { - 负责人:朱惠惠 - 关联数据表:DB042、DB043 - 当前状态:待交叉评审 -- 用途:商家在 `Draft` 或 `Published` 状态下更新尚未开始的秒杀活动参数;`Ongoing`/`Ended`/`Cancelled` 状态不允许修改。 +- 用途:活动创建人在 `Draft` 状态下修改完整活动计划;发布后只允许取消,不再允许编辑。 - 方法与路径:`PATCH /api/merchant/seckill-activities/{activityId}` - operationId:`Seckill_UpdateActivity` @@ -4011,9 +3906,10 @@ SeckillActivityResponse { ```text UpdateSeckillActivityRequest { + productId?: uuid // 可选;变更后仍须是 OnSale 商品 activityName?: string // 可选 seckillPrice?: number // 可选 - totalStock?: integer // 可选;只能调大或保持;不得小于已售数量 + plannedQuantity?: integer // 可选;正整数,可调大或调小 perBuyerLimit?: integer // 可选 startAt?: string // 可选;不得早于 now() endAt?: string // 可选 @@ -4031,29 +3927,28 @@ UpdateSeckillActivityRequest { |---:|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 字段格式或时间窗口非法 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | -| 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | -| 409 | `SECKILL.INVALID_STATUS` | 活动已 `Ongoing`/`Ended`/`Cancelled` | -| 409 | `SECKILL.STOCK_BELOW_SOLD` | `totalStock` 小于已售数量 | -| 409 | `SECKILL.TIME_WINDOW_CONFLICT` | 与其他活动时间窗口重叠 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家 | +| 404 | `SECKILL.ACTIVITY_NOT_FOUND` | 活动不存在或不是当前商家创建 | +| 409 | `SECKILL.INVALID_STATUS` | 活动不是 `Draft` | +| 409 | `CATALOG.PRODUCT_NOT_ON_SALE` | 目标商品不是 `OnSale` | +| 409 | `SECKILL.STOCK_EXCEEDS_AVAILABLE` | `plannedQuantity` 超过当前普通可售库存 | #### 业务规则与并发 -- 仅允许在 `Draft` 或尚未开始的 `Published` 状态更新;状态字段由 `status IN ('Draft','Published') AND start_at > now()` 条件更新保证。 -- `totalStock` 只允许在 `Draft` 修改;发布后库存配额已经从 Catalog 普通库存划转,不通过本接口调整。 -- `totalStock` 只允许调大或保持;调整后必须满足 `remainingStock + soldCount + frozenCount = totalStock`。 -- 修改后 `startAt` 与 `endAt` 必须保持 `startAt ≥ now()` 与 `endAt > startAt`。 +- 仅允许 `status=Draft` 且 `createdByMerchantUserId=currentUserId` 的活动更新;活动不存在或非创建人统一 404。 +- `productId`、价格、时间、`plannedQuantity` 和限购均可在草稿中修改;计划量可调大或调小,但必须为正、不得超过目标商品当前普通可售库存,单用户限购不得超过计划量。 +- 修改后仍满足 `startAt >= databaseNow` 与 `endAt > startAt`;不维护 `remaining + sold + frozen`,因为 Draft 尚未形成已划拨库存,本期也没有冻结量。 #### 缓存、事件或外部依赖 -- 同步更新 Redis 缓存:`cache:seckill:activity:{activityId}`(仅元数据,不含库存)。 +- Draft 不公开且不缓存;更新只写 PostgreSQL 活动计划。 #### 验证场景 - 草稿活动更新名称与价格 → 200。 -- 草稿活动 `totalStock` 调小到 `soldCount` 以下 → 409 / `SECKILL.STOCK_BELOW_SOLD`。 -- 进行中活动尝试改价 → 409 / `SECKILL.INVALID_STATUS`。 -- 时间窗口与他人活动重叠 → 409 / `SECKILL.TIME_WINDOW_CONFLICT`。 +- 草稿活动在正数且不超过普通库存范围内调大或调小计划量 → 200。 +- `Published`、`Ongoing`、`Ended` 或 `Cancelled` 活动尝试改价 → 409 / `SECKILL.INVALID_STATUS`。 +- 非活动创建人访问 → 404,不泄露活动存在性。 ### A222 商家发布秒杀活动 @@ -4065,50 +3960,52 @@ UpdateSeckillActivityRequest { - 负责人:朱惠惠 - 关联数据表:DB042、DB043 - 当前状态:待交叉评审 -- 用途:商家直接发布 `Draft` 活动;本期没有审核流程,服务端按数据库当前时间决定立即进入 `Ongoing` 或先进入 `Published`,后续按 `endAt` 推进到 `Ended`。 +- 用途:活动创建人在开始时间前发布 `Draft`;重新校验后原子划拨普通库存并固定进入 `Published`,由 Worker 到时推进为 `Ongoing`。 - 方法与路径:`POST /api/merchant/seckill-activities/{activityId}/publish` - operationId:`Seckill_PublishActivity` #### 请求 - Route 参数:`activityId: uuid` -- Header:`Authorization: Bearer `(必填,角色 Merchant) +- Header:`Authorization: Bearer `(必填,角色 Merchant)、`Idempotency-Key: `(必填) - Body:无 #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`SeckillActivityResponse`(`startAt <= databaseNow` 时 `status="Ongoing"`,否则 `status="Published"`) +- 响应 Schema:`SeckillActivityResponse`(`status="Published"`,`allocatedQuantity=plannedQuantity`、`remainingStock=allocatedQuantity`、`soldCount=0`) #### 失败响应 | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | -| 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | -| 409 | `SECKILL.INVALID_STATUS` | 活动已发布或已结束 | -| 409 | `SECKILL.TIME_WINDOW_EXPIRED` | 数据库当前时间已经达到或超过 `endAt`,该草稿活动不能再发布 | -| 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品已下架,禁止发布 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家 | +| 404 | `SECKILL.ACTIVITY_NOT_FOUND` | 活动不存在或不是当前商家创建 | +| 409 | `SECKILL.INVALID_STATUS` | 新请求中的活动不是 `Draft` | +| 409 | `SECKILL.START_TIME_REACHED` | 数据库当前时间已经达到或超过 `startAt` | +| 409 | `CATALOG.PRODUCT_NOT_ON_SALE` | 商品已不是 `OnSale` | +| 409 | `SECKILL.STOCK_EXCEEDS_AVAILABLE` | 普通可售库存不足以划拨计划量 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同活动或请求 | | 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 库存划转或数据库暂时不可用 | #### 业务规则与并发 -- 发布事务先读取一次数据库当前时间;若 `now() >= endAt`,立即返回 `SECKILL.TIME_WINDOW_EXPIRED`,不得调用 Catalog 或划转库存。时间有效时,再通过 Catalog 公开应用契约按 `activityId` 幂等地把 `totalStock` 从普通可售库存划转为秒杀配额,并按同一数据库时间把活动从 `Draft` 条件更新为 `Ongoing`(`startAt <= now() < endAt`)或 `Published`(`now() < startAt`);任一步失败整体回滚,不产生双份可售库存。 -- 发布、更新和取消均按 `created_by_merchant_user_id = current_user_id` 校验活动操作归属;这只约束活动创建人,不引入多商户商品租户。 -- 商品已下架或普通可售库存不足时拒绝发布;商家修正商品或活动库存后重试。 -- 事务提交后尽力失效并预热 Redis 活动缓存;缓存失败只影响性能并进入重试,不否定已提交的发布结果。Worker 只需把尚未开始的 `Published` 按 `startAt` 推进为 `Ongoing`,并把到期的 `Ongoing` 推进为 `Ended`。 +- 完成身份、幂等键格式和路由字段校验后,先按商家、接口、键和请求指纹读取持久化结果;同键同请求重放首次完整结果,不读取当前活动或库存;同键换内容返回 409。 +- 新请求必须同时满足活动属于当前商家、`status=Draft`、`databaseNow < startAt`、商品仍为 `OnSale` 且普通库存足够; fresh key 请求已发布活动返回状态冲突。 +- 在一个 PostgreSQL 原子结果中把 `plannedQuantity` 从普通库存划入独立秒杀库存,写入 `allocatedQuantity`、`remainingStock=allocatedQuantity`、`soldCount=0`,把状态推进为 `Published`,并保存确定幂等结果与可靠缓存失效事实;任一步失败整体回滚,不产生双份库存或重复划拨。 +- Worker 只在权威时间到达后把 `Published` 推进为 `Ongoing`,并在结束时间推进为 `Ended`;发布接口本身永远不直接返回 `Ongoing`。 #### 缓存、事件或外部依赖 -- Redis:`cache:seckill:activity:{activityId}`、`cache:seckill:list:active`。 +- 秒杀活动、状态和库存不进入 Redis/C07。发布划拨减少普通库存后,事务提交后立即失效目标 A103 和唯一固定首页,并在 3 秒后二次失效;缓存失效失败不回滚已提交发布结果。 #### 验证场景 -- 未来开始的草稿活动发布 → 200 + `Published`;立即开始的草稿活动发布 → 200 + `Ongoing`;两者普通库存与秒杀配额总量均守恒。 -- 已超过 `endAt` 的草稿活动发布 → 409 / `SECKILL.TIME_WINDOW_EXPIRED`,普通库存和秒杀配额均不变化。 -- 重复发布 → 409 / `SECKILL.INVALID_STATUS`。 -- 商品已下架 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 +- 开始前草稿发布 → 200 + `Published`,普通库存与独立秒杀库存总量守恒。 +- 已到或超过 `startAt` 的草稿发布 → 409 / `SECKILL.START_TIME_REACHED`,库存不变化。 +- 同一幂等键重复发布 → 重放首次 200 且不重复划拨;新键再次发布 → 409 / `SECKILL.INVALID_STATUS`。 +- 商品已下架 → 409 / `CATALOG.PRODUCT_NOT_ON_SALE`。 ### A223 商家取消秒杀活动 @@ -4139,7 +4036,7 @@ CancelSeckillActivityRequest { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`SeckillActivityResponse`(`status="Cancelled"`,含 `cancelReason`) +- 响应 Schema:`SeckillActivityResponse`(`status="Cancelled"`,含 `cancelReason`、`cancelledAt`;Draft 的库存字段仍为空,已发布/进行中活动保留已划拨数量) #### 失败响应 @@ -4147,19 +4044,21 @@ CancelSeckillActivityRequest { |---:|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 字段格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | -| 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家 | +| 404 | `SECKILL.ACTIVITY_NOT_FOUND` | 活动不存在或不是当前商家创建 | | 409 | `SECKILL.INVALID_STATUS` | 活动已 `Ended` 或已 `Cancelled` | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同取消请求 | #### 业务规则与并发 -- 条件更新:`status IN ('Draft','Published','Ongoing') → 'Cancelled'`;影响行数为 0 时按 409 处理。 -- 取消时 `remainingStock` 保留为冻结状态,不自动回收到普通商品库存。 +- 完成固定校验后先读取持久化幂等结果;同键同请求重放首次取消结果,新键对 `Ended`/`Cancelled` 请求返回 409。 +- 条件更新:`status IN ('Draft','Published','Ongoing') → 'Cancelled'`;状态、`cancelReason`、`cancelledAt` 与确定幂等结果在同一事务提交。 +- Draft 取消时没有已划拨库存;`Published`/`Ongoing` 取消后,未售的 `remainingStock` 继续隔离留在原活动且不可售,不存在 `frozenCount`,也不回到普通库存。 - 已存在秒杀订单沿用 M04 状态机;C03 超时取消时回补到原秒杀库存通道。 #### 缓存、事件或外部依赖 -- 删除 Redis 缓存:`cache:seckill:activity:{activityId}`、`cache:seckill:list:active`。 +- 秒杀活动不缓存;取消不改变普通库存,因此不触发 C07 商品详情或固定首页失效。 #### 验证场景 @@ -4227,7 +4126,7 @@ SeckillActivityListResponse { - 商家查询本人活动 → 200,按 `startAt desc` 排序。 - 状态筛选 `Ongoing` → 仅返回进行中活动。 -- 商家访问他人活动 → 403,不泄露他人数据。 +- 本接口固定按当前创建人过滤,不存在查询他人活动的参数;商家详情归属由 A225 校验。 ### A225 商家秒杀活动详情 @@ -4275,14 +4174,15 @@ SeckillOrderStatsResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家或不拥有该活动 | -| 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | +| 403 | `AUTH.FORBIDDEN` | 当前账号不是商家 | +| 404 | `SECKILL.ACTIVITY_NOT_FOUND` | 活动不存在或不是当前商家创建 | #### 业务规则与并发 - 严格按 `created_by_merchant_user_id = current_user_id` 过滤;非创建人访问返回 404,避免泄露活动存在性。 - 订单统计通过 Ordering 公开查询契约取得;Seckill 不读取 Ordering 内部订单表,也不维护平行订单事实。 - `cancelReason` 与 `cancelledAt` 仅在 `status=Cancelled` 时返回。 +- 活动自身 `plannedQuantity`、`allocatedQuantity`、`remainingStock`、`soldCount` 是库存权威:Draft 只有计划量,发布后才有已划拨/剩余/已售数量;`orderStats` 只是附加汇总,不能用订单笔数替代按数量扣减、已售数量或买家限购占用。 #### 缓存、事件或外部依赖 @@ -4319,7 +4219,7 @@ SeckillOrderStatsResponse { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`SeckillActivityListResponse`(仅公开字段,`status` 仅返回 `Published` / `Ongoing`) +- 响应 Schema:`PublicSeckillActivityListResponse`,元素固定包含 `activityId`、`product`(`productId`、`name`、`mainImageUrl`)、`status`(`Published`/`Ongoing`)、`seckillPrice`、`originalPrice`、`startAt`、`endAt`、`remainingStock`、`soldCount`、`isSoldOut`、`serverTime`、`resultVersion`。 #### 失败响应 @@ -4331,11 +4231,11 @@ SeckillOrderStatsResponse { - 仅返回 `status IN ('Published','Ongoing')` 的活动;已结束、已取消或草稿活动不公开。 - 排序默认按 `startAt asc`(即将开始优先),相同 `startAt` 时按 `activityId` 稳定排序。 -- 公开响应中 `remainingStock` 不返回具体数字,仅返回 `isSoldOut` 布尔;具体剩余库存通过 A227 查询。 +- `remainingStock`、`soldCount` 和 `isSoldOut` 从同一已提交库存事实派生;`serverTime` 使用服务端权威 UTC 时间,`resultVersion` 随活动/库存结果单调变化,前端不得用旧结果覆盖新结果。 #### 缓存、事件或外部依赖 -- Redis:`cache:seckill:list:active`,TTL 30 秒;活动状态变更或售罄时主动失效。 +- A226 每次直读 PostgreSQL 活动与库存事实;秒杀列表、状态、剩余库存、已售数量和售罄结果不进入 Redis/C07。 #### 验证场景 @@ -4366,7 +4266,8 @@ SeckillOrderStatsResponse { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`SeckillActivityDetailResponse`(仅公开字段,`cancelReason` 等内部字段不返回) +- 响应 Schema:`PublicSeckillActivityDetailResponse`,固定包含 A226 单项全部公开字段;不复用含 `orderStats`、`cancelReason` 或创建人信息的商家详情 Schema。 +- 若携带可验证且状态正常的 Buyer JWT,额外返回 `currentBuyerOccupiedQuantity` 与 `currentBuyerRemainingQuantity`;未携带或可选 Token 无效/过期/禁用时按游客返回,不返回私人限购字段。 #### 失败响应 @@ -4378,17 +4279,18 @@ SeckillOrderStatsResponse { #### 业务规则与并发 - 仅返回 `status IN ('Published','Ongoing')` 的活动;其他状态返回 410。 -- 已登录买家响应额外包含 `currentBuyerOrderCount`、`currentBuyerRemaining`(用于限购提示),按 `(activity_id, buyer_id)` 实时统计。 +- 已登录买家提示按 `(activity_id, buyer_id)` 的当前有效占用数量计算,不按订单笔数计算;待支付和已支付订单按购买数量占用,取消成功才按数量释放。 +- `remainingStock`、`soldCount`、`isSoldOut`、`serverTime` 与 `resultVersion` 均来自权威 PostgreSQL 结果,客户端按版本应用更新。 #### 缓存、事件或外部依赖 -- Redis:`cache:seckill:activity:{activityId}`,TTL 30 秒;活动状态变更或库存售罄时主动失效。 +- A227 每次直读 PostgreSQL;不缓存活动详情或限购数量,C07 只服务普通商品固定首页和 A103 商品详情。 #### 验证场景 - 游客访问进行中活动 → 200,含商品基础信息、秒杀价、开始/结束时间。 - 已结束活动 → 410 / `SECKILL.ACTIVITY_GONE`。 -- 已登录买家访问 → 额外返回当前用户已下单数量与剩余可购数量。 +- 已登录买家访问 → 额外返回当前有效占用数量与剩余可购数量。 ### A228 秒杀下单 @@ -4431,8 +4333,12 @@ PlaceSeckillOrderResponse { seckillPrice: number totalAmount: number // seckillPrice × quantity,服务端计算 status: "PendingPayment" - expiresAt: string // 订单支付截止时间,UTC ISO 8601 + paymentDeadline: string // 固定订单支付截止时间,UTC ISO 8601 remainingStock: integer // 扣减后剩余库存(供前端展示) + soldCount: integer + isSoldOut: boolean + serverTime: string + resultVersion: integer } ``` @@ -4443,16 +4349,17 @@ PlaceSeckillOrderResponse { | 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | -| 404 | `RESOURCE.NOT_FOUND` | 活动不存在 | +| 404 | `SECKILL.ACTIVITY_NOT_FOUND` | 活动不存在 | +| 404 | `IDENTITY.ADDRESS_NOT_FOUND` | 地址不存在或不属于当前买家 | | 409 | `SECKILL.NOT_STARTED` | 活动尚未开始 | | 409 | `SECKILL.ALREADY_ENDED` | 活动已结束 | +| 409 | `SECKILL.ACTIVITY_CANCELLED` | 活动已取消 | | 409 | `SECKILL.SOLD_OUT` | 秒杀库存售罄 | | 409 | `SECKILL.PER_BUYER_LIMIT_EXCEEDED` | 超过单用户限购 | | 409 | `SECKILL.QUANTITY_EXCEEDS_LIMIT` | 单次购买数量超过限购或库存 | -| 409 | `RESOURCE.CONFLICT` | 地址不存在或不归属当前买家 | -| 409 | `CATALOG.PRODUCT_UNPUBLISHED` | 商品已下架 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键被用于不同请求内容 | | 429 | `COMMON.RATE_LIMITED` | 触发限流(秒杀入口限流阈值) | +| 503 | `ORDER.DEFAULT_MERCHANT_UNAVAILABLE` | 唯一启用默认商家缺失、重复、禁用或并发失效 | | 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Redis 限流、库存通道或下游服务不可用 | | 500 | `COMMON.INTERNAL_ERROR` | 未处理的服务端错误 | @@ -4460,29 +4367,32 @@ PlaceSeckillOrderResponse { - 秒杀下单为高风险操作,必须使用 `Idempotency-Key`;缺失时返回 400 / `COMMON.VALIDATION_FAILED`。 - 完成认证和固定请求字段校验后,先读取 PostgreSQL 幂等记录:相同 Key + 相同请求指纹直接重放首次结果,不再经过限流、时间、库存或限购校验;相同 Key + 不同指纹立即返回 `409 / IDEMPOTENCY.KEY_REUSED`。只有全新 Key 才进入后续可变业务校验。 +- 全新请求在幂等检查后才执行正式限流,并读取活动、地址归属、买家限购占用和 Identity 的唯一启用默认商家;A228 不再次用 Catalog 当前上下架状态推翻已发布活动的独立库存资格。 - 同一数据库事务内顺序: - 1. 按 `UPDATE seckill_inventory SET remaining = remaining - :qty, sold = sold + :qty, updated_at = now() WHERE activity_id = :aid AND status='Ongoing' AND start_at <= now() AND end_at > now() AND remaining >= :qty` 条件扣减秒杀库存;影响行数为 0 时整体事务回滚。 - 2. 在 DB044 `seckill_buyer_quotas` 对 `(activity_id, buyer_id)` 建唯一配额行,使用原子 UPSERT/条件更新保证 `purchased_quantity + :qty <= perBuyerLimit`;普通聚合查询或 Redis 不能作为限购正确性边界。取消成功按 `orderId` 幂等释放一次对应数量。 - 3. 通过 Ordering 公开应用契约创建 DB061 `orders` 与 DB062 `order_items` 的共享订单事实,保存 `seckillActivityId`、秒杀价、原价快照,并把活动的 `createdByMerchantUserId` 固定为该订单的 `assignedMerchantUserId`;Seckill 不建立第二套订单状态机。 - 4. Ordering 在同一受控事务中写入 `OrderCreatedIntegrationEvent` Outbox;Seckill 不再另造平行的订单创建事件。 + 1. 按 `Ongoing + 权威时间窗口 + remaining >= quantity` 条件扣减秒杀库存并增加 `soldCount`;影响行数为 0 时不产生部分结果。 + 2. 按 `(activityId, buyerId)` 对当前有效占用数量执行原子条件更新,保证累加后不超过 `perBuyerLimit`;普通聚合查询或 Redis 不能作为限购正确性边界。取消成功按 `orderId` 和原数量最多释放一次。 + 3. 通过 Ordering 统一创建 DB061/DB062 共享订单事实,使用 Identity 解析出的唯一启用默认商家写入 `assignedMerchantUserId`,保存地址快照、订单项与成交价快照、`Seckill` 来源、`seckillActivityId` 和固定 `paymentDeadline`。活动 `createdByMerchantUserId` 只决定活动管理权,绝不参与订单分配。 + 4. 秒杀库存、限购占用、共享订单与快照、可靠待发布订单事实和确定幂等结果整体提交;任一步失败全部回滚,Seckill 不建立第二套订单状态机或事件。 - 不写入普通商品库存;`products.stock` 不受秒杀下单影响。 -- 新请求的拒绝顺序为限流、时间窗口、库存、单用户限购及其他业务异常;已命中的幂等重放或 Key 冲突在这些可变校验之前处理。 +- 成功、未开始、已结束、已取消、售罄、超限、地址无效、默认商家不可用和已正式返回的 429 都是可重放的确定结果,必须与本次 Key 持久绑定后再返回;数据库断连、事务提交未知或依赖中断等未形成确定结果的失败不固化,客户端用原 Key 重试。 +- 成功响应以及上述确定失败的 ProblemDetails `extensions.activitySnapshot` 都返回处理后的 `remainingStock`、`soldCount`、`isSoldOut`、`serverTime`、`resultVersion`,保证同一交互刷新且旧结果不能覆盖新结果。 #### 缓存、事件或外部依赖 - PostgreSQL:幂等请求指纹、首次结果、秒杀库存条件扣减和共享订单创建处于同一受控事务;相同 Key 重放首次结果,不依赖 Redis 保存唯一事实。 -- Redis:仅用于入口限流和活动元数据缓存;失败时按本接口的 429/503 降级规则处理,不参与库存与幂等正确性。 -- Outbox:Ordering 发布 `OrderCreatedIntegrationEvent` 供 M09 消费;C03 不消费订单创建事件,只按 Ordering 的 `expiresAt` 周期扫描待支付订单。 +- Redis:只可用于入口限流,不保存活动、库存、限购或幂等唯一事实;限流依赖故障不得伪装为售罄或成功。 +- 可靠待发布事实:Ordering 在来源事务中保存订单已创建事实,事务后才由传输器投递给 M09;C03 只按 Ordering 的 `paymentDeadline` 扫描待支付订单。 #### 验证场景 -- 100 并发抢 10 件库存、单用户限购 1 → 恰好 10 笔成功订单,其余 90 笔以 `SOLD_OUT` 或 `PER_BUYER_LIMIT_EXCEEDED` 失败;库存 `remaining=0`、`sold=10`。 +- 100 个不同买家分别使用不同幂等键并发抢 10 件库存、单用户限购 1 → 恰好 10 笔成功订单,其余请求明确失败;库存 `remaining=0`、`sold=10`。 - 同一买家两次提交限购 1 的活动 → 第二次返回 `PER_BUYER_LIMIT_EXCEEDED`,不重复扣减。 - 同一幂等键重复提交 → 第二次返回首次成功订单号,不重复扣减。 - 活动未开始 → 409 / `SECKILL.NOT_STARTED`。 - 活动已结束 → 409 / `SECKILL.ALREADY_ENDED`。 -- 商品已下架 → 409 / `CATALOG.PRODUCT_UNPUBLISHED`。 -- 地址不属于当前买家 → 409 / `RESOURCE.CONFLICT`,不泄露地址存在性。 +- 活动发布后普通商品下架不反向改写已划拨秒杀库存资格;A228 仍按活动状态、时间、库存和限购裁决。 +- 地址不属于当前买家 → 404 / `IDENTITY.ADDRESS_NOT_FOUND`,不泄露地址存在性。 +- 默认商家配置缺失、重复或禁用 → 503 / `ORDER.DEFAULT_MERCHANT_UNAVAILABLE`,不创建无人负责订单。 > 来源:[`interface-wqq.md`](interface/interface-wqq.md)。A301~A308 已统一归入 Ordering,A302/A303 已承接秒杀订单查询;DB061/DB062、跨模块应用契约和状态字段仍待评审。 @@ -4519,12 +4429,13 @@ PlaceSeckillOrderResponse { ``` - **校验规则**: - `addressId`:必填,UUID格式,必须属于当前买家 - - `cartItemIds`:必填,非空数组,每个元素为UUID格式 + - `cartItemIds`:必填,非空、元素唯一,每个元素为 UUID;全部条目必须存在且属于当前买家 - `Idempotency-Key` 只从 Header 读取,Body 不重复传递 #### 成功响应 - **HTTP状态**:`201 Created` +- **Response Header**:`Location: /api/orders/{orderId}`(A303) - **响应Schema**:`CreateOrderResponse` - **示例**: ```json @@ -4533,11 +4444,10 @@ PlaceSeckillOrderResponse { "message": "ok", "data": { "orderId": "3f0ed9a9-3c61-4ab6-a8dd-a54ea8dd78af", - "orderNo": "ORD20260724001", "totalAmount": 299.00, "status": "PendingPayment", "createdAt": "2026-07-24T10:00:00Z", - "expiresAt": "2026-07-24T10:30:00Z" + "paymentDeadline": "2026-07-24T10:30:00Z" } } ``` @@ -4548,9 +4458,10 @@ PlaceSeckillOrderResponse { |---|---|---| | 400 | ORDER.INVALID_PARAM | 参数格式错误 | | 400 | ORDER.EMPTY_CART_ITEMS | 购物车商品列表为空 | -| 400 | ORDER.INVALID_ADDRESS | 收货地址无效或不归属当前用户 | | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 403 | AUTH.FORBIDDEN | 当前账号不是买家 | +| 404 | IDENTITY.ADDRESS_NOT_FOUND | 地址不存在或不属于当前买家 | +| 404 | CART.ITEM_NOT_FOUND | 任一购物车条目不存在或不属于当前买家 | | 409 | ORDER.STOCK_INSUFFICIENT | 商品库存不足 | | 409 | ORDER.ITEM_NOT_AVAILABLE | 商品已下架或不可售 | | 409 | IDEMPOTENCY.KEY_REUSED | 同一幂等键被用于不同请求内容 | @@ -4558,22 +4469,24 @@ PlaceSeckillOrderResponse { #### 业务规则与并发 -1. 同一幂等键只创建一张订单,重复请求返回首次成功结果 -2. 库存扣减使用条件更新 `WHERE stock >= quantity`,避免超卖 -3. 订单金额由服务端计算,不接受客户端传入 -4. 订单项保存商品名称、图片、单价快照 -5. 本期是单店 B2C,不拆多商户子订单;普通订单创建时通过 Identity 公开应用契约解析唯一且启用的默认商家运营账号,并把 `assignedMerchantUserId` 保存为订单处理与通知归属。未配置、配置重复或账号不可用时整单失败,不创建无人处理的订单。 +1. 完成身份、幂等键格式和固定请求结构校验后,先按买家、接口、键和请求指纹读取 PostgreSQL 幂等结果,再读取地址、购物车、商品、库存和默认商家等可变事实;同键同请求重放首次完整结果,同键换内容返回 409。 +2. 服务端从购物车重读每项商品 ID、数量和选中/归属事实,从 Catalog 重读 `OnSale`、实时价格与普通库存;不接受客户端传入数量、价格、金额或商家。 +3. 普通库存扣减使用条件更新 `WHERE stock >= quantity`,避免超卖;订单金额由服务端按已确认实时价格计算。 +4. 本期使用 UUID `orderId` 作为对外订单号,不生成暴露业务量的顺序型 `ORD...` 编号。订单项保存 `orderItemId`、商品 ID、名称、图片、成交单价和数量快照,地址保存完整下单时快照。 +5. 普通订单和秒杀订单都通过 Identity 解析同一唯一启用默认商家并写入 `assignedMerchantUserId`;未配置、重复、禁用或与下单并发失效时整单失败,不创建无人处理订单。 +6. 商品/库存/默认商家不可用等已经正式裁决的业务失败可与幂等键持久绑定并稳定重放;数据库连接中断、事务提交未知等未形成确定结果的失败不固化。 #### 缓存、事件或外部依赖 -- 发布 `OrderCreatedIntegrationEvent` 到 Outbox -- 订单事实写入 DB061 `orders`、DB062 `order_items`;地址、购物车、商品、库存和默认商家运营账号通过对应模块公开应用契约协作,不直接访问其他模块内部表 +- 普通库存扣减、订单/订单项、地址快照、默认商家归属、固定 `paymentDeadline`、本次购物车条目清理、可靠订单已创建待发布事实和确定幂等结果在同一 PostgreSQL 事务提交;任一步失败整体回滚,购物车原样保留。 +- RabbitMQ 只在事务提交后传输待发布事实;不能用“已发消息”代替来源事务中的可靠记录。 +- 普通库存扣减提交后触发目标 A103 与唯一固定首页立即/3 秒二次失效;缓存失败不回滚订单。秒杀订单扣减独立库存,不触发 C07。 #### 验证场景 1. 正常提交订单:返回201,订单号 2. 库存不足:返回409,订单未创建 -3. 地址无效:返回400 +3. 地址无效或任一购物车条目越权:返回404且整单不创建 4. 幂等键重复:返回原订单号,不重复扣库存 --- @@ -4585,7 +4498,7 @@ PlaceSeckillOrderResponse { - **负责人**:韦乾强 - **关联数据表**:DB061(orders)、DB062(order_items) - **当前状态**:部分定义 -- **用途**:买家分页查询自己的普通或秒杀订单,支持按状态、订单来源、秒杀活动和创建时间筛选 +- **用途**:买家分页查询自己的普通或秒杀订单,支持按状态、订单来源和秒杀活动筛选 - **方法与路径**:`GET /api/orders` - **operationId**:`Ordering_ListOrders` - **请求Schema**:无 @@ -4603,7 +4516,6 @@ PlaceSeckillOrderResponse { - `status`(可选):筛选订单状态,`PendingPayment`/`Paid`/`Shipped`/`Completed`/`Cancelled` - `orderType`(可选):`Normal` / `Seckill` - `seckillActivityId`(可选,UUID):按秒杀活动筛选;传入时 `orderType` 固定按 `Seckill` 处理 - - `createdFrom`、`createdTo`(可选,UTC ISO 8601):创建时间范围 - **Header**:`Authorization: Bearer `(必需) - **Body**:无 @@ -4620,14 +4532,15 @@ PlaceSeckillOrderResponse { "items": [ { "orderId": "uuid", - "orderNo": "ORD20260724001", "orderType": "Seckill", "seckillActivityId": "uuid", "seckillActivityName": "暑期秒杀", "status": "PendingPayment", "totalAmount": 299.00, "createdAt": "2026-07-24T10:00:00Z", - "expiresAt": "2026-07-24T10:30:00Z", + "paymentDeadline": "2026-07-24T10:30:00Z", + "canPay": true, + "availableActions": ["pay", "cancel"], "itemSummary": "商品A x1,商品B x2" } ], @@ -4652,10 +4565,11 @@ PlaceSeckillOrderResponse { 1. 订单按 `createdAt desc, orderId desc` 稳定排序。 2. `itemSummary` 最多展示 3 个商品名称,多的显示“+X件”。 3. A229 已取消;秒杀订单列表由本接口通过 `orderType=Seckill` 或 `seckillActivityId` 查询,不建立第二套订单查询事实。 +4. `canPay` 与 `availableActions` 由服务端同时按 `status=PendingPayment` 和 `databaseNow < paymentDeadline` 派生;已经过期但 C03 尚未提交取消的待支付订单不得返回支付入口。 #### 缓存、事件或外部依赖 -无 +- 订单列表不进入 C07 或其他业务缓存,每次按当前买家和 PostgreSQL 已提交订单事实查询。 #### 验证场景 @@ -4699,7 +4613,6 @@ PlaceSeckillOrderResponse { "message": "ok", "data": { "orderId": "uuid", - "orderNo": "ORD20260724001", "orderType": "Seckill", "seckill": { "activityId": "uuid", @@ -4710,7 +4623,11 @@ PlaceSeckillOrderResponse { "status": "PendingPayment", "totalAmount": 299.00, "createdAt": "2026-07-24T10:00:00Z", - "expiresAt": "2026-07-24T10:30:00Z", + "paymentDeadline": "2026-07-24T10:30:00Z", + "canPay": true, + "payment": null, + "cancellation": null, + "completion": null, "addressSnapshot": { "receiverName": "张三", "phone": "138****8888", @@ -4721,11 +4638,18 @@ PlaceSeckillOrderResponse { }, "items": [ { + "orderItemId": "uuid", "productId": "uuid", "productName": "商品A", "imageUrl": "https://...", "unitPrice": 199.00, "quantity": 1, + "shippedQuantity": 0, + "afterSales": { + "processingQuantity": 0, + "refundedQuantity": 0, + "remainingApplicableQuantity": 1 + }, "subtotal": 199.00 } ], @@ -4742,25 +4666,27 @@ PlaceSeckillOrderResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | -| 404 | ORDER.NOT_FOUND | 订单不存在 | -| 403 | ORDER.ACCESS_DENIED | 订单不属于当前用户 | +| 404 | ORDER.NOT_FOUND | 订单不存在或不属于当前买家 | #### 业务规则与并发 -1. 订单项为快照,包含下单时的商品名称、图片和成交单价。 -2. 地址为快照,包含下单时的收货信息。 -3. 普通订单 `orderType=Normal` 且 `seckill=null`;秒杀订单返回活动 ID、活动名称、原价和秒杀价快照,承接已取消的 A230。 -4. `availableActions` 根据当前状态展示可执行操作。 +1. 订单项为快照,包含稳定 `orderItemId`、下单时商品名称、图片、成交单价和购买数量;地址为下单时完整快照。 +2. 普通订单 `orderType=Normal` 且 `seckill=null`;秒杀订单返回活动 ID、活动名称、原价和秒杀价快照,承接已取消的 A230。 +3. 支付成功时 `payment` 返回最小摘要:`paymentId`、`source`(`Wallet`/`SimulatedChannel`)、`amount`、`paidAt`;没有确定支付事实时为 `null`,不能仅按订单状态猜测。 +4. `cancellation` 在已取消时返回 `cancelledAt` 与 `reason`(`BuyerRequested`/`PaymentExpired`);`completion` 在已完成时返回唯一 `completedAt` 与 `completedBy`(`BuyerConfirmed`/`AutoCompleted`)。 +5. 每个订单项返回购买数量、实际已发货数量,以及处理中售后数量、已退款数量和剩余可申请数量;售后摘要由 M10 公开契约派生,不修改订单核心状态。 +6. `canPay` 与 `availableActions` 按最新订单状态、`paymentDeadline`、评价资格和售后资格派生;过期但尚未被 C03 取消的 `PendingPayment` 订单也必须 `canPay=false`。 +7. `statusHistory` 覆盖创建、支付、发货、完成或取消的真实已提交时间点,不生成计划时间。 #### 缓存、事件或外部依赖 -无 +- 订单详情不缓存;支付与售后摘要通过公开应用契约读取已提交事实,不直接访问模块内部表。 #### 验证场景 1. 正常查询:返回完整订单详情 2. 订单不存在:返回404 -3. 跨用户访问:返回403 +3. 跨用户访问:返回404,不泄露订单存在性 --- @@ -4800,7 +4726,7 @@ PlaceSeckillOrderResponse { "orderId": "uuid", "status": "Cancelled", "cancelledAt": "2026-07-24T11:00:00Z", - "cancelReason": "BUYER_CANCELLED" + "cancelReason": "BuyerRequested" } } ``` @@ -4810,28 +4736,27 @@ PlaceSeckillOrderResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | -| 404 | ORDER.NOT_FOUND | 订单不存在 | -| 403 | ORDER.ACCESS_DENIED | 订单不属于当前用户 | -| 409 | ORDER.INVALID_STATUS | 订单状态不允许取消(已支付/已发货/已完成);已取消返回现有成功结果 | +| 404 | ORDER.NOT_FOUND | 订单不存在或不属于当前买家 | +| 409 | ORDER.INVALID_STATUS | 支付或后续状态已经胜出;响应包含当前订单状态,已取消则返回现有成功结果 | #### 业务规则与并发 -1. `PendingPayment` 状态执行取消;订单已经是 `Cancelled` 时返回现有 `200` 结果,不重复回补库存;其他状态返回 409。 -2. 取消与库存回补通过公开应用契约处于同一受控事务:普通订单回补 Catalog,秒杀订单按 `seckillActivityId` 回补 Seckill 原活动库存并释放对应限购名额。 -3. 使用 `WHERE status = 'PendingPayment'` 条件更新和库存侧 `(orderId, orderItemId, reason)` 唯一幂等键,保证并发时最多取消和回补一次。 -4. `cancelReason` 记录为 `BUYER_CANCELLED`;C03 超时取消复用同一取消用例,仅将原因改为 `TIMEOUT`。 +1. `PendingPayment` 状态执行首次取消;订单已经是 `Cancelled` 时返回唯一现有 200 结果,不重复回补库存;支付或后续状态胜出时返回 409 和当前状态。 +2. 取消来源由服务端权威时间与调用上下文确定:买家在截止前主动取消为 `BuyerRequested`;C03 或截止后仍为待支付的取消为 `PaymentExpired`。客户端不能传入或覆盖原因。 +3. 订单状态、`cancelledAt`、取消原因、每个订单项按原通道回补、秒杀限购释放和可靠订单已取消待发布事实在同一 PostgreSQL 事务提交;普通订单回补 Catalog,秒杀订单按 `seckillActivityId` 回补原活动,任一步失败整体回滚。 +4. 使用 `WHERE status='PendingPayment'` 条件更新和库存侧 `(orderId, orderItemId, reason)` 唯一幂等事实,保证取消、支付和 C03 竞争时最多一方推进,且库存只回补一次。 #### 缓存、事件或外部依赖 -- 发布 `OrderCancelledIntegrationEvent` 到 Outbox -- 库存回补根据订单库存通道调用 Catalog 或 Seckill 公开应用契约,不直接修改其他模块内部表 +- 来源事务保存可靠待发布取消事实,事务提交后才由 RabbitMQ 传输;库存回补根据订单快照的原库存通道调用 Catalog 或 Seckill 公开应用契约,不依赖客户端输入。 +- 普通库存回补提交后立即/3 秒二次失效目标 A103 与固定首页;秒杀原活动回补不改变普通库存,不触发 C07。缓存失败不回滚取消。 #### 验证场景 1. 正常取消:返回成功,库存回补 2. 重复取消:返回幂等成功 3. 订单已支付:返回409 -4. 跨用户取消:返回403 +4. 跨用户取消:返回404 --- @@ -4874,11 +4799,14 @@ PlaceSeckillOrderResponse { "items": [ { "orderId": "uuid", - "orderNo": "ORD20260724001", - "buyerUsername": "user123", + "buyer": { "displayName": "用***23" }, "status": "Paid", "totalAmount": 299.00, "createdAt": "2026-07-24T10:00:00Z", + "paidAt": "2026-07-24T10:05:00Z", + "shippedAt": null, + "completedAt": null, + "cancelledAt": null, "itemCount": 2 } ], @@ -4901,7 +4829,9 @@ PlaceSeckillOrderResponse { #### 业务规则与并发 1. 只返回 `assignedMerchantUserId = currentUserId` 的订单;本项目不按商户租户拆分商品或结算。 -2. 订单按创建时间倒序排列 +2. 排序固定为 `createdAt desc, orderId desc`;所有筛选都追加相同稳定次序。 +3. 列表只返回脱敏的最小买家摘要,不返回收货地址、完整手机号或其他私人资料;履约所需地址只在 A306 当前授权详情中返回。 +4. `Paid` 只表示订单可以进入履约复核;是否实际可发货仍须结合 M10 处理中售后、已退款数量和剩余可履约数量判断。 #### 缓存、事件或外部依赖 @@ -4948,15 +4878,19 @@ PlaceSeckillOrderResponse { "message": "ok", "data": { "orderId": "uuid", - "orderNo": "ORD20260724001", - "buyerUsername": "user123", + "buyer": { "displayName": "用***23" }, "status": "Paid", "totalAmount": 299.00, "createdAt": "2026-07-24T10:00:00Z", - "paidAt": "2026-07-24T10:05:00Z", + "payment": { + "paymentId": "uuid", + "source": "Wallet", + "amount": 299.00, + "paidAt": "2026-07-24T10:05:00Z" + }, "addressSnapshot": { "receiverName": "张三", - "phone": "138****8888", + "phone": "13800008888", "province": "广东省", "city": "深圳市", "district": "南山区", @@ -4970,15 +4904,20 @@ PlaceSeckillOrderResponse { "imageUrl": "https://...", "unitPrice": 199.00, "quantity": 1, + "processingAfterSalesQuantity": 0, "refundedQuantity": 0, - "fulfillableQuantity": 1, + "remainingFulfillableQuantity": 1, + "shippedQuantity": 0, "subtotal": 199.00 } ], - "fulfillment": { - "state": "ReadyToShip", - "blockReason": null - }, + "canShip": true, + "hasBlockingAfterSales": false, + "blockReason": null, + "statusHistory": [ + { "status": "PendingPayment", "time": "2026-07-24T10:00:00Z" }, + { "status": "Paid", "time": "2026-07-24T10:05:00Z" } + ], "availableActions": ["ship"] } } @@ -4989,14 +4928,14 @@ PlaceSeckillOrderResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | -| 404 | ORDER.NOT_FOUND | 订单不存在 | -| 403 | ORDER.ACCESS_DENIED | 订单未分配给当前商家运营账号 | +| 404 | ORDER.NOT_FOUND | 订单不存在或未分配给当前 `assignedMerchantUserId` | #### 业务规则与并发 -1. 只返回分配给当前运营账号的整单及其订单项,不把一个订单拆成多商户子订单。 -2. `fulfillment.state` 是根据订单状态和 AfterSales 履约快照得到的展示字段,可取 `ReadyToShip`、`BlockedByAfterSales`、`PartiallyRefunded`、`FullyRefunded`、`Shipped`、`Completed`;它不是新的订单核心状态。 -3. `refundedQuantity` 与 `fulfillableQuantity` 由售后终态数量计算;存在处理中售后或无剩余可履约数量时,`availableActions` 不返回 `ship`。 +1. 只返回分配给当前运营账号的整单及其订单项,不把一个订单拆成多商户子订单;不属于当前商家与不存在统一 404。 +2. 每个订单项返回购买数量、处理中售后数量、已退款数量、剩余可履约数量与实际已发货数量;这些数量来自同一 M10 履约快照,服务端保证口径不互相重叠。 +3. 使用明确 `canShip`、`hasBlockingAfterSales`、`blockReason` 派生发货入口;存在处理中售后或无剩余可履约数量时不返回 `ship`。 +4. 返回最小支付摘要和完整已提交状态时间线;地址详情只向当前 `assignedMerchantUserId` 返回实际履约所需快照,列表接口不得返回。 #### 缓存、事件或外部依赖 @@ -5006,7 +4945,7 @@ PlaceSeckillOrderResponse { 1. 正常查询:返回完整订单详情 2. 订单不存在:返回404 -3. 跨商家访问:返回403 +3. 跨商家访问:返回404 4. 存在处理中售后或部分退款:返回可理解的履约状态和准确剩余数量,不错误展示发货入口 --- @@ -5025,23 +4964,22 @@ PlaceSeckillOrderResponse { - **响应Schema**:`ShipOrderResponse` - **身份与Policy**:MerchantOnly - **资源归属**:订单的 `assignedMerchantUserId` 必须等于当前账号 -- **幂等要求**:以订单号为幂等键,重复发货返回成功 +- **幂等要求**:必需 `Idempotency-Key`;同键同请求稳定重放首次确定结果,同键换内容返回 409 #### 请求 - **Route参数**:`orderId`(必需,UUID) - **Query参数**:无 -- **Header**:`Authorization: Bearer `(必需) +- **Header**:`Authorization: Bearer `(必需)、`Idempotency-Key: `(必需) - **Body**: ```json { - "expressCompany": "顺丰速运", - "trackingNo": "SF1234567890" + "note": "已完成打包并交付线下配送" } ``` - **校验规则**: - - `expressCompany`:必填,1-50字符 - - `trackingNo`:必填,1-50字符 + - `note`:可选,0~200 字;本期不接入真实物流公司或运单号 + - 客户端不提交发货数量;服务端根据购买数量和 M10 最新履约快照计算 #### 成功响应 @@ -5056,8 +4994,7 @@ PlaceSeckillOrderResponse { "orderId": "uuid", "status": "Shipped", "shippedAt": "2026-07-24T12:00:00Z", - "expressCompany": "顺丰速运", - "trackingNo": "SF1234567890", + "note": "已完成打包并交付线下配送", "shippedItems": [ { "orderItemId": "uuid", @@ -5073,32 +5010,31 @@ PlaceSeckillOrderResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | -| 404 | ORDER.NOT_FOUND | 订单不存在 | -| 403 | ORDER.ACCESS_DENIED | 订单未分配给当前商家运营账号 | +| 404 | ORDER.NOT_FOUND | 订单不存在或未分配给当前 `assignedMerchantUserId` | | 409 | ORDER.INVALID_STATUS | 订单状态不允许发货(只有已支付可发货) | | 409 | ORDER.AFTER_SALES_IN_PROGRESS | 订单存在会影响履约的处理中售后申请 | | 409 | ORDER.NO_FULFILLABLE_ITEMS | 全部订单项均已退款,没有剩余可发货数量 | +| 409 | IDEMPOTENCY.KEY_REUSED | 同一幂等键用于不同发货请求 | #### 业务规则与并发 -1. `Paid` 状态执行首次发货;已经是 `Shipped` 且物流公司、单号与首次请求一致时返回现有 `200` 结果。 -2. 已是 `Shipped` 但物流载荷不同,或处于其他不允许状态时返回 409;条件更新为 0 后必须读取现状再判定,不能把所有重复请求都当错误。 -3. 使用条件更新 `WHERE status = 'Paid'` 防止重复副作用,并记录发货时间、物流公司和物流单号。 -4. 发货前通过 AfterSales 公开应用契约取得履约快照。`PendingReview`、`PendingReturn`、`PendingReceipt`、`Refunding`、`RefundFailed` 等仍可能改变履约结果的申请阻断发货;`Rejected`、`Cancelled` 不阻断。 -5. `Refunded` 数量从原购买数量中扣除;仍有剩余数量时只发出剩余可履约数量,并在响应 `shippedItems` 中返回实际发货明细;全部数量均已退款时拒绝发货。 -6. A307 与 A412 提交售后必须先通过 Ordering 公开应用契约在同一 PostgreSQL 事务中锁定同一 `orders` 行并取得最新履约快照,锁保持到各自业务写入提交;禁止“先查询、后另开事务更新”。若发货先提交,售后按已发货规则重新判断;若售后申请先提交,发货必须看到占用结果并按上述规则处理。 +1. 完成身份、幂等键格式、路由和请求结构校验后先读取持久化幂等结果;同键同请求重放首次结果,不重新检查当前状态,同键换内容返回 409。 +2. 新请求只允许当前 `assignedMerchantUserId` 对 `Paid` 订单发货;条件更新未命中后读取最新状态,确定失败与幂等键绑定,瞬态未知失败不固化。 +3. 发货前通过 AfterSales 公开应用契约取得履约快照。`PendingReview`、`PendingReturn`、`PendingReceipt`、`Refunding`、`RefundFailed` 等仍可能改变履约结果的申请阻断发货;`Rejected`、`Cancelled` 不阻断。 +4. 服务端以购买数量减去已退款及处理中占用量计算每项实际发货数量;仍有剩余时只发出剩余可履约数量,全部无剩余时拒绝。客户端不能指定数量。 +5. A307 与 A412 必须在同一 PostgreSQL 事务协调入口锁定同一订单行并取得最新履约快照;若发货先提交,售后按已发货规则重判;若售后先提交,发货必须看到占用结果。 +6. 订单状态推进为 `Shipped`、`shippedAt`、每项实际 `shippedQuantity`、可选说明、确定幂等结果和可靠订单已发货待发布事实在同一事务提交,任一步失败整体回滚。 #### 缓存、事件或外部依赖 -- 发布 `OrderShippedIntegrationEvent` 到 Outbox -- 在事务内锁定 Ordering 订单行后调用 AfterSales 的履约查询公开应用契约;Ordering 不直接读取或修改售后内部表。 +- 来源事务保存可靠订单已发货待发布事实,提交后才由 RabbitMQ 传输;Ordering 不直接读取或修改售后内部表。 #### 验证场景 1. 正常发货:返回成功,状态变为Shipped 2. 重复发货:返回幂等成功 3. 订单未支付:返回409 -4. 跨商家发货:返回403 +4. 跨商家发货:返回404 5. 存在处理中售后:返回409且订单仍为Paid 6. 部分退款完成:仅发出剩余数量;全部退款完成:返回无可履约商品 @@ -5140,7 +5076,7 @@ PlaceSeckillOrderResponse { "orderId": "uuid", "status": "Completed", "completedAt": "2026-07-24T14:00:00Z", - "completedBy": "BUYER_CONFIRMED" + "completedBy": "BuyerConfirmed" } } ``` @@ -5150,27 +5086,27 @@ PlaceSeckillOrderResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | -| 404 | ORDER.NOT_FOUND | 订单不存在 | -| 403 | ORDER.ACCESS_DENIED | 订单不属于当前用户 | +| 404 | ORDER.NOT_FOUND | 订单不存在或不属于当前买家 | | 409 | ORDER.INVALID_STATUS | 订单状态不允许确认收货(只有已发货可确认) | #### 业务规则与并发 -1. `Shipped` 状态执行首次确认;已是 `Completed` 且 `completedBy = BUYER_CONFIRMED` 时返回现有 `200` 结果。 -2. 已由 Worker 自动完成或处于其他不允许状态时返回 409;条件更新为 0 后读取现状再区分幂等重放与状态冲突。 -3. 使用条件更新 `WHERE status = 'Shipped'` 防止重复副作用,记录 `completed_at` 和 `completed_by = 'BUYER_CONFIRMED'`。 -4. 确认收货后触发评价入口开放(若 X01 已实现)。 +1. 当前买家对 `Shipped` 订单执行首次确认,使用 `WHERE status='Shipped'` 与自动完成 Worker 竞争。 +2. 条件更新未命中后读取现状:若订单已为 `Completed`,无论完成方式是 `BuyerConfirmed` 还是 `AutoCompleted`,都返回唯一已提交的 `completedAt`、`completedBy` 和当前 200 结果;不能因 Worker 先完成而返回 409。 +3. 其他不允许状态返回 409。首次成功时,订单状态、唯一完成时间、`completedBy=BuyerConfirmed` 和可靠订单已完成待发布事实在同一事务提交。 +4. 完成后 M07 根据 `Completed` 订单项开放评价资格;A308 不直接写评价事实。 #### 缓存、事件或外部依赖 -- 发布统一的 `OrderCompletedIntegrationEvent` 到 Outbox;买家确认和 Worker 自动完成共用同一“订单已完成”事实 +- 来源事务保存统一可靠订单已完成待发布事实;买家确认和 Worker 自动完成共用同一事件语义,提交后才由 RabbitMQ 传输。完成不改变库存,也不触发 C07。 #### 验证场景 1. 正常确认收货:返回成功,状态变为 Completed 2. 重复确认:返回幂等成功 3. 订单未发货:返回 409 -4. 跨用户确认:返回 403 +4. 跨用户确认:返回 404 +5. Worker 已自动完成后买家重试:返回 200 与同一 `completedAt`、`completedBy=AutoCompleted` --- @@ -5195,12 +5131,12 @@ PlaceSeckillOrderResponse { #### 请求 - **Route 参数**:(无) -- **Query 参数**:`currency`(可选,默认 `CNY`) +- **Query 参数**:(无;币种固定为 `CNY`) - **Header**:`Authorization: Bearer ` - **Body**:(无) - **校验规则**: - JWT 有效、账号状态正常、令牌版本未过期 - - 钱包不存在时按需初始化(业务策略可由实现层决定,本接口约定返回余额 0) + - 钱包不存在时返回 `balance=0` 的确定结果;A401 不因 GET 创建钱包记录,首次充值/扣款在写事务中按需创建 #### 成功响应 @@ -5212,7 +5148,6 @@ PlaceSeckillOrderResponse { "code": "success", "message": "ok", "data": { - "walletId": "f5c2a8b9-3c61-4ab6-a8dd-a54ea8dd78af", "balance": 100.50, "currency": "CNY", "updatedAt": "2026-07-23T08:30:00Z" @@ -5235,7 +5170,7 @@ PlaceSeckillOrderResponse { - 仅返回当前 buyerId 钱包;商家/管理员无访问权限(按 1.6.2 资源归属规则) - 余额以 PostgreSQL 实时值为准,不使用 Redis 缓存 -- 不返回钱包创建时间、内部审计字段 +- 不返回钱包内部 ID、创建时间或审计字段;无记录时返回 `balance=0`、`currency=CNY`、`updatedAt=null`,且不产生写操作,与已有零余额对客户端同口径。 #### 缓存、事件或外部依赖 @@ -5275,13 +5210,11 @@ PlaceSeckillOrderResponse { - **Body**: ```json { - "amount": 100.50, - "channelNote": "MOCK_TOPUP" + "amount": 100.50 } ``` - **校验规则**: - `amount` 必填,decimal,最多 2 位小数,`> 0` 且 `≤ 10000.00`(`PAYMENT.TOPUP_EXCEEDS_LIMIT`) - - `channelNote` 选填,默认 `MOCK_TOPUP`,仅作观测标识 - `Idempotency-Key` 必填,UUID 格式;相同 buyerId + 相同 Key + 相同 amount → 返回首次结果 - 同一 Key 不同 amount → 409 + `IDEMPOTENCY.KEY_REUSED` @@ -5296,13 +5229,10 @@ PlaceSeckillOrderResponse { "message": "ok", "data": { "topupId": "c7a1d4e6-...", - "walletId": "f5c2a8b9-...", "amount": 100.50, "currency": "CNY", - "status": "Succeeded", - "createdAt": "2026-07-23T08:30:00Z", - "succeededAt": "2026-07-23T08:30:01Z", - "newBalance": 200.50 + "creditedAt": "2026-07-23T08:30:01Z", + "balanceAfter": 200.50 } } ``` @@ -5321,8 +5251,9 @@ PlaceSeckillOrderResponse { #### 业务规则与并发 -- 钱包余额增加 + 钱包流水写入同一事务(按 PAY-R06) -- 幂等键级别唯一约束存于 DB082;命中直接返回首次成功结果 +- 钱包余额增加、充值记录、钱包流水和首次幂等结果在同一事务提交(按 PAY-R06)。 +- 完成身份、幂等键格式和固定请求结构校验后先查询 PostgreSQL 幂等结果;命中同指纹直接重放首次完整结果,同键换金额返回 409 +- 本期不建立 `Pending` / `Failed` / `Succeeded` 充值状态机;失败请求不生成充值记录。金额非法等确定失败可持久化后重放,数据库未知失败不固化。 - 单笔上限 10000.00 元(业务规则 PAY-R16,zhy 7-23 提交 db840e4 强调) - 充值成功后才更新余额;不为重试创建多条 `wallet_ledgers` @@ -5362,7 +5293,6 @@ PlaceSeckillOrderResponse { - **Route 参数**:(无) - **Query 参数**: - - `status`(可选):`Succeeded` / `Failed` / `Pending` - `createdFrom`(可选):ISO 8601 UTC,包含 - `createdTo`(可选):ISO 8601 UTC,不包含 - `page`(默认 `1`) @@ -5373,7 +5303,6 @@ PlaceSeckillOrderResponse { - **Body**:(无) - **校验规则**: - `createdFrom ≤ createdTo`(否则 400) - - `status` 枚举必须白名单 - `pageSize` ∈ [1, 100] #### 成功响应 @@ -5391,9 +5320,7 @@ PlaceSeckillOrderResponse { "topupId": "c7a1d4e6-...", "amount": 100.50, "currency": "CNY", - "status": "Succeeded", - "createdAt": "2026-07-23T08:30:00Z", - "succeededAt": "2026-07-23T08:30:01Z" + "creditedAt": "2026-07-23T08:30:01Z" } ], "page": 1, @@ -5408,14 +5335,14 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | Query 参数错误(status 不在白名单、时间范围非法) | +| 400 | `COMMON.VALIDATION_FAILED` | 分页或时间范围参数非法 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 仅返回当前 buyerId 记录 +- 仅返回当前 buyerId 已成功到账的充值事实;本期没有 `Pending` / `Failed` 充值状态机或对应查询筛选 - 列表按 `createdAt desc, topupId desc` 稳定排序,避免翻页重复 #### 缓存、事件或外部依赖 @@ -5470,15 +5397,17 @@ PlaceSeckillOrderResponse { "message": "ok", "data": { "orderId": "3f0ed9a9-...", - "orderAmount": 199.00, - "paidAmount": 0.00, - "orderStatus": "PendingPayment", - "canPay": true, + "payableAmount": 199.00, "currency": "CNY", + "orderStatus": "PendingPayment", "walletBalance": 100.50, - "insufficient": true, - "availableChannels": ["MOCK_WALLET"], - "expiresAt": "2026-07-23T09:00:00Z" + "shortfallAmount": 98.50, + "availableChannels": ["Wallet"], + "paymentDeadline": "2026-07-23T09:00:00Z", + "serverTime": "2026-07-23T08:35:00Z", + "canPay": true, + "nonPayableReason": null, + "paymentSummary": null } } ``` @@ -5497,18 +5426,20 @@ PlaceSeckillOrderResponse { - 不修改订单或钱包状态,纯查询 - 余额、订单金额、应付以服务端实时值(按 PAY-R01) -- 订单已支付 → 返回 `200`、`canPay=false`、`orderStatus=Paid` 和现有支付结果摘要,便于用户确认支付结果;已取消订单返回 409 +- `canPay` 必须同时满足订单属于本人、`status=PendingPayment` 且 `serverTime < paymentDeadline`;到期但 C03 尚未取消时也返回 `canPay=false` 并请求 M04/C03 取消通道。 +- `shortfallAmount = max(payableAmount - walletBalance, 0)`;`nonPayableReason` 只使用稳定枚举或 `null`,页面不得从自然语言推断流程。 +- 订单已支付 → 返回 `200`、`canPay=false`、`orderStatus=Paid` 和真实 `paymentSummary`;已取消订单返回 409。买家可选渠道本期只有 `Wallet`,C08 不增加前台通道入口。 #### 缓存、事件或外部依赖 -- 缓存:可短暂缓存(短 TTL 5s),不允许跨用户复用 +- 缓存:不缓存;订单状态、截止时间和钱包余额每次按已提交事实读取 - 事件:无 - 外部依赖:PostgreSQL + 钱包表 #### 验证场景 -- 正常:订单本人 + `PendingPayment` + 余额不足 → 返回 `insufficient=true` -- 正常:订单本人 + `PendingPayment` + 余额充足 → 返回 `insufficient=false` +- 正常:订单本人 + `PendingPayment` + 余额不足 → 返回正数 `shortfallAmount` +- 正常:订单本人 + `PendingPayment` + 余额充足 → 返回 `shortfallAmount=0` - 异常:他人订单 → 404 + `RESOURCE.NOT_FOUND` - 正常:订单已支付 → 200,`canPay=false`,不再显示支付按钮 @@ -5538,14 +5469,13 @@ PlaceSeckillOrderResponse { - **Body**: ```json { - "expectedAmount": 199.00, - "currency": "CNY" + "expectedAmount": 199.00 } ``` - **校验规则**: - `Idempotency-Key` 必填 - - `expectedAmount` 必填,订单金额由服务端校验(PAY-R01),与订单金额不一致 → 409 + `PAYMENT.AMOUNT_MISMATCH` - - 订单状态必须为 `PendingPayment`,否则 409 + `PAYMENT.ORDER_NOT_PAYABLE` + - `expectedAmount` 选填,只用于识别页面旧值;最终扣款金额和币种始终来自 Ordering 持久化应付事实 + - 新请求必须同时满足订单为 `PendingPayment` 且 `databaseNow < paymentDeadline`;到期返回 `PAYMENT.DEADLINE_EXPIRED` 并触发同一 M04/C03 过期取消通道 - 订单归属当前 buyerId #### 成功响应 @@ -5563,6 +5493,7 @@ PlaceSeckillOrderResponse { "amount": 199.00, "currency": "CNY", "status": "Succeeded", + "source": "Wallet", "walletBalanceAfter": 1.50, "paidAt": "2026-07-23T08:35:00Z" } @@ -5578,6 +5509,7 @@ PlaceSeckillOrderResponse { | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 订单不存在或非本人 | | 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 订单状态非 `PendingPayment` 或已取消 | +| 409 | `PAYMENT.DEADLINE_EXPIRED` | 已达到订单固定支付截止时间;不再允许支付 | | 409 | `PAYMENT.AMOUNT_MISMATCH` | `expectedAmount` 与订单金额不一致 | | 409 | `PAYMENT.INSUFFICIENT_BALANCE` | 钱包余额不足 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | @@ -5586,16 +5518,17 @@ PlaceSeckillOrderResponse { #### 业务规则与并发 -- 钱包条件扣减 + 钱包流水 + 支付记录 + 订单状态 + Outbox **同一事务**(按 PAY-R06) -- 与 C03 订单超时取消通过 `WHERE order.status = 'PendingPayment'` 条件竞争,唯一胜出(按 PAY-R05) +- 完成身份、幂等键格式和固定请求结构校验后先查 PostgreSQL 幂等结果,再读取订单、余额和截止时间;同键同指纹重放首次确定结果,同键换内容返回 409。 +- 新请求以 Ordering 应付金额与币种为扣款事实,`expectedAmount` 只做可选旧页面冲突检查。余额不足、已取消、已过期等确定业务失败与幂等键持久绑定后返回;基础设施或提交结果未知不固化。 +- 钱包条件扣减 + 钱包流水 + `source=Wallet` 的支付记录 + 订单 `Paid` + 可靠支付成功待发布事实 + 幂等结果 **同一事务**(按 PAY-R06) +- 与买家取消、C03 超时取消和 C08 模拟通道回调同时通过 `WHERE status='PendingPayment' AND databaseNow < paymentDeadline` 竞争,唯一胜出(按 PAY-R05) - 余额不得为负(条件更新 + CHECK 约束)(按 PAY-R02) -- 成功提交后写入 `Idempotency-Key` 记录,相同 Key + 相同 amount + 相同 orderId → 返回首次结果。 -- 同一订单已经支付成功时,无论请求使用原 Key 还是新的 Key,均返回既有 `200 PaymentResultResponse`,不重复扣款、写流水或发布事件;只有同一 Key 被用于不同请求内容时返回 `IDEMPOTENCY.KEY_REUSED`。 +- 同一订单已经由 Wallet 或 C08 `SimulatedChannel` 支付成功时,无论请求使用原 Key 还是新 Key,均返回既有 `200 PaymentResultResponse`,不重复扣款、写流水或发布事件;C08 支付结果的 `walletBalanceAfter` 为 `null`。 #### 缓存、事件或外部依赖 - 缓存:资金幂等结果持久化在 PostgreSQL,不以 Redis 作为唯一事实 -- 事件:发布 `OrderPaidIntegrationEvent`(架构 §7.4 已确定第一条集成事件) +- 事件:来源事务保存可靠支付成功待发布事实,提交后才由 RabbitMQ 传输 - 外部依赖:PostgreSQL + Ordering 公开应用契约;不得直接依赖 Ordering 内部 DbContext 或仓储 #### 验证场景 @@ -5605,6 +5538,7 @@ PlaceSeckillOrderResponse { - 异常:余额不足 → 409 + `PAYMENT.INSUFFICIENT_BALANCE` - 重复:订单已支付 → 200,返回既有支付结果,不重复扣款 - 异常:订单已取消 → 409 + `PAYMENT.ORDER_NOT_PAYABLE` +- 异常:订单已到 `paymentDeadline` → 409 + `PAYMENT.DEADLINE_EXPIRED`,进入同一过期取消通道 - 异常:金额不一致 → 409 + `PAYMENT.AMOUNT_MISMATCH` - 并发:与 C03 同时操作 → 唯一胜出,败方 409 + `PAYMENT.ORDER_NOT_PAYABLE` @@ -5639,8 +5573,8 @@ PlaceSeckillOrderResponse { #### 成功响应 - **HTTP 状态**:`200 OK` -- **响应 Schema**:`PaymentResultResponse`(同 A405) -- **示例**:(同 A405 成功响应) +- **响应 Schema**:`OrderPaymentLookupResponse`,含 `orderId`、`orderStatus`、`paymentResult`(`PaymentResultResponse`,可空) +- **示例**:没有已确定支付事实时返回 `200` 与 `"paymentResult": null`;有记录时返回本人已确认的支付结果 #### 失败响应 @@ -5649,13 +5583,12 @@ PlaceSeckillOrderResponse { | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 订单不存在或非本人 | -| 404 | `PAYMENT.NOT_FOUND` | 订单未发起过支付(订单未处于 `PendingPayment` / `Paid`) | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 同一订单只返回最新一笔成功支付;如有多笔识别为异常(P420 回调场景) -- 订单已取消但有迟到成功支付 → 返回 `PaymentResult`,订单状态仍为 `Cancelled`,并标注对账状态(架构 §7.12) +- 只返回当前订单已经确定的 Wallet 或 `SimulatedChannel` 支付事实;没有记录时明确返回 `paymentResult=null`,不得根据订单是否 `PendingPayment`、`Paid` 或 `Cancelled` 猜测。 +- C08 迟到成功回调只形成 `Difference` 来源,不伪造成功支付记录;因此已取消且无确定支付事实的订单仍返回 `paymentResult=null`。 #### 缓存、事件或外部依赖 @@ -5666,7 +5599,7 @@ PlaceSeckillOrderResponse { #### 验证场景 - 正常:订单已支付 → 返回支付结果 -- 异常:订单未支付 → 404 + `PAYMENT.NOT_FOUND` +- 正常:订单没有确定支付事实 → 200 + `paymentResult=null` - 异常:他人订单 → 404 + `RESOURCE.NOT_FOUND` --- @@ -5691,14 +5624,13 @@ PlaceSeckillOrderResponse { - **Route 参数**:(无) - **Query 参数**: - - `status`(可选):`Succeeded` / `Failed` / `Pending` - `orderId`(可选):按订单过滤 - `createdFrom` / `createdTo`(可选):时间范围 - `page` / `pageSize` / `sortBy` / `sortOrder`(标准分页) - **Header**:`Authorization: Bearer ` - **Body**:(无) - **校验规则**: - - 标准分页 + 时间范围 + 枚举白名单 + - 标准分页与时间范围校验 #### 成功响应 @@ -5716,9 +5648,9 @@ PlaceSeckillOrderResponse { "orderId": "3f0ed9a9-...", "amount": 199.00, "currency": "CNY", - "status": "Succeeded", + "source": "Wallet", "createdAt": "2026-07-23T08:35:00Z", - "succeededAt": "2026-07-23T08:35:01Z" + "paidAt": "2026-07-23T08:35:01Z" } ], "page": 1, @@ -5740,7 +5672,8 @@ PlaceSeckillOrderResponse { #### 业务规则与并发 -- 仅返回当前 buyerId 记录 +- 仅返回当前 buyerId 已确认支付事实;本期没有 `Pending`/`Failed` 支付查询状态 +- `source` 只返回 `Wallet` 或 `SimulatedChannel`,不暴露回调处理、差异内部状态或幂等键 - 默认排序 `createdAt desc, paymentId desc` #### 缓存、事件或外部依赖 @@ -5796,10 +5729,9 @@ PlaceSeckillOrderResponse { "orderId": "3f0ed9a9-...", "amount": 199.00, "currency": "CNY", - "status": "Succeeded", + "source": "Wallet", "createdAt": "2026-07-23T08:35:00Z", - "succeededAt": "2026-07-23T08:35:01Z", - "idempotencyKey": "uuid-..." + "paidAt": "2026-07-23T08:35:01Z" } } ``` @@ -5815,7 +5747,7 @@ PlaceSeckillOrderResponse { #### 业务规则与并发 -- 不返回内部审计字段;幂等键可对外展示以便客户端排错 +- 不返回幂等键、回调处理状态、内部审计字段或对账差异;`source` 只公开 `Wallet` / `SimulatedChannel` #### 缓存、事件或外部依赖 @@ -5870,12 +5802,13 @@ PlaceSeckillOrderResponse { "data": { "orderId": "3f0ed9a9-...", "orderItemId": "5a7c...", + "orderStatus": "Paid", "eligible": true, "reason": null, - "maxRefundableAmount": 100.00, - "maxRefundableQuantity": 1, "availableTypes": ["RefundOnly"], - "deadlineAt": "2026-07-30T08:30:00Z" + "remainingEligibleQuantity": 1, + "maxRefundableAmount": 100.00, + "deadlineAt": null } } ``` @@ -5895,6 +5828,8 @@ PlaceSeckillOrderResponse { - 不符合业务条件时仍返回 `200`、`eligible=false` 和稳定 `reason`,便于页面直接展示原因;订单项不存在或不属于当前买家仍统一返回 404。 - 可申请类型根据订单状态、履约情况和剩余可售后数量计算,不由客户端推断。 - 未发货的 `Paid` 订单只允许 `RefundOnly`;`Shipped` 或在售后期限内的 `Completed` 订单可以按资格返回 `RefundOnly`、`ReturnAndRefund`。 +- `deadlineAt` 仅在 `Completed` 订单上返回“完成后 7 天”的售后截止时间;`Paid` 与 `Shipped` 返回 `null`,不得为其增加流程外固定售后期限。 +- 预检只返回查询时快照,不锁定资格、数量、期限或订单状态;A412 提交时必须在订单行串行边界内重新校验。 #### 缓存、事件或外部依赖 @@ -5991,16 +5926,19 @@ PlaceSeckillOrderResponse { #### 业务规则与并发 -- 退款金额由服务端计算(M10 规则:"不接受任意金额") -- 申请数量不得超过剩余可售后数量(防重复申请) -- 状态写入 `PendingReview`(M10 状态机) +- 完成固定字段校验后,必须先按当前买家、接口编号和 `Idempotency-Key` 查询幂等结果,再读取订单、履约状态和剩余可售后数量;命中同请求时重放首次结果,不得因订单后续状态变化把重放改成失败。 +- 退款金额由服务端按订单项实付快照计算(M10 规则:“不接受任意金额”),不接受客户端金额、商家归属或库存来源。 +- 申请数量不得超过剩余可售后数量(防重复申请)。 +- 状态写入 `PendingReview`(M10 状态机)。 - 同一订单项可按剩余数量分次申请;仅处理中和已退款数量占用额度,不因存在另一笔 `PendingReview` 就整项禁止申请。 - A412 必须与 A307 共用 Ordering 提供的订单级变更契约:在同一 PostgreSQL 事务中锁定目标 `orders` 行、读取最新履约状态并保持到售后申请写入提交,禁止查询后另开事务插入。申请先提交时占用数量进入履约快照;发货先提交时,本请求按已发货后的类型和库存规则重新校验。 +- 申请、数量占用、服务端计算金额、首条状态时间线、订单指定商家通知可靠事实和幂等成功结果必须在同一事务形成;任一写入失败则整体不生效。 +- 已完成业务校验后能够确定的 404/409 结果也按 1.12.1 保存并重放;鉴权失败、请求格式错误和未知内部错误不保存为业务幂等结果。 #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:发布已在 Messaging 契约登记的 `AfterSalesApplicationSubmittedIntegrationEvent`。 +- 事件:可靠形成已在 Messaging 契约登记的 `AfterSalesApplicationSubmittedIntegrationEvent`,由 M09 只通知订单指定商家。 - 外部依赖:PostgreSQL + Ordering 公开应用契约 #### 验证场景 @@ -6129,7 +6067,7 @@ PlaceSeckillOrderResponse { #### 成功响应 - **HTTP 状态**:`200 OK` -- **响应 Schema**:`AfterSalesRequestDetailResponse`(含 `timeline` 字段) +- **响应 Schema**:`AfterSalesRequestDetailResponse` - **示例**: ```json { @@ -6139,6 +6077,13 @@ PlaceSeckillOrderResponse { "requestId": "b9c1...", "orderId": "3f0ed9a9-...", "orderItemId": "5a7c...", + "orderItemSnapshot": { + "productName": "有机纯牛奶", + "primaryImageUrl": "https://cdn.example.test/products/p1/main.jpg", + "paidUnitPrice": 100.00, + "purchasedQuantity": 2, + "orderType": "Ordinary" + }, "type": "RefundOnly", "quantity": 1, "calculatedAmount": 100.00, @@ -6147,13 +6092,30 @@ PlaceSeckillOrderResponse { "reasonNote": "外包装破损", "status": "PendingReview", "createdAt": "2026-07-23T08:30:00Z", + "audit": null, + "returnInfo": null, + "refundOperation": null, "timeline": [ - { "status": "PendingReview", "at": "2026-07-23T08:30:00Z", "actor": "buyer" } - ] + { "status": "PendingReview", "at": "2026-07-23T08:30:00Z", "actorType": "Buyer" } + ], + "actions": { + "canCancel": true, + "canAudit": false, + "canSubmitReturnInfo": false, + "canConfirmReceipt": false, + "canRetryRefund": false + } } } ``` +- `orderItemSnapshot` 固定返回下单时的商品名称、主图、实付单价、购买数量和订单类型;秒杀订单还返回 `seckillActivityId`,但不暴露内部库存实现字段。 +- `audit` 在已审核后返回 `decision`、`note`、`actorDisplayName`、`auditedAt`,不暴露内部账号 ID。 +- `returnInfo` 在买家已寄回后返回 `carrier`、`trackingNumber`、`shippedAt`、`note`。 +- `refundOperation` 在退款操作建立后返回 `refundOperationId`、`amount`、`status`(`Processing` / `Succeeded` / `DefiniteFailure`)、`startedAt`、`completedAt`、`failedAt` 和可安全展示的 `failureCode`;结果未知时保持 `Processing`,不得返回钱包账户内部流水、异常堆栈或第三方原始报文。 +- `timeline` 返回申请创建、撤销、审核、寄回、确认收货、退款中、退款成功或确定失败等完整领域状态变化;A418 不再提供重复时间线接口。 +- `actions` 只按当前身份、账号状态、资源归属和当前已提交状态给出页面提示,执行动作时仍须由对应命令接口重新校验。 + #### 失败响应 | HTTP 状态 | 业务错误码 | 触发条件 | @@ -6165,8 +6127,11 @@ PlaceSeckillOrderResponse { #### 业务规则与并发 -- 时间线读取模块内的 `after_sales_status_histories`(DB087 待数据库设计确认),不读取通用操作审计 -- 不返回内部审计字段(如 merchant 内部 ID) +- 时间线读取模块内的 `after_sales_status_histories`(DB087 待数据库设计确认),不读取通用操作审计。 +- 详情必须一次返回订单项快照、服务端金额、买家申请内容、审核结果、退货说明、同一退款操作的当前结果和完整状态时间线,不要求客户端拼接已取消的 A418/A432/A433。 +- 买家仅可读取本人申请;商家仅可读取订单 `assignedMerchantUserId` 等于当前账号的申请。申请不存在和不在授权范围统一返回 404,禁止泄露资源是否存在。 +- `Refunding`、`RefundFailed` 都是可查询真实状态;结果未知时 `refundOperation.status=Processing`,不得伪造成失败或省略。 +- 不返回内部审计字段、商家内部 ID、支付签名、钱包余额变更实现细节或可被用于越权的数据。 #### 缓存、事件或外部依赖 @@ -6232,9 +6197,11 @@ PlaceSeckillOrderResponse { #### 业务规则与并发 -- 审核通过后不允许撤销(M10 业务规则) -- 状态条件更新:`WHERE status = 'PendingReview' AND buyer_id = currentBuyerId` -- 撤销后保留领域状态历史 +- 完成固定字段校验后先查询幂等结果;同 Key 同请求即使申请后来已审核,也重放首次撤销结果,同 Key 不同请求返回 `IDEMPOTENCY.KEY_REUSED`。 +- 本人申请已经为 `Cancelled` 时,即使使用新 Key,也返回当前取消详情,不重复释放数量或新增时间线。 +- 审核通过后不允许首次撤销(M10 业务规则)。 +- 状态条件更新:`WHERE status = 'PendingReview' AND buyer_id = currentBuyerId`;撤销与商家审核竞争时只有一个状态迁移提交,败方返回当前已提交状态且不得回退。 +- `Cancelled`、处理中数量释放、领域状态时间线和幂等成功结果必须在同一事务形成,不发布没有消费者的跨模块事件。 #### 缓存、事件或外部依赖 @@ -6280,7 +6247,7 @@ PlaceSeckillOrderResponse { ``` - **校验规则**: - 申请关联订单分配给当前商家账号 - - 申请状态必须为 `PendingReview` + - 首次审核时申请状态必须为 `PendingReview`;幂等重放先于状态校验并返回首次已提交结果 - `decision` 枚举:`Approve` / `Reject` - `Idempotency-Key` 必填 @@ -6288,7 +6255,7 @@ PlaceSeckillOrderResponse { - **HTTP 状态**:`200 OK` - **响应 Schema**:`AfterSalesRequestDetailResponse` -- **示例**:(拒绝时 `status=Rejected`;退货退款审核通过时 `status=PendingReturn`;仅退款全部子操作成功时 `status=Refunded`) +- **示例**:(拒绝时 `status=Rejected`;退货退款审核通过时 `status=PendingReturn`;仅退款审核通过后至少返回已提交的 `Refunding`,后续也可能重放或读取到 `Refunded` / `RefundFailed`) #### 失败响应 @@ -6300,48 +6267,52 @@ PlaceSeckillOrderResponse { | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | | 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `PendingReview` | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | -| 503 | `AFTER_SALES.REFUND_FAILED` | 审核已通过,但同步退款或必要库存回补失败;申请已记录为 `RefundFailed` | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 商家不能修改买家原始申请内容(业务规则) -- 状态条件更新同时校验关联订单 `assigned_merchant_user_id = currentUserId` +- 完成固定字段校验后先查询幂等结果;命中同请求时返回首次已提交结果或该退款操作可确认的当前结果,不再按当前状态重复审核。 +- 商家不能修改买家原始申请内容(业务规则)。 +- 状态条件更新同时校验关联订单 `assigned_merchant_user_id = currentUserId`;买家撤销与审核竞争时只有一个状态迁移提交。 - 审核结果由申请类型决定,客户端不能通过布尔字段选择是否退款: - - `ReturnAndRefund` 审核通过只进入 `PendingReturn`,等待 A434 和 A417,不在审核时退款或回补库存。 - - `RefundOnly` 审核通过后以 `Refunding` 作为事务内过渡并同步调用 Payment 退款应用契约;若 Ordering 快照表明订单仍为 `Paid` 且未发货,还必须按普通/秒杀原通道调用 Catalog 或 Seckill 库存回补契约。全部成功后本次 HTTP 返回 `Refunded`。 - - `RefundOnly` 对 `Shipped` 或 `Completed` 订单只退款、不回补库存。 -- 退款、必要的库存回补与售后终态通过公开应用契约加入同一受控数据库事务;任一步失败均不留下部分资金/库存结果,并在独立失败记录中把申请置为 `RefundFailed` 供 A419 重试。 + - `Reject`:原子进入 `Rejected`、释放申请数量、记录审核意见和买家通知可靠事实。 + - `Approve + ReturnAndRefund`:原子进入 `PendingReturn`、记录审核意见和买家通知可靠事实,等待 A434 和 A417,不在审核时退款或回补库存。 + - `Approve + RefundOnly`:原子记录同意意见、建立或关联该申请唯一退款操作并进入 `Refunding`,随后由退款执行器推进;HTTP 返回最新已提交状态,不要求在一次请求内伪造为 `Refunded`。 +- `RefundOnly` 若 Ordering 快照表明订单仍为 `Paid` 且未发货,退款成功结果还必须按普通/秒杀原通道回补库存;对 `Shipped` 或 `Completed` 订单只退款、不回补库存。 +- 退款结果未知时保持 `Refunding` 并核实同一尝试;只有得到确定失败且确认余额、库存均未增加时才进入 `RefundFailed`。已经提交审核事实后,不用 503 隐藏或回滚当前状态。 +- 每份申请最多建立一个稳定退款操作;相同审核重放不得创建第二笔退款或第二次库存回补。 +- 退款成功时,钱包入账、必要库存回补、`Refunded`、时间线和买家通知可靠事实必须形成完整原子结果;任一步失败不得留下部分资金或库存结果。 - 售后状态历史记录审核人、审核意见和状态变化;这是领域时间线,不是未选择的通用后台操作日志。 #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:发布 `AfterSalesApplicationAuditedIntegrationEvent` +- 事件:审核事务可靠形成 `AfterSalesApplicationAuditedIntegrationEvent`,由 M09 只通知申请买家;退款最终结果另由唯一退款操作形成。 - 外部依赖:PostgreSQL + Ordering 售后快照 + Payment 退款应用契约 + Catalog/Seckill 库存回补应用契约 #### 验证场景 -- 正常:商家 Approve → 退货退款进入 `PendingReturn`;仅退款同步完成后返回 `Refunded` +- 正常:商家 Approve → 退货退款进入 `PendingReturn`;仅退款可靠进入 `Refunding` 并返回当前状态 - 正常:商家 Reject → 状态进入 `Rejected` -- 异常:退款或必要库存回补失败 → 503 + `AFTER_SALES.REFUND_FAILED`,详情可查询到 `RefundFailed` +- 正常:退款执行已完成或得到确定失败 → 同一调用重放可返回 `Refunded` 或 `RefundFailed` +- 异常:退款结果未知 → 保持并返回 `Refunding`,不创建第二笔退款 - 异常:买家角色调用 → 403 + `AUTH.FORBIDDEN` - 异常:他人商家申请 → 404 + `RESOURCE.NOT_FOUND` - 异常:状态已非 `PendingReview` → 409 + `AFTER_SALES.INVALID_STATUS` --- -### A417 商家确认退货 +### A417 商家确认收货 - **模块 / Tag**:AfterSales - **需求编号**:M10-FR11 - **负责人**:张海洋 - **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_status_histories` - **当前状态**:待交叉评审 -- **用途**:商家确认收到退货,触发退款流程 -- **方法与路径**:`POST /api/after-sales/requests/{requestId}/confirm-return` -- **operationId**:`AfterSales_ConfirmReturn` -- **请求 Schema**:`ConfirmReturnRequest` +- **用途**:订单指定商家确认已收到整笔退货,可靠进入退款流程 +- **方法与路径**:`POST /api/after-sales/requests/{requestId}/confirm-receipt` +- **operationId**:`AfterSales_ConfirmReceipt` +- **请求 Schema**:`ConfirmAfterSalesReceiptRequest` - **响应 Schema**:`AfterSalesRequestDetailResponse` - **身份与 Policy**:JWT Bearer + `MerchantOnly` - **资源归属**:关联订单的 `assignedMerchantUserId` 必须等于当前商家账号 @@ -6355,22 +6326,22 @@ PlaceSeckillOrderResponse { - **Body**: ```json { - "receivedQuantity": 1, "note": "已收到退货" } ``` - **校验规则**: - 申请关联订单分配给当前商家账号 - 申请类型必须为 `ReturnAndRefund` - - 申请状态必须为 `PendingReceipt` - - 本期不支持部分收货,`receivedQuantity` 必须等于申请数量 + - 首次确认时申请状态必须为 `PendingReceipt`;幂等重放或同一退款操作已有结果时返回当前状态 + - `note` 选填,≤ 500 字 + - 本期只确认整笔申请数量,不接收客户端收货数量或退款金额 - `Idempotency-Key` 必填 #### 成功响应 - **HTTP 状态**:`200 OK` - **响应 Schema**:`AfterSalesRequestDetailResponse` -- **示例**:(同 A412 详情,全部子操作成功后 `status=Refunded`) +- **示例**:(同 A414 详情;首次成功至少返回已提交的 `Refunding`,后续也可能重放或读取到 `Refunded` / `RefundFailed`) #### 失败响应 @@ -6382,30 +6353,30 @@ PlaceSeckillOrderResponse { | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | | 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `PendingReceipt` | | 409 | `AFTER_SALES.WRONG_TYPE` | 申请类型非 `ReturnAndRefund` | -| 409 | `AFTER_SALES.RETURN_QUANTITY_MISMATCH` | 收货数量与申请数量不一致 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | -| 503 | `AFTER_SALES.REFUND_FAILED` | 已确认收货,但同步退款或库存回补失败;申请已记录为 `RefundFailed` | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 状态条件更新同时校验关联订单 `assigned_merchant_user_id = currentUserId` -- 确认收到后以 `Refunding` 作为事务内过渡;根据 Ordering 快照的 `orderType` 与 `seckillActivityId`,通过公开应用契约幂等回补 Catalog 普通库存或 Seckill 原活动库存,并同步执行 Payment 退款。HTTP 成功时已经进入 `Refunded`。 -- 库存回补数量等于整笔申请数量;本期不拆分部分收货或部分退款。仅退货退款在 A417 回补,已发货/已完成订单的仅退款不回补。 -- 回补、退款和售后终态加入同一受控数据库事务:全部成功后转为 `Refunded`;任一步失败不保留部分结果,并在独立失败记录中置为 `RefundFailed`,不得回滚成“从未确认收货”。 +- 完成固定字段校验后先查询幂等结果;相同 Key 同请求即使申请已进入 `Refunding` / `Refunded` / `RefundFailed`,也返回同一退款操作的当前结果,不重复确认或退款。 +- 状态条件更新同时校验关联订单 `assigned_merchant_user_id = currentUserId`。 +- 首次确认时,收货事实、该申请唯一退款操作和 `PendingReceipt → Refunding` 状态时间线必须原子提交;退款失败也不得回到 `PendingReturn` 或抹去已确认收货事实。 +- 退款执行器根据 Ordering 快照的 `orderType` 与 `seckillActivityId`,在退款成功原子结果中按原通道回补整笔申请数量;本期不支持部分收货或部分退款。 +- 退款结果未知时保持 `Refunding` 并核实原尝试;确定失败且余额、库存均未增加时进入 `RefundFailed`;全部成功时资金入账、原通道库存回补、`Refunded`、时间线和买家通知可靠事实形成完整结果。 #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:Payment 退款应用契约只发布一次 `RefundCompletedIntegrationEvent`;收货确认与库存回补不另发没有消费者的事件。 +- 事件:退款成功只可靠形成一次 `RefundCompletedIntegrationEvent` 并由 M09 通知申请买家;收货确认与库存回补不另发没有消费者的事件。 - 外部依赖:PostgreSQL + Payment 退款应用契约 + Catalog/Seckill 库存回补应用契约 #### 验证场景 -- 正常:商家确认退货 → 正确库存通道回补、退款成功,状态进入 `Refunded` +- 正常:商家确认收货 → 原子进入 `Refunding`,后续按正确库存通道形成一次完整退款结果 +- 正常:同 Key 重放 → 返回同一退款操作当前状态,不重复退款或回补 - 异常:申请类型为 `RefundOnly` → 409 + `AFTER_SALES.WRONG_TYPE` - 异常:状态非 `PendingReceipt` → 409 + `AFTER_SALES.INVALID_STATUS` -- 异常:收货数量与申请数量不一致 → 409 + `AFTER_SALES.RETURN_QUANTITY_MISMATCH` +- 异常:退款结果未知 → 保持 `Refunding`,不误报失败 --- @@ -6416,7 +6387,7 @@ PlaceSeckillOrderResponse { - **负责人**:张海洋 - **关联数据表**:DB086(待评审)— `after_sales_requests`、DB088(待评审)— `refunds` - **当前状态**:待交叉评审 -- **用途**:商家或系统对状态为 `RefundFailed` 的申请触发重试 +- **用途**:订单指定商家对状态为 `RefundFailed` 的申请安全重试原退款操作 - **方法与路径**:`POST /api/after-sales/requests/{requestId}/retry-refund` - **operationId**:`AfterSales_RetryRefund` - **请求 Schema**:`RetryRefundRequest` @@ -6438,14 +6409,14 @@ PlaceSeckillOrderResponse { ``` - **校验规则**: - 申请关联订单分配给当前商家账号 - - 申请状态必须为 `RefundFailed` + - 首次开始该次人工重试时申请状态必须为 `RefundFailed`;已经为 `Refunding` 或 `Refunded` 时返回同一退款操作当前结果 - `Idempotency-Key` 必填 #### 成功响应 - **HTTP 状态**:`200 OK` - **响应 Schema**:`AfterSalesRequestDetailResponse` -- **示例**:(同 A412 详情,重试成功后 `status=Refunded`) +- **示例**:(同 A414 详情;返回同一退款操作当前的 `Refunding` / `Refunded` / `RefundFailed`) #### 失败响应 @@ -6454,27 +6425,32 @@ PlaceSeckillOrderResponse { | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Merchant | | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | -| 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `RefundFailed` | +| 409 | `AFTER_SALES.INVALID_STATUS` | 状态既非可重试的 `RefundFailed`,也不是同一退款操作可返回的 `Refunding` / `Refunded` | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | -| 503 | `AFTER_SALES.REFUND_FAILED` | 本次退款或必要库存回补再次失败;申请仍为 `RefundFailed` | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 状态条件更新同时校验关联订单 `assigned_merchant_user_id = currentUserId` -- 重试时以 `Refunding` 作为事务内过渡,按申请中尚未完成的退款/库存结果复用同一组业务幂等键;普通库存、秒杀库存和资金入账均不得重复。HTTP 成功时返回 `Refunded`。 -- 全部子操作成功后转为 `Refunded`;任一步再次失败时仍为 `RefundFailed`,并保留安全错误码供排查。 +- 完成固定字段校验后先查询幂等结果;命中同 Key 同请求时返回原调用结果或同一退款操作的当前确定状态,不因状态已经变化而重新报错。 +- 状态条件更新同时校验关联订单 `assigned_merchant_user_id = currentUserId`。 +- 首次人工重试只能把 `RefundFailed → Refunding`,并继续使用原 `refundOperationId`、金额、收款人、申请数量和库存通道;不得重新审核或创建第二笔业务退款。 +- 商家人工重试与系统恢复任务竞争时,最多一个执行器取得同一退款操作的执行权,其他调用返回 `Refunding` 或已确定结果。 +- 若发现旧尝试结果未知,保持 `Refunding` 并核实原尝试,不立即开始另一笔无法去重的退款。 +- 成功时只形成一次钱包入账、必要库存回补、`Refunded`、时间线和买家通知可靠事实;得到确定失败且确认没有部分资金/库存结果时才重新进入 `RefundFailed`。 +- 系统恢复不使用 Merchant JWT 或 A419,而通过 AfterSales 内部应用能力按相同约束推进。 #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:重试动作本身不发布集成事件;最终只按结果发布 `RefundCompletedIntegrationEvent` 或由 AfterSales 形成 `RefundFailedIntegrationEvent`。 +- 事件:重试动作本身不发布集成事件;最终只按结果可靠形成 `RefundCompletedIntegrationEvent` 或 `RefundFailedIntegrationEvent`,两者都只通知申请买家。 - 外部依赖:PostgreSQL + Ordering 售后快照 + Payment 退款应用契约 + Catalog/Seckill 库存回补应用契约 #### 验证场景 -- 正常:商家对 `RefundFailed` 重试 → 同步完成并返回 `Refunded` -- 异常:再次失败 → 503 + `AFTER_SALES.REFUND_FAILED`,状态保持 `RefundFailed` +- 正常:商家对 `RefundFailed` 重试 → 唯一进入 `Refunding`,返回当前状态 +- 正常:原退款已成功 → 重放当前 `Refunded`,不重复入账或回补 +- 正常:再次得到确定失败 → 返回可查询的 `RefundFailed` +- 异常:原尝试结果未知 → 返回 `Refunding` 并继续核实 - 异常:状态非 `RefundFailed` → 409 + `AFTER_SALES.INVALID_STATUS` --- @@ -6486,20 +6462,20 @@ PlaceSeckillOrderResponse { - **负责人**:张海洋 - **关联数据表**:DB089(待评审)— `payment_callbacks`、DB085(待评审)— `payments`;订单状态通过 Ordering 公开应用契约协作 - **当前状态**:待交叉评审 -- **用途**:接收模拟支付渠道的回调,更新支付与订单状态 +- **用途**:接收受控模拟支付通道回调,完成来源鉴别、回调幂等、支付流水聚合、订单竞争和确定结果回执 - **方法与路径**:`POST /api/payment/callbacks` - **operationId**:`Payment_ReceiveCallback` - **请求 Schema**:`PaymentCallbackRequest` - **响应 Schema**:`PaymentCallbackResponse` - **身份与 Policy**:模拟渠道 HMAC 签名鉴权(无用户 JWT) - **资源归属**:N/A(系统级) -- **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 支付回调) +- **幂等要求**:不使用 `Idempotency-Key`;来源与固定字段校验通过后按 `callbackId + 规范请求指纹` 持久化幂等 #### 请求 - **Route 参数**:(无) - **Query 参数**:(无) -- **Header**:`Idempotency-Key: `(必填)、`X-Callback-Signature: `(必填)、`Content-Type: application/json` +- **Header**:`X-Callback-Key-Id: `、`X-Callback-Timestamp: `、`X-Callback-Signature: `(均必填)、`Content-Type: application/json` - **Body**: ```json { @@ -6513,18 +6489,23 @@ PlaceSeckillOrderResponse { } ``` - **校验规则**: - - `callbackId` 必填,全局唯一 - - `paymentSerialNumber` 必填 + - `callbackId` 必填,UUID;同一标识只能绑定一份规范请求内容和一份首次确定结果 + - `paymentSerialNumber` 必填,1~100 字符;标识一次模拟通道支付尝试,不是第二个回调幂等键 - `result` 枚举:`Success` / `Failed` - - `amount` 必填,decimal - - 签名验证:`X-Callback-Signature` 通过 HMAC 校验(按 C08-FR02) - - `Idempotency-Key` 必填(与 `callbackId` 同值) + - `orderId` 必填,UUID;`amount` 必填且 > 0,最多两位小数;`currency` 固定 `CNY` + - `occurredAt` 必填,ISO 8601 UTC,只用于追踪和对账,是否可支付以服务端处理时间和订单 `paymentDeadline` 为准 + - `X-Callback-Timestamp` 使用 Unix 秒,与服务端时间允许偏差固定为 300 秒 + - `keyId` 仅允许命中当前密钥或轮换宽限期内的上一把密钥;未知、过期密钥与错误签名统一返回无细节的签名错误 + - `bodyHash = lowercase-hex(SHA256(raw UTF-8 body))` + - `canonical = keyId + "\n" + timestamp + "\n" + bodyHash` + - `signature = Base64(HMAC-SHA256(secret, UTF8(canonical)))`,使用常量时间比较 + - 验签必须基于未被 JSON 反序列化改写的原始请求字节;成功后再做固定字段校验和回调幂等检查 #### 成功响应 - **HTTP 状态**:`200 OK` - **响应 Schema**:`PaymentCallbackResponse` -- **状态枚举**:`Processed`(正常处理或幂等重放)/ `RecordedForReconciliation`(迟到成功已登记对账差异) +- **状态枚举**:`ProcessedSuccess` / `ProcessedFailure` / `Ignored` / `Difference` - **示例**: ```json { @@ -6532,7 +6513,9 @@ PlaceSeckillOrderResponse { "message": "ok", "data": { "callbackId": "5a8e...", - "status": "Processed", + "status": "ProcessedSuccess", + "orderId": "3f0ed9a9-...", + "paymentSerialNumber": "psn-...", "processedAt": "2026-07-23T08:35:01Z" } } @@ -6544,31 +6527,41 @@ PlaceSeckillOrderResponse { |---|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | | 401 | `PAYMENT.CALLBACK_INVALID_SIGNATURE` | 签名验证失败 | -| 409 | `PAYMENT.CALLBACK_AMOUNT_MISMATCH` | 回调金额与订单金额不一致 | -| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一回调幂等键被用于不同请求内容 | +| 409 | `PAYMENT.CALLBACK_ID_REUSED` | 同一 `callbackId` 被用于不同规范请求内容 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 回调 ID 与支付流水号建**唯一约束**(按 C08 业务规则) -- 同事务:支付记录 + 订单状态 + Inbox/处理记录 + Outbox(按 C08-FR05) -- 重复回调返回首次结果,不重复记账 -- 乱序:按订单当前状态 + 事件时间决定接受/忽略/登记差异 -- 已取消订单收到迟到成功回调时,原子登记对账差异并返回 `200`;`PaymentCallbackResponse.status=RecordedForReconciliation`,不得直接把订单改为 `Paid`。已成功受理的回调不以非 2xx 诱发渠道重复重试。 +- 来源与固定字段校验通过后,先按 `callbackId + 规范请求指纹` 查询首次确定结果:同标识同内容重放原 HTTP 状态与响应;同标识不同内容拒绝并记录安全冲突。 +- `paymentSerialNumber` 第一次出现时绑定订单、金额和币种;同一流水允许不同 `callbackId` 表达先失败后成功、先成功后失败或重复成功等乱序信号。后续若改变不可变绑定则形成 `Difference` 来源,不在幂等门口静默丢弃。 +- 服务端取得新回调唯一处理资格后,读取支付流水聚合、权威订单状态、应付金额、`paymentDeadline` 和已有成功来源,并按固定矩阵裁决: + - `PendingPayment`、服务端当前时间早于 `paymentDeadline`、金额与币种一致且 `result=Success`:`ProcessedSuccess`。 + - 同样可支付但 `result=Failed`:`ProcessedFailure`,订单保持待支付。 + - `PendingPayment` 但服务端当前时间已经达到 `paymentDeadline`,且 `result=Success`:`Difference`,不创建成功支付,并触发 M04/C03 统一过期取消。 + - `PendingPayment` 但服务端当前时间已经达到 `paymentDeadline`,且 `result=Failed`:`ProcessedFailure`,不创建成功支付,并触发 M04/C03 统一过期取消。 + - 已支付后收到失败、已取消或其他不可支付终态收到失败、同一流水在已成功后重复成功:`Ignored`。 + - 订单缺失、金额/币种/绑定不符、到期或已取消后收到成功、已有其他成功支付来源后收到成功:`Difference`。 +- `ProcessedSuccess` 必须原子提交 `source=SimulatedChannel` 的支付事实、订单 `PendingPayment → Paid`、回调终态和可靠支付成功事实,绝不扣减小金库。 +- `ProcessedFailure`、`Ignored` 和 `Difference` 也必须先保存完整回调终态与首次回执再返回。`Difference` 只保存完整差异来源,不在回调事务中提前创建 A424 差异条目。 +- 合法业务冲突统一返回 `200 + Difference`;签名错误、固定字段非法和基础设施故障不占用四种业务终态。 +- 模拟成功回调、A405 小金库支付、A304 买家取消和 C03 超时取消共同条件竞争 `PendingPayment` 与 `paymentDeadline`,最多一个形成合法订单终态。 #### 缓存、事件或外部依赖 -- 缓存:幂等记录存在 DB(不依赖 Redis) -- 事件:仅实际完成支付时发布 `OrderPaidIntegrationEvent`;重复回调和登记对账差异不发布没有消费者的处理事件。 +- 缓存:回调聚合和幂等结果保存在 PostgreSQL,不依赖 Redis。 +- 事件:仅 `ProcessedSuccess` 可靠形成 `OrderPaidIntegrationEvent`;`ProcessedFailure`、`Ignored` 和 `Difference` 不形成 M09 消息。 - 外部依赖:PostgreSQL + Ordering 模块 #### 验证场景 -- 正常:未处理过的回调 → 处理成功 +- 正常:可支付订单收到成功回调 → 200 + `ProcessedSuccess`,订单与支付事实原子提交 +- 正常:同一流水先失败后成功 → 分别返回 `ProcessedFailure`、`ProcessedSuccess` +- 正常:同一流水先成功后失败 → 后续失败返回 `Ignored`,订单不回退 - 重复:相同 `callbackId` → 返回首次结果,不重复处理 - 异常:签名错误 → 401 + `PAYMENT.CALLBACK_INVALID_SIGNATURE` -- 异常:金额不一致 → 409 + `PAYMENT.CALLBACK_AMOUNT_MISMATCH` -- 边界:已取消订单收到 Success 回调 → 200 + `RecordedForReconciliation`,订单不直接改 `Paid` +- 边界:金额不一致或订单缺失 → 200 + `Difference`,不创建成功支付 +- 边界:已取消、已到期或已由其他来源支付后收到 Success → 200 + `Difference`,订单终态不变 +- 边界:到期订单收到 Failed → 200 + `ProcessedFailure`,同时进入同一过期取消通道且不再开放支付 --- @@ -6593,7 +6586,7 @@ PlaceSeckillOrderResponse { - **Route 参数**:(无) - **Query 参数**: - `dateFrom` / `dateTo`(可选):按对账日期过滤 - - `status`(可选):`Pending` / `Matched` / `HasDifferences` / `Resolved` + - `status`(可选):`Matched` / `HasDifferences` / `Resolved` - 标准分页 + 排序 - **Header**:`Authorization: Bearer ` - **Body**:(无) @@ -6614,14 +6607,16 @@ PlaceSeckillOrderResponse { "items": [ { "batchId": "...", - "reconciliationDate": "2026-07-23", + "businessDate": "2026-07-22", "rangeFrom": "2026-07-22T00:00:00Z", "rangeTo": "2026-07-23T00:00:00Z", + "watermarkAt": "2026-07-23T00:00:05Z", "totalCount": 100, "matchedCount": 98, "differenceCount": 2, "status": "HasDifferences", - "createdAt": "2026-07-23T01:00:00Z" + "createdAt": "2026-07-23T01:00:00Z", + "resolvedAt": null } ], "page": 1, @@ -6643,8 +6638,11 @@ PlaceSeckillOrderResponse { #### 业务规则与并发 -- 仅管理员访问(按 C08 业务规则:"对账数据仅向管理员开放") -- 默认排序 `reconciliationDate desc, batchId desc` +- 仅管理员访问(按 C08 业务规则:“对账数据仅向管理员开放”)。 +- 批次提交时直接进入 `Matched` 或 `HasDifferences`;最后一个差异闭环后才进入 `Resolved`,不存在 `Pending` 批次。 +- `businessDate` 是按服务端成功提交时间归属的上一完整 UTC 业务日;`rangeFrom` / `rangeTo` 是半开区间,`watermarkAt` 是该批次一致读取水位。 +- 计数单位是“比较单元”:同一业务问题及同一比较规则只计一次,并满足 `totalCount = matchedCount + differenceCount`。 +- 默认排序 `businessDate desc, batchId desc`。 #### 缓存、事件或外部依赖 @@ -6695,17 +6693,24 @@ PlaceSeckillOrderResponse { "message": "ok", "data": { "batchId": "...", - "reconciliationDate": "2026-07-23", + "businessDate": "2026-07-22", "rangeFrom": "2026-07-22T00:00:00Z", "rangeTo": "2026-07-23T00:00:00Z", + "watermarkAt": "2026-07-23T00:00:05Z", "totalCount": 100, "matchedCount": 98, "differenceCount": 2, "status": "HasDifferences", - "summary": { - "byType": { "PaymentSucceededOrderNotUpdated": 1, "RefundAmountMismatch": 1 } + "differenceCountsByType": { + "PaymentSucceededOrderNotUpdated": 1, + "RefundAmountMismatch": 1 }, - "createdAt": "2026-07-23T01:00:00Z" + "comparisonSummary": { + "paymentUnits": 80, + "refundUnits": 20 + }, + "createdAt": "2026-07-23T01:00:00Z", + "resolvedAt": null } } ``` @@ -6721,7 +6726,8 @@ PlaceSeckillOrderResponse { #### 业务规则与并发 -- 详情含按差异类型汇总(按 C08-FR07 至少识别"支付成功但订单未更新"等) +- 详情返回与 A422 相同的日期、半开范围、水位、比较单元计数、状态和时间,并增加 `differenceCountsByType` 与支付/退款比较单元汇总。 +- `totalCount = matchedCount + differenceCount`;同一业务问题与同一比较规则即使同时来自回调 `Difference` 和横向比对,也只归入一个差异单元并保留多份证据。 #### 缓存、事件或外部依赖 @@ -6780,12 +6786,18 @@ PlaceSeckillOrderResponse { "differenceId": "...", "batchId": "...", "type": "LateSuccessCallback", + "subjectType": "Order", + "subjectId": "3f0ed9a9-...", "orderId": "3f0ed9a9-...", "paymentId": "8d2e9d11-...", + "refundOperationId": null, "callbackId": "5a8e...", - "description": "已取消订单收到迟到成功回调", "status": "Pending", - "createdAt": "2026-07-23T01:00:00Z" + "currentAssigneeUserId": null, + "claimExpiresAt": null, + "evidenceCount": 2, + "createdAt": "2026-07-23T01:00:00Z", + "resolvedAt": null } ], "page": 1, @@ -6810,11 +6822,20 @@ PlaceSeckillOrderResponse { - 差异类型至少识别: - `PaymentSucceededOrderNotUpdated`:支付成功但订单未更新 - `OrderPaidPaymentMissing`:订单已支付但缺支付记录或流水 + - `MultipleSuccessfulPaymentSources`:同一订单出现多个成功支付来源 - `LateSuccessCallback`:已取消订单收到迟到成功回调 + - `CallbackBindingMismatch`:同一支付流水后续回调改变订单、金额或币种绑定 + - `ProcessedSuccessPaymentMissing`:回调为 `ProcessedSuccess`,但缺成功支付事实 + - `RefundedOperationMissing`:售后已 `Refunded` 但缺成功退款操作 + - `DuplicateRefundOperation`:同一售后申请存在多个成功退款操作 + - `RefundSucceededAfterSalesNotUpdated`:退款操作成功但售后未进入 `Refunded` - `RefundSucceededWalletCreditMissing`:售后退款成功但小金库未入账 - `DuplicateWalletCredit`:同一退款发生重复入账 - `RefundAmountMismatch`:退款记录、钱包流水或入账金额不一致 - 状态管理(按 C08-FR08):`Pending` / `InProgress` / `Resolved` +- 摘要固定返回差异主体 `subjectType + subjectId`、关联 `orderId` / `paymentId` / `refundOperationId` / `callbackId`、当前领取人、领取有效期、证据数量和创建/解决时间;不在列表中回传原始签名或敏感渠道报文。 +- 同一回调差异来源和每日横向比对指向同一业务对象及比较规则时,合并为一个差异并累积证据引用。 +- 默认排序固定为 `createdAt desc, differenceId desc`。 #### 缓存、事件或外部依赖 @@ -6836,7 +6857,7 @@ PlaceSeckillOrderResponse { - **负责人**:张海洋 - **关联数据表**:DB091(待评审)— `reconciliation_differences` - **当前状态**:待交叉评审 -- **用途**:管理员处理对账差异并标记状态 +- **用途**:管理员领取、释放、接管或闭环对账差异 - **方法与路径**:`POST /api/admin/reconciliation/differences/{differenceId}/process` - **operationId**:`Payment_ProcessReconciliationDifference` - **请求 Schema**:`ProcessDifferenceRequest` @@ -6853,15 +6874,24 @@ PlaceSeckillOrderResponse { - **Body**: ```json { - "action": "MarkResolved", - "resolutionNote": "确认为模拟渠道测试回调,已通知商家" + "action": "Resolve", + "resolutionType": "CorrectedByControlledAction", + "resolutionNote": "已通过订单模块受控动作完成纠正并复核一致", + "evidenceRefs": ["evidence://ordering/action/9c10..."], + "controlledActionRef": "ordering-action:9c10..." } ``` - **校验规则**: - `differenceId` 必填 - - `action` 枚举:`MarkInProgress` / `MarkResolved` - - 当前状态必须为 `Pending`(`MarkInProgress`)或 `InProgress`(`MarkResolved`) - - `resolutionNote` 必填,≤ 1000 字 + - `action` 枚举:`Claim` / `Release` / `Takeover` / `Resolve` + - `Claim`:当前状态必须为 `Pending` + - `Release`:当前状态必须为 `InProgress` 且当前管理员仍是有效领取人;`resolutionNote` 作为释放原因必填 + - `Takeover`:当前状态必须为 `InProgress`,且当前无领取人,或原领取人被禁用、主动释放、领取已过期;必须填写接管原因 + - `Resolve`:当前状态必须为 `InProgress` 且当前管理员仍是有效领取人 + - `resolutionType` 仅 `Resolve` 必填,枚举:`CorrectedByControlledAction` / `ConfirmedNoBusinessImpact` + - `resolutionNote` 在 `Release` / `Takeover` / `Resolve` 时必填,1~1000 字 + - `evidenceRefs` 在 `Resolve` 时至少 1 项;单项 1~500 字,最多 20 项 + - `CorrectedByControlledAction` 必须填写 `controlledActionRef`,引用事实所属模块已经成功提交的受控动作 - `Idempotency-Key` 必填 #### 成功响应 @@ -6878,7 +6908,11 @@ PlaceSeckillOrderResponse { "batchId": "...", "type": "LateSuccessCallback", "status": "Resolved", - "resolutionNote": "确认为模拟渠道测试回调,已通知商家", + "resolutionType": "CorrectedByControlledAction", + "resolutionNote": "已通过订单模块受控动作完成纠正并复核一致", + "currentAssigneeUserId": "admin-uuid", + "evidenceRefs": ["evidence://ordering/action/9c10..."], + "verificationResult": "Matched", "resolvedAt": "2026-07-23T03:00:00Z", "resolvedBy": "admin-uuid" } @@ -6893,15 +6927,23 @@ PlaceSeckillOrderResponse { | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Admin | | 404 | `PAYMENT.RECONCILIATION_DIFFERENCE_NOT_FOUND` | 差异不存在 | -| 409 | `PAYMENT.RECONCILIATION_DIFFERENCE_ALREADY_PROCESSED` | 状态非法 | +| 409 | `PAYMENT.RECONCILIATION_INVALID_STATUS` | 当前状态不允许该动作 | +| 409 | `PAYMENT.RECONCILIATION_CLAIM_CONFLICT` | 领取人或领取有效期已变化 | +| 409 | `PAYMENT.RECONCILIATION_STILL_MISMATCHED` | 重跑原比较规则后仍不一致,差异保持 `InProgress` | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 状态条件更新:`WHERE status = 'Pending'` 或 `WHERE status = 'InProgress'` -- 差异修复必须可追踪(按 C08 业务规则),不能通过直接改库隐藏原因 -- 修复后保留 `resolutionNote` 和处理人 +- 完成固定字段校验后先查询幂等结果;同 Key 同请求重放首次结果,同 Key 不同请求返回 `IDEMPOTENCY.KEY_REUSED`,再读取当前状态和领取权。 +- `Claim` 使用条件更新唯一推进 `Pending → InProgress` 并记录领取人、领取时间和服务端配置的有效期;并发领取只有一人成功。 +- `Release` 保持差异为 `InProgress`,只把 `currentAssigneeUserId`、`claimedAt`、`claimExpiresAt` 清空并记录释放原因与时间;不得把差异退回 `Pending` 或清空既有领取历史。 +- `Takeover` 在 `InProgress` 上写入新的当前领取人和有效期,并保留释放、失效和转交历史;任何时刻只有当前有效领取人可提交 `Resolve`。 +- C08 不直接修改订单、支付、退款或钱包表。确需纠正时,管理员先调用事实所属模块的受控业务动作,再在 `Resolve` 中引用其已提交结果。 +- `Resolve` 必须重新读取权威事实并重跑生成该差异的同一比较规则;仍不一致时返回 `PAYMENT.RECONCILIATION_STILL_MISMATCHED` 并保持 `InProgress`,不能只凭文字说明关闭。 +- 仅“所属模块已纠正且复核一致”或“按固定规则确认无未决资金/订单影响”可进入 `Resolved`。 +- 差异关闭、处置类型、说明、证据、处理人、复核结果、状态时间线,以及关闭最后一条差异时批次 `HasDifferences → Resolved`,必须在同一事务提交。 +- 可确定的领取冲突、状态冲突和复核仍不一致结果也保存为可重放幂等结果;提交结果未知不固化为业务失败。 #### 缓存、事件或外部依赖 @@ -6911,153 +6953,118 @@ PlaceSeckillOrderResponse { #### 验证场景 -- 正常:管理员 MarkResolved → 状态进入 `Resolved` -- 异常:状态已为 `Resolved` → 409 + `PAYMENT.RECONCILIATION_DIFFERENCE_ALREADY_PROCESSED` +- 正常:管理员 Claim → 唯一领取并进入 `InProgress` +- 正常:当前领取人 Resolve 且复核一致 → 差异进入 `Resolved`,必要时同事务关闭批次 +- 异常:仍不一致 → 409 + `PAYMENT.RECONCILIATION_STILL_MISMATCHED`,保持 `InProgress` +- 异常:状态已为 `Resolved` → 409 + `PAYMENT.RECONCILIATION_INVALID_STATUS` - 异常:买家调用 → 403 + `AUTH.FORBIDDEN` --- -### A432 退款详情 +### A426 差异详情 - **模块 / Tag**:Payment -- **需求编号**:M10-FR04 +- **需求编号**:C08-FR07 / FR08 - **负责人**:张海洋 -- **关联数据表**:DB088(待评审)— `refunds` +- **关联数据表**:DB091(待评审)— `reconciliation_differences` 及其来源、证据和状态时间线 - **当前状态**:待交叉评审 -- **用途**:查询单笔退款详情 -- **方法与路径**:`GET /api/refunds/{refundId}` -- **operationId**:`Payment_GetRefund` +- **用途**:管理员查看单条差异的比较规则、权威事实、全部来源证据、领取与处置时间线 +- **方法与路径**:`GET /api/admin/reconciliation/differences/{differenceId}` +- **operationId**:`Payment_GetReconciliationDifference` - **请求 Schema**:(无) -- **响应 Schema**:`RefundDetailResponse` -- **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` -- **资源归属**:买家只看本人退款;商家只看关联售后订单分配给当前账号的退款 +- **响应 Schema**:`ReconciliationDifferenceDetailResponse` +- **身份与 Policy**:JWT Bearer + `AdminOnly` +- **资源归属**:N/A(管理员) - **幂等要求**:GET 天然幂等 #### 请求 -- **Route 参数**:`refundId`(UUID) +- **Route 参数**:`differenceId`(UUID) - **Query 参数**:(无) -- **Header**:`Authorization: Bearer ` -- **Body**:(无) -- **校验规则**: - - `refundId` UUID 格式 - - 资源归属当前买家,或关联售后订单分配给当前商家账号 - -#### 成功响应 - -- **HTTP 状态**:`200 OK` -- **响应 Schema**:`RefundDetailResponse`,独立于内部 `RefundResult`: - -```text -RefundDetailResponse { - refundId: uuid - requestId: uuid - paymentId: uuid - amount: decimal - currency: "CNY" - status: "Succeeded" | "Failed" - createdAt: string - completedAt: string? - failureMessage: string? -} -``` - -#### 失败响应 - -| HTTP 状态 | 业务错误码 | 触发条件 | -|---|---|---| -| 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | -| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer / Merchant | -| 404 | `RESOURCE.NOT_FOUND` | 退款不存在或不在授权范围 | -| 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | - -#### 业务规则与并发 - -- 不返回内部审计字段 - -#### 缓存、事件或外部依赖 - -- 缓存:不缓存 -- 事件:无 -- 外部依赖:PostgreSQL - -#### 验证场景 - -- 正常:本人退款 → 返回详情 -- 异常:他人退款 → 404 + `RESOURCE.NOT_FOUND` - ---- - -### A433 退款列表 - -- **模块 / Tag**:Payment -- **需求编号**:M10-FR03 -- **负责人**:张海洋 -- **关联数据表**:DB088(待评审)— `refunds` -- **当前状态**:待交叉评审 -- **用途**:分页查询退款记录 -- **方法与路径**:`GET /api/refunds` -- **operationId**:`Payment_ListRefunds` -- **请求 Schema**:`ListRefundsQuery` -- **响应 Schema**:`RefundListResponse` -- **身份与 Policy**:JWT Bearer + `BuyerOnly` / `MerchantOnly` -- **资源归属**:买家只看本人退款;商家只看关联售后订单分配给当前账号的退款 -- **幂等要求**:GET 天然幂等 - -#### 请求 - -- **Route 参数**:(无) -- **Query 参数**: - - `status`(可选):`Succeeded` / `Failed` - - `createdFrom` / `createdTo`(可选):时间范围 - - 标准分页 + 排序 -- **Header**:`Authorization: Bearer ` +- **Header**:`Authorization: Bearer ` - **Body**:(无) -- **校验规则**: - - 标准分页 + 时间范围 + 枚举白名单 +- **校验规则**:`differenceId` 必须是 UUID #### 成功响应 - **HTTP 状态**:`200 OK` -- **响应 Schema**:`RefundListResponse` +- **响应 Schema**:`ReconciliationDifferenceDetailResponse` - **示例**: ```json { "code": "success", "message": "ok", "data": { - "items": [ + "differenceId": "8c42...", + "batchId": "f12a...", + "type": "LateSuccessCallback", + "status": "InProgress", + "subjectType": "Order", + "subjectId": "3f0ed9a9-...", + "comparisonRule": "订单取消后不得存在新的成功支付来源", + "expectedFacts": { + "orderStatus": "Cancelled", + "successfulPaymentCount": 0 + }, + "actualFacts": { + "orderStatus": "Cancelled", + "successfulPaymentCount": 0, + "lateSuccessCallbackCount": 1 + }, + "references": { + "orderId": "3f0ed9a9-...", + "paymentId": null, + "refundOperationId": null, + "callbackIds": ["5a8e..."] + }, + "evidenceRefs": [ { - "refundId": "...", - "requestId": "b9c1...", - "amount": 100.00, - "currency": "CNY", - "status": "Succeeded", - "createdAt": "2026-07-23T09:00:00Z", - "succeededAt": "2026-07-23T09:00:01Z" + "sourceType": "PaymentCallback", + "sourceId": "5a8e...", + "observedAt": "2026-07-23T00:40:00Z" } ], - "page": 1, - "pageSize": 10, - "total": 1, - "totalPages": 1 + "claim": { + "currentAssigneeUserId": "admin-uuid", + "claimedAt": "2026-07-23T02:00:00Z", + "claimExpiresAt": "2026-07-23T02:30:00Z" + }, + "resolution": null, + "verification": { + "lastVerifiedAt": null, + "result": null, + "remainingMismatchReason": null + }, + "timeline": [ + { "action": "Created", "at": "2026-07-23T01:00:00Z", "actorType": "System" }, + { "action": "Claimed", "at": "2026-07-23T02:00:00Z", "actorType": "Admin" } + ], + "createdAt": "2026-07-23T01:00:00Z", + "resolvedAt": null } } ``` +- `expectedFacts` / `actualFacts` 返回生成该差异时使用的固定比较口径与安全业务摘要,不回传支付签名、密钥、钱包敏感字段或异常堆栈。 +- `references` 包含适用的 `orderId`、`paymentId`、`refundOperationId`、`callbackIds`;不存在的关联项返回 `null` 或空数组,不伪造标识。 +- `evidenceRefs` 返回全部发现来源。回调 `Difference` 和横向比对命中同一业务对象与比较规则时保留多份证据,但仍是一条差异。 +- `claim` 返回当前领取人、领取时间和有效期;`resolution` 在已关闭后返回处置类型、说明、证据引用、受控动作引用、处理人和处理时间。 +- `verification` 返回最后复核时间、`Matched` / `StillMismatched` 结果和仍不一致原因;`timeline` 保留创建、领取、释放、接管、复核失败和解决全过程。 + #### 失败响应 | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 参数错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | -| 403 | `AUTH.FORBIDDEN` | 角色非 Buyer / Merchant | +| 403 | `AUTH.FORBIDDEN` | 角色非 Admin | +| 404 | `PAYMENT.RECONCILIATION_DIFFERENCE_NOT_FOUND` | 差异不存在 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 买家视图按 `buyerId` 过滤;商家视图按关联订单 `assignedMerchantUserId` 过滤 -- 默认排序 `createdAt desc, refundId desc` +- 只读取已提交的批次、差异、来源、证据、领取和处置事实,不在 GET 中自动领取、修复或改变状态。 +- 当前领取人失效或过期仍如实返回,是否可接管由 A425 在执行时重新校验。 +- 管理员只能通过 A425 引用所属模块已完成的受控动作并重跑比较规则,不能在详情接口直接修改资金或订单。 #### 缓存、事件或外部依赖 @@ -7067,8 +7074,10 @@ RefundDetailResponse { #### 验证场景 -- 正常:买家 → 返回本人退款 -- 正常:商家 → 返回授权范围退款 +- 正常:管理员查看差异 → 返回比较事实、全部证据、领取信息和完整时间线 +- 正常:差异已解决 → 返回处置类型、证据、处理人与最终复核结果 +- 异常:差异不存在 → 404 + `PAYMENT.RECONCILIATION_DIFFERENCE_NOT_FOUND` +- 异常:非管理员调用 → 403 + `AUTH.FORBIDDEN` --- @@ -7105,7 +7114,7 @@ RefundDetailResponse { - **校验规则**: - 申请归属当前 buyerId - 申请类型必须为 `ReturnAndRefund` - - 申请状态必须为 `PendingReturn`(商家审核通过后、待退货) + - 首次提交时申请状态必须为 `PendingReturn`(商家审核通过后、待退货);已保存相同规范化内容时按重放规则返回 - `carrier` 必填,1~50 字;本期不接入真实物流平台,允许填写“其他”及实际承运方名称 - `trackingNumber` 必填,1~50 字符 - `shippedAt` 选填,ISO 8601 UTC 且不得晚于当前时间;缺省时使用服务端提交时间 @@ -7156,27 +7165,31 @@ RefundDetailResponse { | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或非本人 | | 409 | `AFTER_SALES.INVALID_STATUS` | 申请状态非 `PendingReturn` | | 409 | `AFTER_SALES.WRONG_TYPE` | 申请类型非 `ReturnAndRefund` | +| 409 | `AFTER_SALES.RETURN_INFO_CONFLICT` | 该申请已保存退货信息,但本次规范化内容不同 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 状态条件更新:`WHERE status = 'PendingReturn' AND buyer_id = currentBuyerId AND type = 'ReturnAndRefund'` -- 提交后状态变为 `PendingReceipt` -- 同一包裹可以承载同一订单的多笔退货申请,不对快递单号施加不符合现实的全局唯一约束;重复提交由申请状态和 `Idempotency-Key` 控制。 -- 领域状态历史记录提交人、必要退货摘要和状态变化 -- 商家在 A417 确认收货后 → 触发 Payment 退款应用契约 +- 完成固定字段校验后先查询幂等结果;同 Key 同内容重放首次结果,同 Key 不同内容返回 `IDEMPOTENCY.KEY_REUSED`,不先用当前状态覆盖首次结果。 +- 首次提交使用条件更新:`WHERE status = 'PendingReturn' AND buyer_id = currentBuyerId AND type = 'ReturnAndRefund'`。 +- 该申请已经保存退货信息时,即使使用新 Key,只要 `carrier`、`trackingNumber`、有效 `shippedAt` 和 `note` 的规范化内容完全相同,也返回当前详情;内容不同则返回 `AFTER_SALES.RETURN_INFO_CONFLICT`,不得静默覆盖。 +- 退货信息、`PendingReturn → PendingReceipt`、领域时间线、订单指定商家通知可靠事实和幂等成功结果必须在同一事务形成。 +- 同一包裹可以承载同一订单的多笔退货申请,不对快递单号施加不符合现实的跨申请全局唯一约束。 +- 商家在 A417 确认收货后才进入同一退款操作,不在本接口退款或回补库存。 #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:发布 `AfterSalesReturnInfoSubmittedIntegrationEvent` +- 事件:可靠形成 `AfterSalesReturnInfoSubmittedIntegrationEvent`,由 M09 只通知订单指定商家。 - 外部依赖:PostgreSQL #### 验证场景 - 正常:买家提交退货物流 → 状态进入 `PendingReceipt` - 重复:相同 Idempotency-Key → 返回首次结果,不重复写入 +- 重复:新 Key + 相同退货内容 → 返回当前结果,不重复写入或通知 +- 冲突:新 Key + 不同退货内容 → 409 + `AFTER_SALES.RETURN_INFO_CONFLICT` - 异常:他人申请 → 404 + `RESOURCE.NOT_FOUND` - 异常:申请类型为 `RefundOnly` → 409 + `AFTER_SALES.WRONG_TYPE` - 异常:状态非 `PendingReturn` → 409 + `AFTER_SALES.INVALID_STATUS` @@ -7210,7 +7223,7 @@ RefundDetailResponse { |---|---|---:|---|---| | `page` | integer | 否 | 1 | 大于等于 1 | | `pageSize` | integer | 否 | 10 | 1~100 | -| `readStatus` | string | 否 | `all` | 仅允许 `all`、`unread` | +| `readStatus` | string | 否 | `all` | 仅允许 `all`、`unread`、`read` | | `type` | `MessageType` | 否 | 无 | 只允许已登记消息类型 | - Header:`Authorization: Bearer `。 @@ -7270,6 +7283,8 @@ RefundDetailResponse { | 400 | `COMMON.VALIDATION_FAILED` | 页码、页大小、已读筛选或消息类型非法 | | 401 | `AUTH.UNAUTHENTICATED` | JWT 缺失或无效 | | 403 | `AUTH.FORBIDDEN` | 当前身份不是买家或商家 | +| 403 | `AUTH.ACCOUNT_DISABLED` | 当前账号已禁用,不读取消息 | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销、账号状态或旧凭证失效事实无法安全确认 | #### 业务规则与并发 @@ -7346,13 +7361,15 @@ RefundDetailResponse { | 400 | `COMMON.VALIDATION_FAILED` | `messageId` 格式非法 | | 401 | `AUTH.UNAUTHENTICATED` | JWT 缺失或无效 | | 403 | `AUTH.FORBIDDEN` | 当前身份不是买家或商家 | +| 403 | `AUTH.ACCOUNT_DISABLED` | 当前账号已禁用,不读取消息 | | 404 | `MESSAGE.NOT_FOUND` | 消息不存在或不属于当前用户 | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销、账号状态或旧凭证失效事实无法安全确认 | #### 业务规则与并发 - 他人消息与不存在消息统一返回 `404` 和 `MESSAGE.NOT_FOUND`,不泄露消息是否存在。 - 查询详情不会自动标记已读;客户端在用户实际打开消息后调用 A504。 -- 每次返回 `action` 前重新校验当前用户对关联资源的访问资格;无资格时返回 `null`,消息正文仍可查看。 +- 每次返回 `action` 前重新校验当前用户对关联资源的访问资格;无资格或目标当前不可用时固定返回 `null`,页面显示“目标暂不可用”,历史消息正文仍可查看。 #### 缓存、事件或外部依赖 @@ -7411,6 +7428,8 @@ RefundDetailResponse { |---|---|---| | 401 | `AUTH.UNAUTHENTICATED` | JWT 缺失或无效 | | 403 | `AUTH.FORBIDDEN` | 当前身份不是买家或商家 | +| 403 | `AUTH.ACCOUNT_DISABLED` | 当前账号已禁用,不读取未读数 | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销、账号状态或旧凭证失效事实无法安全确认 | #### 业务规则与并发 @@ -7476,7 +7495,9 @@ RefundDetailResponse { | 400 | `COMMON.VALIDATION_FAILED` | `messageId` 格式非法 | | 401 | `AUTH.UNAUTHENTICATED` | JWT 缺失或无效 | | 403 | `AUTH.FORBIDDEN` | 当前身份不是买家或商家 | +| 403 | `AUTH.ACCOUNT_DISABLED` | 当前账号已禁用,不修改消息 | | 404 | `MESSAGE.NOT_FOUND` | 消息不存在或不属于当前用户 | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销、账号状态或旧凭证失效事实无法安全确认 | #### 业务规则与并发 @@ -7516,7 +7537,7 @@ RefundDetailResponse { - Query 参数:无。 - Header:`Authorization: Bearer `。 - Body:无。 -- 校验规则:服务端在操作开始时生成 UTC 截止时间,不接受客户端传入用户 ID 或截止时间。 +- 校验规则:服务端在操作开始时读取当前用户已提交消息的稳定单调序列高水位,不接受客户端传入用户 ID、时间或高水位。 #### 成功响应 @@ -7530,6 +7551,7 @@ RefundDetailResponse { "message": "ok", "data": { "markedCount": 5, + "highWatermark": 18245, "readAt": "2026-07-24T02:40:00Z" } } @@ -7541,11 +7563,15 @@ RefundDetailResponse { |---|---|---| | 401 | `AUTH.UNAUTHENTICATED` | JWT 缺失或无效 | | 403 | `AUTH.FORBIDDEN` | 当前身份不是买家或商家 | +| 403 | `AUTH.ACCOUNT_DISABLED` | 当前账号已禁用,不修改消息 | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销、账号状态或旧凭证失效事实无法安全确认 | #### 业务规则与并发 -- 更新条件必须包含当前认证用户、`isRead = false` 和 `createdAt <= readAt`。 -- 操作期间在截止时间之后到达的新消息保持未读。 +- 更新条件必须包含当前认证用户、`isRead = false` 和 `serverSequence <= highWatermark`。 +- `serverSequence` 是消息落库时由 PostgreSQL 产生的稳定单调序列;`highWatermark` 是本次动作开始时当前用户可见的已提交消息上界。不能用 `createdAt` 或 `readAt` 划定范围。 +- `readAt` 只表示本次批量写入的服务端时间,不承担消息范围截止语义。 +- 高水位捕获后到达的新消息保持未读;即使新消息与本批消息拥有相同 `createdAt`,也不能被误标为已读。 - `markedCount` 是本次首次变为已读的记录数,不是用户历史消息总数。 #### 缓存、事件或外部依赖 @@ -7555,7 +7581,7 @@ RefundDetailResponse { #### 验证场景 -- 验证存在多条未读、没有未读、重复调用和操作期间并发到达新消息。 +- 验证存在多条未读、没有未读、重复调用、操作期间并发到达新消息,以及相同 `createdAt` 但序列高于高水位的消息。 - 使用两个用户确认只更新当前用户数据。 ### A506 API 存活检查 @@ -7623,7 +7649,7 @@ RefundDetailResponse { - 负责人:罗皓晨 - 关联数据表:无 - 当前状态:待交叉评审 -- 用途:判断实例是否具备接收业务流量的必要依赖。 +- 用途:判断实例是否满足固定全局流量准入门槛,并同时公开非阻断依赖的能力降级状态。 - 方法与路径:`GET /health/ready` - operationId:`Infrastructure_GetReadiness` - 请求 Schema:无 @@ -7642,27 +7668,31 @@ RefundDetailResponse { #### 成功响应 -- HTTP 状态:全部必需依赖可用时为 `200 OK` +- HTTP 状态:安全配置、运行版本、目标 Migration 版本和 PostgreSQL 四项固定全局门槛均通过时为 `200 OK` - 响应 Schema:`ReadinessStatusResponse` - 本接口不使用通用业务包装。 - 示例: ```json { - "status": "healthy", + "status": "ready", "service": "mall-api", "instanceId": "api-1", + "version": "commit-sha-or-version-tag", "checkedAt": "2026-07-24T02:45:00Z", - "checks": [ - { - "name": "postgres", - "status": "healthy" - }, - { - "name": "redis", - "status": "healthy" - } - ] + "globalGates": { + "secureConfiguration": "healthy", + "runtimeVersion": "healthy", + "migrationVersion": "healthy", + "postgres": "healthy" + }, + "capabilities": { + "catalogCache": "available", + "protectedAuthentication": "available", + "realTimeMessaging": "available", + "outboxDelivery": "available", + "objectWrites": "available" + } } ``` @@ -7670,29 +7700,42 @@ RefundDetailResponse { | HTTP 状态 | 响应 | 触发条件 | |---|---|---| -| 503 | `ReadinessStatusResponse` | PostgreSQL 或当前阶段已启用且被配置为必需的依赖不可用 | +| 503 | `ReadinessStatusResponse` | 安全配置、运行版本、目标 Migration 版本或 PostgreSQL 任一固定全局门槛失败 | `503` 示例: ```json { - "status": "unhealthy", + "status": "notReady", "service": "mall-api", "instanceId": "api-1", + "version": "commit-sha-or-version-tag", "checkedAt": "2026-07-24T02:46:00Z", - "checks": [ - { - "name": "postgres", - "status": "unhealthy" - } - ] + "globalGates": { + "secureConfiguration": "healthy", + "runtimeVersion": "healthy", + "migrationVersion": "healthy", + "postgres": "unhealthy" + }, + "capabilities": { + "catalogCache": "fallback", + "protectedAuthentication": "failClosed", + "realTimeMessaging": "disabled", + "outboxDelivery": "paused", + "objectWrites": "disabled" + } } ``` #### 业务规则与并发 -- PostgreSQL 始终属于就绪必需依赖。 -- Redis、RabbitMQ 和对象存储仅在当前阶段启用且配置为该实例必要依赖时参与就绪判断;未启用依赖不能错误阻塞就绪。 +- 全局 `Ready` 固定且只由四项门槛决定:安全配置完整、运行版本兼容、Migration 版本等于部署目标、PostgreSQL 可用。任一失败都返回 `503`,Nginx 不再向该实例分发新流量。 +- Redis、RabbitMQ、SeaweedFS 不改变全局 `200/503`,而是分别改变 `capabilities`: + - Redis 不可用:`catalogCache=fallback`、`protectedAuthentication=failClosed`、`realTimeMessaging=disabled`。 + - RabbitMQ 不可用:`outboxDelivery=paused`;业务事务已提交的 Outbox 事实保留。 + - SeaweedFS 不可用:`objectWrites=disabled`;与对象写入无关的能力按各自契约继续。 +- Redis 基础连通恢复与安全事实恢复是两个阶段:基础健康后可恢复 C07 公开缓存;只有有效期内撤销、账号禁用和旧凭证失效事实已经从受控来源重建并通过安全健康检查后,`protectedAuthentication` 和 `realTimeMessaging` 才能从失败关闭恢复。 +- 未启用的可选能力使用 `disabled`,不能伪装为 `available`,也不能因此把全局实例标成 `notReady`。 - 响应不得包含连接字符串、主机、端口、异常消息、堆栈或凭据。 #### 缓存、事件或外部依赖 @@ -7702,9 +7745,11 @@ RefundDetailResponse { #### 验证场景 -- PostgreSQL 正常时返回 `200`。 -- PostgreSQL 不可用时返回 `503`。 -- 未启用 RabbitMQ 或对象存储时不把它们报告为失败。 +- 四项固定全局门槛全部通过时返回 `200`;其中任一失败时返回 `503`。 +- Redis 不可用但四项门槛正常时仍返回 `200`,同时准确返回缓存回退、受保护鉴权失败关闭和实时禁用。 +- Redis 恢复但撤销事实尚未安全重建时,缓存可以恢复,受保护鉴权与实时能力继续失败关闭。 +- RabbitMQ 或 SeaweedFS 不可用时仍按全局门槛返回状态,并分别报告 Outbox 暂停或对象写入禁用。 +- 未启用 RabbitMQ 或对象存储时明确返回 `disabled`,不伪装为健康。 - 两个实例使用同一契约并返回不同 `instanceId`。 ## 四、非 HTTP 契约与模块协作 @@ -7713,12 +7758,18 @@ RefundDetailResponse { | 编号 | 当前类型 | 处理结论 | 替代契约 | |---|---|---|---| +| A005 | 已取消 HTTP | 本期只签发一个 JWT,不提供刷新令牌与刷新入口 | A002 登录后重新登录获取新 Token | +| A009 | 已取消 HTTP | 本期无独立展示资料编辑能力 | A008 查询本人资料;A006/A007 分别处理手机号与用户名 | +| A023 | 已取消 HTTP | 本期不提供一键清空浏览历史 | A021/A024/A025 管理查询、记录与开关 | +| A144 | 已取消 HTTP | 本期不提供单条公开评价详情入口 | A140 承载公开评价列表展示;A143 只承载订单项评价资格与是否已评 | | A229 | 已取消 HTTP | 不再建立秒杀订单列表端点 | A302,使用 `orderType=Seckill` 或 `seckillActivityId` | | A230 | 已取消 HTTP | 不再建立秒杀订单详情端点 | A303,返回可选秒杀活动与价格快照 | | A418 | 已取消 HTTP | 不再单独查询“审核日志” | A414 统一返回售后领域状态时间线 | | A431 | 已取消 HTTP | 不再建立公开退款命令端点 | Payment 退款应用契约,见 4.2.1 | +| A432 | 已取消 HTTP | 不再建立独立退款详情端点 | A414 返回同一退款操作摘要、结果与时间线 | +| A433 | 已取消 HTTP | 不再建立独立退款列表端点 | A413/A414 按售后申请查询退款状态与结果 | -以上编号均不得重新分配。OpenAPI 只生成 103 个有效 HTTP 契约,不生成这四个端点。 +以上 10 个编号均不得重新分配。OpenAPI 只生成 99 个活动 HTTP 契约,不生成这些历史端点。 ### 4.2 模块间公开应用契约 @@ -7727,23 +7778,28 @@ RefundDetailResponse { | 提供模块 | 使用方 | 公开能力 | 最小输入与输出 | 事实所有权 | |---|---|---|---|---| | Identity | Ordering | 校验本人地址并返回地址快照 | `buyerId + addressId -> AddressSnapshot` | Identity 拥有地址;Ordering 只保存下单快照 | -| Identity | Ordering | 解析并校验订单处理商家 | 普通订单必须解析唯一且启用的默认商家运营账号;秒杀订单校验活动创建人仍可用 | Identity 拥有账号与默认标记;Ordering 保存 `assignedMerchantUserId` 快照 | -| Identity | Review | 返回当前买家的安全展示名 | `buyerId -> maskedDisplayName`,无展示名时回退到自动用户名的脱敏值 | Identity 拥有用户资料;Review 只在评价创建时保存展示名快照 | -| Ordering、AfterSales、Seckill | Identity | 校验非默认商家能否禁用 | `merchantUserId -> hasBlockingWork + reason`,覆盖待履约/售后窗口与申请/未结束活动 | 各业务模块拥有工作状态;Identity 只在全部允许时改变账号状态 | -| Catalog | Cart、Ordering | 查询可售商品快照、条件扣减及幂等回补普通库存 | 商品/订单项/数量/原因/幂等键 -> 商品快照或库存结果 | Catalog 拥有商品与普通库存 | +| Identity | Ordering | 解析并校验订单处理商家 | 普通订单与秒杀订单都必须解析唯一且启用的默认商家运营账号;活动创建人只拥有活动管理权 | Identity 拥有账号与默认标记;Ordering 保存 `assignedMerchantUserId` 快照 | +| Identity | Review | 返回当前买家的安全展示名 | `buyerId -> maskedUsername`,只使用自动用户名的脱敏快照 | Identity 拥有自动用户名;Review 只在评价创建时保存脱敏展示快照 | +| Identity | Messaging | 解析并校验事件派生接收身份 | `userId + expectedRole -> exists + actualRole + accountStatus`;账号禁用但身份和归属仍有效时返回可保存,不要求账号启用 | Identity 拥有账号身份与角色;Messaging 用于整事件校验和按角色生成文案,不跨模块读账号表 | +| Ordering、AfterSales、Seckill | Identity | 校验非默认商家能否禁用并约束新责任受理 | `merchantUserId -> blockingReason[]`,精确覆盖 `PendingPayment`、仍有可履约数量的 `Paid`、`Shipped`、完成后 7 天售后窗口、任一非终态售后申请、任一未结束秒杀活动;新责任受理同时校验账号状态 | 各业务模块拥有责任事实;Identity 只在全部允许时改变账号状态 | +| Catalog | Cart、Ordering、AfterSales | 查询可售商品快照、条件扣减及幂等回补普通库存 | 商品/订单项/数量/原因/稳定操作标识 -> 商品快照或库存结果 | Catalog 拥有商品与普通库存;AfterSales 仅在退款成功完整结果中按原通道回补 | | Catalog | Seckill | 发布活动时原子划转普通库存到秒杀配额 | 商品、活动、数量、幂等键 -> 划转结果 | Catalog 扣减普通库存;Seckill 拥有已划转配额 | | Cart | Ordering | 读取本人已选条目并在下单成功后清理 | `buyerId + cartItemIds -> CheckoutItems` | Cart 拥有购物车 | -| Ordering | Payment | 查询支付快照并按状态条件标记已支付 | `buyerId + orderId -> amount + currency + assignedMerchantUserId + order status` | Ordering 拥有订单状态与处理商家归属 | +| Ordering | Payment | 查询支付快照并按状态条件标记已支付 | `buyerId + orderId -> amount + currency + assignedMerchantUserId + orderStatus + paymentDeadline` | Ordering 拥有订单状态、截止时间与处理商家归属 | +| Payment | Ordering、M05、C08 | 查询订单已有成功支付来源 | `orderId -> existingPaymentSummary? { paymentId, source, amount, paidAt }` | Payment 拥有 `Wallet` / `SimulatedChannel` 成功支付事实;同一订单最多一个成功来源 | | Ordering | AfterSales | 在共享事务中锁定订单履约变更并返回售后校验快照 | `orderId + buyerId + orderItemId -> locked order status + 实付 + orderType + seckillActivityId + assignedMerchantUserId` | Ordering 拥有订单行和履约状态;行锁保持到调用方事务提交 | | AfterSales | Ordering | 查询发货阻断与剩余可履约数量 | `orderId -> hasBlockingRequest + item[{orderItemId, refundedQuantity}]` | AfterSales 拥有申请状态和已退款数量;Ordering 计算并固化实际发货数量 | | Ordering | Review | 校验评价资格并返回订单项快照 | `buyerId + orderItemId -> Completed + product/order snapshot` | Ordering 拥有订单完成与订单项归属事实 | | Ordering | Catalog | 判断商品是否存在历史订单关联 | `productId -> hasHistoricalOrders` | Ordering 拥有历史订单关联;Catalog 据此保护删除 | | Ordering | Seckill | 在秒杀库存条件扣减成功后创建共享订单事实 | 活动/商品/买家/地址/价格快照 -> 订单结果 | Ordering 是唯一订单事实来源 | | Seckill | Ordering、AfterSales | 幂等回补原活动库存并释放买家限购额度 | 活动/订单项/买家/数量/原因/幂等键 -> 回补结果 | Seckill 拥有活动库存与买家配额 | -| Payment | AfterSales | 幂等退款入账 | `CreateRefundCommand -> RefundResult` | Payment 拥有钱包、退款和资金流水;AfterSales 拥有申请状态 | -| Ordering、Payment、AfterSales | Messaging | 发布带明确接收账号的已发生事实 | 标准事件 Envelope + `recipients[]` + 最小资源快照 | 来源模块决定业务事实和接收人;Messaging 只持久化与推送 | +| Payment | AfterSales | 执行或核实同一退款操作 | `ExecuteRefundCommand -> RefundExecutionResult`,稳定 `refundOperationId`,结果为 `Succeeded` / `DefiniteFailure` / `Unknown` | Payment 拥有钱包、退款和资金流水;AfterSales 拥有申请状态 | +| Ordering、Payment、AfterSales | C08 对账 Worker | 按固定 UTC 范围和一致水位读取已提交比较事实 | `businessDate + rangeFrom + rangeTo + watermarkAt -> 订单支付终态、成功支付与回调聚合、售后终态、退款操作和钱包入账比较单元` | 各来源模块拥有原事实;C08 只保存批次、差异、证据和处置时间线,不跨模块直接读表 | +| Ordering、Payment、AfterSales | Messaging | 发布已提交业务事实与归属快照 | 标准事件 Envelope + 稳定 `eventId` + 业务归属字段 + 最小资源快照;不接收 `recipients[]` | 来源模块拥有业务事实与归属;Messaging 按 4.3.6 固定矩阵派生接收人并持久化 | + +本期采用单店 B2C,不建设商户租户、拆单、结算或商品归属模型。Identity 的默认商家标记最多一个,并由启动配置/种子数据保证存在一个启用账号;默认商家不能通过 A016 直接禁用,非默认商家存在固定阻断责任时也拒绝禁用,本期不自动重新分配。普通订单和秒杀订单都使用唯一启用的默认商家账号;活动 `createdByMerchantUserId` 只用于活动管理授权。Ordering 持久化 `assignedMerchantUserId`,商家订单、售后和消息必须精确校验该账号,不得向全部 Merchant 角色广播。 -本期采用单店 B2C,不建设商户租户、拆单、结算或商品归属模型。Identity 的默认商家标记最多一个,并由启动配置/种子数据保证存在一个启用账号;默认商家不能通过 A016 直接禁用,非默认商家存在阻断工作时也拒绝禁用,本期不自动重新分配。普通订单使用默认账号,秒杀订单使用活动的 `createdByMerchantUserId`;Ordering 持久化 `assignedMerchantUserId`。商家订单、售后和消息必须精确校验该账号,不得向全部 Merchant 角色广播。 +非默认商家禁用与新责任受理必须形成唯一先后结果:新订单、售后责任或未结束活动先成立则禁用被阻断;禁用先成立则 Ordering、AfterSales、Seckill 后续责任受理必须拒绝该账号。责任依赖不可用时不能把未知当作无责任;本期不改派既有业务,也不把商品引用或维护记录当作阻断条件。 发货与提交售后不得采用“先查后改”。A307 与 A412 都必须先通过 Ordering 应用契约在当前共享事务中锁定同一 `orders` 行并复核最新状态,锁保持到各自业务写入提交;A307 随后读取 AfterSales 履约快照。发货先提交时售后按已发货规则重算,售后先提交时处理中申请阻断发货、已退款数量从实际发货数量中扣除。该协作不新增 HTTP 接口或订单核心状态。 @@ -7752,75 +7808,82 @@ RefundDetailResponse { #### 4.2.1 A431 历史取消编号的替代退款应用契约 -> A431 是已取消的 HTTP 历史追踪编号,不再分配路径或 `operationId`。以下内容定义其替代方案 `IRefundService`,该应用契约不使用 Axxx;A432/A433 仍是外部查询接口。 +> A431 是已取消的 HTTP 历史追踪编号,不再分配路径或 `operationId`。A432/A433 也已取消,退款摘要、结果和时间线统一由 A414 返回。以下内部应用契约不使用 Axxx。 - **模块 / Tag**:Payment(应用服务层) - **需求编号**:M10-FR07 - **负责人**:张海洋 - **关联数据表**:DB088(待评审)— `refunds`、DB081(待评审)— `wallets`、DB083(待评审)— `wallet_ledgers` - **当前状态**:已设计(内部契约) -- **用途**:由 AfterSales 模块审核通过后(来源 A416 / A417 / A419)调用,将售后金额幂等退回买家小金库 +- **用途**:由 AfterSales 对同一稳定退款操作执行、核实或恢复,将服务端确定金额至多一次退回申请买家小金库 - **调用方式**:进程内应用服务调用(**非 HTTP**) -- **应用服务签名**:`IRefundService.CreateRefundAsync(CreateRefundCommand command, CancellationToken cancellationToken) → RefundResult` -- **命令 Schema**:`CreateRefundCommand`(公开应用能力) -- **结果 Schema**:`RefundResult` +- **应用服务签名**:`IRefundService.ExecuteAsync(ExecuteRefundCommand command, CancellationToken cancellationToken) → RefundExecutionResult` +- **命令 Schema**:`ExecuteRefundCommand`(公开应用能力) +- **结果 Schema**:`RefundExecutionResult` - **身份与 Policy**:内部模块信任(无 Policy) - **资源归属**:AfterSales 先校验申请状态与归属;Payment 再按本模块持有的原支付事实校验买家、币种和可退款上限 -- **幂等要求**:**必须支持 `IdempotencyKey`**(同 1.12.1 退款入账) +- **幂等要求**:以稳定 `refundOperationId` 作为业务身份;人工重试和系统恢复始终复用,不为每次尝试创建新业务退款 ##### 命令输入 -- **目标申请**:`requestId`(UUID) -- **原支付**:`paymentId`(UUID) +- **退款操作**:`refundOperationId`(UUID,首次进入 `Refunding` 时由 M10 建立) +- **目标申请**:`afterSalesRequestId`(UUID) +- **原支付**:`originalPaymentId`(UUID) - **收款买家**:`buyerId`(UUID) -- **期待金额**:`expectedAmount`(decimal) -- **币种**:`currency`(默认 `CNY`) -- **幂等键**:`IdempotencyKey`(必填,UUID) +- **退款金额**:`amount`(decimal,由 M10 根据订单项实付快照确定) +- **币种**:`currency`(固定 `CNY`) ##### 校验规则 -- AfterSales 调用前必须已把申请推进到 `Refunding`;Payment 不读取或修改 AfterSales 内部表。 -- `paymentId`、`buyerId` 与 Payment 持有的原支付事实必须一致,`expectedAmount` 不得超过剩余可退款金额。 -- 同一 `IdempotencyKey` + 相同 `amount` → 返回首次结果 -- 同一 `IdempotencyKey` + 不同 `amount` → 抛 `IdempotencyKeyReusedException` +- AfterSales 调用前必须已经原子建立同一 `refundOperationId` 并把申请推进到 `Refunding`;Payment 不接受客户端身份、金额或库存通道。 +- `refundOperationId` 必须稳定绑定 `afterSalesRequestId`、`originalPaymentId`、`buyerId`、`amount` 和 `currency`;任一绑定改变都拒绝且不覆盖原操作。 +- `originalPaymentId`、`buyerId`、币种和可退款上限必须与 Payment 持有的原支付事实一致。 +- 同一操作已成功时直接返回首次成功结果;已知确定失败时只允许同一操作开启受控重试;旧尝试结果未知时先核实,不开启第二笔退款。 ##### 结果输出 -- **RefundResult**: - - `refundId`:string - - `requestId`:string +- **RefundExecutionResult**: + - `refundOperationId`:UUID + - `afterSalesRequestId`:UUID + - `originalPaymentId`:UUID - `buyerId`:string - `amount`:decimal - `currency`:string - - `status`:`Succeeded` / `Failed` - - `walletBalanceAfter`:decimal + - `status`:`Succeeded` / `DefiniteFailure` / `Unknown` + - `walletBalanceAfter`:decimal?(仅 `Succeeded`) + - `completedAt`:UTC 时间?(仅 `Succeeded`) + - `failureCode`:string?(仅 `DefiniteFailure`,只返回安全稳定码) ##### 业务规则与并发 -- Payment 只操作本模块的钱包、退款、流水和 Outbox;由 AfterSales 编排时,这些写入加入同一受控 PostgreSQL 事务,但 Payment 不直接更新 `after_sales_requests`。 -- AfterSales 拥有申请状态并协调必要的库存回补:全部子操作成功后转为 `Refunded`;事务失败后用独立失败记录保留 `RefundFailed`,供 A419 安全重试。 -- 退款成功后订单**保持原核心状态**(按 M10 业务规则 + 架构 §7.6) -- 退款流水必须纳入 C08 每日对账(按 M10 业务规则) -- AfterSales 不直接改钱包数据;通过 Payment 公开应用能力**幂等**退回(按架构 §7.2) +- 每个 `refundOperationId` 任一时刻最多一个执行器;A416、A417、A419 和系统恢复并发时,其他调用读取 `Unknown`/当前状态或首次确定结果。 +- `Succeeded` 是完整原子结果:退款操作成功、买家钱包只入账一次、钱包流水、必要的 Catalog 普通库存或 Seckill 原活动库存回补、M10 `Refunded`、状态时间线和买家退款成功通知可靠事实必须一起提交。 +- `DefiniteFailure` 只有在已确认钱包与库存均无副作用时才能形成;M10 随后以完整失败结果进入 `RefundFailed`、记录时间线和只通知买家的失败事实。不得描述为“同一事务整体回滚后仍在该事务保存失败状态”。 +- `Unknown` 表示执行结果尚不能确认。申请保持 `Refunding`,继续核实原尝试;不得把超时直接当作失败或立即开始第二笔退款。 +- 订单核心履约状态不因退款改写;库存是否回补以及原通道由 M10 固定矩阵决定。 +- 成功退款操作、M10 `Refunded` 和钱包入账必须纳入 C08 每日三方对账;`RefundFailed` 与未知 `Refunding` 不伪造成功记录。 +- AfterSales 不直接修改钱包,Payment 不绕过 Catalog/Seckill 或 M10 所有权;完整成功结果由应用层通过公开契约编排受控共享 PostgreSQL 事务。 ##### 缓存、事件或外部依赖 -- 缓存:幂等记录存在 DB -- 事件:成功发布一次只包含申请买家接收人的 `RefundCompletedIntegrationEvent`;失败由 AfterSales 保存 `RefundFailed` 状态并形成包含买家与订单处理商家的通知事实 +- 缓存:退款操作、尝试和确定结果都保存在 PostgreSQL,不依赖 Redis。 +- 事件:成功可靠形成一次只通知申请买家的 `RefundCompletedIntegrationEvent`;确定失败由 AfterSales 可靠形成只通知申请买家的 `RefundFailedIntegrationEvent`;未知结果不发成功或失败消息。 - 外部依赖:PostgreSQL;AfterSales 仅通过本公开应用契约调用 ##### 验证场景 -- 正常:审核通过触发 → 退款成功,余额增加 -- 重复:相同 IdempotencyKey → 返回首次结果,不重复入账 -- 异常:金额不一致 → Payment 事务不入账,AfterSales 保存 `RefundFailed` -- 异常:写流水失败 → Payment 事务整体回滚,AfterSales 保存 `RefundFailed` +- 正常:同一退款操作完成 → 一次钱包入账、必要库存回补和 `Refunded` +- 重复:成功操作再次执行 → 返回首次 `Succeeded`,不重复入账或回补 +- 异常:得到确定失败 → 返回 `DefiniteFailure`,确认无部分资金/库存结果后进入 `RefundFailed` +- 异常:提交或外部结果未知 → 返回 `Unknown`,保持 `Refunding` 并核实原尝试 +- 并发:人工与系统同时重试 → 一个执行器推进,其余返回当前结果 ##### 调用方 - A416 仅退款审核通过 → AfterSales 进程内调用 -- A417 商家确认收到退货 → AfterSales 进程内调用 +- A417 商家确认收货 → AfterSales 进程内调用 - A419 退款失败重试 → AfterSales 进程内调用 +- AfterSales 系统恢复任务 → 同一内部应用能力,不伪造 Merchant JWT --- @@ -7842,7 +7905,7 @@ RefundDetailResponse { | `AfterSalesPendingReturn` | 退货申请审核通过,提醒买家寄回商品 | | `AfterSalesReturnSubmitted` | 买家已提交寄回信息,提醒指定商家处理 | | `RefundSucceeded` | 售后退款成功并已退回小金库 | -| `RefundFailed` | 售后退款失败,可在售后详情查看或重试 | +| `RefundFailed` | 售后退款得到确定失败结果,买家可在售后详情查看;买家不能发起重试 | 后续新增消息类型属于兼容性扩展。客户端必须对未知值使用“业务通知”兜底展示,不能因此白屏。 @@ -7892,37 +7955,42 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 | 字段 | 类型 | 必需 | 说明 | |---|---|---:|---| -| `messageId` | UUID | 是 | 全局唯一业务消息 ID,也是 Inbox 第一去重键 | +| `eventId` | UUID | 是 | 整个已提交来源事件的稳定标识,也是 Inbox 第一去重键;不是最终消息 ID | | `type` | string | 是 | 下表白名单值 | | `schemaVersion` | string | 是 | 当前固定 `v1` | | `occurredAt` | UTC 时间 | 是 | 业务事实发生时间 | -| `recipients` | array | 是 | 至少 1 项;每项包含 `userId` 和 `role`(`Buyer`/`Merchant`),明确列出接收账号,不允许按全角色广播 | | `aggregateId` | UUID | 是 | 订单、支付或售后申请 ID | | `correlationId` | string | 是 | 跨请求与消息链路追踪标识,与当前 Trace 关联但不暴露内部实现 | +| `ownership` | object | 是 | 固定接收矩阵所需的 `buyerId`、`assignedMerchantUserId` 等业务归属;来源不直接给接收人数组 | | `data` | object | 是 | 仅包含生成标题、摘要、正文与安全跳转所需的最小业务快照 | 事件登记与消息映射: | 来源模块 | `type` | Routing Key | 精确接收账号来源 | `MessageType` | `data` 最小字段 | 默认跳转 | |---|---|---|---|---|---|---| -| Ordering | `OrderCreatedIntegrationEvent` | `ordering.order.created.v1` | 订单 `buyerId` | `OrderCreated` | `orderId`、`orderNo`、`totalAmount` | `OrderDetail` | -| Ordering | `OrderCancelledIntegrationEvent` | `ordering.order.cancelled.v1` | 订单 `buyerId` | `OrderCancelled` | `orderId`、`orderNo`、`cancelReason` | `OrderDetail` | +| Ordering | `OrderCreatedIntegrationEvent` | `ordering.order.created.v1` | 订单 `buyerId` | `OrderCreated` | `orderId`、`totalAmount` | `OrderDetail` | +| Ordering | `OrderCancelledIntegrationEvent` | `ordering.order.cancelled.v1` | 订单 `buyerId` | `OrderCancelled` | `orderId`、`cancelReason` | `OrderDetail` | | Payment | `OrderPaidIntegrationEvent` | `payment.order.paid.v1` | 订单 `buyerId`、`assignedMerchantUserId` | `PaymentSucceeded` | `orderId`、`paymentId`、`amount` | 买家 `PaymentDetail`;商家 `OrderDetail` | -| Ordering | `OrderShippedIntegrationEvent` | `ordering.order.shipped.v1` | 订单 `buyerId` | `OrderShipped` | `orderId`、`orderNo`、`shippedAt` | `OrderDetail` | -| Ordering | `OrderCompletedIntegrationEvent` | `ordering.order.completed.v1` | 订单 `buyerId` | `OrderCompleted` | `orderId`、`orderNo`、`completedAt`、`completedBy` | `OrderDetail` | -| AfterSales | `AfterSalesApplicationSubmittedIntegrationEvent` | `after-sales.request.submitted.v1` | 申请 `buyerId`、订单 `assignedMerchantUserId` | `AfterSalesSubmitted` | `requestId`、`orderId`、`type` | `AfterSalesDetail` | +| Ordering | `OrderShippedIntegrationEvent` | `ordering.order.shipped.v1` | 订单 `buyerId` | `OrderShipped` | `orderId`、`shippedAt` | `OrderDetail` | +| Ordering | `OrderCompletedIntegrationEvent` | `ordering.order.completed.v1` | 订单 `buyerId` | `OrderCompleted` | `orderId`、`completedAt`、`completedBy` | `OrderDetail` | +| AfterSales | `AfterSalesApplicationSubmittedIntegrationEvent` | `after-sales.request.submitted.v1` | 订单 `assignedMerchantUserId` | `AfterSalesSubmitted` | `requestId`、`orderId`、`type` | `AfterSalesDetail` | | AfterSales | `AfterSalesApplicationAuditedIntegrationEvent` | `after-sales.request.audited.v1` | 申请 `buyerId` | `AfterSalesReviewed` 或 `AfterSalesPendingReturn` | `requestId`、`decision`、`status` | `AfterSalesDetail` | | AfterSales | `AfterSalesReturnInfoSubmittedIntegrationEvent` | `after-sales.return-info.submitted.v1` | 订单 `assignedMerchantUserId` | `AfterSalesReturnSubmitted` | `requestId`、`status` | `AfterSalesDetail` | -| Payment | `RefundCompletedIntegrationEvent` | `payment.refund.completed.v1` | 申请 `buyerId` | `RefundSucceeded` | `requestId`、`refundId`、`amount` | `AfterSalesDetail` | -| AfterSales | `RefundFailedIntegrationEvent` | `after-sales.refund.failed.v1` | 申请 `buyerId`、订单 `assignedMerchantUserId` | `RefundFailed` | `requestId`、`failureCode` | `AfterSalesDetail` | +| Payment | `RefundCompletedIntegrationEvent` | `payment.refund.completed.v1` | 申请 `buyerId` | `RefundSucceeded` | `requestId`、`refundOperationId`、`amount` | `AfterSalesDetail` | +| AfterSales | `RefundFailedIntegrationEvent` | `after-sales.refund.failed.v1` | 申请 `buyerId` | `RefundFailed` | `requestId`、`failureCode` | `AfterSalesDetail` | 传输与幂等规则: - 事件发布到 `eshop.events` Exchange;Routing Key 使用上表固定值,新增事件仍遵守 `...v1`。 - 来源模块在业务事务中写 Outbox;Worker 发布 RabbitMQ;Messaging 在保存消息的同一事务中写 Inbox。 -- Messaging 对 `recipients` 逐项生成消息,并以 `(messageId, recipientUserId, MessageType)` 建唯一约束;重复投递返回已处理结果,不重复生成消息或未读数。 +- 来源模块只提交已经发生的事实、稳定 `eventId` 和真实业务归属,不得自由指定 `recipients[]`。Messaging 必须按上表从 `ownership` 整事件派生全部必需接收人。 +- Messaging 先整体校验事件白名单、字段、归属、接收账号与角色;任一必需接收人缺失、角色错误或归属不符时,整事件零消息、记录 `traceId` 和安全原因并告警,不得先保存部分接收人的消息。 +- 每个接收人的持久化消息由 Messaging 单独生成 `messageId`;`eventId` 与 `messageId` 不得复用。Inbox 处理结果、全部接收人消息和 `(eventId, recipientUserId, MessageType)` 唯一结果必须在同一事务提交。 +- 同一 `eventId` 重复投递返回整事件既有结果,不重复生成消息、增加未读数或再次触发同一通知。 +- 已禁用但身份与业务归属仍有效的账号仍是合法接收人,消息照常保存;禁用只阻断消息查询、已读操作和实时推送,不删除历史。 - `data` 不包含完整手机号、地址、支付凭证、JWT、密码或内部前端路由;消息文案由 Messaging 按每项接收人的 `role` 选择模板。 -- 消息保存成功后才触发 SignalR;RabbitMQ 或实时推送失败不回滚已经提交的来源业务事实。 +- 支付失败、回调 `Ignored`、回调 `Difference`、对账差异发现和管理员对账处置是明确的“无消息事实”;未知或非法事件类型必须拒绝并告警,不能静默当作正常无消息。 +- 消息保存成功后才触发 SignalR;RabbitMQ 或实时推送失败不回滚已经提交的来源业务事实或 M09 消息。 #### 4.3.7 Hub 连接 @@ -7932,12 +8000,16 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 | 鉴权 | `BuyerOnly / MerchantOnly`(满足其中任一) | | 身份来源 | 服务端认证上下文中的用户 ID 和角色 | | 客户端订阅参数 | 无,不接受客户端传入任意 `userId`、角色或组名 | +| 传输 | 仅 WebSockets,客户端固定 `skipNegotiation = true` | | 多实例 | 使用 Redis Backplane | | 事实来源 | PostgreSQL 中的 M09 消息 | -浏览器在 WebSocket 握手限制下可通过 SignalR `accessTokenFactory` 传递令牌。服务端只允许在 `/hubs/messaging` 握手路径读取受控的 `access_token` Query,并必须在 Nginx、ASP.NET Core、Serilog 和 Trace 中完整脱敏;集成、演示和发布环境只使用 HTTPS/WSS。 +浏览器在 WebSocket 握手限制下可通过 SignalR `accessTokenFactory` 传递令牌。服务端只允许在 `/hubs/messaging` 握手路径读取受控的 `access_token` Query,并必须在 Nginx、ASP.NET Core、Serilog 和 Trace 中完整脱敏;集成、演示和发布环境只使用 HTTPS/WSS。Nginx 负责 WebSocket Upgrade;本期不启用 SSE、长轮询或会话亲和,WebSocket 不可用时只回退 A501/A503 HTTP 补查。 -客户端主动退出后关闭连接。非主动断线使用有限退避自动重连;初次连接和每次重连成功后调用 A503,并按需调用 A501 补查断线期间消息。 +- 非主动断线固定按“立即、2 秒、5 秒、10 秒”进行四次重连;四次均失败后暂停,只有浏览器恢复在线或用户手动重试才开始新一轮。 +- 初次连接和每次重连成功后都调用 A503 校正权威未读数,并按需调用 A501/A502 补查;服务端不承诺重放断线期间的实时事件。 +- 用户主动退出、JWT 到期、手机号修改、账号禁用或全部旧凭证失效时关闭既有连接;撤销或账号状态无法安全确认时同样关闭,不能维持“未知但放行”的连接。 +- 服务端断开时清理本实例连接状态;单实例内存连接表不作为跨实例唯一在线事实,目标用户的跨实例连接由 SignalR + Redis Backplane 协作触达。 #### 4.3.8 服务端事件 `MessageCreated` @@ -7975,13 +8047,37 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 规则: - 只有消息数据库事务成功提交后才能推送。 -- 推送失败不回滚业务事务或消息记录,也不把消息重新标记为未生成。 -- 客户端按 `messageId` 去重轻提示;不得仅凭推送载荷修改订单、支付或售后最终状态。 +- 推送前重新确认目标连接身份仍有效;账号已禁用、Token 已失效或认证事实无法安全确认时关闭连接且不推送。 +- 向目标用户全部有效在线连接发送;客户端按 `messageId` 去重轻提示后必须调用 A503 校正角标,不得执行“本地未读数 + 1”。 +- Redis Backplane 或发送失败时记录 `messageId`、实例标识和 `traceId`;不回滚业务事务或 M09 消息,也不把消息重新标记为未生成。 +- 重连后不重放历史 `MessageCreated`;由 A501/A503 补偿。客户端不得仅凭推送载荷修改订单、支付或售后最终状态。 - 本期不提供客户端调用的聊天、广播、已送达回执、任意加组或按用户订阅 Hub 方法。 -### 4.4 Worker 内部契约 +### 4.4 C07 缓存协作契约 + +> C07 是 A102 固定首页和 A103 商品自身公开详情的服务端实现能力,不新增 Axxx 或 DBxxx,也不改变接口字段、公开范围、状态码或交易规则。PostgreSQL 始终是商品、价格、库存和销售状态的事实来源。 + +#### 4.4.1 缓存范围与读取 + +- 只有 A102 的固定首页形态进入缓存:无筛选、仅 `OnSale`、按 `createdAt desc, productId desc` 取前 12 条;普通库存为 0 的商品仍返回并标记售罄。其他分类、关键词、普通列表和组合筛选全部直读 PostgreSQL。 +- A103 只缓存商品自身公开字段,不含评价、评分、收藏、购物车、商家管理字段或任何身份化数据;M07 变化不触发 C07。 +- 正常值 TTL 固定 60 秒;固定首页空结果和 A103 不存在/不可公开的短空值 TTL 固定 10 秒。 +- 同一 Key 只允许一个跨实例填充者。其他请求最多等待 500 毫秒后重读;仍未命中则直查 PostgreSQL 并返回,不继续争抢填充资格。 +- 取得填充资格后有效回填窗口最多 2 秒;超过窗口的查询结果仍可按接口返回,但不得再写入缓存。 +- Redis 不可用、缓存损坏或读写失败时回退 PostgreSQL,并记录命中、未命中、耗时、错误与降级指标;Redis 不保存交易事实。 -#### 4.4.1 C01 秒杀活动生命周期 +#### 4.4.2 提交后失效 + +- 所属业务事实成功提交后立即删除受影响 Key,并在提交后第 3 秒执行同一组二次删除;失效失败不回滚已经提交的商品、订单或售后事务。 +- 精确失效来源包括:商品名称、价格、普通库存、描述、分类展示或商品分类关系、图片新增/删除/排序/主图、上架、下架、删除、普通订单扣减、主动或超时取消回补、C01 发布划拨普通库存、M10 普通库存售后回补。 +- 新建草稿只清理同标识详情短空值;其未上架前不进入固定首页。分类展示或关系变化只失效受影响商品及确实受影响的固定首页。 +- C01 活动内部抢购、秒杀订单取消、M10 回补原活动库存,以及 M07 评价、评价图和评分变化均不触发 C07。 +- 提交前旧查询最迟可能在提交后第 2 秒回填;二次删除通常清除该旧值。两次删除均失败时,正常旧值兜底上限为提交后 62 秒,旧空值为提交后 12 秒。 +- F08 下单、取消和 M10 售后退款始终重读 PostgreSQL,不等待缓存一致,也不使用缓存结果作为价格、状态或库存条件。 + +### 4.5 Worker 内部契约 + +#### 4.5.1 C01 秒杀活动生命周期 > 本任务由 `Mall.Worker` 执行,不提供 HTTP API,也不占用 Axxx 编号。 @@ -7989,7 +8085,7 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 1. 周期扫描 `Published` 且 `startAt <= now()` 的活动,使用条件更新推进为 `Ongoing`。 2. 周期扫描 `Ongoing` 且 `endAt <= now()` 的活动,使用条件更新推进为 `Ended`。 -3. 每次状态成功变化后触发对应活动列表和详情缓存失效;PostgreSQL 仍是活动状态与库存事实来源。 +3. 活动列表、详情、状态和活动库存不进入 C07;状态推进只写 PostgreSQL,不触发活动缓存失效。 4. `Draft` 和 `Cancelled` 不由 Worker 自动推进;A223 取消与 Worker 竞争时,只有一个条件更新成功。 ##### 幂等与恢复 @@ -8002,51 +8098,55 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 - 已发布活动到达开始时间后可通过 A228 抢购;结束时间后 A228 返回活动已结束。 - 取消与自动开始并发时,最终只出现 `Cancelled` 或 `Ongoing` 中一个合法结果。 -- Worker 重启或多实例重复扫描不会重复推进、重复失效缓存或改写库存。 +- Worker 重启或多实例重复扫描不会重复推进或改写库存。 -#### 4.4.2 C03 订单超时自动取消 +#### 4.5.2 C03 订单超时自动取消 > **说明**:C03订单超时自动取消由Worker后台任务执行,不对外提供HTTP API。接口设计记录其与外部系统的交互关系。 ##### 业务规则 -1. **超时时间配置**:订单超时时间通过配置项 `OrderTimeoutMinutes` 管理,默认30分钟 -2. **扫描策略**:创建订单时按当时生效的配置固化 `expiresAt`;Worker 定时扫描 `PendingPayment` 且 `expiresAt <= now()` 的订单,后续配置变化不追溯改变既有订单 -3. **取消事务**:复用 A304 的内部取消用例,在同一受控事务内完成 `PendingPayment → Cancelled`、按普通/秒杀原通道回补库存并释放秒杀限购名额、写入 `cancelled_at` 和 `cancel_reason = 'TIMEOUT'` -4. **幂等保证**:使用条件更新 `WHERE status = 'PendingPayment'`,同一订单多次扫描只有一次成功 -5. **支付竞争**:与M05支付并发时,条件更新确保只有一个成功 +1. M04 创建每张订单时按当时生效且可追踪的配置固化 `paymentDeadline`;后续配置变化不追溯修改历史订单。 +2. Worker 只扫描 `PendingPayment` 且 `paymentDeadline <= 权威数据库时间` 的订单。达到截止时间后即使尚未扫描,M05 与 C08 也必须拒绝支付。 +3. Worker 通过受信任 Ordering 内部过期取消契约调用 M04,不复用买家 JWT 或把 A304 HTTP 当内部接口;系统原因固定为 `PaymentExpired`。 +4. 首次成功必须原子形成 `PendingPayment → Cancelled`、`cancelledAt`、`cancelReason=PaymentExpired`、普通/秒杀原通道库存回补、秒杀限购释放和买家取消通知可靠事实。 +5. 普通库存回补在提交后触发 C07 精确失效;秒杀原活动库存回补不触发 C07,活动已结束或取消也不把库存转回普通通道或重开活动。 +6. Worker、M05 到期触发和买家到期后取消复用同一结果;多次扫描、重启和消息重投都不得重复取消、回补或通知。 ##### 任务触发 -- `Mall.Worker` 周期扫描 `expiresAt <= now` 且仍为 `PendingPayment` 的订单;不依赖进程内定时器或消费“订单创建”事件保存唯一任务事实 +- `Mall.Worker` 按稳定顺序领取有界批次的到期待支付订单;批量大小、扫描间隔和单轮重试次数是后续可观测配置,不在业务契约中写死。 +- 多实例可重复发现候选,但最终由 M04 条件竞争保证一个取消完整结果;进程内集合或单机锁不作为唯一正确性保障。 +- 暂时失败时记录订单标识与 `traceId` 并按退避策略进入后续重试;订单保持“已过期但尚待取消”的 `PendingPayment`,仍不可支付。 +- Worker 重启后重新扫描 PostgreSQL 共享事实,不依赖内存定时器或未持久化队列保存唯一到期责任。 ##### 事件发布 -- 发布 `OrderCancelledIntegrationEvent`(`cancelReason = 'TIMEOUT'`)到 Outbox,供 M09 站内消息消费 +- 首次取消完整结果可靠形成 `OrderCancelledIntegrationEvent`(`cancelReason = PaymentExpired`),只由 M09 通知当前买家。 ##### 关键实现点 -1. 扫描间隔建议 ≤ 超时时间/2 -2. 每批次处理上限100条,避免长时间锁表 -3. 单轮扫描内同一订单最多尝试 3 次;每次失败先回滚订单事务,再独立持久化失败次数、原因和结果 -4. 失败重试锁定并重新校验同一订单 ID;3 次均失败时告警并加入本轮排除集合,订单保持 `PendingPayment`,退避后由后续扫描继续,Worker 重启后也能恢复 -5. 多实例 Worker 在同一单笔订单事务内使用 `SELECT ... FOR UPDATE SKIP LOCKED LIMIT 1` 完成领取和处理,行锁保持到取消事务提交或回滚 +1. 候选领取必须有界且稳定排序,可使用 `FOR UPDATE SKIP LOCKED` 或等价的数据库条件竞争,但不能让长事务覆盖整批业务。 +2. 每笔尝试重新读取订单状态和 `paymentDeadline`;已合法支付返回 `Paid` 并退出,已取消重放当前结果。 +3. 一个多项订单中任一原库存通道回补失败时,整笔取消不成立;不得留下部分库存已回补或部分限购已释放。 +4. 取消事务失败不发布成功事件;下一次只重试同一 M04 过期取消能力。 ##### 验证场景 1. 普通与秒杀超时订单均被自动取消并回补原库存通道 -2. 买家在超时前支付成功,取消被跳过 -3. 并发取消与支付只有一个成功 -4. Worker重启后继续扫描,不漏扫 +2. 截止前买家支付成功时取消被跳过;达到截止时间后所有支付通道立即拒绝 +3. 截止前支付与主动取消只有一个成功;截止后只允许过期取消执行或重试 +4. 秒杀活动结束或取消后仍回补原活动封闭库存,不增加普通库存或重新开放抢购 +5. Worker 重启和多实例重复扫描继续处理,不漏扫且不重复回补 -#### 4.4.3 M04-04 发货超时自动完成 +#### 4.5.3 M04-04 发货超时自动完成 > 本任务由 `Mall.Worker` 执行,不提供 HTTP API,也不占用 Axxx 编号;买家主动确认仍使用 A308。 ##### 业务规则 1. 周期扫描 `Shipped` 且 `shippedAt + 7 days <= now()` 的订单。 -2. 复用 A308 的完成订单用例,使用 `WHERE status = 'Shipped'` 条件更新为 `Completed`,并记录 `completedAt`、`completedBy = 'Auto'`。 +2. 复用 Ordering 的完成订单应用能力,使用 `WHERE status = 'Shipped'` 条件更新为 `Completed`,并记录 `completedAt`、`completedBy = AutoCompleted`;买家主动确认的来源固定为 `BuyerConfirmed`。 3. 成功后只写一次 `OrderCompletedIntegrationEvent` Outbox;与买家主动确认并发时仅一个条件更新成功,失败方读取并返回当前终态,不重复发布事件。 4. 订单事实和发货时间均来自 PostgreSQL;Worker 重启后继续扫描,不依赖进程内定时器保存唯一任务事实。 @@ -8057,22 +8157,50 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 3. 多实例 Worker 与 A308 并发时只产生一次完成状态和一条完成事件。 4. Worker 重启后继续扫描,不漏掉已到期订单。 -#### 4.4.4 C08 每日对账 +#### 4.5.4 C08 每日对账 -> 本任务由 `Mall.Worker` 执行,不提供 HTTP API,也不占用 Axxx 编号;A422~A425 只负责查询和处理已生成的对账事实。 +> 本任务由 `Mall.Worker` 执行,不提供 HTTP API,也不占用 Axxx 编号;A422~A426 只负责查询和闭环已生成的对账事实。 ##### 业务规则 -1. 按 UTC 自然日生成前一日对账批次,日期范围采用左闭右开;同一范围建立唯一约束,重复执行复用同一批次。 -2. 对比 Payment 的支付、退款、钱包流水与 Ordering 的订单支付状态,生成匹配数量和稳定差异记录;不得通过直接改库隐藏差异。 -3. 批次与差异写入 PostgreSQL;只有批次完整核对成功后才标记 `Matched` 或 `HasDifferences`。 -4. 任务失败保持可重试状态并记录 `traceId`;重试不会生成第二个矛盾批次或重复差异。 +1. 按业务结果的服务端成功提交时间生成上一完整 UTC 自然日批次,范围左闭右开,并固定一致读取 `watermarkAt`;不使用客户端时间、回调发生时间或接收时间归属。 +2. 同一 `businessDate + rangeFrom + rangeTo` 最多一个有效批次;`watermarkAt` 是该批次首次生成时冻结的内容而非唯一键维度。重复执行返回既有批次,任务中断时批次与全部差异必须同时不存在或同时完整。 +3. 支付比较覆盖 Wallet 与 SimulatedChannel 成功支付、Ordering `Paid`、四种回调终态及成功来源唯一性;退款比较覆盖 M10 `Refunded`、成功退款操作与买家钱包入账三方事实。 +4. 回调 `Difference` 是差异来源而非预建管理员条目;同一业务对象与同一比较规则被多个来源发现时只形成一个差异单元,并保留全部证据引用。 +5. 批次与全部差异在一个原子结果中生成;无差异直接为 `Matched`,有差异为 `HasDifferences`,不存在 `Pending` 批次。 +6. 本任务不自动修复订单、支付、退款或钱包,也不发送管理员站内消息;管理员通过 A422~A426 查询、领取、引用受控动作并复核闭环。 ##### 验证场景 -- 同一日期任务重复执行只得到一个批次。 -- “支付成功但订单未更新”、迟到成功回调、退款与流水不一致均生成可由 A424/A425 处理的差异。 -- Worker 中途失败后重试可完成原批次,已登记差异不重复。 +- 同一业务日期与固定范围即使稍后取得不同候选水位,重复执行也只返回首次完整批次。 +- “支付成功但订单未更新”、重复成功来源、迟到成功回调、退款与流水不一致均生成可由 A424~A426 查询和 A425 闭环的差异。 +- 回调来源与横向比对命中同一问题时只生成一个差异并保留全部证据。 +- Worker 中途失败后重试得到完整批次,不留下半批次或重复差异。 + +### 4.6 C10 运行协作契约 + +> 本节是部署与运行的非 HTTP 契约;A506/A507 只暴露存活、全局就绪和能力状态,不替代启动门禁、流量摘除、依赖降级或优雅停止。 + +#### 4.6.1 启动与迁移门禁 + +1. 前端、Nginx、两个 API、Worker 和一次性 Migrator 必须来自同一 Commit SHA 或版本 Tag;不以 `latest` 作为唯一可追溯版本。 +2. PostgreSQL 就绪后,只允许同版本一次性 Migrator 执行迁移;Migrator 成功退出且数据库达到目标 Migration 版本后,API/Worker 才能启动或进入就绪。 +3. API 与 Worker 不在启动时自动执行 Migration;迁移失败时阻止业务流量,不能由多个实例并发补跑。 +4. Nginx 只向 A507 全局 `Ready` 的实例分发新流量,负责业务请求头转发、WebSocket Upgrade,并对 `/hubs/messaging` 的 `access_token` 在全链路日志与 Trace 中脱敏。 + +#### 4.6.2 依赖降级与安全恢复 + +- Redis 故障时,C07 公开查询回退 PostgreSQL;C06 实时推送关闭;依赖撤销、账号禁用或旧凭证失效事实的受保护 HTTP/Hub 失败关闭。 +- Redis 恢复后,公开缓存可在基础健康时恢复;受保护能力必须等待有效期内安全事实从受控来源重建并通过安全健康检查,不能让旧 Token 在恢复窗口复活。 +- RabbitMQ 故障时来源业务事务和 PostgreSQL Outbox 继续按各模块契约提交,事件投递暂停;恢复后重投并由消费者 Inbox 防重。 +- SeaweedFS 故障时新上传和依赖对象写入的操作明确失败;其他能力按各自契约继续,不写入无效对象引用。 +- PostgreSQL 故障时实例全局 `NotReady`,全部数据库业务不得用 Redis 或内存伪造成功。 + +#### 4.6.3 优雅停止 + +- 单个 API 实例按“停止分发新流量 → API 有界排空 → 关闭本实例 Hub 连接 → 刷新日志与遥测 → 停止目标实例”执行;Worker 和共享依赖继续运行。 +- 整套环境停止时,Nginx 先停止新业务流量,API 有界排空并关闭 Hub;Worker 随后停止领取新任务,当前任务要么完成提交,要么安全释放并保留可重试事实。 +- 日常停止在应用排空和遥测刷新后再停止共享依赖,保留 PostgreSQL、Redis、RabbitMQ 与 SeaweedFS 数据卷;清空数据必须走独立、明确的破坏性步骤。 --- @@ -8084,19 +8212,19 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 | 负责人 | 追踪编号 | 有效 HTTP | 本次已闭合 | 仍需确认 | |---|---:|---:|---|---| -| 唐宇昊 | 25 | 25 | A024/A025、F03 权限、令牌撤销、账号状态幂等 | DB001~DB006、刷新令牌 Schema、OpenAPI | -| 顾欣月 | 22 | 22 | A144、图片先后顺序、公开评价字段与展示名快照、C04 索引边界 | DB021~DB025、OpenAPI | +| 唐宇昊 | 25 | 22 | A005/A009/A023 取消、A024/A025、身份边界、令牌撤销、账号状态幂等 | DB001~DB006、OpenAPI | +| 顾欣月 | 23 | 22 | A115 分类删除、A144 取消、图片原子边界、公开评价字段与用户名脱敏快照、C04 索引边界 | DB021~DB025、OpenAPI | | 朱惠惠 | 19 | 17 | A229/A230 取消、商家活动路径、库存/限购边界、生命周期 Worker | DB041~DB044、公开应用契约签名、OpenAPI | | 韦乾强 | 8 | 8 | A308、Ordering 命名、秒杀查询复用、取消与自动完成 Worker | DB061/DB062 完整字段、公开应用契约签名、OpenAPI | -| 张海洋 | 26 | 24 | A418/A431 取消、A434、退款契约、回调幂等与每日对账 Worker | DB081~DB091、退款事务编排、OpenAPI | +| 张海洋 | 27 | 23 | A418/A431/A432/A433 取消、A426/A434、唯一退款操作、回调四终态与差异闭环 | DB081~DB091、退款事务编排、OpenAPI | | 罗皓晨 | 7 | 7 | 买家/商家授权、事件映射、SignalR、健康检查 | DB101~DB120、来源模块评审、OpenAPI | -| **合计** | **107** | **103** | **4 个取消历史编号已隔离** | **尚不能宣称冻结或已实现** | +| **合计** | **109** | **99** | **10 个历史取消编号已隔离** | **尚不能宣称冻结或已实现** | ### 5.2 已确认的综合决策 -1. A024/A025、A144、A308、A434 已补齐,不再列为缺失接口。 +1. A024/A025、A115、A308、A426、A434 已按流程补齐;A005/A009/A023/A144 只保留历史取消编号。 2. A229/A230 取消,秒杀订单列表和详情由 A302/A303 承接。 -3. A418 取消,售后状态时间线由 A414 一次返回,不建设重复的审核日志接口。 +3. A418/A432/A433 取消,售后状态、审核时间线和同一退款操作摘要由 A414 一次返回。 4. A431 已取消并只保留历史追踪编号,退款通过不占 Axxx 的 Payment 公开应用契约完成。 5. A305~A307 归属 Ordering;商家身份只影响路径和 Policy,不新增 Merchant 业务模块。 6. F03 的 A006~A014 统一为 BuyerOnly;商家资料维护不在本期范围。 @@ -8108,10 +8236,10 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 ### 5.3 冻结前必须完成 1. 六名负责人完成个人数据库设计并汇总到《数据库设计》,逐项反查本文件中的 DBxxx、字段、约束、索引和事务边界。 -2. 将 103 个有效 HTTP 契约落成真实 OpenAPI,校验路径、方法、`operationId`、Schema 引用、状态码和安全方案均唯一有效。 +2. 将 99 个活动 HTTP 契约落成真实 OpenAPI,校验路径、方法、`operationId`、Schema 引用、状态码和安全方案均唯一有效。 3. 把 4.2 的模块间应用边界落实为公开 Contracts/Application 接口,不允许跨模块直接读写内部表。 4. Ordering、Payment、AfterSales 负责人确认 4.3 的事件字段、接收账号和触发时机;Messaging 完成 Inbox 去重与断线补偿设计。 -5. 为 4.4 的四类 Worker 固定扫描索引、批次大小、重试与多实例互斥策略,并登记对应 DBxxx。 +5. 为 4.5 的四类 Worker 按流程边界设计扫描索引、可观测配置、重试与多实例互斥策略,并登记对应 DBxxx。 6. 对订单、秒杀、支付、回调、退款、售后和全部已读并发场景建立契约测试或验收用例。 7. 每个负责人至少由一名其他成员完成交叉评审;确认后的接口才可把状态从“部分定义/待交叉评审”改为“已确认”。 -- Gitee From d2f646ad2f0fac26adafd2d84f7fc212f5934084 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Sat, 25 Jul 2026 00:54:50 +0800 Subject: [PATCH 109/118] =?UTF-8?q?docs(interface):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E5=85=A8=E9=93=BE=E8=B7=AF=E5=AE=A1=E8=AE=A1=E7=BC=BA=E5=8F=A3?= =?UTF-8?q?=EF=BC=9B=E7=BB=9F=E4=B8=80=E6=B5=81=E7=A8=8B=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 99 +-- ...34\347\264\242\346\265\201\347\250\213.md" | 15 +- ...06\345\223\201\346\265\201\347\250\213.md" | 47 +- ...41\347\220\206\346\265\201\347\250\213.md" | 70 +- ...04\344\273\267\346\265\201\347\250\213.md" | 10 +- ...50\351\200\201\346\265\201\347\250\213.md" | 10 +- ...23\345\255\230\346\265\201\347\250\213.md" | 10 +- ...57\347\224\250\346\265\201\347\250\213.md" | 4 +- ...10\346\201\257\346\265\201\347\250\213.md" | 8 +- ...50\345\206\214\346\265\201\347\250\213.md" | 4 +- ...00\345\207\272\346\265\201\347\250\213.md" | 8 +- ...60\345\235\200\346\265\201\347\250\213.md" | 20 +- ...41\347\220\206\346\265\201\347\250\213.md" | 8 +- ...06\345\217\262\346\265\201\347\250\213.md" | 16 +- ...05\346\227\266\346\265\201\347\250\213.md" | 12 +- ...42\345\215\225\346\265\201\347\250\213.md" | 47 +- ...45\347\272\246\346\265\201\347\250\213.md" | 10 +- ...22\346\235\200\346\265\201\347\250\213.md" | 57 +- ...51\350\275\246\346\265\201\347\250\213.md" | 10 +- ...71\350\264\246\346\265\201\347\250\213.md" | 16 +- ...57\344\273\230\346\265\201\347\250\213.md" | 26 +- ...56\345\220\216\346\265\201\347\250\213.md" | 26 +- ...01\347\250\213\350\256\276\350\256\241.md" | 60 +- ...45\345\217\243\350\256\276\350\256\241.md" | 683 ++++++++++++------ ...66\346\236\204\350\256\276\350\256\241.md" | 2 +- 25 files changed, 790 insertions(+), 488 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index e16521e..6b37c68 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -1,6 +1,6 @@ # 电子商城需求规格说明书 -> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.12 +> 班级与组别:24级1班第7组 统稿人:罗皓晨 共同编写:唐宇昊、顾欣月、朱惠惠、韦乾强、张海洋、罗皓晨 编写日期:2026-07-22 版本:v0.14 > > 文档状态:内容和格式已完成内部统一,待全组与指导教师评审后冻结 > 截止:第 1 周周三提交初稿 @@ -21,6 +21,8 @@ | v0.10 | 2026-07-24 | 罗皓晨 | 冻结 X03 事件接收人、整事件消息原子性和全部已读水位,并统一 C06 WebSocket、凭证失效、角标补查和固定重连边界 | | v0.11 | 2026-07-24 | 罗皓晨 | 冻结 C07 固定首页、详情缓存、TTL、跨实例填充、二次失效和普通/秒杀库存失效矩阵 | | v0.12 | 2026-07-24 | 罗皓晨 | 冻结 C10 一次性 Migrator、全局就绪与能力降级、Redis 安全恢复、WebSocket 落点和优雅停止边界 | +| v0.13 | 2026-07-24 | 罗皓晨 | 冻结分类存储状态与购物端有效状态、父子层级、顶级分类筛选范围及商品计数口径 | +| v0.14 | 2026-07-24 | 罗皓晨 | 冻结下单确定结果与瞬态失败的幂等边界,并统一订单状态筛选非法值处理 | ## 业务流程设计入口 @@ -499,7 +501,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | 编号 | 功能 | 详细要求 | |---|---|---| | M02-01-FR01 | 商品分页 | 返回商品摘要、当前页数据、总记录数和分页信息;翻页后条件保持不变 | -| M02-01-FR02 | 分类筛选 | 展示有效分类并支持按分类筛选;停用分类不再作为购物端筛选入口 | +| M02-01-FR02 | 分类筛选 | 展示有效分类并支持按分类筛选;有效分类要求自身及全部祖先均启用,停用父分类时整个子树退出入口但不改写子分类存储状态;选择顶级分类包含其直属及有效子分类商品,选择子分类只匹配该分类 | | M02-01-FR03 | 关键词搜索 | 对输入去除首尾空白并校验长度;F05 至少支持商品名称模糊匹配 | | M02-01-FR04 | 组合筛选 | 关键词、分类、价格区间和仅看有货可组合使用,条件之间按“同时满足”处理 | | M02-01-FR05 | 排序 | 支持经过白名单约束的排序项及方向,非法字段不得直接拼接为查询语句 | @@ -523,6 +525,8 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, - 商品销售状态只有草稿、已上架和已下架三种;满足删除条件并完成物理删除后,商品作为终止结果不再存在,“已删除”不是第四种销售状态。 - 已上架商品即使库存为零仍保留在公开列表、搜索与详情中,并明确标记“售罄”;仅购买、加购或结算动作因库存不足被拒绝。 - 停用分类只从购物端分类筛选入口移除,不自动下架或隐藏其下已上架商品;用户仍可从全部商品、关键词搜索、收藏或历史记录进入这些商品。 +- 分类最多一层父子关系。购物端“有效分类”要求自身为启用,且子分类的父分类也为启用;停用父分类时整个子树退出筛选入口,但子分类自身状态不被改写,父分类重新启用后,原本自身为启用的子分类随之恢复。 +- 顶级分类筛选范围为直接归属该顶级分类以及归属其有效子分类的已上架商品;子分类筛选只匹配直接归属该子分类的已上架商品。分类入口显示的商品数必须与同一筛选范围一致。 - 页码、每页数量、价格范围和排序项必须校验;价格下限大于上限时应明确提示。 - 空结果属于正常结果,返回空集合和正确分页信息,不以系统异常处理。 - 搜索输入必须参数化处理,不得拼接 SQL;响应不得包含商品内部备注或未公开状态。 @@ -659,7 +663,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | F07-FR04 | 删除条目 | 买家可单条或多条删除自己的购物车条目;条目不存在或属于他人时拒绝并返回资源不存在或无权限;幂等执行,多次删除同一 ID 结果一致。 | | F07-FR05 | 全选与单选 | 买家对可见且可用的条目进行全选、反选和单条切换;选中状态保存在服务端;刷新后状态保留;失效条目不允许被选中。 | | F07-FR06 | 服务端计价 | 选中条目总额由服务端按实时单价计算并返回;前端可本地显示,但结算和下单一律以服务端返回的金额为准,金额不接受客户端传入。 | -| F07-FR07 | 失效标记 | 商品下架、库存归零或被禁用时,购物车中对应条目标记为不可结算,保留可见、可删、可下调,但不允许调大、累加或选中进入结算;前端用明确文案展示失效原因。 | +| F07-FR07 | 失效标记 | 商品状态为 `Draft` / `OffSale` 或实时可售库存归零时,购物车中对应条目标记为不可结算,保留可见、可删、可下调,但不允许调大、累加或选中进入结算;分类停用不反向改变仍为 `OnSale` 商品的可结算资格;前端用明确文案展示失效原因。 | | F07-FR08 | 数量上限 | 加车和改数量必须实时校验库存;超过上限时拒绝并返回当前最大可设值;不依赖前端控制,避免被绕过。 | | F07-FR09 | 下单清理 | 提交订单成功后,订单模块在同一事务中删除该订单覆盖的全部购物车条目;若订单事务回滚,则购物车条目保留原状。 | | F07-FR10 | 清空购物车 | 买家可一键清空自己购物车中的全部条目;幂等且只影响本人;已失效条目一并清理。 | @@ -673,7 +677,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, 买家在商品详情或列表提交加车请求,服务端校验登录身份、商品上下架和实时库存,通过后将数量累加到同一商品条目并写库,返回最新条目和有效期。商品不可加时拒绝并返回原因。 -买家进入购物车页,服务端返回当前买家全部条目及实时单价;下架、库存归零或被禁用的条目标记为不可结算并附原因,仍可显示、修改数量或删除。买家调整数量、删除条目或切换选中状态,每次操作都即时写库;选中与未选中状态以服务端记录为准。 +买家进入购物车页,服务端返回当前买家全部条目及实时单价;商品为 `Draft` / `OffSale` 或实时可售库存归零时,条目标记为不可结算并附原因,仍可显示、下调数量或删除。分类停用不反向改变仍为 `OnSale` 商品的可结算资格。买家调整数量、删除条目或切换选中状态,每次操作都即时写库;选中与未选中状态以服务端记录为准。 买家点“去结算”,服务端再次校验选中条目的上下架、库存、数量上限和归属。通过则把选中条目和实时总价交给订单模块并进入提交订单流程;不通过则返回失败原因并把对应条目标记失效。提交订单成功后,订单模块在同一事务中删除选中条目;订单回滚则购物车原状保留。 @@ -684,7 +688,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, - 购物车主键为 `(买家ID, 商品ID)`,同一组合在同一购物车中只允许一条;重复加入按累加处理,禁止多行同时存在。 - 数量约束:每次加车和修改都必须满足 `1 ≤ 数量 ≤ 商品当前实时可售库存`;调小或删除不受上限约束,但不允许设为 0 或负数。 - 价格与计价:单价取商品当前上架价格,不在下单前生成最终快照;选中条目总额 = Σ(实时单价 × 当前数量),由服务端计算并返回。 -- 上下架与库存:商品下架后已加入条目仍可显示、可删、可下调,但禁止调大、累加或选中下单;库存归零或商品禁用同理。 +- 商品状态与库存:商品为 `Draft` / `OffSale` 后,已加入条目仍可显示、可删、可下调,但禁止调大、累加或选中下单;实时可售库存归零同理。分类停用不反向改变仍为 `OnSale` 商品的可结算资格。 - 结算准入:去结算前必须再次校验选中条目的上下架、库存和归属;任何一个条目不通过都拒绝整单并把对应条目标记失效。 - 下单清理:订单提交与购物车清理在同一事务;订单回滚则购物车条目原状保留。买家主动取消或订单超时取消(C03)后,本期不自动恢复购物车条目,避免与重新加入的状态混淆。 - 接口幂等:加车接受稳定的请求幂等标识;同一操作窗口内重复提交视为同一请求,结果相同且不重复累加。标识传递方式与有效窗口由 OpenAPI 和接口设计确定。 @@ -715,8 +719,8 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, - 同一买家同一商品多次加入只生成一条记录,数量按接口调用顺序正确累加,最终数量不超过实时库存上限。 - 修改数量超过商品实时可售库存时拒绝,返回最大可设值;改为 0、负数或非整数被拒绝并提示原因。 - 选中条目总额由服务端按实时单价计算;前端篡改金额或数量再提交被服务端拒绝,未出现订单总额与数据库计算结果不一致的情况。 -- 商品下架后,已加入条目在购物车页被标记“不可结算”,不可调大、不可累加、不能被勾选进入结算;库存为 0 或被禁用时同样标记。 -- 失效条目可下调数量、可删除;下调到合法值后条目恢复正常可结算状态。 +- 商品为 `Draft` / `OffSale` 后,已加入条目在购物车页被标记“不可结算”,不可调大、不可累加、不能被勾选进入结算;实时可售库存为 0 时同样标记;分类停用不反向改变仍为 `OnSale` 商品的可结算资格。 +- 失效条目可下调数量、可删除;只有商品重新处于 `OnSale` 且下调后的数量不超过实时可售库存时,条目才恢复正常可结算状态。 - 越权:用买家 B 身份请求买家 A 的条目被拒绝,响应不暴露该条目是否存在及归属信息。 - 下单成功后,对应购物车条目在同一事务内被清除;订单事务回滚时购物车条目原状保留,未出现“订单失败但条目丢失”的情况。 - 买家主动取消订单或 C03 自动取消后,对应购物车条目本期不自动恢复,符合本期范围说明。 @@ -759,25 +763,26 @@ M04-01 要求买家选择购物车中的商品和收货地址完成订单提交 | M04-01-FR04 | 订单创建 | 事务扣减库存成功后创建唯一订单事实,保存订单号、买家 ID、地址快照、`assignedMerchantUserId`、`PendingPayment` 状态、服务端计算总额、支付截止时间、创建时间和幂等键;客户端不得指定处理商家或订单金额。 | | M04-01-FR05 | 订单项快照 | 为每个订单项创建订单项记录(`order_items`):保存商品 ID、商品名称(快照)、商品主图 URL(快照)、成交单价(快照,下单时服务端的实时价格)、购买数量。订单项单价以创建订单时的服务端实时价格为准,不受后续商品改价影响。 | | M04-01-FR06 | 购物车清理 | 在订单事务内清理本次已下单的购物车条目;清理失败时整笔订单事务回滚,不产生订单、不扣减库存,也不丢失购物车条目。 | -| M04-01-FR07 | 幂等键设计 | 买家客户端生成唯一幂等键(UUID),随下单请求一同发送;服务端以 `(user_id, idempotency_key)` 为唯一约束,同一买家同一幂等键只创建一张订单;重复提交返回首次成功创建的订单号,不重复扣库存、不重复创建订单项。 | +| M04-01-FR07 | 幂等键设计 | 买家客户端生成唯一幂等键(UUID),随下单请求一同发送;服务端以 `(user_id, idempotency_key)` 为唯一范围并绑定地址与购物车条目指纹。同键同内容重放首次确定结果:成功时返回同一订单,库存不足、商品不可售、总额不合法或已确定的默认商家配置不可用时重放同一拒绝;同键换内容拒绝。依赖中断、数据库连接失败或事务结果未知等未形成确定裁决的失败不得固化,可用同一键安全重试。 | | M04-01-FR08 | 价格服务端计算 | 订单总额和订单项实付单价均由服务端计算,不接受前端传入;商品最新价格从 `products.price` 实时读取;订单项保存快照后,商品后续改价不影响已有订单。 | | M04-01-FR09 | 事件通知 | 订单创建成功后,通过 Outbox 发布 `OrderCreatedIntegrationEvent`,只将当前买家列为接收账号,供 M09 站内消息消费;商家待处理提醒统一由支付成功事件触发,避免重复通知。C03 直接扫描 PostgreSQL 的支付截止时间,不消费该事件保存唯一调度事实。 | #### 4. 主流程 1. 买家在购物车页面选择要结算的商品,点击"提交订单"并选择收货地址,客户端生成幂等键并发送请求。 -2. 服务端校验买家身份(JWT + `role=buyer`),解析买家 ID。 -3. 校验所有选中商品是否属于当前买家购物车、是否可售、库存是否充足、数量是否合法。 -4. 校验收货地址是否属于当前买家且状态正常。 -5. 开启数据库事务: +2. 服务端校验买家身份、幂等键格式和固定请求结构,并以地址与购物车条目形成规范请求指纹。 +3. 服务端先查询该买家与幂等键:同键同内容已有确定结果时原样重放成功或拒绝;同键换内容时拒绝复用;没有确定结果时继续读取可变事实。 +4. 校验所有选中商品是否属于当前买家购物车、是否可售、库存是否充足、数量是否合法。 +5. 校验收货地址是否属于当前买家且状态正常,并解析唯一且启用的默认商家。 +6. 开启数据库事务: a. 按商品维度依次执行条件更新扣减库存(`WHERE stock >= quantity`),任一失败则整体回滚。 b. 解析并保存处理商家账号,创建状态为 `PendingPayment` 的订单主记录。 c. 创建每个订单项记录,保存商品信息快照和成交单价。 d. 删除已下单的购物车条目。 e. 写入 Outbox `OrderCreatedIntegrationEvent` 事件。 提交事务。 -6. 事务提交成功后,返回订单号给客户端。 -7. 客户端跳转到支付页面或收银台,等待买家操作。 +7. 事务提交成功后,返回订单号给客户端。 +8. 客户端跳转到支付页面或收银台,等待买家操作。 #### 5. 业务规则与权限 @@ -785,7 +790,7 @@ M04-01 要求买家选择购物车中的商品和收货地址完成订单提交 - 订单总额 = Σ(订单项成交单价 × 订单项数量),由服务端计算,前端不可篡改。 - 每个订单项的单价以**下单时刻**的服务端实时价格为准,与购物车中的价格或前端传入价格无关。 - 库存扣减使用条件更新而非先查后改,避免并发超卖。 -- 幂等键 `(user_id, idempotency_key)` 建立唯一约束,重复请求返回原订单号而非报错。 +- 幂等键 `(user_id, idempotency_key)` 绑定规范请求指纹;确定成功与确定业务拒绝均稳定重放,换内容复用返回冲突。只有形成明确裁决的结果可以固化,依赖中断、连接失败和事务结果未知不得写成可重放结果。 - 购物车清理与订单创建处于同一事务;清理失败时整笔事务回滚,购物车条目保持原状,买家可根据提示重新提交。 - 地址快照保存下单时刻的完整地址文本,后续地址修改不影响已有订单。 - 商品名称/图片快照保存下单时刻的完整信息,后续商品信息修改不影响已有订单。 @@ -802,10 +807,11 @@ M04-01 要求买家选择购物车中的商品和收货地址完成订单提交 | 商品数量非法(≤0 或超过库存) | 整单失败,返回参数错误 | 修改数量后重新提交 | | 收货地址不存在或不归属当前买家 | 整单失败,返回"收货地址无效" | 重新选择地址 | | 订单总额计算后为 0 或负数 | 整单失败,返回"订单金额异常" | 联系客服或重新下单 | -| 同一幂等键重复提交 | 返回首次成功的订单号,不重复扣库存 | 无感知,得到订单号 | +| 同一幂等键同内容重复提交 | 原样重放首次确定的成功订单或业务拒绝,不重复扣库存 | 获得稳定结果,不出现先失败后又意外建单 | +| 同一幂等键换地址或购物车条目 | 拒绝复用该幂等键 | 生成新幂等键后重新提交 | | 并发扣减库存竞争 | 条件更新失败的事务回滚,返回库存不足错误 | 重试或减少数量 | -| 数据库连接失败 | 返回服务异常,附 `traceId`,不暴露内部细节 | 稍后重试 | -| 默认商家未配置、重复或不可用 | 整单失败并提示服务暂不可用,不扣库存、不清理购物车 | 稍后重试并由管理员修复配置 | +| 数据库连接失败、依赖中断或事务结果未知 | 返回服务异常,附 `traceId`,不固化为确定幂等结果 | 使用同一幂等键安全重试 | +| 已成功读取默认商家配置且确认未配置、重复或禁用 | 形成确定拒绝,不扣库存、不清理购物车;同键同内容稳定重放 | 管理员修复配置后由买家发起新提交并使用新幂等键 | | Outbox 记录写入失败 | 与订单、库存和购物车变更一起回滚,当前请求失败且可安全重试 | 提示稍后重试,购物车和库存保持原状 | | Outbox 已写入但 RabbitMQ 发布失败 | 订单保持成功,由 Worker 重试发布,不回滚业务事务 | 正常进入支付,消息稍后补发 | @@ -821,6 +827,8 @@ M04-01 要求买家选择购物车中的商品和收货地址完成订单提交 | 4 | 故意选择库存为 0 的商品提交订单 | 整单失败,返回"库存不足";购物车商品未被删除,库存未扣减。 | | 5 | 故意选择不属于自己的收货地址提交订单 | 整单失败,返回"收货地址无效";不暴露地址是否存在。 | | 6 | 使用同一幂等键重复提交相同订单 | 第二次返回首次成功的订单号,不重复扣库存,订单项不重复。 | +| 6A | 使用同一幂等键重放已确定的库存不足,再将同一键换用其他地址或条目 | 同内容仍返回同一库存拒绝;换内容返回幂等键复用冲突,不创建订单。 | +| 6B | 模拟依赖中断或事务结果未知后使用同一幂等键重试 | 未确定失败没有被固化;恢复后可继续获得唯一确定结果,且不重复扣库存。 | | 7 | 两台设备同时对同一商品提交订单(库存为 1) | 只有一台设备成功创建订单,另一台返回库存不足;最终只有一笔有效订单。 | | 8 | 商品在下单前被商家下架,同时提交订单 | 整单失败,返回"部分商品已下架";库存未扣减。 | | 9 | 检查数据库 `orders` 表 | 订单号唯一、状态为 `PendingPayment`、金额由服务端计算、地址和商品信息为快照。 | @@ -836,7 +844,7 @@ M04-01 要求买家选择购物车中的商品和收货地址完成订单提交 - 保存订单总额计算过程截图:订单项单价 × 数量求和与订单总额一致。 - 保存购物车在下单前后的对比截图,证明已下单商品已从购物车移除。 - 保存整单失败的异常场景录屏(库存不足、地址无效、已下架),包含错误提示和库存/购物车未被影响。 -- 保存幂等键重复提交测试:两次请求使用同一幂等键,第二次返回相同订单号,库存只扣一次。 +- 保存幂等键重复提交测试:确定成功和确定业务拒绝均可同内容稳定重放,换内容复用被拒绝;瞬态依赖失败不固化,恢复后使用同一键仍只形成一个确定结果。 - 保存并发抢库存测试:两台设备同时提交,只有一次成功,数据库只有一笔有效订单。 - 保存订单、订单项、商品库存和购物车清理前后的数据证据。 - 能解释为何选择条件更新而非先查后改扣减库存,以及并发下的正确性保障。 @@ -916,7 +924,7 @@ M04-02 要求买家分页查看自己的订单列表,并按订单状态筛选 | 场景 | 处理方式 | 买家体验 | |---|---|---| | 订单列表查询成功但无订单 | 返回空列表,附带"暂无订单"提示文案 | 引导去购物车添加商品 | -| 订单状态筛选参数非法 | 使用边界值或默认返回全部,不报错 | 正常显示筛选结果 | +| 订单状态筛选参数非法 | 拒绝请求并返回可定位到 `status` 的字段错误,不把非法值静默当作“全部” | 保留当前筛选并提示重新选择 | | 分页参数超出范围(page ≤ 0 或 > 总页数) | 返回空列表或最后一页数据,不报错 | 正常翻页 | | 查询他人订单详情(猜测订单号) | 返回 403 或 404,不泄露订单是否存在 | 无感知 | | 买家跨身份访问(商家/管理员身份调用) | 返回 403,不返回任何订单数据 | 无感知 | @@ -1249,8 +1257,8 @@ M04 提供订单金额、状态和支付截止时间,M09 接收支付结果通 | 编号 | 功能 | 详细要求 | |---|---|---| -| M06-01-FR01 | 分类查询 | 展示分类名称、层级、排序和启停状态,供商品编辑和购物端筛选使用 | -| M06-01-FR02 | 分类维护 | 商家可新增、编辑、启用或停用分类;名称、父级关系和状态必须校验,编辑分类元数据不受商品引用关系阻断 | +| M06-01-FR01 | 分类查询 | 展示分类名称、层级、排序、存储启停状态和购物端有效状态,供商品编辑和购物端筛选使用 | +| M06-01-FR02 | 分类维护 | 商家可新增、编辑、启用或停用分类;名称、最多一层父级关系和状态必须校验;子分类只有自身及父分类均启用时才在购物端有效。已有直属子分类的顶级分类不得直接改为其他分类的子分类,系统不隐式级联改绑;编辑其他分类元数据不受商品引用关系阻断 | | M06-01-FR03 | 分类保护 | 只有物理删除需要检查商品或历史引用;存在引用时拒绝删除,可采用停用方式退出购物端分类筛选入口 | | M06-01-FR04 | 商品列表 | 支持按关键词、分类、上下架状态分页查询,清楚展示价格、库存和状态 | | M06-01-FR05 | 新建商品 | 录入名称、分类、价格、库存、主图/图片和描述,校验通过后保存 | @@ -1273,10 +1281,12 @@ M04 提供订单金额、状态和支付截止时间,M09 接收支付结果通 #### 5. 业务规则与权限 -- 商品名称、合法的分类关联、当前价格、库存和主图为必填;价格不得为负,库存必须为非负整数。新建、切换分类或上架时分类必须已启用,既有商品在原分类后来停用时仍可修改不改变分类归属的字段。 +- 商品名称、合法的分类关联、当前价格、库存和主图为必填;价格不得为负,库存必须为非负整数。新建、切换分类或上架时分类必须在购物端有效(自身及父分类均启用),既有商品在原分类或其父分类后来停用时仍可修改不改变分类归属的字段。 - 商品销售状态只有草稿、已上架和已下架三种;物理删除是实体不存在的终止结果,不得保存为“已删除”状态。 -- 上架前必须满足完整性校验;新建商品不得绑定已停用分类,已有商品切换分类时不得选择已停用分类。 -- 分类停用后,已有已上架商品继续公开展示;它们可以继续修改不改变分类归属的字段,但一旦下架,必须先迁移到启用分类或重新启用原分类才能再次上架。 +- 上架前必须满足完整性校验;新建商品不得绑定购物端无效分类,已有商品切换分类时不得选择无效分类。 +- 分类自身或其父分类停用后,已有已上架商品继续公开展示;它们可以继续修改不改变分类归属的字段,但一旦下架,必须先迁移到有效分类,或使原分类及其父分类均启用后才能再次上架。 +- 停用顶级分类不批量修改子分类的存储状态,但整个子树立即退出购物端分类筛选;重新启用顶级分类后,只有自身仍为启用的子分类恢复为有效入口。启用一个父分类仍停用的子分类只保存其启用状态,不提前公开。 +- 顶级分类已有直属子分类时,不得把该顶级分类直接改绑为另一顶级分类的子分类,否则会产生超过一层的层级;必须先逐个迁移或删除直属子分类,系统不得隐式级联修改它们。 - 已上架商品库存降为零时保持已上架并在购物端标记售罄,不自动下架。 - 本期为单店 B2C 统一经营目录;所有正常商家账号维护同一套分类和商品,不按商家账号隔离商品所有权。 - 下架不删除购物车记录、收藏记录、浏览记录或历史订单快照,由对应模块显示不可售状态。 @@ -1291,8 +1301,10 @@ M04 提供订单金额、状态和支付截止时间,M09 接收支付结果通 |---|---| | 字段、价格、库存或分类非法 | 拒绝保存并返回字段级错误,前端保留用户已填写内容 | | 编辑仍有商品引用的分类名称、层级或排序 | 允许在校验通过后保存;引用关系只阻止物理删除 | +| 将仍有直属子分类的顶级分类改为子分类 | 拒绝保存并提示先处理直属子分类;不隐式级联改绑,不产生两层子级 | | 停用仍有已上架商品的分类 | 分类从购物端筛选入口移除,已有已上架商品继续公开且不自动下架 | -| 已停用分类下商品尝试重新上架 | 拒绝上架,提示迁移到启用分类或先重新启用原分类 | +| 停用父分类但子分类仍为启用 | 父分类及整个子树退出购物端筛选,子分类存储状态保持不变;父分类恢复后按子分类自身状态重新生效 | +| 购物端无效分类下商品尝试重新上架 | 拒绝上架,提示迁移到有效分类,或使原分类及其父分类均启用 | | 图片上传失败 | 明确标记失败图片并允许重试,不清空其他表单字段 | | 两名操作人并发编辑 | 后提交者收到冲突提示,不静默覆盖已生效修改 | | 删除存在订单、购物车、收藏、浏览、评价、秒杀等任何历史关联的商品 | 拒绝物理删除,提示改为下架并永久保留商品事实 | @@ -1307,6 +1319,7 @@ M04 提供订单金额、状态和支付截止时间,M09 接收支付结果通 - 商品必填项、价格、库存、分类和图片校验在前后端均生效。 - 下架商品不会出现在购物端列表和搜索结果,旧链接不再允许购买;历史订单快照保持可读。 - 停用分类不会自动隐藏其下已上架商品;库存为零的已上架商品保持可见并显示售罄。 +- 已有直属子分类的顶级分类不能直接变成子分类,分类树始终最多一层且不会发生隐式级联改绑。 - 商家账号共同维护单店统一经营目录,不按当前操作人过滤商品归属。 - 游客、买家和管理员无法进入或调用商家写操作,后端返回正确的认证或授权结果。 - 并发编辑、图片失败、保存失败和删除受限时均有明确反馈,已填写内容不会无故丢失。 @@ -2549,32 +2562,32 @@ Electron 后续原则上复用 PC Web 原型,只补充窗口、桌面导航和 | 教师编号 | 模块与负责人 | 页面或操作入口 | 接口契约(Axxx) | 测试用例 | 当前状态 | |---|---|---|---|---|---| -| F01 | M01-01 用户注册—唐宇昊 | PC Web 注册页 | A001 | 待测试计划登记 | 注册后显式登录、角色拒绝和原子账号创建已统一,待接口同步 | -| F02 | M01-02 登录与退出—唐宇昊 | PC Web 统一登录页、退出入口 | A002~A004;A005 为历史取消编号 | 待测试计划登记 | 单一 JWT、退出和全部旧凭证失效已统一,待接口同步 | -| F03 | M01-03 个人信息与地址—唐宇昊 | PC Web 买家个人中心、地址管理 | A006~A008、A010~A014;A009 为历史取消编号 | 待测试计划登记 | 资料字段、并发修改和独立默认地址切换已统一,待接口同步 | -| F04 | M02-01 商品列表与分类—顾欣月 | 购物端商品列表、分类入口 | A101、A102 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | -| F05 | M02-01 商品搜索—顾欣月 | 购物端搜索框、商品列表 | A102 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | -| F06 | M02-02 商品详情—顾欣月 | 商品详情页 | A103 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | -| F07 | M03-01 购物车—朱惠惠 | 商品加购入口、购物车页 | A201~A208 | 待测试计划登记 | 接口草案已汇总,待交叉评审 | +| F01 | M01-01 用户注册—唐宇昊 | PC Web 注册页 | A001 | 待测试计划登记 | 注册后显式登录、角色拒绝和原子账号创建已统一;接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| F02 | M01-02 登录与退出—唐宇昊 | PC Web 统一登录页、退出入口 | A002~A004;A005 为历史取消编号 | 待测试计划登记 | 单一 JWT、退出和全部旧凭证失效已统一;接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| F03 | M01-03 个人信息与地址—唐宇昊 | PC Web 买家个人中心、地址管理 | A006~A008、A010~A014;A009 为历史取消编号 | 待测试计划登记 | 资料字段、并发修改和独立默认地址切换已统一;接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| F04 | M02-01 商品列表与分类—顾欣月 | 购物端商品列表、分类入口 | A101、A102 | 待测试计划登记 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| F05 | M02-01 商品搜索—顾欣月 | 购物端搜索框、商品列表 | A102 | 待测试计划登记 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| F06 | M02-02 商品详情—顾欣月 | 商品详情页 | A103 | 待测试计划登记 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| F07 | M03-01 购物车—朱惠惠 | 商品加购入口、购物车页 | A201~A208 | 待测试计划登记 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | | F08 | M04-01 提交订单—韦乾强 | 购物车结算、提交订单页 | A301 | 待测试计划登记 | 幂等、超时和商家归属已统一,待数据库、OpenAPI 与交叉评审 | -| F09 | M04-02/M04-03/M04-04 订单查询、取消与完成—韦乾强 | 买家订单列表、订单详情、确认收货 | A302~A304、A308 | 待测试计划登记 | 接口草案已闭合,待数据库、OpenAPI 与交叉评审 | +| F09 | M04-02/M04-03/M04-04 订单查询、取消与完成—韦乾强 | 买家订单列表、订单详情、确认收货 | A302~A304、A308 | 待测试计划登记 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | | F10 | M05-01 模拟支付—张海洋 | 小金库、统一收银台、支付记录 | A401~A408 | 待测试计划登记 | 重复支付与幂等语义已统一,待数据库、OpenAPI 与交叉评审 | | F11 | M06-01 后台商品管理—顾欣月 | 商家分类管理、商品管理 | A110~A115、A120~A128 | 待测试计划登记 | 创建、受约束分类删除、图片顺序与同步索引边界已统一,待数据库、OpenAPI 与交叉评审 | | F12 | M06-02 后台订单管理—韦乾强 | 商家订单列表、详情和发货入口 | A305~A307 | 待测试计划登记 | Ordering 命名、状态与商家归属已统一,待数据库、OpenAPI 与交叉评审 | -| F13 | M06-03 后台用户管理—唐宇昊 | 管理端用户管理 | A015~A017 | 待测试计划登记 | 买家/商家分流、固定责任清单、禁用竞争和全部旧凭证失效已统一,待接口同步 | +| F13 | M06-03 后台用户管理—唐宇昊 | 管理端用户管理 | A015~A017 | 待测试计划登记 | 买家/商家分流、固定责任清单、禁用竞争和全部旧凭证失效已统一;接口已按流程重建,待数据库、OpenAPI 与交叉评审 | | X01 | M07 商品评价与晒图—顾欣月 | 订单评价入口、商品评价区 | A140~A143;A144 为历史取消编号 | 待测试计划登记 | 订单入口、提交重检、唯一评价、公开计分和图片边界已统一,待数据库、OpenAPI 与交叉评审 | -| X02 | M08 收藏与浏览历史—唐宇昊 | 商品收藏、收藏页、浏览历史页 | A018~A022、A024、A025;A023 为历史取消编号 | 待测试计划登记 | 收藏幂等、默认开启、最近 200 条及关闭后旧历史可见已统一,待接口同步 | -| X03 | M09 站内消息—罗皓晨 | 买家/商家消息中心 | A501~A505 | 待测试计划登记 | 固定接收人、整事件原子性和稳定已读水位已冻结,待数据库、接口与实现承接 | +| X02 | M08 收藏与浏览历史—唐宇昊 | 商品收藏、收藏页、浏览历史页 | A018~A022、A024、A025;A023 为历史取消编号 | 待测试计划登记 | 收藏幂等、默认开启、最近 200 条及关闭后旧历史可见已统一;接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| X03 | M09 站内消息—罗皓晨 | 买家/商家消息中心 | A501~A505 | 待测试计划登记 | 固定接收人、整事件原子性和稳定已读水位已冻结;接口已按流程重建,待数据库、实现与交叉评审 | | X04 | M10 售后流程—张海洋 | 买家售后申请、商家售后审核 | A411~A417、A419、A434;退款入账使用 Payment 应用契约 | 待测试计划登记 | A418/A431/A432/A433 为取消历史号,待数据库、OpenAPI 与交叉评审 | -| C01 | 秒杀与防超卖—朱惠惠 | 秒杀活动页、商家活动管理、压测脚本 | A220~A228;订单查询复用 A302/A303;生命周期为 Worker 内部契约 | 待挑战测试登记 | 原子限购、库存通道与状态推进已定义,待数据库、OpenAPI 与跨模块联调 | +| C01 | 秒杀与防超卖—朱惠惠 | 秒杀活动页、商家活动管理、压测脚本 | A220~A228;A229/A230 为历史取消编号;订单查询复用 A302/A303;生命周期为 Worker 内部契约 | 待挑战测试登记 | 原子限购、库存通道与状态推进已定义,待数据库、OpenAPI 与跨模块联调 | | C03 | 订单超时取消—韦乾强 | 订单倒计时、Worker 验收脚本 | 复用 A304;超时扫描为 Worker 内部契约 | 待挑战测试登记 | Worker 扫描、取消复用和原库存通道回补已定义,待数据库与测试评审 | -| C04 | 商品搜索进阶—顾欣月 | 商品搜索页、性能对比报告 | A102 | 待挑战测试登记 | 接口草案已汇总,待交叉评审 | -| C06 | 实时消息推送—罗皓晨 | 实时通知、连接状态、消息中心 | SignalR 契约(无 Axxx),补查复用 A501~A505 | 待挑战测试登记 | WebSockets 跳过协商、固定重连、凭证失效与权威角标补查已冻结,待接口、部署与测试承接 | -| C07 | 缓存与性能优化—罗皓晨,顾欣月协作 | 首页、商品详情、压测报告 | 复用 A102 的固定首页摘要调用、A103 商品详情;不缓存普通列表与搜索 | 待挑战测试登记 | 60/10 秒 TTL、500 毫秒跨实例填充等待、提交后立即/3 秒双删和 62 秒兜底已冻结,待接口、实现与压测承接 | +| C04 | 商品搜索进阶—顾欣月 | 商品搜索页、性能对比报告 | A102 | 待挑战测试登记 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| C06 | 实时消息推送—罗皓晨 | 实时通知、连接状态、消息中心 | SignalR 契约(无 Axxx),补查复用 A501~A505 | 待挑战测试登记 | WebSockets、固定重连、失败关闭与权威补查契约已按流程重建,待部署、实现与测试承接 | +| C07 | 缓存与性能优化—罗皓晨,顾欣月协作 | 首页、商品详情、压测报告 | 复用 A102 的固定首页摘要调用、A103 商品详情;不缓存普通列表与搜索 | 待挑战测试登记 | 缓存范围、60/10 秒 TTL、单填充和双删契约已按流程重建,待实现与压测承接 | | C08 | 支付回调幂等与对账—张海洋 | 回调重放脚本、管理端对账 | A421~A426;退款入账使用 Payment 应用契约 | 待挑战测试登记 | A431/A432/A433 为取消历史号,待数据库、OpenAPI 与交叉评审 | -| C10 | 容器化与负载均衡—罗皓晨 | 统一访问入口、健康与实例验证 | A506、A507 | 待部署验收登记 | Migrator、全局就绪/能力降级、Redis 安全恢复、WebSocket 与优雅停止已冻结,待接口、部署和验收承接 | +| C10 | 容器化与负载均衡—罗皓晨 | 统一访问入口、健康与实例验证 | A506、A507 | 待部署验收登记 | Migrator、全局就绪/能力降级、Redis 安全恢复、WebSocket 与优雅停止契约已按流程重建,待部署和验收承接 | -负责人补齐缺少接口并完成交叉评审后,应把对应状态更新为“已确认”;生成真实 OpenAPI 后再补充 `operationId` 校验结果。测试计划完成后,应替换为真实测试用例编号,并随需求变更同步维护状态。 +99 个活动 HTTP 及必要的非 HTTP 协作契约均已按流程重建。负责人完成数据库反查、真实 OpenAPI 和交叉评审后,才可把对应状态更新为“已确认”;测试计划完成后,应替换为真实测试用例编号,并随需求变更同步维护状态。 ### 9.2 已确认范围 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" index a50447e..ded89c8 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" @@ -20,7 +20,7 @@ C04 在 M02-01 的基础搜索入口上增加中文分词模糊搜索、多条 |---|---|---| | C04 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 完整定义、待交叉评审 | 已确认角色、分支、降级和模块出入口 | -| 搜索适配器接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| 搜索适配器接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | | DBxxx 索引维护 | 部分定义 | 本文不发明表字段、索引名和分词参数 | | 性能对比原始结果 | 缺失 | 本文登记对比场景与口径,不预填压测结论 | @@ -65,7 +65,7 @@ flowchart TD E -- "是" --> F["分词并按倒排索引召回候选商品"] E -- "否" --> G["记录内部降级原因
执行安全的基础模糊查询"] F --> H["按相关度形成候选顺序"] - D --> I["统一强制已上架过滤
应用启用分类、价格区间和仅看有货条件"] + D --> I["统一强制已上架过滤
应用购物端有效分类、价格区间和仅看有货条件"] G --> I H --> I I --> J["执行白名单排序及稳定次级排序"] @@ -80,7 +80,7 @@ flowchart TD - 中文分词对商品名称、分类名称和商品描述进行字符 N-gram 等效分词,支持中文多词查询、部分匹配和可解释的模糊召回。 - 有关键词时优先使用进阶中文搜索;只有进阶能力不可用时才回退基础模糊查询。无关键词时不执行分词,直接进入统一筛选。 - 无论进阶、基础降级还是无关键词路径,都必须强制已上架过滤,并应用分类、价格区间、仅看有货、白名单排序和分页,任何路径都不能跳过。 -- 分类筛选只接受启用分类;停用分类下的已有已上架商品仍可在未指定分类和关键词搜索中返回。 +- 分类筛选只接受购物端有效分类(自身及父级均启用);顶级分类筛选包含直接归属自身及其有效直属子分类的商品,子分类筛选只匹配直接归属自身的商品。分类失效不影响已有已上架商品在未指定分类和关键词搜索中返回。 - 排序至少支持相关度及经过白名单约束的价格或时间排序;相关度相同时按上架时间和商品 ID 稳定排序,价格或时间相同时按商品 ID 稳定排序。 - 结果一致性:搜索结果最终以商品当前状态为准,索引中的旧数据不得让下架商品重新公开。 @@ -202,7 +202,7 @@ flowchart TD | 高并发下响应变慢 | 通过压测记录瓶颈和资源参数 | 不以缓存掩盖错误结果 | | 商家修改商品、分类名称或销售状态 | 检索文本、销售状态与数据库索引随同一事实提交 | 下一次搜索读取同一已提交结果 | | 商家物理删除商品 | 商品事实与数据库索引同步移除 | 不再返回已不存在商品 | -| 所选分类已停用 | 拒绝该分类筛选条件并提供返回全部商品入口 | 该分类下已上架商品仍可在未指定分类或关键词搜索中命中 | +| 所选分类自身或父级已停用 | 拒绝该分类筛选条件并提供返回全部商品入口 | 受影响子树中的已上架商品仍可在未指定分类或关键词搜索中命中 | | 索引缺失且基础查询可用 | 记录内部原因并回退基础模糊查询 | 仍统一执行已上架过滤、筛选、稳定排序与分页 | ## 九、由流程派生的接口契约映射 @@ -227,15 +227,16 @@ flowchart TD - C07:A102 的普通列表、关键词搜索和组合筛选不进入缓存;C04 只读取 PostgreSQL 商品事实。 - M04:下单时由 M04 重读商品事实进行条件扣减,不信任搜索结果中的价格或库存。 -## 十一、由流程反查出的接口与数据待评审项 +## 十一、接口承接结果与数据库、验证待评审项 1. 字符 N-gram 的最小长度和最大长度参数需要在数据库设计中明确,避免过短导致误命中或过长导致索引过大。 2. 倒排索引的具体列(商品名称、分类名称、描述)需要在数据库设计中确认是否全列建立或部分建立;具体索引类型(pg_trgm/GIN 等)由数据库设计统一约定,本流程图与文字不重复枚举。 -3. 相关度排序的”相同条件下顺序应稳定”需要明确次级排序字段,建议在数据库设计中统一。 -4. A102 不返回索引缺失、查询计划等内部原因;如需表达能力受限,只返回与前端约定的通用提示标记,具体错误留在日志和指标。 +3. A102 已冻结稳定次级排序:相关度相同时依次按 Catalog 持有的上架时间倒序、`productId` 倒序;价格或创建时间相同时追加 `productId`。 +4. A102 不返回索引缺失、查询计划等内部原因;进阶能力不可用时先执行保持完整字段范围、强制过滤和稳定排序的安全降级,无法保证时返回 `503 CATALOG.SEARCH_UNAVAILABLE`,具体原因只留在日志和指标。 5. 性能对比环境的固定参数(CPU、内存、PostgreSQL 配置、连接池大小)需要在执行前统一记录,避免环境差异影响结论。 6. 进阶搜索失败时的告警和可观测性要求,需要与 M00 公共基建的可观测性约束一致。 7. DBxxx 索引维护语句与分词参数需要在数据库设计任务中给出可执行定义,不在本流程中预填。 +8. A102 必须沿用 M02 的分类有效性及筛选范围:顶级分类包含直接归属自身及有效直属子分类,子分类只匹配自身;进阶和降级查询不得各自解释分类层级。 ## 十二、验收证据清单 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" index e265758..bf10373 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" @@ -19,7 +19,7 @@ | M02-01/F04、F05 需求 | 完整定义 | 作为业务语义事实源 | | M02-02/F06 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 完整定义、待交叉评审 | 已确认角色、状态、分支和模块出入口 | -| 分类与商品接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| 分类与商品接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | | DBxxx 分类/商品表 | 部分定义 | 本文不发明表字段、状态码和索引 | | C07 缓存协作 | 完整定义 | 只接入固定首页摘要和 A103 商品自身详情;60/10 秒 TTL、3 秒二次失效和 62 秒兜底已冻结 | | C04 中文搜索进阶 | 完整定义,已完成统稿校准 | 在 M02-01 同一查询入口增强,强制过滤、同步索引、降级和性能口径与本文一致 | @@ -38,7 +38,7 @@ flowchart LR CAT -->|"下单重读与库存条件更新"| ORD["M04 Ordering"] CAT -->|"预定义、不可由用户任意组合参数的已上架商品摘要"| HOME["购物端固定首页商品摘要"] - CAT -->|"启用分类与已上架商品(强制已上架过滤)"| LIST["购物端普通列表/搜索页 F04/F05"] + CAT -->|"购物端有效分类与已上架商品(强制已上架过滤)"| LIST["购物端普通列表/搜索页 F04/F05"] CAT -->|"商品公开信息与可售状态"| DET["商品详情页 F06"] LIST -->|"商品 ID"| DET CART -. "收藏、加购或购买意图从详情页进入" .-> CAT @@ -58,7 +58,8 @@ flowchart LR - 公开浏览只暴露已上架商品;草稿和已下架商品不得通过搜索参数绕过。满足删除条件并完成物理删除后,商品已不存在,“已删除”不是可查询的销售状态。 - 本期是单店 B2C 统一经营目录,所有正常商家账号维护同一套分类和商品;M02 不按创建人或操作人分割公开商品。 - 已上架商品库存为零时仍公开展示并标记售罄;库存只控制加购、结算和购买资格,不自动改变商品销售状态。 -- 停用分类只从购物端分类筛选入口移除,不自动下架或隐藏其下已上架商品。已有链接、全部商品和关键词搜索仍可发现这些商品。 +- 分类最多一层父子关系。购物端“有效分类”要求分类自身为启用,且子分类的父分类也为启用;停用顶级分类时整个子树退出筛选入口,但不改写子分类存储状态,重新启用顶级分类后,原本自身为启用的子分类恢复为有效。 +- 分类失效只影响分类筛选入口,不自动下架或隐藏其下已上架商品。已有链接、全部商品和关键词搜索仍可发现这些商品。 - **公开浏览对游客和买家均开放**:携带过期或无效令牌访问公开商品接口时,按游客处理并正常返回商品数据,不得因令牌状态拒绝。401/403 仅在受保护写操作(收藏、加购、购买、评价提交)出现。 - 购物端的价格和库存只能作为浏览口径,下单与购物车写入必须由服务端在 M03、M04 中重新校验。 - 商品模块不直接访问用户私有数据;买家专属操作(收藏、加购、购买)由 M08、M03、M04 提供,商品模块只提供事实输入和入口。 @@ -71,10 +72,10 @@ flowchart LR ```mermaid flowchart TD - A["游客/买家/商家/管理员进入商品列表"] --> B["从事实源加载启用分类和已上架商品第一页
关键词去除首尾空白"] + A["游客/买家/商家/管理员进入商品列表"] --> B["从事实源加载购物端有效分类和已上架商品第一页
关键词去除首尾空白"] B --> C{"参数是否合法?"} C -- "否" --> X["返回字段级错误并保留查询条件"] - C -- "是" --> D["服务端强制过滤为已上架商品
分类、价格区间、仅看有货、白名单排序组合生效
不因所属分类停用而隐藏商品"] + C -- "是" --> D["服务端强制过滤为已上架商品
分类、价格区间、仅看有货、白名单排序组合生效
未指定分类时不因分类失效而隐藏商品"] D --> E{"是否有匹配结果?"} E -- "否" --> F["返回空集合与正确分页信息
展示当前条件并提供清空筛选入口"] E -- "是" --> G["展示主图、名称、当前价格、库存摘要"] @@ -88,7 +89,8 @@ flowchart TD 组合筛选规则: - 关键词、分类、价格区间和仅看有货之间按“同时满足”处理;价格下限不得大于上限。 -- 分类筛选入口只列出启用分类;停用分类不再作为可选条件。停用分类下已有已上架商品仍出现在未指定分类的列表和关键词搜索结果中。 +- 分类筛选入口只列出购物端有效分类,不得返回父分类已停用但自身仍为启用的孤立子分类。停用分类下已有已上架商品仍出现在未指定分类的列表和关键词搜索结果中。 +- 选择顶级分类时,筛选直接归属该顶级分类及其有效直属子分类的已上架商品;选择子分类时,只筛选直接归属该子分类的已上架商品。分类入口的 `productCount` 必须按相同范围统计去重后的已上架商品,不能按另一套范围显示数量。 - “仅看有货”未选中时,库存为零的已上架商品正常返回并标记售罄;选中后才排除库存为零的商品。 - 排序项和方向必须走白名单,禁止把客户端字段直接拼为查询语句。 - 条件恢复:关键词、筛选、排序和页码应当反映在页面地址或等效可恢复状态中,刷新或返回时无需重新选择。 @@ -117,7 +119,7 @@ flowchart TD - 价格、库存和上下架状态以服务端结果为准;C07 商品详情缓存可以在已约定的一致性窗口内短暂返回旧公开值,但不得作为加购、结算或下单依据。 - 已上架且库存为零时保留详情并明确标记售罄,不提供加购或购买入口。 -- 所属分类停用时,只要商品仍为已上架就继续公开;详情可以展示分类信息,但购物端分类筛选入口不再提供该分类。 +- 所属分类自身或其父分类停用时,只要商品仍为已上架就继续公开;详情可以展示分类信息,但购物端分类筛选入口不再提供受影响分类。 - 商品主图加载失败时使用占位图,不阻断其他信息浏览。 - 后台改价、改库存、上下架或修改内容后,详情在 C07 已约定的一致性窗口内收敛到 PostgreSQL 最新值;超过窗口不得继续返回旧值。 - 图片合规:单个商品最多 8 张,单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 400~4096 像素,第一张作为主图。 @@ -178,7 +180,7 @@ flowchart TD | 商品 ID 不存在或商品已物理删除 | 返回“商品不存在”,提供返回列表入口 | 不暴露内部异常;删除不是销售状态 | | 商品已下架 | 显示“暂不可售”,禁用购买 | 历史订单快照仍可读 | | 商品已上架但库存为零 | 继续展示并标记售罄 | 不提供加购、结算或购买入口 | -| 商品所属分类被停用 | 分类不再出现在筛选入口,商品继续按已上架状态公开 | 不自动下架,不从全部商品或关键词搜索隐藏 | +| 商品所属分类自身或其父分类被停用 | 该分类及受影响子树不再出现在筛选入口,商品继续按已上架状态公开 | 不改写子分类存储状态,不自动下架,不从全部商品或关键词搜索隐藏 | | 请求数量超过当前库存 | 拒绝加购、结算或购买并刷新库存事实 | 由 M03/M04 决定是否调小或重新选择 | | 关键词、分类、价格或排序非法 | 字段级错误,保留查询条件 | 不执行查询 | | 图片加载失败 | 使用占位图 | 不阻断价格、库存和描述浏览 | @@ -192,9 +194,9 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 查询启用分类 | A101 Catalog 分类 | 只返回购物端筛选入口使用的启用分类;分类停用不改变其下商品销售状态 | 待交叉评审 | -| 查询固定首页摘要、普通列表与组合筛选/关键词搜索 | A102 Catalog 列表/搜索 | 只有无筛选、`OnSale`、`createdAt DESC, productId DESC`、前 12 条的固定首页摘要可由 C07 缓存;所有用户控制的普通列表、关键词和组合筛选每次按 PostgreSQL 强制已上架过滤、分页、白名单排序并正常返回空结果;库存为零时标记售罄,所属分类停用时仍可通过全部商品和关键词命中;F05 与 C04 共用同一契约 | 待重建详细契约 | -| 查询商品详情 | A103 Catalog 详情 | 仅公开已上架商品;允许库存为零并标记售罄,允许所属分类停用;缓存只含商品自身公开字段,不含 M07 评价/评分或个人字段;旧值最坏不超过提交后 62 秒 | 待重建详细契约 | +| 查询购物端有效分类 | A101 Catalog 分类 | 只返回自身和父级均启用的分类,不返回孤立子分类;顶级分类的 `productCount` 统计自身及有效直属子分类范围,子分类只统计自身范围;分类失效不改变商品销售状态 | 待交叉评审 | +| 查询固定首页摘要、普通列表与组合筛选/关键词搜索 | A102 Catalog 列表/搜索 | 只有无筛选、`OnSale`、`createdAt DESC, productId DESC`、前 12 条的固定首页摘要可由 C07 缓存;所有用户控制的普通列表、关键词和组合筛选每次按 PostgreSQL 强制已上架过滤、分页、白名单排序并正常返回空结果;顶级分类包含自身及有效直属子分类,子分类只匹配自身;库存为零时标记售罄,分类失效时仍可通过全部商品和关键词命中;F05 与 C04 共用同一契约 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 查询商品详情 | A103 Catalog 详情 | 仅公开已上架商品;允许库存为零并标记售罄,允许所属分类停用;缓存只含商品自身公开字段,不含 M07 评价/评分或个人字段;旧值最坏不超过提交后 62 秒 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | | 公开评价汇总与列表(X01 衔接) | A140(由 M07 流程派生) | 商品详情只读取公开评价和评分汇总,不混入上传、提交或资格判断能力 | 待交叉评审 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 @@ -207,29 +209,30 @@ flowchart TD - M04 订单:下单时由 M04 重读本模块的最新事实进行条件扣减,不信任购物端传入的金额和库存。 - M07 评价:商品详情只读取 M07 公开评价与评分汇总;评价提交入口必须从已完成订单走 M07,商品详情不开放绕过入口。 -## 十、由流程反查出的接口与数据待评审项 +## 十、接口承接结果与剩余数据待评审项 -1. 公开浏览接口(A101~A103)必须明确"无登录或令牌失效时按游客返回"的契约;接口设计需与 M01 的 JWT 鉴权边界统一,避免公开接口误判为受保护接口。 -2. 列表与详情对已上架过滤必须服务端强制;库存为零的已上架商品需要返回可区分的售罄结果,已下架与不存在的详情结果由接口契约分别定义。 -3. 排序白名单字段集尚未在需求中枚举;接口设计前需要 M02 与评审人员确认价格、时间、相关度的默认与可选顺序。 -4. 图片合规校验在前端完成上传限制后仍需服务端再次校验;接口字段需要明确”主图”与”附加图”的上传顺序和替换规则。 -5. 商品详情是否暴露最新库存数或仅暴露”有货/无货”摘要,由需求决定展示口径;接口返回字段需要和前端展示要求对齐。 -6. 评价公开汇总字段(平均分、总条数的计算时机)由 M07 派生;本期不得把它混入 C07 商品详情缓存,避免评价变更扩大商品缓存失效范围。 -7. C04 进阶搜索替换 F05 基础模糊查询时,需要保留 A102 商品列表/搜索接口的请求字段和返回口径,避免前端按字段名硬编码查询逻辑。 -8. DBxxx 分类/商品表字段尚未形成可实施的完整定义,数据库字段、约束、索引和 GIN/pg_trgm 维护策略必须由数据库设计任务另行确认。 +1. A101~A103 已明确为公开接口,不要求 JWT;客户端即使携带失效令牌也按游客公开范围处理,不误判为受保护接口。 +2. A102/A103 已由服务端强制只公开 `OnSale` 商品;A103 对不存在、不可公开与 `OnSale` 但库存为零分别返回确定结果。 +3. A102 已冻结排序白名单:`relevance`、`price`、`createdAt`;有关键词默认相关度、无关键词默认创建时间,并追加稳定次级排序。 +4. A127/A128 已冻结图片上传、顺序和主图规则:服务端复核格式与尺寸,首图成为主图,显式替换主图在同一事务完成,删除主图时按固定顺序提升下一张。 +5. A103 已同时返回实时 `stock` 与 `stockStatus`;库存为零的 `OnSale` 商品返回 `SoldOut` 并保留详情,不作为下单事实。 +6. 评价公开汇总字段由 M07/A140 单独派生;本期不混入 A103 或 C07 商品详情缓存,避免评价变更扩大商品缓存失效范围。 +7. C04 进阶搜索继续由 A102 承载,保留商品列表、筛选、分页和返回口径;实现不得建立第二套前端字段契约。 +8. DBxxx 分类/商品表字段尚未形成可实施的完整定义,数据库字段、约束、索引和 GIN/pg_trgm 维护策略仍由后续数据库设计按本流程与接口派生。 9. 接口和测试必须承接 C07 已冻结的固定首页语义、A103 字段隔离、60/10 秒 TTL、跨实例单填充、3 秒二次失效及 62 秒最坏窗口,不得继续保留“缓存参数待确认”的旧口径。 +10. A101/A102 必须承接分类有效状态、父子层级、顶级分类筛选范围和同口径 `productCount`;后台存储状态不得直接当作购物端有效状态。 ## 十一、验收证据清单 对照 `验收标准.md` 的 F04、F05、F06、N02、N04、N05: -- [ ] F04:分页数据、总数和翻页结果正确,刷新或返回后查询条件仍可恢复;分类筛选有效,停用分类不出现在购物端筛选入口。 +- [ ] F04:分页数据、总数和翻页结果正确,刷新或返回后查询条件仍可恢复;顶级分类和子分类筛选范围正确,购物端无效分类不出现在筛选入口。 - [ ] F05:关键词模糊搜索、组合筛选和白名单排序可独立及组合生效;F05 由 C04 替换底层实现后口径不变。 - [ ] F06:商品名称、图片、描述、价格、库存和分类展示正确,并在 C07 一致性窗口内收敛到后台最新有效修改;有货、售罄、下架、不存在和加载失败状态均能清楚区分。 - [ ] N04:购物端任何身份均无法搜索到草稿或已下架商品,已物理删除商品不再存在;库存为零的已上架商品仍可见并标记售罄。 - [ ] N02:图片失败或接口失败时页面仍可理解、可返回或可重试,不出现空白页。 - [ ] N05:Chrome / Edge 最新版正常显示,无明显样式错乱。 -- [ ] 分类:停用分类不再作为筛选入口,但其下已上架商品仍能通过全部商品、关键词搜索和详情访问。 +- [ ] 分类:停用顶级分类后整个子树不再作为筛选入口,但其下已上架商品仍能通过全部商品、关键词搜索和详情访问。 - [ ] 缓存:A102 仅固定首页摘要调用和 A103 商品详情允许存在受一致性窗口约束的旧值;分类、普通列表和搜索直读 PostgreSQL;缓存不可用时回退事实源。 - [ ] X01 衔接:已选 X01 的评分与评价入口展示正常,但未满足条件的用户不能从详情页绕过订单资格提交评价。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" index 3202e58..2324cc2 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -14,13 +14,13 @@ 本模块不包含多商家数据隔离、批量导入导出、定时上架、复杂审批流、商品操作审计功能或管理员代商家修改商品。 -本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。M06-01 商家写操作接口编号落在 A110~A128 范围(A110~A115 后台分类、A120~A128 后台商品),与 M02 公开浏览 A101~A103、M07 评价 A140~A144 同属 Catalog/Review 编号段(A101~A200);该编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。M06-01 商家写操作接口编号落在 A110~A128 范围(A110~A115 后台分类、A120~A128 后台商品),与 M02 公开浏览 A101~A103、M07 评价 A140~A143(A144 为历史取消号)同属 Catalog/Review 编号段(A101~A200);该编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M06-01/F11 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 完整定义、待交叉评审 | 已确认角色、状态、分支和模块出入口 | -| 商家端写操作接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| 商家端写操作接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | | DBxxx 分类/商品表 | 部分定义 | 本文不发明表字段、状态码和索引 | | C07 缓存失效协作 | 完整定义 | 商品事实提交后按冻结矩阵立即失效并在 3 秒后二次失效,不混入商家写操作核心结果 | | C04 搜索索引更新 | 完整定义,已完成统稿校准 | PostgreSQL 同步维护索引,失败安全回退基础搜索,不建设独立索引任务 | @@ -73,8 +73,8 @@ flowchart TD V -- "否" --> Y["字段级错误,保留已填内容"] V -- "是" --> I["保存分类元数据"] F --> J{"切换为停用?"} - J -- "否" --> K["启用分类并恢复购物端筛选入口"] - J -- "是" --> L["停用分类并移出购物端筛选入口
不改变已有商品销售状态"] + J -- "否" --> K["保存启用状态
仅在父分类也启用时恢复购物端入口"] + J -- "是" --> L["保存停用状态
若为顶级分类则整棵子树退出购物端入口
不改写子分类状态或商品销售状态"] I --> M["返回最新分类列表"] K --> M L --> M @@ -83,10 +83,12 @@ flowchart TD 关键规则: -- 分类只有启用和停用两种状态。停用分类不再作为购物端筛选入口,但不自动下架或隐藏其下已有已上架商品。 -- 新建商品不得绑定停用分类;已有商品切换分类时不得选择停用分类。停用分类下的既有商品可继续修改不改变分类归属的字段。 -- 停用分类下的商品一旦处于草稿或已下架,必须迁移到启用分类或重新启用原分类后才能上架。 -- 分类层级、名称和排序可以在校验通过后正常编辑;商品或历史引用只阻止物理删除,不能阻止分类元数据编辑。 +- 分类只有启用和停用两种存储状态,层级最多一层父子关系;购物端有效状态是派生结果:分类自身启用,且子分类的父分类也启用。 +- 停用顶级分类会让整个子树退出购物端筛选入口,但不批量改写子分类存储状态,也不自动下架或隐藏其下已有已上架商品。重新启用顶级分类后,只有自身仍为启用的子分类恢复为有效入口。 +- 新建商品不得绑定购物端无效分类;已有商品切换分类时不得选择无效分类。无效分类下的既有商品可继续修改不改变分类归属的字段。 +- 无效分类下的商品一旦处于草稿或已下架,必须迁移到有效分类,或使原分类及其父分类均启用后才能上架。 +- 分类名称、父级和排序可以在校验通过后正常编辑;移到停用父分类下会立即变为购物端无效,但不改写自身存储状态。商品或历史引用只阻止物理删除,不能阻止分类元数据编辑。 +- 已有直属子分类的顶级分类不得直接改绑为另一顶级分类的子分类;商家必须先逐个迁移或删除直属子分类,系统不隐式级联改绑,避免产生两层子级。 - 分类名称、父级关系和启停状态必须校验;非法输入返回字段级错误并保留已填内容。 - 分类名称修改成功时,关联商品用于 C04 的检索文本和数据库索引必须在同一 PostgreSQL 事实提交中同步反映新名称;不能先返回分类成功再等待独立索引任务。 @@ -97,10 +99,10 @@ flowchart TD A["商家进入商品管理"] --> B{"选择操作?"} B -- "新建商品" --> C["填写名称、分类、价格、库存、主图/图片和描述"] B -- "编辑商品" --> D["按商品 ID 加载当前内容并保留已填字段"] - C --> NC{"必填项、价格、库存、启用分类和图片合规?"} + C --> NC{"必填项、价格、库存、购物端有效分类和图片合规?"} NC -- "否" --> X["字段级错误,保留表单内容并标记失败字段"] NC -- "是" --> G["原子保存商品事实"] - D --> EC{"字段是否合法?
若切换分类,目标分类是否启用?"} + D --> EC{"字段是否合法?
若切换分类,目标分类是否在购物端有效?"} EC -- "否" --> X EC -- "是" --> F{"编辑依据的并发标记是否仍有效?"} F -- "否" --> Y["返回冲突提示,不静默覆盖已生效修改"] @@ -112,8 +114,8 @@ flowchart TD 商品字段与图片校验: -- 商品名称、合法的分类关联、当前价格、库存和主图为必填;价格不得为负,库存必须为非负整数。新建、改绑分类和上架时分类必须已启用;既有商品的原分类后来停用时仍可修改不改变分类归属的字段。 -- 新建商品只能绑定启用分类;既有商品在停用分类下可以修改名称、价格、库存、图片和描述,但不能改绑另一个停用分类。 +- 商品名称、合法的分类关联、当前价格、库存和主图为必填;价格不得为负,库存必须为非负整数。新建、改绑分类和上架时分类必须在购物端有效(自身及父分类均启用);既有商品的原分类或其父分类后来停用时仍可修改不改变分类归属的字段。 +- 新建商品只能绑定购物端有效分类;既有商品在无效分类下可以修改名称、价格、库存、图片和描述,但不能改绑另一个无效分类。 - 图片上传到 S3 Compatible Object Storage;单商品最多 8 张,单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 400~4096 像素,第一张作为主图。 - 图片上传失败时明确标记失败图片并允许重试,不清空其他表单字段。 - 编辑商品时使用并发标记或条件更新防止静默覆盖;冲突时保留已填写内容并提示刷新确认。 @@ -125,7 +127,7 @@ flowchart TD A["商家选择目标商品"] --> B{"选择操作?"} B -- "上架" --> US{"当前销售状态?"} US -- "已上架" --> U0["返回既有已上架结果
不重复产生状态变化"] - US -- "草稿或已下架" --> C{"完整性校验通过且分类已启用?"} + US -- "草稿或已下架" --> C{"完整性校验通过且分类在购物端有效?"} C -- "否" --> X["拒绝上架并指出缺失字段或停用分类"] C -- "是" --> D["事务内将商品状态置为已上架"] B -- "下架" --> DS{"当前销售状态?"} @@ -145,7 +147,7 @@ flowchart TD - 下架不删除购物车记录、收藏记录、浏览记录或历史订单快照,由对应模块显示不可售状态。 - 下架商品的旧链接只能显示不可售状态,不提供购买入口;历史订单快照不受影响。 - 已上架但库存为 0 的商品继续出现在公开列表、搜索和详情中,明确标记售罄并禁用购买,不自动下架。 -- 所属分类停用不改变已有商品状态;已有已上架商品继续公开。只有后续重新上架时才要求分类已启用。 +- 所属分类自身或其父分类停用不改变已有商品状态;已有已上架商品继续公开。只有后续重新上架时才要求分类在购物端有效。 - 物理删除只允许草稿或已下架且没有订单、购物车、收藏、浏览、评价、秒杀等任何历史关联的商品;已上架商品必须先完成下架。 ## 六、商品销售状态机与并发边界 @@ -199,8 +201,11 @@ flowchart TD | 游客、买家或管理员访问后台写接口 | 拒绝 | 401/403,不返回后台数据 | | 字段、价格、库存或分类非法 | 拒绝保存 | 字段级错误,前端保留用户已填写内容 | | 编辑存在商品引用的分类元数据 | 校验名称、层级和排序后允许保存 | 引用关系不阻止编辑 | +| 将仍有直属子分类的顶级分类改为子分类 | 拒绝保存 | 先处理直属子分类,不隐式级联改绑 | | 停用存在已上架商品的分类 | 停用分类筛选入口 | 已有已上架商品继续公开,不自动下架 | -| 停用分类下商品尝试重新上架 | 拒绝上架 | 迁移到启用分类或先重新启用原分类 | +| 停用顶级分类但子分类仍为启用 | 顶级分类及整个子树退出购物端筛选,不改写子分类存储状态 | 顶级分类恢复后按子分类自身状态重新生效 | +| 启用父分类仍停用的子分类 | 保存子分类启用状态,但购物端有效状态仍为否 | 不提前公开孤立子分类 | +| 无效分类下商品尝试重新上架 | 拒绝上架 | 迁移到有效分类,或使原分类及其父分类均启用 | | 已上架商品库存降为零 | 保持已上架并标记售罄 | 不提供加购、结算和购买 | | 图片上传失败 | 标记失败图片并允许重试 | 不清空其他表单字段 | | 两名操作人并发编辑 | 后提交者收到冲突提示 | 不静默覆盖已生效修改 | @@ -219,16 +224,16 @@ flowchart TD |---|---|---|---| | 商家分页查询商品(全状态) | A120 后台商品列表 | 对所有正常商家返回同一经营目录,支持分页、关键词、分类和三种销售状态组合查询,不按操作人过滤商品归属 | 待交叉评审 | | 后台商品详情 | A121 后台商品详情 | 返回含 version 字段的全状态商品事实,供编辑并发校验 | 待交叉评审 | -| 新建商品 | A122 商品创建 | 校验字段、启用分类和图片,保存为草稿并返回商品事实 | 待交叉评审 | -| 编辑商品 | A123 商品编辑 | 允许停用分类下既有商品修改非分类字段;改绑分类只能选择启用分类;并发保护后保存并返回最新事实 | 待交叉评审 | -| 商品上架 | A125 商品上架 | 校验完整性和启用分类;从草稿或已下架变为已上架 | 待交叉评审 | +| 新建商品 | A122 商品创建 | 校验字段、购物端有效分类和图片,保存为草稿并返回商品事实 | 待交叉评审 | +| 编辑商品 | A123 商品编辑 | 允许无效分类下既有商品修改非分类字段;改绑分类只能选择购物端有效分类;并发保护后保存并返回最新事实 | 待交叉评审 | +| 商品上架 | A125 商品上架 | 校验完整性和购物端有效分类;从草稿或已下架变为已上架 | 待交叉评审 | | 商品下架 | A126 商品下架 | 事务内将状态置为已下架;购物端列表与搜索立即不再返回 | 待交叉评审 | | 商品后台删除 | A124 商品删除 | 仅允许草稿或已下架且无任何历史关联时物理删除;删除后实体不存在,不返回“已删除”状态 | 待交叉评审 | -| 后台分类列表 | A110 后台分类列表 | 返回全状态分类,购物端只返回启用分类 | 待交叉评审 | -| 新建分类 | A111 新建分类 | 校验名称、父级、排序和初始状态,事务内保存 | 待交叉评审 | -| 编辑分类 | A112 编辑分类 | 校验名称、父级与排序后保存;商品或历史引用不能阻止元数据编辑;分类名称变更与关联商品检索事实同步提交 | 待交叉评审 | -| 启用分类 | A113 启用分类 | 切换启停状态并校验依赖 | 待交叉评审 | -| 停用分类 | A114 停用分类 | 移出 A101 分类筛选入口,但不改变已有商品状态;A102 仍可在全部商品和关键词搜索中返回其下已上架商品 | 待交叉评审 | +| 后台分类列表 | A110 后台分类列表 | 返回全部分类的存储状态和派生的购物端有效状态;不得把父分类停用但自身启用的子分类标成有效 | 待交叉评审 | +| 新建分类 | A111 新建分类 | 校验名称、最多一层父级、排序和初始存储状态;允许在停用父分类下保存启用子分类,但返回购物端无效 | 待交叉评审 | +| 编辑分类 | A112 编辑分类 | 校验名称、最多一层父级与排序后保存;有直属子分类的顶级分类不得直接改为子分类;改绑到停用父分类时自身状态不变、购物端有效状态变为否;分类名称变更与关联商品检索事实同步提交 | 待交叉评审 | +| 启用分类 | A113 启用分类 | 保存启用状态;父分类仍停用时返回购物端无效,启用顶级分类时恢复自身启用的直属子分类入口 | 待交叉评审 | +| 停用分类 | A114 停用分类 | 停用顶级分类时整棵子树移出 A101,但不改写子分类状态或商品状态;A102 仍可在全部商品和关键词搜索中返回其下已上架商品 | 待交叉评审 | | 删除分类 | A115 删除分类 | 仅允许无子分类、无商品和无其他历史引用的分类物理删除;任一引用存在则整体拒绝,并发新增引用与删除只允许一个结果,删除后不保留伪状态 | 待交叉评审 | | 上传商品图片 | A127 商品图片上传 | 按合规要求上传到对象存储,返回主图与附加图标识 | 待交叉评审 | | 删除商品图片 | A128 商品图片删除 | 删除商品图片关联与对象存储对象,保持引用一致 | 待交叉评审 | @@ -239,20 +244,21 @@ flowchart TD - C07 缓存:商品事务提交后立即失效目标详情;名称、价格、库存、分类展示、图片/主图、销售状态或首页成员资格变化时同步失效唯一固定首页,并在提交后 3 秒对同一 Key 二次失效。分类、后台商品列表及购物端普通列表和搜索不缓存。缓存不可用不影响商品事务;正常值 60 秒、空值 10 秒,双删失败时正常旧值最坏不超过提交后 62 秒、旧空值不超过 12 秒。 - C04 搜索:商品创建、编辑、上下架、删除或关联分类名称修改时,检索文本、销售状态与 PostgreSQL 数据库索引同步维护;本模块不建设独立索引同步任务,事务回滚时检索事实同样回滚;进阶搜索不可用时由 C04 回退基础模糊查询。 -- M03 购物车:商品下架、库存归零或被禁用后由购物车模块按 M03 规则标记失效,不反向写入商品状态。 +- M03 购物车:商品变为 `Draft` / `OffSale` 或实时可售库存归零后,由购物车模块按 M03 规则标记失效;分类停用不反向改变仍为 `OnSale` 商品的可结算资格,也不反向写入商品状态。 - M04 订单:商品事务不修改历史订单的地址或商品快照;价格或上下架变更不影响已有订单。 - M01 Identity:本模块不修改账号、角色或令牌状态;账号禁用由 M06-03 独立流程处理。 -## 十一、由流程反查出的接口与数据待评审项 +## 十一、接口承接结果与剩余数据待评审项 -1. 商品并发保护方式尚未在需求中枚举(version/etag、`WHERE updated_at` 等),需要在接口设计前与 Ordering 的并发口径统一。 +1. A121/A123/A125/A126 已统一使用整数 `version` 与条件更新承接商品编辑、上架和下架并发;冲突返回稳定错误且不静默覆盖。 2. 商品物理删除必须按全量历史关联判断,不按订单状态排除已取消订单;订单、购物车、收藏、浏览、评价和秒杀等任何历史引用均阻止删除,后续由数据库设计落实约束。 -3. 停用分类下已有已上架商品继续公开已冻结;接口需要确保 A101 移除分类筛选入口时,A102/A103 不按分类启停状态额外隐藏商品。 -4. 图片上传顺序和替换规则的接口字段(主图上传后是否自动替换旧主图)尚未定义,需在接口设计前与命名规范统一。 -5. 商品写接口必须在事务提交后向 C07 提供受影响商品及首页范围,支持立即和 3 秒二次失效;失效失败记录可追踪错误,但不能把缓存失败伪装成商品保存失败。 -6. 搜索索引异常时的降级语义需要在接口契约和 C04 搜索实现之间达成一致;搜索不接入 C07,商家端不感知底层使用哪种索引实现。 -7. DBxxx 分类/商品表字段尚未形成可实施的完整定义,数据库字段、约束、索引和 GIN/pg_trgm 维护策略必须由数据库设计任务另行确认。 -8. 商品图片上传接口与对象存储的兼容边界需要与系统架构设计同步,避免不同商家端入口使用不同的上传契约。 +3. 无效分类下已有已上架商品继续公开已冻结;接口需要确保 A101 按“自身及父级均启用”移除受影响入口时,A102/A103 不按分类有效状态额外隐藏商品。 +4. A110~A114 必须同时返回分类存储状态与派生的购物端有效状态,并承接顶级分类停用整棵子树退出、子分类状态不改写和父分类恢复后的重新生效规则。 +5. A127/A128 已冻结 `isPrimary`、`sortOrder`、首图主图、显式主图替换和删除主图后的固定提升规则;所有主图切换与图片关联写入形成一个一致事务结果。 +6. 商品写接口必须在事务提交后向 C07 提供受影响商品及首页范围,支持立即和 3 秒二次失效;失效失败记录可追踪错误,但不能把缓存失败伪装成商品保存失败。 +7. 搜索索引异常时的降级语义需要在接口契约和 C04 搜索实现之间达成一致;搜索不接入 C07,商家端不感知底层使用哪种索引实现。 +8. DBxxx 分类/商品表字段尚未形成可实施的完整定义,数据库字段、约束、索引和 GIN/pg_trgm 维护策略仍由后续数据库设计按本流程与接口派生。 +9. A122/A127 上传先写受控对象,再在数据库事务内提交商品/图片关联、排序与主图事实;数据库提交失败时清理本次对象,清理失败进入补偿。A128 删除先提交图片关联与新主图事实,再删除对象;对象删除失败进入补偿。所有商家端入口复用同一顺序。 ## 十二、验收证据清单 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" index 366ee7b..c8441b0 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" @@ -14,13 +14,13 @@ 本模块不包含追评、评价点赞、买家自删、匿名评价、商家回复或隐藏、自动内容审核和评价运营后台。 -本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。当前流程派生 A140~A143;旧 A144 没有业务入口,应在下游接口整合中取消。接口编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 +本文先依据需求确定业务参与者、状态、判断分支和模块出入口,再由这些流程步骤派生接口能力。当前流程派生 A140~A143;旧 A144 没有业务入口,已在下游接口整合中取消并保留为历史编号。接口编号仅用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M07/X01 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 完整定义、待交叉评审 | 已确认角色、状态、分支和模块出入口 | -| 评价接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| 评价接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | | DBxxx 评价表 | 模板/占位 | 本文不发明表字段、暂存结构、状态码和索引 | | F06 评价公开读取 | 完整定义 | 商品详情只读取,不在本模块内重复实现 | @@ -190,7 +190,7 @@ flowchart TD | 查询订单项评价资格/结果 | A143 评价资格 | 仅为当前买家的订单详情返回可评价或已评价提示;该结果不替代 A142 提交时重检 | 待交叉评审 | | 商品公开评价分页 + 评分汇总 | A140 公开评价 | 无需登录即可分页返回全部成功评价的评分、文字、图片、时间和脱敏展示名,并返回总数与平均分 | 待交叉评审 | -接口详细定义与实现必须承接上述流程结果。现有 A144“单条评价详情/举报链路”没有需求与流程入口,后续接口整合应取消该编号,不得反向新增评价详情页或举报流程。HTTP 状态码、请求字段和错误码不得反向写入业务图。 +接口详细定义与实现必须承接上述流程结果。现有 A144“单条评价详情/举报链路”没有需求与流程入口,已取消并保留为历史编号,不得反向新增评价详情页或举报流程。HTTP 状态码、请求字段和错误码不得反向写入业务图。 ## 九、扩展接入边界 @@ -209,7 +209,7 @@ flowchart TD 5. A140 是公开读取,游客、买家、商家和管理员使用同一公开字段;A142、A143 才要求当前买家身份。 6. 评价、脱敏展示名快照和成功图片关联必须形成一个完整业务结果,任一必要写入失败时不得留下可公开的部分评价。 7. DBxxx 评价表及图片关联字段尚未形成可实施的完整定义,由后续数据库设计统一派生;流程不预设对象键、暂存表或清理调度。 -8. A144 缺少独立业务入口,应在接口整合中转为历史取消号;不得为保留旧编号而补造举报或评价详情需求。 +8. A144 缺少独立业务入口,已在接口整合中转为历史取消号;不得为保留旧编号而补造举报或评价详情需求。 ## 十一、验收证据清单 @@ -236,7 +236,7 @@ flowchart TD - [ ] 状态名称与需求规格说明书一致;没有新增订单状态。 - [ ] Mermaid 节点使用业务动作,没有用接口编号、HTTP 路径、DTO、技术模式名称或 SQL 片段代替流程;没有自行发明接口编号、字段名或数据库状态码。 - [ ] **M07 不修改 M04 订单项状态**:流程图与状态机中不出现”订单项变为已评价”,已评价事实由 Review 唯一评价记录派生;订单项状态机只由 Ordering 维护。 -- [ ] 当前流程只派生 A140~A143;A144 无业务来源,应在接口整合时取消,不为编号补造需求。 +- [x] 当前流程只派生 A140~A143;A144 无业务来源,已在接口整合中取消并保留历史编号,未为编号补造需求。 - [ ] 已对照根文档 3.6 节中评价接入点校准入口位置,X01 不可绕过订单资格。 升级到”待交叉评审”的条件:自检完成、主流程与异常分支齐全、并发场景验证、订单项越界问题已修正、Mermaid 可正常渲染、相对链接有效、UTF-8 编码。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C06-\345\256\236\346\227\266\346\216\250\351\200\201\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C06-\345\256\236\346\227\266\346\216\250\351\200\201\346\265\201\347\250\213.md" index 2d2a698..c790a64 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C06-\345\256\236\346\227\266\346\216\250\351\200\201\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C06-\345\256\236\346\227\266\346\216\250\351\200\201\346\265\201\347\250\213.md" @@ -4,7 +4,7 @@ > 覆盖:C06;经 X03/M09 接入核心业务事实 > 基础核心流程:F08、F09、F10、F12;鉴权依赖 F02、F13;接入 X04 售后来源 > 直接协作:M09 Messaging、M01 Identity、C10 多实例运行环境 -> 文档状态:完整定义;业务与运行边界已冻结,待接口、部署、实现和测试承接 +> 文档状态:完整定义;业务与运行边界已冻结,Hub 与事件契约已按本文重建,待部署、实现和测试承接 > 需求事实源:[需求规格说明书 C06](../../../01-需求文档/需求规格说明书.md) 的“C06 实时消息推送”完整七节 ## 一、范围与事实来源 @@ -17,8 +17,8 @@ C06 在 M09 消息已经成功持久化之后,为当前 PC Web 的已登录买 |---|---|---| | C06 需求与教师验收 | 完整定义 | 作为传输、断线重连、多标签页、多实例和持久化补查边界 | | 本文业务流程 | 完整定义 | 冻结连接生命周期、定向推送、权威补查、固定重连和失败隔离 | -| 接口设计 4.3.7、4.3.8 | 部分定义,待按本文补齐 | 由流程派生 Hub 与服务端事件映射 | -| A501~A505 | 部分定义,待按 M09 补齐 | 用于补查、详情和已读校正,不为 SignalR 新增 Axxx | +| 接口设计 4.3.7、4.3.8 | 已按本文重建、未冻结 | 由流程派生 Hub 与服务端事件映射;待部署、实现与交叉评审 | +| A501~A505 | 完整定义,待交叉评审 | 用于补查、详情和已读校正,不为 SignalR 新增 Axxx;待数据库、OpenAPI 与交叉评审 | | Redis Backplane / C10 | 技术与部署能力待验证 | 只承接跨实例通道,不保存唯一消息事实 | ## 二、直接出入口与不可变结果 @@ -160,8 +160,8 @@ SignalR 连接和服务端事件不是 HTTP 接口,不占用 Axxx,也不得 | 流程能力 | 当前派生契约 | 事实来源 | 当前状态 | |---|---|---|---| -| 买家或商家以 WebSockets 跳过协商建立本人实时连接 | 接口设计 4.3.7“Hub 连接” | M01 认证上下文 | 待补齐固定传输和失败关闭契约 | -| M09 消息提交后向全部有效在线连接发送提示 | 接口设计 4.3.8“MessageCreated” | M09 已持久化消息 | 待补齐提示语义与最小载荷 | +| 买家或商家以 WebSockets 跳过协商建立本人实时连接 | 接口设计 4.3.7“Hub 连接” | M01 认证上下文 | 固定传输和失败关闭契约已补齐,待部署与测试 | +| M09 消息提交后向全部有效在线连接发送提示 | 接口设计 4.3.8“MessageCreated” | M09 已持久化消息 | 提示语义与最小载荷已补齐,待实现与测试 | | 初次连接、重连和收到提示后校正未读数 | A503 | M09/PostgreSQL | 待补齐权威补查时机 | | 补查断线期间消息 | A501;查看详情时使用 A502 | M09/PostgreSQL | 待交叉评审 | | 任一标签页标记已读并校正 | A504、A505,随后复用 A503 | M09/PostgreSQL | 待交叉评审 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" index ba35b00..916d003 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" @@ -4,7 +4,7 @@ > 覆盖:C07 > 基础核心流程:F04 固定首页、F06 商品详情、F11 商品管理;F08/F09、C01、X04 仅在改变普通库存时触发失效 > 直接协作:顾欣月(M02 Catalog)、韦乾强(M04 Ordering)、朱惠惠(C01 Seckill)、张海洋(M10 AfterSales)、M00/C10 公共 Redis 与多实例环境 -> 文档状态:完整定义;缓存范围、参数和失效矩阵已冻结,待接口、架构、实现和压测承接 +> 文档状态:完整定义;缓存范围、参数和失效矩阵已冻结,接口与非 HTTP 缓存契约已按本文重建,待实现和压测承接 > 需求事实源:[需求规格说明书 C07](../../../01-需求文档/需求规格说明书.md) 的“C07 缓存与性能优化”完整七节 ## 一、范围、职责与事实来源 @@ -17,7 +17,7 @@ PostgreSQL 始终是价格、库存、上下架状态和商品内容的事实来 |---|---|---| | C07 需求与教师验收 | 完整定义 | 作为 Cache-Aside、写后失效、降级和压测边界 | | 本文业务流程 | 完整定义 | 冻结缓存对象、读取参数、事务后双删、跨实例填充、库存通道和错误出口 | -| A102、A103 与 Catalog 写接口 | 部分定义,待按本文补齐 | 由流程映射,不改变原接口响应 | +| A102、A103 与 Catalog 写接口 | 已按本文重建、未冻结 | 由流程映射,不改变原接口响应;待数据库、OpenAPI 与交叉评审 | | DB022、DB023 | 仅为接口文档引用,数据库主文档未确认 | 不把接口引用写成已冻结表设计 | | Redis Key、TTL 与热点保护 | 流程参数已冻结 | 正常值 60 秒、空值 10 秒、回填窗口 2 秒、二次失效 3 秒、等待 500 毫秒 | @@ -132,7 +132,7 @@ flowchart TD | 新建草稿商品 | 清理同标识短空值 | 不失效 | 未上架商品不得进入公开结果 | | 名称、价格、普通库存、描述变化 | 失效 | 若字段出现在首页摘要或库存售罄标记变化则失效 | 普通库存变化包括下列普通订单与划拨动作 | | 分类展示信息或商品分类关系变化 | 失效受影响商品 | 若首页展示字段或 `OnSale` 成员资格受影响则失效 | 分类、普通列表本身不进入 C07 | -| 商品图片新增、删除、排序或主图变化 | 失效 | 若首页缩略图变化则失效 | 接口设计必须补齐所有图片写动作的失效后置条件 | +| 商品图片新增、删除、排序或主图变化 | 失效 | 若首页缩略图变化则失效 | 接口设计已补齐所有图片写动作的失效后置条件 | | 上架 | 清理短空值/旧详情 | 失效 | 上架后下一次查询方可公开 | | 下架 | 失效 | 失效 | 购物端旧链接不再允许购买 | | 满足约束后删除 | 失效 | 失效 | 不影响历史订单快照 | @@ -194,8 +194,8 @@ stateDiagram-v2 | 固定首页公开查询 | A102 的无筛选固定首页语义;其他 A102 请求不缓存 | 数据库设计待汇总 | 待补齐固定排序、12 条和售罄字段 | | 购物端商品自身公开详情 | A103 | 数据库设计待汇总 | 待明确排除 M07 评价、评分和个人字段 | | 商品、分类展示和图片写入后失效 | A112、A122~A128 提交后的内部协作 | 目标表设计待数据库汇总 | 待为创建短空值、名称、价格、描述、分类展示、图片、主图、状态和删除逐项补齐后置条件 | -| 普通订单与普通库存售后回补后失效 | Catalog 公开库存应用能力,不新增 Axxx | 库存事实待数据库汇总 | 待在 M04/M10 下游契约承接原库存通道 | -| C01 发布划拨后失效;活动内部库存不失效 | C01 与 Catalog 的内部协作 | 普通库存与活动库存设计待汇总 | 待接口/事件契约承接冻结矩阵 | +| 普通订单与普通库存售后回补后失效 | Catalog 公开库存应用能力,不新增 Axxx | 库存事实待数据库汇总 | M04/M10 下游应用契约已承接原库存通道,待数据库和公开签名 | +| C01 发布划拨后失效;活动内部库存不失效 | C01 与 Catalog 的内部协作 | 普通库存与活动库存设计待汇总 | 接口/事件契约已承接冻结矩阵,待数据库和公开签名 | | Redis 故障回退 PostgreSQL | 继续复用固定首页/A103 响应口径 | Redis 不登记 DBxxx | 待架构和测试承接 | 架构承接章节: diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C10-\351\253\230\345\217\257\347\224\250\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C10-\351\253\230\345\217\257\347\224\250\346\265\201\347\250\213.md" index 463a1ee..389cc8d 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C10-\351\253\230\345\217\257\347\224\250\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C10-\351\253\230\345\217\257\347\224\250\346\265\201\347\250\213.md" @@ -17,7 +17,7 @@ C10 使用 Docker Compose 从同一版本启动 Nginx、PC Web、两个 API 实 |---|---|---| | C10 需求与教师验收 | 完整定义 | 作为 Compose、双 API、单实例故障和登录态验收边界 | | 本文部署流程 | 完整定义 | 冻结 Migrator 门禁、流量准入、能力降级、故障切换、安全恢复和优雅停止 | -| A506、A507 | 部分定义,待按本文补齐 | 承接存活、全局就绪和能力状态,不单独证明业务连续性 | +| A506、A507 | 已按本文重建、未冻结 | 承接存活、全局就绪和能力状态,不单独证明业务连续性;待部署与交叉评审 | | Compose/Nginx/镜像/Secret | 设计阶段,尚无真实资产证据 | 不写成已部署或已验证 | | 各业务模块幂等与数据一致性 | 由各模块负责 | C10 不代替订单、支付、库存等业务规则 | @@ -241,7 +241,7 @@ flowchart TD | 判断全局就绪与能力状态 | A507 `/health/ready` | 无 DBxxx | 待补齐配置/版本/Migration/PostgreSQL 门槛及 Redis/RabbitMQ/SeaweedFS 能力状态 | | 证明两个实例分别响应 | A506/A507 的受控 `instanceId` 或结构化日志 | 无 DBxxx | 待部署资产承接 | | Nginx 转发 WebSockets 跳过协商的 SignalR 连接 | 接口设计 4.3.7“Hub 连接” | Redis Backplane 不登记 DBxxx | 待 Nginx/双实例验证 | -| 实例切换后保持认证授权并失败关闭 | 接口设计 1.6;系统架构 8 | 撤销事实的持久化/可重建来源待数据库设计承接 | 待补齐 Redis 故障与安全恢复契约 | +| 实例切换后保持认证授权并失败关闭 | 接口设计 1.6、4.6;系统架构 8 | 撤销事实的持久化/可重建来源待数据库设计承接 | Redis 故障与安全恢复契约已补齐,待数据库、部署与测试 | | 唯一 Migrator 门禁 | Compose 服务依赖与镜像版本约定,不新增 Axxx | Migration 历史由数据库设计承接 | 待部署资产与失败演练 | | Worker、Outbox 与依赖恢复 | 系统架构 7.4 及对应业务 Worker 契约 | 相关 DBxxx 尚未冻结 | 各模块分别负责 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/M09-\347\253\231\345\206\205\346\266\210\346\201\257\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/M09-\347\253\231\345\206\205\346\266\210\346\201\257\346\265\201\347\250\213.md" index ff719ae..bd2c528 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/M09-\347\253\231\345\206\205\346\266\210\346\201\257\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/M09-\347\253\231\345\206\205\346\266\210\346\201\257\346\265\201\347\250\213.md" @@ -4,7 +4,7 @@ > 覆盖:M09、X03 > 基础核心流程:F08、F09、F10、F12;鉴权依赖 F02、F13;接入 X04 售后来源 > 直接协作:韦乾强(M04 Ordering)、张海洋(M05 Payment、M10 AfterSales)、唐宇昊(M01 Identity) -> 文档状态:完整定义;业务语义已按需求冻结,待接口、数据库、实现和测试承接 +> 文档状态:完整定义;业务语义已按需求冻结,接口已按本文重建,待数据库、实现和测试承接 > 需求事实源:[需求规格说明书 M09](../../../01-需求文档/需求规格说明书.md) 的“M09 站内消息通知(X03)”完整七节 ## 一、范围与事实来源 @@ -17,7 +17,7 @@ M09 负责把订单、支付和售后模块已经提交的业务事实转换为 |---|---|---| | M09/X03 需求 | 完整定义 | 作为角色、规则、异常和验收事实源 | | 本文业务流程 | 完整定义 | 冻结事件入口、固定接收人、整事件原子性、消息状态、异常和模块出口 | -| A501~A505、接口设计 4.3 | 部分定义,待按本文补齐 | 由流程派生并做契约映射,不作为流程输入 | +| A501~A505、接口设计 4.3 | 已按本文重建、未冻结 | 由流程派生并做契约映射,不作为流程输入;待数据库、OpenAPI 与交叉评审 | | DB101~DB120 | 模板/占位,未冻结 | 不发明消息、Inbox 或 Outbox 的具体 DBxxx、字段、约束和索引 | | C06 实时推送 | 独立挑战流程 | 只在消息提交成功后接入,不承担消息持久化 | @@ -159,7 +159,7 @@ stateDiagram-v2 | 售后审核或待寄回 | M10 AfterSales | 申请买家 | 买家售后详情 | 文案必须反映已提交审核结果 | | 买家提交寄回信息 | M10 AfterSales | 订单指定处理商家 | 商家售后详情 | 不按全部商家广播 | | 退款成功或确定失败 | M05 Payment / M10 AfterSales | 申请买家 | 售后详情 | 不通知商家;消息不反向修改退款状态 | -| 支付失败、回调 `Ignored`、回调 `Difference`、对账差异发现或管理员对账处置 | M05 Payment / C08 | 无 | 无 | 记录在支付/对账事实和审计中,不生成 M09 消息 | +| 支付失败、回调 `Ignored`、回调 `Difference`、对账差异发现或管理员对账处置 | M05 Payment / C08 | 无 | 无 | 保留支付业务追踪、回调聚合与对账证据,不生成 M09 消息 | ## 七、异常、补偿与责任 @@ -184,7 +184,7 @@ stateDiagram-v2 | 流程能力 | 当前派生契约 | 数据设计 | 当前状态 | |---|---|---|---| -| 接收已提交事实并按固定矩阵整事件幂等生成消息 | 接口设计 4.3.6“业务模块到 Messaging 的集成事件” | DB101~DB120 尚未分配具体表 | 待按固定矩阵、整事件原子性和无消息事实补齐非 HTTP 契约 | +| 接收已提交事实并按固定矩阵整事件幂等生成消息 | 接口设计 4.3.6“业务模块到 Messaging 的集成事件” | DB101~DB120 尚未分配具体表 | 非 HTTP 契约已按固定矩阵、整事件原子性和无消息事实补齐,待数据库与交叉评审 | | 查询本人消息列表 | A501 | 待 `database-lhc.md` 和数据库主文档确认 | 待交叉评审 | | 查询本人消息详情与安全操作入口 | A502 | 待确认 | 待交叉评审 | | 查询本人未读数 | A503 | 待确认 | 待交叉评审 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" index de42b59..b93d1c3 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" @@ -19,7 +19,7 @@ A001 由本流程派生,仅在流程评审通过后用于契约映射;现有 |---|---|---| | M01-01/F01 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 完整定义 | 已确认角色、判断、状态、事务边界和模块出入口 | -| A001 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| A001 接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | | DBxxx 用户表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | | M00 公共认证能力 | 内部 P0 | 只登记接入点,规则由 M00 维护 | @@ -174,7 +174,7 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 公开注册创建买家账号 | A001 | 拒绝角色字段,依次完成基础校验、手机号唯一性、用户名生成、最终密码校验,并原子创建账号与默认资料;成功后不签发登录凭证 | 流程已确认,待接口同步 | +| 公开注册创建买家账号 | A001 | 拒绝角色字段,依次完成基础校验、手机号唯一性、用户名生成、最终密码校验,并原子创建账号与默认资料;成功后不签发登录凭证 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | 接口仅承载“创建买家账号”这一业务结果;字段格式、错误码和 `traceId` 由接口设计在评审前统一,不得反向写入业务流程。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" index eb3e3bc..8510547 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" @@ -19,7 +19,7 @@ A002~A004 由本流程派生;历史清单中的 A005 刷新凭证没有业 |---|---|---| | M01-02/F02 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 完整定义 | 已确认角色、判断、状态、失效边界和模块出入口 | -| A002~A004 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| A002~A004 接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | | A005 刷新凭证 | 无需求来源 | 取消,不进入实现 | | DBxxx 账号/令牌表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | | C10 多实例认证 | 完整定义 | 任一实例一致验证;共享失效事实不可确认时失败关闭,恢复安全事实后才重新开放 | @@ -183,9 +183,9 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 提交手机号和密码登录 | A002 | 校验凭据与账号状态,只签发一个有明确有效期的 JWT,返回角色与账号摘要 | 流程已确认,待接口同步 | -| 主动退出当前登录态 | A003 | 撤销当前 JWT;已明确撤销时可重复返回相同结果,未知时不得返回成功 | 流程已确认,待接口同步 | -| 登录态恢复与当前账号查询 | A004 | 校验 JWT、账号状态和失效事实,返回当前账号摘要与角色 | 流程已确认,待接口同步 | +| 提交手机号和密码登录 | A002 | 校验凭据与账号状态,只签发一个有明确有效期的 JWT,返回角色与账号摘要 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 主动退出当前登录态 | A003 | 撤销当前 JWT;已明确撤销时可重复返回相同结果,未知时不得返回成功 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 登录态恢复与当前账号查询 | A004 | 校验 JWT、账号状态和失效事实,返回当前账号摘要与角色 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | | 刷新登录凭证 | A005 | 无需求来源;本期凭证到期后重新登录 | 取消,保留历史编号 | 接口必须承载“当前账号可登录”这一业务结果;HTTP 状态码、错误码和 `traceId` 由接口设计在评审前统一,不得反向写入业务流程。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" index e1b102e..c663c35 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" @@ -19,10 +19,10 @@ A006~A008、A010~A014 由本流程派生;历史清单中的 A009 展示资 |---|---|---| | M01-03/F03 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 完整定义 | 已确认角色、判断、状态、事务边界和模块出入口 | -| A006~A008、A010~A014 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| A006~A008、A010~A014 接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | | A009 展示资料修改 | 无需求来源 | 取消,不进入实现 | | DBxxx 用户资料/地址表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | -| M04 地址快照契约 | 部分定义 | 只登记接入点;快照字段由 Ordering 评审 | +| M04 地址快照契约 | 完整定义,待交叉评审 | 已冻结完整收件人、联系电话、省市区和详细地址;M04 在订单事务内保存快照 | ## 二、模块直接出入口 @@ -188,15 +188,15 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 修改手机号 | A006 | 验证当前密码和旧账号状态,校验新手机号格式与唯一性,原子形成新手机号和全部旧凭证失效结果 | 流程已确认,待接口同步 | -| 重置用户名 | A007 | 由服务端生成唯一用户名,仅在提交成功后消耗唯一重置机会,并发最多一个成功 | 流程已确认,待接口同步 | -| 获取本人资料 | A008 | 只返回用户名、默认头像、掩码手机号和用户名重置机会状态 | 流程已确认,待接口同步 | +| 修改手机号 | A006 | 验证当前密码和旧账号状态,校验新手机号格式与唯一性,原子形成新手机号和全部旧凭证失效结果 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 重置用户名 | A007 | 由服务端生成唯一用户名,仅在提交成功后消耗唯一重置机会,并发最多一个成功 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 获取本人资料 | A008 | 只返回用户名、默认头像、掩码手机号和用户名重置机会状态 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | | 修改本人展示资料 | A009 | 展示名、简介等字段没有需求来源 | 取消,保留历史编号 | -| 查询本人地址列表 | A010 | 按当前买家返回本人地址列表与默认标记 | 流程已确认,待接口同步 | -| 新增地址 | A011 | 校验必填字段并固定创建非默认地址,不接受默认标记或固定数量上限 | 流程已确认,待接口同步 | -| 编辑地址 | A012 | 仅修改本人地址并重新校验字段,不允许修改默认标记 | 流程已确认,待接口同步 | -| 删除地址 | A013 | 仅删除本人地址,删除默认地址时不自动指定其他地址 | 流程已确认,待接口同步 | -| 设置默认地址 | A014 | 独立动作原子切换默认地址,保证最终最多一个 | 流程已确认,待接口同步 | +| 查询本人地址列表 | A010 | 按当前买家返回本人地址列表与默认标记 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 新增地址 | A011 | 校验必填字段并固定创建非默认地址,不接受默认标记或固定数量上限 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 编辑地址 | A012 | 仅修改本人地址并重新校验字段,不允许修改默认标记 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 删除地址 | A013 | 仅删除本人地址,删除默认地址时不自动指定其他地址 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 设置默认地址 | A014 | 独立动作原子切换默认地址,保证最终最多一个 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | 接口仅承载本流程结果;错误码、HTTP 状态码和 `traceId` 由接口设计统一,不得反向写入业务流程。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" index 10035c4..94fa257 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -19,7 +19,7 @@ A015~A017 由本流程派生,仅在流程评审通过后用于契约映射 |---|---|---| | M06-03/F13 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 完整定义 | 已确认角色分流、责任阻断、竞争结果、状态与模块出入口 | -| A015~A017 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| A015~A017 接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | | DBxxx 账号/操作记录表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | | C10 多实例凭证校验 | 完整定义 | 所有实例一致遵守账号状态与失效事实;不可确认及安全恢复完成前失败关闭 | @@ -204,9 +204,9 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 分页查询账号 | A015 | 只返回买家和商家账号摘要、受控筛选结果与稳定分页 | 流程已确认,待接口同步 | -| 禁用账号 | A016 | 使用稳定请求标识;先按买家/商家分流,商家保护默认账号并复核固定责任清单;成功时账号禁用、全部旧凭证失效和最小追踪形成确定结果 | 流程已确认,待接口同步 | -| 启用账号 | A017 | 使用稳定请求标识恢复账号状态但不恢复任何旧凭证;重放首次结果或返回当前状态 | 流程已确认,待接口同步 | +| 分页查询账号 | A015 | 只返回买家和商家账号摘要、受控筛选结果与稳定分页 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 禁用账号 | A016 | 使用稳定请求标识;先按买家/商家分流,商家保护默认账号并复核固定责任清单;成功时账号禁用、全部旧凭证失效和最小追踪形成确定结果 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 启用账号 | A017 | 使用稳定请求标识恢复账号状态但不恢复任何旧凭证;重放首次结果或返回当前状态 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | 接口仅承载本流程结果;错误码、HTTP 状态码和 `traceId` 由接口设计统一,不得反向写入业务流程。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" index a7aafa7..df6b3a6 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" @@ -19,7 +19,7 @@ A018~A022、A024、A025 由本流程派生;历史清单中的 A023 清空历 |---|---|---| | M08/X02 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 完整定义 | 已确认角色、幂等、开关、记录上限、并发和模块出入口 | -| A018~A022、A024、A025 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| A018~A022、A024、A025 接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | | A023 清空浏览历史 | 无需求来源 | 取消,不进入实现 | | DBxxx 收藏/浏览表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | | M02 商品事实 | 完整定义,已完成统稿校准 | 只登记接入点;价格、库存与销售状态由 Catalog 权威流程提供 | @@ -170,14 +170,14 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 收藏列表 | A018 | 按收藏时间和记录 ID 稳定分页,返回当前商品摘要与可用状态;下架或不存在商品保留占位 | 流程已确认,待接口同步 | -| 收藏商品 | A019 | 先返回本人既有收藏;没有记录时校验商品已上架再创建,并用唯一性处理并发 | 流程已确认,待接口同步 | -| 取消收藏 | A020 | 按当前买家和商品删除本人收藏;记录不存在时仍返回未收藏成功结果 | 流程已确认,待接口同步 | -| 浏览历史列表 | A021 | 不受开关影响,始终按浏览时间和记录 ID 稳定分页返回已有历史 | 流程已确认,待接口同步 | -| 修改浏览记录开关 | A022 | 设置后续浏览写入开关;关闭不删除、不隐藏旧历史,并与浏览写入形成确定顺序 | 流程已确认,待接口同步 | +| 收藏列表 | A018 | 按收藏时间和记录 ID 稳定分页,返回当前商品摘要与可用状态;下架或不存在商品保留占位 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 收藏商品 | A019 | 先返回本人既有收藏;没有记录时校验商品已上架再创建,并用唯一性处理并发 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 取消收藏 | A020 | 按当前买家和商品删除本人收藏;记录不存在时仍返回未收藏成功结果 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 浏览历史列表 | A021 | 不受开关影响,始终按浏览时间和记录 ID 稳定分页返回已有历史 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 修改浏览记录开关 | A022 | 设置后续浏览写入开关;关闭不删除、不隐藏旧历史,并与浏览写入形成确定顺序 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | | 清空浏览历史 | A023 | 无需求来源;本期保留旧历史且不提供清空能力 | 取消,保留历史编号 | -| 记录浏览历史 | A024 | 服务端重检商品存在且 OnSale,仅在开关开启时写入或更新时间,并同步保留最近 200 条 | 流程已确认,待接口同步 | -| 查询浏览记录开关 | A025 | 无设置事实时返回默认开启,查询不产生写操作 | 流程已确认,待接口同步 | +| 记录浏览历史 | A024 | 服务端重检商品存在且 OnSale,仅在开关开启时写入或更新时间,并同步保留最近 200 条 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 查询浏览记录开关 | A025 | 无设置事实时返回默认开启,查询不产生写操作 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | 接口仅承载本流程结果;错误码、HTTP 状态码和 `traceId` 由接口设计统一,不得反向写入业务流程。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" index 402dec0..eca23b9 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" @@ -49,7 +49,7 @@ flowchart LR 边界规则: -- Worker 和 M05 的过期触发使用系统内部权限,不要求订单买家处于登录状态,也不伪装成买家主动取消。 +- Worker、M05 与 A421/C08 回调处理的过期触发使用系统内部权限,不要求订单买家处于登录状态,也不伪装成买家主动取消。 - 买家主动取消由 M04 接收;如果调用时已经达到截止时间,M04 同样按 `PaymentExpired` 处理。 - 商家和管理员不能人工调用 C03 取消某个订单。 - C03 不读取或修改钱包、支付流水、购物车、售后或消息内部数据。 @@ -101,7 +101,7 @@ flowchart TD flowchart TD A["订单仍显示 PendingPayment"] --> B{"权威时间是否早于 paymentDeadline?"} B -- "是" --> C["M05 / C08 支付可与买家主动取消竞争"] - B -- "否" --> D["支付无条件拒绝;M05 或 Worker 可触发过期取消"] + B -- "否" --> D["支付无条件拒绝;M05、A421/C08 或 Worker 可触发过期取消"] C --> E{"哪个合法动作先提交?"} E -- "支付" --> P["Paid;后续取消读取最终状态并退出"] E -- "主动取消" --> Q["Cancelled;后续支付被拒绝"] @@ -116,7 +116,7 @@ flowchart TD - 达到截止时间后,支付不再是合法竞争者;只剩过期取消的首次执行或重试。 - 到期瞬间若支付已经合法提交为 `Paid`,Worker 读取最终状态后跳过;仅仅“请求已到达”但尚未在截止时间前形成合法结果不算支付成功。 - 取消先提交后到达的同步支付或回调不得把订单改回 `Paid`;C08 的迟到成功进入差异处理。 -- 买家请求、M05 过期触发和 Worker 同时取消时,只有一个取消事务回补,其他入口重放首次结果。 +- 买家请求、M05 过期触发、A421/C08 到期回调和 Worker 同时取消时,只有一个取消事务回补,其他入口重放首次结果。 ## 六、普通与秒杀库存回补 @@ -168,13 +168,13 @@ C03 不派生新的公开 HTTP 接口。它依赖 M04 的 Ordering 内部应用 | 内部业务动作 | 调用方 | 契约必须承载的结果 | |---|---|---| | 查询到期候选 | C03 Worker | 固定截止时间、待支付状态、稳定顺序和有界分页 | -| 过期取消订单 | C03 Worker、M05 过期路径 | 系统身份、到期复核、统一 `PaymentExpired` 原因、首次结果或幂等重放 | +| 过期取消订单 | C03 Worker、M05 过期路径、A421/C08 到期回调 | 系统身份、到期复核、统一 `PaymentExpired` 原因、首次结果或幂等重放 | | 查询取消最终结果 | Worker 恢复 / 重试 | `Cancelled`、其他终态、过期待重试,不泄露无关数据 | 后续接口、架构和数据设计必须承接: 1. 到期条件来自每张订单固定的支付截止时间,不重新用“创建时间 + 当前配置”推导。 -2. 系统内部取消与买家所有权鉴权分离,但仍只允许受信任 Worker / Payment 调用。 +2. 系统内部取消与买家所有权鉴权分离,但仍只允许受信任 Worker、M05 或 A421/C08 回调处理调用。 3. 取消操作必须复用 M04 的状态、原库存通道、限购释放和可靠事实完整结果。 4. 多实例正确性来自共享订单状态与唯一业务推进,不来自单机内存。 5. 任务执行记录与订单业务状态分离;失败次数、退避和告警不能变成新订单状态。 @@ -194,7 +194,7 @@ C03 不派生新的公开 HTTP 接口。它依赖 M04 的 Ordering 内部应用 - [ ] 正式订单在创建后 30 分钟到期;演示参数明确标注且不修改历史订单截止时间。 - [ ] 权威时间早于截止时间才允许支付;达到截止时间后即使 Worker 未扫描也拒绝所有支付通道。 - [ ] Worker 只处理已到期且仍为 `PendingPayment` 的订单,使用有界批次和稳定顺序。 -- [ ] Worker、M05 过期路径和买家到期后取消复用 M04 同一过期取消能力。 +- [ ] Worker、M05 过期路径、A421/C08 到期回调和买家到期后取消复用 M04 同一过期取消能力。 - [ ] 支付与截止前主动取消只有一个结果;截止后支付不再参与竞争。 - [ ] 过期取消暂时失败时订单仍不可支付,恢复后继续取消,不伪装成功。 - [ ] 普通订单恢复 Catalog 库存;秒杀订单恢复原活动独立库存并释放限购数量。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" index a520e04..8e46387 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" @@ -104,18 +104,20 @@ flowchart TD A["状态正常的买家从购物车选择条目和收货地址"] --> B["生成本次提交的唯一幂等标识"] B --> C{"同一买家、同一幂等标识是否已有确定结果?"} C -- "同内容已成功" --> R["重放首次订单号、金额、状态和支付截止时间"] + C -- "同内容已有确定拒绝" --> R2["重放首次业务拒绝
不因库存或配置后来变化改写本次结果"] C -- "换内容复用" --> X["拒绝标识复用,不创建新订单"] C -- "没有结果" --> D["读取本人选中条目、当前数量、商品状态、实时价格和库存来源"] D --> E["读取并校验本人有效地址,解析唯一启用的默认商家"] - E --> F{"所有条目、地址和商家事实均可用?"} - F -- "否" --> Y["整单拒绝;购物车、库存和订单均不变化"] + E --> F{"权威事实是否成功读取并形成明确裁决?"} + F -- "依赖中断或结果不明" --> W["不固化幂等结果
使用同一标识查询或安全重试"] + F -- "明确不可用" --> Y["固化确定业务拒绝
购物车、库存和订单均不变化"] F -- "是" --> G["按权威商品顺序重新校验可售、数量、库存与金额"] G --> H{"每项都可原子扣减?"} - H -- "否" --> Z["整单失败;已发生的临时变更全部不生效"] + H -- "否" --> Z["整体回滚并固化确定业务拒绝
已发生的临时变更全部不生效"] H -- "是" --> I["形成订单号、默认商家、PendingPayment、固定支付截止时间、地址与订单项快照"] I --> J["形成完整原子结果:各原通道库存扣减、订单与订单项、选中购物车清理、订单创建通知事实"] J --> K{"完整结果提交?"} - K -- "否" --> T["全部不生效;购物车和库存保持提交前状态,可安全重试"] + K -- "明确失败或结果未知" --> T["明确回滚时全部不生效
结果未知时保留原标识查询已提交事实后再重试"] K -- "是" --> U["返回订单号、服务端总额、PendingPayment 和支付截止时间"] ``` @@ -129,7 +131,8 @@ flowchart TD - 普通购物车订单由 M04 协调 Catalog 库存扣减;秒杀入口由 C01 协调独立活动库存与限购,并在同一原子边界调用 M04 的统一订单创建能力。两条入口都由 M04 生成共享订单、指定商家、快照和固定支付截止时间,订单项库存来源不得混用。 - 普通库存扣减完整结果提交后触发 C07 失效目标详情和固定首页;秒杀抢购只改变活动独立库存,不触发 C07。缓存失效失败不回滚订单。 - 任一条目不可售、数量非法、库存不足、地址无效、默认商家不可用、购物车清理失败或可靠创建事实失败时,整单不成立。 -- 同一买家、同一幂等标识、同一内容只形成一张订单。第一次结果未知时先查询原结果,不能用相同动作再扣一次库存。 +- 同一买家、同一幂等标识绑定地址与购物车条目指纹。同内容的确定成功或确定业务拒绝均稳定重放,换内容复用被拒绝;库存不足、商品不可售、总额不合法,以及成功读取配置后确认默认商家缺失、重复或禁用属于确定拒绝。 +- 依赖中断、数据库连接失败、事务回滚结果未知或提交结果未知不固化为幂等结果;第一次结果未知时先查询原结果,再用同一标识安全重试,不能用相同动作再扣一次库存。 - 支付截止时间在订单创建时按当时可追踪配置固定;正式口径为创建后 30 分钟,演示参数只能缩短演示等待,不改变正式规则。 - 订单创建成功只通知当前买家;商家待处理提醒在支付成功后产生,避免未付款订单干扰履约。 @@ -320,8 +323,9 @@ flowchart TD | 购物车条目、地址或订单不属于当前买家 | 拒绝且不泄露资源 | 不创建 / 不修改订单 | | 商品下架、数量非法或库存竞争失败 | 整单拒绝 | 不部分扣库存、不清购物车 | | 默认商家缺失、重复、禁用或并发被禁用 | 整单拒绝或按先后唯一裁决 | 不创建无人处理订单 | -| 同幂等标识同内容重试 | 重放首次订单 | 不重复扣库存 | +| 同幂等标识同内容重试 | 重放首次确定成功或确定业务拒绝 | 不重复扣库存,不让同一次提交先拒绝后意外建单 | | 同幂等标识换内容 | 拒绝 | 不覆盖首次结果 | +| 依赖中断、数据库连接失败或事务结果未知 | 不固化失败;先查已有结果,再用同一标识重试 | 最终至多形成一个确定结果 | | 支付与取消并发 | 只有一个状态推进成功 | 不出现既支付又取消 | | 过期取消暂时失败 | 保持过期 `PendingPayment` 并重试 | 仍不可支付,库存不丢失 | | 普通 / 秒杀回补任一步失败 | 整个取消不成立 | 不出现取消但未完整回补 | @@ -342,36 +346,37 @@ flowchart TD ## 十二、由流程派生的接口契约映射 -本节是业务流程的下游映射。现有接口若缺少截止时间、指定商家、幂等、来源库存、完成方式或状态竞争,应重建契约,不得删除前文分支迁就旧请求体。 +本节是业务流程的下游映射。A301~A304、A308 已按截止时间、指定商家、幂等、来源库存、完成方式和状态竞争重建详细契约;后续数据库、OpenAPI 与实现不得删除前文分支迁就旧请求体。 | 已确认业务能力 | 派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 购物车提交订单 | A301 | 必填幂等标识、选中购物车条目标识、本人地址;服务端金额 / 商家 / 截止时间;整单原子结果 | 待重建详细契约 | -| 买家订单列表 | A302 | 本人隔离、五状态筛选、稳定分页、最新摘要 | 待重建详细契约 | -| 买家订单详情 | A303 | 全部快照、截止时间、状态时间线、真实支付来源、售后与操作入口事实 | 待重建详细契约 | -| 买家取消订单 | A304 | 本人授权、首次取消或幂等重放、按时间确定取消原因、原库存通道完整回补 | 待重建详细契约 | -| 买家确认收货 | A308 | 本人 `Shipped → Completed`、二次确认、主动 / 自动完成竞争的当前结果 | 待重建详细契约 | -| 支付状态推进 | Payment 内部应用契约 | 应付金额、截止时间和 `PendingPayment → Paid` 唯一竞争 | 待按流程补齐 | -| 过期取消 | Ordering 内部应用契约 | 系统身份、到期复核、统一取消与幂等回补 | 待按流程补齐 | -| 自动完成 | Ordering 内部应用契约 | 到期复核、`Shipped → Completed` 与主动确认竞争 | 待按流程补齐 | +| 购物车提交订单 | A301 | 必填幂等标识、选中购物车条目标识、本人地址;服务端金额 / 商家 / 截止时间;确定成功与确定拒绝重放,瞬态失败不固化;整单原子结果 | 接口完整定义,待数据库、公开应用签名、OpenAPI 与交叉评审 | +| 买家订单列表 | A302 | 本人隔离、五状态筛选、稳定分页、最新摘要 | 接口完整定义,待数据库、公开应用签名、OpenAPI 与交叉评审 | +| 买家订单详情 | A303 | 全部快照、截止时间、状态时间线、真实支付来源、售后与操作入口事实 | 接口完整定义,待数据库、公开应用签名、OpenAPI 与交叉评审 | +| 买家取消订单 | A304 | 本人授权、首次取消或幂等重放、按时间确定取消原因、原库存通道完整回补 | 接口完整定义,待数据库、公开应用签名、OpenAPI 与交叉评审 | +| 买家确认收货 | A308 | 本人 `Shipped → Completed`、二次确认、主动 / 自动完成竞争的当前结果 | 接口完整定义,待数据库、公开应用签名、OpenAPI 与交叉评审 | +| 支付状态推进 | Payment 内部应用契约 | 应付金额、截止时间和 `PendingPayment → Paid` 唯一竞争 | 已按流程登记跨模块边界;待公开应用签名、数据库与测试承接 | +| 过期取消 | Ordering 内部应用契约 | 系统身份、到期复核、统一取消与幂等回补 | 已按流程登记跨模块边界;待公开应用签名、数据库与测试承接 | +| 自动完成 | Ordering 内部应用契约 | 到期复核、`Shipped → Completed` 与主动确认竞争 | 已按流程登记跨模块边界;待公开应用签名、数据库与测试承接 | A305~A307 由 M06-02 商家履约流程派生,本文件不以旧商家接口反向定义商家页面。 接口阶段必须满足: 1. A301 的幂等标识为必填;请求只传选中购物车条目标识和地址标识,不传最终数量清单、价格、总额或商家。 -2. A304 对已 `Cancelled` 的本人订单重放首次成功,对 `Paid` / `Shipped` / `Completed` 明确拒绝;调用时达到截止时间则使用过期原因。 -3. A303 在过期但尚未取消成功时必须表达“不可支付、取消待重试”,不能仅凭 `PendingPayment` 展示支付按钮。 -4. Payment 内部契约同时校验状态和截止时间;成功来源可为小金库或受控模拟通道。 -5. A308 返回唯一完成时间和方式;已完成重试不重复通知。 -6. 所有资源归属都由服务端当前身份判断,错误响应不泄露他人订单存在性。 +2. A301 同内容重放首次确定成功或确定业务拒绝,换内容复用拒绝;依赖中断、连接失败和事务结果未知不得固化,可用同一标识安全重试。 +3. A304 对已 `Cancelled` 的本人订单重放首次成功,对 `Paid` / `Shipped` / `Completed` 明确拒绝;调用时达到截止时间则使用过期原因。 +4. A303 在过期但尚未取消成功时必须表达“不可支付、取消待重试”,不能仅凭 `PendingPayment` 展示支付按钮。 +5. Payment 内部契约同时校验状态和截止时间;成功来源可为小金库或受控模拟通道。 +6. A308 返回唯一完成时间和方式;已完成重试不重复通知。 +7. 所有资源归属都由服务端当前身份判断,错误响应不泄露他人订单存在性。 ## 十三、验收证据清单 - [ ] 提交订单只使用本人选中购物车条目、本人地址和必填幂等标识;服务端重读数量、价格、库存和默认商家。 - [ ] 正常下单原子形成库存扣减、订单 / 订单项快照、购物车清理和创建通知事实,并返回固定支付截止时间。 - [ ] 任一商品不可售、库存不足、地址无效、默认商家不可用或可靠事实失败时整单不成立,库存和购物车不留部分变化。 -- [ ] 同幂等标识同内容返回首次订单;换内容拒绝;未知结果先查询原订单,不重复扣库存。 +- [ ] 同幂等标识同内容重放首次确定成功或确定业务拒绝,换内容拒绝;瞬态失败不固化,未知结果先查询原订单再用同一标识重试,不重复扣库存。 - [ ] 默认商家分配与账号禁用并发时只有一个合法结果,不产生无人处理订单。 - [ ] 买家列表和详情严格隔离,五种状态筛选、快照、金额、时间线和真实支付来源正确。 - [ ] 截止时间前可支付;达到截止时间后即使 C03 未运行也不能支付。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M06-02-\345\225\206\345\256\266\345\261\245\347\272\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M06-02-\345\225\206\345\256\266\345\261\245\347\272\246\346\265\201\347\250\213.md" index 997efae..a7ef39c 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M06-02-\345\225\206\345\256\266\345\261\245\347\272\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M06-02-\345\225\206\345\256\266\345\261\245\347\272\246\346\265\201\347\250\213.md" @@ -201,11 +201,11 @@ flowchart TD | 已确认业务能力 | 派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 商家订单列表 | A305 | 当前 `assignedMerchantUserId` 强制范围、五状态筛选、稳定分页和最小买家摘要 | 待重建详细契约 | -| 商家订单详情 | A306 | 授权快照、最小敏感信息、售后阻断、已退款和剩余可履约数量 | 待重建详细契约 | -| 商家发货 | A307 | 必填稳定请求身份、服务端实际发货数量、售后竞争、`Paid → Shipped` 和幂等重放 | 待重建详细契约 | -| 售后履约快照 | AfterSales 内部应用契约 | 非终态申请、已退款数量、剩余可履约数量和同订单串行复核 | 待按流程补齐 | -| 发货状态推进 | Ordering 内部应用契约 | 指定商家、`Paid → Shipped`、时间 / 数量 / 说明和可靠通知事实 | 待按流程补齐 | +| 商家订单列表 | A305 | 当前 `assignedMerchantUserId` 强制范围、五状态筛选、稳定分页和最小买家摘要 | 接口完整定义,待数据库、公开应用签名、OpenAPI 与交叉评审 | +| 商家订单详情 | A306 | 授权快照、最小敏感信息、售后阻断、已退款和剩余可履约数量 | 接口完整定义,待数据库、公开应用签名、OpenAPI 与交叉评审 | +| 商家发货 | A307 | 必填稳定请求身份、服务端实际发货数量、售后竞争、`Paid → Shipped` 和幂等重放 | 接口完整定义,待数据库、公开应用签名、OpenAPI 与交叉评审 | +| 售后履约快照 | AfterSales 内部应用契约 | 非终态申请、已退款数量、剩余可履约数量和同订单串行复核 | 已按流程登记跨模块边界;待公开应用签名、数据库与测试承接 | +| 发货状态推进 | Ordering 内部应用契约 | 指定商家、`Paid → Shipped`、时间 / 数量 / 说明和可靠通知事实 | 已按流程登记跨模块边界;待公开应用签名、数据库与测试承接 | 接口阶段必须满足: diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" index d67114e..5016bf0 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" @@ -3,7 +3,7 @@ > 负责人:朱惠惠 > 覆盖:C01-01、M03-01 与 M04-01 的秒杀衔接、X04 不参与秒杀取消回补 > 基础核心流程:F11(商品上下架)、F04/F06(活动浏览)、F08(下单)、F10(支付)、F09/F12(取消 / 发货) -> 直接协作:顾欣月(M02 Catalog 与 M06-01 活动维护)、唐宇昊(M01 默认商家与账号状态)、韦乾强(M04 Ordering 与 C03 超时取消)、张海洋(M05 Payment 与 C08 回调) +> 直接协作:顾欣月(M02 Catalog 与 M06-01 商品、普通库存协作)、唐宇昊(M01 默认商家与账号状态)、韦乾强(M04 Ordering 与 C03 超时取消)、张海洋(M05 Payment 与 C08 回调);秒杀活动维护仍由 C01 负责 > 文档状态:已按需求校准,可作为接口与数据库设计输入;待 Catalog/Ordering 交叉评审 > 需求事实源:[需求规格说明书 C01](../../../01-需求文档/需求规格说明书.md) 的“C01 秒杀与防超卖”完整七节 @@ -19,7 +19,7 @@ | 本文业务流程 | 已校准、待交叉评审 | 明确状态、动作、原子结果、异常与模块边界 | | M04/M05/M06-02/M09/M10 核心流程 | 完整定义,已完成统稿校准 | 复用其公开业务出入口,不建立第二套订单链路 | | C03/C07/C08/C10 挑战流程 | 完整定义,相交边界已冻结 | 只承接与秒杀直接相交的责任 | -| A220~A228 接口 | 部分定义、未冻结 | 待按本文第九章重新派生和补齐 | +| A220~A228 接口 | 已按本文重建、未冻结 | 待数据库、OpenAPI、公开应用签名与交叉评审 | | 秒杀相关数据设计 | 模板/占位 | 待全部流程完成后从零统一设计 | | X04 售后退款 | 独立扩展 | 不并入“待支付订单取消回补”流程 | @@ -27,10 +27,10 @@ ```mermaid flowchart LR - MERCHANT["已认证且账号正常的商家"] -->|"维护本人有权管理商品的秒杀活动"| SEC["C01 秒杀
活动生命周期、独立库存、个人限购"] + MERCHANT["已认证且账号正常的商家"] -->|"从统一经营目录选择商品,维护本人创建的秒杀活动"| SEC["C01 秒杀
活动生命周期、独立库存、个人限购"] PUBLIC["游客 / 买家"] -->|"浏览即将开始或进行中的活动"| SEC BUYER["已认证且账号正常的买家"] -->|"立即抢购:活动、数量、收货地址、稳定请求标识"| SEC - CAT["M02 Catalog
商品归属、销售状态、普通可售库存与当前价格"] -->|"创建、发布前校验与库存划拨输入"| SEC + CAT["M02 Catalog
统一经营目录、销售状态、普通可售库存与当前价格"] -->|"创建、发布前校验与库存划拨输入"| SEC SEC -->|"权威活动状态、倒计时、剩余库存、已售数量"| PUBLIC SEC -->|"原子成功:秒杀库存扣减、限购占用、共享订单与价格快照"| ORD["M04 Ordering"] @@ -48,7 +48,7 @@ flowchart LR 边界约束: - 游客可以浏览公开活动,但抢购必须使用已认证且状态正常的买家身份;商家和管理员不能以其当前角色参与抢购。 -- 商家只能维护本人有权管理商品的活动,也只能查看本人活动的经营结果;越权时按不存在 / 无权限拒绝,不泄露他人活动。 +- 所有状态正常的商家都从同一经营目录选择合格商品,不建立商品所有权;活动创建人只能维护和查看本人创建的活动及其经营结果,越权时按不存在 / 无权限拒绝,不泄露他人活动。 - 秒杀订单复用 M04 的共享订单状态机,不建立独立订单表或独立支付、发货、消息通道。 - 秒杀库存从普通可售库存中一次性划拨后成为独立通道;普通下单不能消耗它,取消回补也不能写回普通库存。 - C01 不读写购物车;秒杀成功、取消或回补均不改变 M03 条目。 @@ -59,7 +59,7 @@ flowchart LR ```mermaid flowchart TD - A["商家进入本人秒杀活动管理"] --> B{"身份正常且有权管理目标商品?"} + A["商家进入本人秒杀活动管理"] --> B{"身份正常且目标商品属于统一经营目录?"} B -- "否" --> X["拒绝,不创建或修改活动"] B -- "是" --> C{"新建还是编辑草稿?"} C -- "新建" --> D["选择已上架商品,填写开始/结束时间、秒杀价、计划秒杀量和单用户限购"] @@ -71,8 +71,8 @@ flowchart TD F -- "是" --> G["保存 Draft 与计划秒杀量;此时不扣普通库存、不产生可抢库存"] G --> H{"商家确认发布?"} H -- "否" --> I["保持 Draft,可继续编辑或取消"] - H -- "是" --> J["重新读取商品归属、销售状态、当前普通可售库存和权威时间"] - J --> K{"仍属于本人、仍可售、尚未到开始时间且普通库存足够?"} + H -- "是" --> J["重新读取商品存在性、销售状态、当前普通可售库存和权威时间"] + J --> K{"商品仍存在且可售、尚未到开始时间且普通库存足够?"} K -- "否" --> L["拒绝发布,草稿与普通库存均保持原状"] K -- "是" --> M["在一个原子结果中把计划量从普通库存划入独立秒杀库存,并推进为 Published"] M --> N{"原子结果整体成功?"} @@ -84,7 +84,7 @@ flowchart TD - 草稿只记录“计划秒杀量”,不代表库存已分配,也不能向买家暴露为可抢库存。 - 保存草稿时即按权威当前时间和普通可售库存检查输入,但这只是“计划量可行性”校验;发布时必须再次读取并校验,不能用草稿保存时的旧库存代替发布判断。 -- 发布是库存归属切换点:必须重新校验商品仍归该商家管理、仍可售、开始时间尚未来到且普通库存足够;划拨与状态推进必须同时成功或同时失败。 +- 发布是库存通道切换点:必须重新校验统一经营目录中的商品仍存在且可售、开始时间尚未来到且普通库存足够;划拨与状态推进必须同时成功或同时失败。 - 发布后不再允许修改商品、价格、时间、秒杀量或限购,避免已公开规则与库存事实发生漂移;商家需要变更时应取消原活动并新建草稿。 - 同一活动只能成功划拨一次;网络重试或重复点击发布不得再次减少普通库存。 - 秒杀价必须为正数;计划秒杀量和单用户限购必须为正整数,且单用户限购不得超过计划秒杀量。 @@ -147,18 +147,20 @@ flowchart TD C -- "同标识不同请求" --> Y["拒绝复用标识,不产生副作用"] C -- "全新请求" --> D{"当前流量是否在可承载上限内?"} D -- "否" --> Z["形成过载的确定业务结果;普通商品入口继续可用"] - D -- "是" --> E["读取活动、商品、买家当前限购占用、地址归属和唯一启用的默认商家"] - E --> F{"活动为 Ongoing、权威时间在窗口内、商品匹配、地址归本人、默认商家唯一可用且请求未超限?"} - F -- "否" --> G["形成未开始 / 已结束 / 已取消 / 超限 / 地址无效 / 默认商家不可用等确定业务结果"] + D -- "是" --> E["读取活动、商品与买家当前限购占用"] + E --> F{"活动为 Ongoing、权威时间在窗口内、商品匹配且请求未超限?"} + F -- "否" --> G["形成未开始 / 已结束 / 已取消 / 超限等确定业务结果"] F -- "是" --> H["开启短事务"] H --> I["以活动、状态、时间窗口和剩余量为条件原子扣减独立秒杀库存"] I --> J{"扣减是否成功?"} - J -- "否" --> K["回滚主事务,形成售罄 / 状态竞争等确定业务结果"] + J -- "否" --> K["回滚主事务,形成库存 / 状态竞争或订单输入失败等确定业务结果"] J -- "是" --> L["原子增加当前买家的活动限购占用,且不得超过上限"] L --> M{"限购占用是否成功?"} M -- "否" --> K M -- "是" --> N["在同一原子边界调用 M04 统一订单创建能力"] - N --> N1["M04 生成共享待支付订单,以唯一启用的默认商家写入 assignedMerchantUserId,保存固定支付截止时间、地址与订单项快照,并记录秒杀来源、活动和成交价"] + N --> N0{"M04 对地址归属、唯一启用默认商家和订单输入复核是否通过?"} + N0 -- "否" --> K + N0 -- "是" --> N1["M04 生成共享待支付订单,以唯一启用的默认商家写入 assignedMerchantUserId,保存固定支付截止时间、地址与订单项快照,并记录秒杀来源、活动和成交价"] N1 --> O["可靠记录订单已创建事实,并绑定本次稳定请求结果"] O --> P{"库存、限购、订单、快照、可靠事实与请求结果是否整体提交?"} P -- "否" --> T["整体回滚;属于未形成确定结果的瞬态失败,原标识可重试"] @@ -180,7 +182,7 @@ flowchart TD - 成功、未开始、已结束、已取消、售罄、超限、地址无效、默认商家不可用和已正式返回的过载结果都属于可重放的确定业务结果,必须先与稳定请求标识持久绑定再返回。数据库连接中断、事务提交失败等无法确认结果的瞬态故障不得伪装成确定业务失败,也不得固化;客户端使用原标识重试。 - 买家限购以“同一活动下当前有效占用量”作为唯一并发事实;待支付与已支付订单都占用名额,只有取消成功才释放。 - 事务保持短小,只处理单个活动和本次订单;事务内不调用外部 HTTP、不等待用户输入、不发送即时消息、不做长计算或全表扫描。 -- 秒杀价、商品归属、订单金额和快照都由服务端重读并计算;客户端价格只能用于展示,不能决定成交金额。 +- 商品有效性、秒杀价、订单金额和快照都由服务端重读并计算;客户端价格只能用于展示,不能决定成交金额。 - 请求充足且没有身份、时间、限购等业务失败时,系统必须持续接受可处理请求直至库存售罄;限流配置不得导致库存仍有剩余却提前停止销售。 ## 六、待支付订单取消、回补与限购释放 @@ -231,7 +233,7 @@ flowchart TD ## 八、跨模块衔接 -- **M02 Catalog / M06-01 商家运营**:提供商品归属、销售状态、普通库存和当前价格;发布时完成普通库存到秒杀库存的原子划拨,之后两个通道互不混用。 +- **M02 Catalog / M06-01 商家运营**:提供统一经营目录中的商品存在性、销售状态、普通库存和当前价格;发布时完成普通库存到秒杀库存的原子划拨,之后两个通道互不混用。 - **M03 Cart**:秒杀立即抢购绕过购物车,成功、失败、取消和回补均不读写购物车条目。 - **M01 Identity / M04 Ordering**:C01 在同一原子边界调用 M04 统一订单创建能力;M04 从 M01 解析唯一启用的默认商家并写入 `assignedMerchantUserId`,生成共享 `PendingPayment` 订单、固定支付截止时间、地址与订单项快照,并保留秒杀来源、活动和成交价,供查询、取消与追溯。活动创建人不替代履约商家,C01 不建立第二套订单创建路径。 - **M05 Payment / C08 回调**:秒杀订单沿用统一支付和幂等回写;支付成功与取消竞争由订单状态条件推进裁决。 @@ -246,25 +248,26 @@ flowchart TD | 已确认流程能力 | 派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 商家创建草稿 | A220 | 校验商品管理权与活动规则,只保存草稿和计划量,不划拨库存 | 待重建详细契约 | -| 商家更新草稿 | A221 | 仅 `Draft` 可修改;发布后拒绝编辑 | 待重建详细契约 | -| 商家发布活动 | A222 | 重新校验并原子划拨普通库存,成功后进入 `Published`;重复请求不重复划拨 | 待重建详细契约 | -| 商家取消活动 | A223 | 仅 `Draft` / `Published` / `Ongoing` 可取消;不回收已划拨库存 | 待重建详细契约 | -| 商家活动列表 | A224 | 仅本人有权管理的活动,支持状态、时间和关键词筛选 | 待重建详细契约 | -| 商家活动详情 | A225 | 返回本人活动、库存与订单汇总;越权不泄露存在性 | 待重建详细契约 | -| 买家公开活动列表 | A226 | 返回 `Published` / `Ongoing` 活动及需求规定的完整展示信息 | 待重建详细契约 | -| 买家公开活动详情 | A227 | 返回权威倒计时、剩余库存、已售数量及当前买家限购提示 | 待重建详细契约 | -| 买家秒杀下单 | A228 | 稳定请求重放、过载保护、条件扣减、限购占用,并由 M04 统一生成共享订单、唯一启用的默认 `assignedMerchantUserId`、固定支付截止时间与快照 | 待重建详细契约 | +| 商家创建草稿 | A220 | 校验统一经营目录中的商品存在性、可售性与活动规则,只保存草稿和计划量,不划拨库存 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 商家更新草稿 | A221 | 仅 `Draft` 可修改;发布后拒绝编辑 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 商家发布活动 | A222 | 重新校验并原子划拨普通库存,成功后进入 `Published`;重复请求不重复划拨 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 商家取消活动 | A223 | 仅 `Draft` / `Published` / `Ongoing` 可取消;不回收已划拨库存 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 商家活动列表 | A224 | 仅本人有权管理的活动,支持状态、时间和关键词筛选 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 商家活动详情 | A225 | 返回本人活动、库存与订单汇总;越权不泄露存在性 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 买家公开活动列表 | A226 | 返回 `Published` / `Ongoing` 活动及需求规定的完整展示信息 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 买家公开活动详情 | A227 | 返回权威倒计时、剩余库存、已售数量及当前买家限购提示 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 买家秒杀下单 | A228 | 稳定请求重放、过载保护、条件扣减、限购占用,并由 M04 统一生成共享订单、唯一启用的默认 `assignedMerchantUserId`、固定支付截止时间与快照 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | | 秒杀订单查询 | 复用 A302 / A303 | 按买家归属查询共享订单和秒杀追溯信息 | 由 M04 契约承载 | | 取消与秒杀回补 | 复用 M04 公开取消契约 | 首次成功取消时按原通道回补并释放限购 | 由 M04 / C03 契约承载 | +| 历史秒杀订单列表 / 详情编号 | A229 / A230 已取消 | 不建立第二套秒杀订单查询;统一由 A302 / A303 返回共享订单及秒杀追溯信息 | 已登记历史取消号 | -HTTP 路径、方法、请求响应字段、状态码、错误码、幂等标识传递方式和 OpenAPI Schema 均在下一阶段由本表派生;不得把旧草案中的字段或错误码反向写回业务流程。 +HTTP 路径、方法、请求响应字段、状态码、错误码、幂等标识传递方式已由本表派生到《接口设计》;真实 OpenAPI Schema、数据库和实现仍须继续承接,不得把旧草案中的字段或错误码反向写回业务流程。 ## 十、接口与数据库后续设计必须承接的事实 1. 接口必须区分草稿保存、发布划拨、状态取消、公开浏览和立即抢购,不能把多个原子边界拼成一个含糊动作。 2. 发布契约必须返回“划拨成功且状态已推进”或“全部未发生”中的一种结果;数据库据此保证同一活动最多成功划拨一次。 -3. 抢购契约必须携带活动、正整数数量、本人收货地址和稳定请求标识;成交价、商品归属、限购和库存全部由服务端确定。 +3. 抢购契约必须携带活动、正整数数量、本人收货地址和稳定请求标识;商品有效性、成交价、限购和库存全部由服务端确定。 4. 数据库必须表达活动状态、计划量、已划拨总量、剩余量、已售量、每名买家当前占用量、共享订单追溯信息、稳定请求结果和取消是否已回补;具体表名与字段在统一数据库设计中确定。 5. 活动期间必须满足“剩余量 + 已售量 = 已划拨总量”;本期不引入冻结量。取消成功时剩余量增加、已售量减少,二者仍保持恒等。 6. 同一活动的买家当前占用量不得超过单用户限购;同一订单最多释放一次,同一稳定请求最多形成一个确定订单结果。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" index d8dc73e..3d7c685 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" @@ -17,7 +17,7 @@ |---|---|---| | M03-01/F07 需求 | 完整定义 | 作为购物车业务语义事实源 | | 本文业务流程 | 完整定义,已完成统稿校准 | 参与者、上游输入、状态派生、原子结果和模块出入口已闭合,可作为下游设计输入 | -| A2xx 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| A2xx 接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | | 购物车相关表(条目、幂等记录) | 模板/占位 | 本文不发明表名、字段、约束或索引 | | X02 收藏与浏览历史 | 独立扩展 | 仅登记边界,不混入 F07 主流程 | | C01 秒杀 | 独立扩展 | 立即抢购绕过购物车,C01 仅与本文确定“不读写购物车”的边界 | @@ -35,7 +35,7 @@ flowchart LR ORD -->|"提交事务成功:清理已下单条目"| CART ORD -->|"事务回滚:购物车条目原状保留"| CART - CAT -->|"商品下架 / 库存归零 / 启用状态变更"| CART + CAT -->|"商品变为 Draft / OffSale,或实时可售库存归零"| CART ID -->|"游客、商家、管理员或账号禁用"| X["拒绝访问,不创建、不读写购物车"] CAT -->|"商品不存在或非已上架"| Y["加购 / 调大请求拒绝"] @@ -121,7 +121,7 @@ flowchart TD I --> F1 ``` -> 说明:调小路径不以实时库存上限作为拒绝条件(库存可能已下降但仍允许把数量往下调),仅校验“数量 > 0”与归属;保存后再按商品是否上架、是否启用以及新数量是否不超过实时库存派生可结算状态。只有商品仍可售且新数量已落入库存上限时,条目才恢复可结算。调大请求必须同时校验销售状态和实时库存上限;数量相等是无副作用操作,不得误判为调大。 +> 说明:调小路径不以实时库存上限作为拒绝条件(库存可能已下降但仍允许把数量往下调),仅校验“数量 > 0”与归属;保存后再按商品是否处于 `OnSale` 以及新数量是否不超过实时库存派生可结算状态。分类是否有效不参与购物车结算判断。只有商品仍可售且新数量已落入库存上限时,条目才恢复可结算。调大请求必须同时校验销售状态和实时库存上限;数量相等是无副作用操作,不得误判为调大。 ### 4.3 删除与清空 @@ -271,8 +271,8 @@ flowchart TD - [ ] 同一买家同一商品多次加入只生成一条记录,数量按调用顺序正确累加,最终数量不超过实时库存上限。 - [ ] 修改数量超过商品实时可售库存时拒绝并返回最大可设值;改为 0、负数或非整数被拒绝并提示原因。 - [ ] 选中条目总额由服务端按实时单价计算,前端篡改金额或数量再提交被服务端拒绝,订单总额与数据库一致。 -- [ ] 商品下架后,已加入条目在购物车页标记“不可结算”,不可调大、不可累加、不能勾选进入结算;库存为 0 或被禁用同样标记。 -- [ ] 失效条目可下调数量、可删除;仅当商品仍处于上架且启用状态、下调后的数量不超过实时可售库存时,条目恢复可结算状态。 +- [ ] 商品变为 `Draft` / `OffSale` 后,已加入条目在购物车页标记“不可结算”,不可调大、不可累加、不能勾选进入结算;实时可售库存为 0 时同样标记;分类停用不反向改变仍为 `OnSale` 商品的可结算资格。 +- [ ] 失效条目可下调数量、可删除;仅当商品处于 `OnSale` 且下调后的数量不超过实时可售库存时,条目恢复可结算状态。 - [ ] 越权:用买家 B 身份请求买家 A 的条目被拒绝,响应不暴露该条目是否存在及归属信息。 - [ ] 下单成功后,对应购物车条目在同一事务内被清除;订单事务回滚时购物车条目原状保留,未出现“订单失败但条目丢失”。 - [ ] 买家主动取消订单或 C03 超时取消后,对应购物车条目本期不自动恢复。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" index a15e475..b833b4a 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" @@ -21,7 +21,7 @@ F10 的默认买家路径仍为 M05 小金库同步支付。C08 不扣买家小 | C08 需求与教师挑战目标 | 完整定义 | 作为回调、对账和差异闭环事实源 | | F10/F09/M10 流程 | 完整定义,已完成统稿校准 | 作为支付、取消和退款协作边界 | | 本文业务流程 | 已校准、待交叉评审 | 冻结回调终态、原子结果、截止时间与对账闭环 | -| A421~A426 接口 | 部分定义、未冻结 | 待按第十一章重新派生 | +| A421~A426 接口 | 已按第十一章重建、未冻结 | 待数据库、OpenAPI、公开应用签名与交叉评审 | | 回调、支付、对账数据设计 | 模板/占位 | 流程完成后统一派生,不在业务图中预设字段 | ## 二、参与者与模块直接出入口 @@ -289,12 +289,12 @@ flowchart TD | 已确认流程能力 | 派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 接收受控模拟支付回调 | A421 | 来源鉴别、回调标识幂等、支付流水聚合与乱序、截止时间和状态竞争、四种确定终态 | 待重建详细契约 | -| 对账批次列表 | A422 | 管理员按日期与状态分页查看 `Matched` / `HasDifferences` / `Resolved` 批次 | 待重建详细契约 | -| 对账批次详情 | A423 | 返回范围、总数、匹配数、差异数、类型汇总和当前闭环状态 | 待重建详细契约 | -| 差异列表 | A424 | 管理员按批次、类型和状态分页查询差异摘要 | 待重建详细契约 | -| 差异领取、转交与解决 | A425 | 条件领取 / 接管、受控处置类型、权威事实复核、原子关闭差异并按需关闭批次 | 待重建详细契约 | -| 差异详情 | A426 | 返回比较规则、期望与实际事实、全部来源证据、领取/转交/处置和复核时间线 | 待重建详细契约 | +| 接收受控模拟支付回调 | A421 | 来源鉴别、回调标识幂等、支付流水聚合与乱序、截止时间和状态竞争、四种确定终态 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 对账批次列表 | A422 | 管理员按日期与状态分页查看 `Matched` / `HasDifferences` / `Resolved` 批次 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 对账批次详情 | A423 | 返回范围、总数、匹配数、差异数、类型汇总和当前闭环状态 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 差异列表 | A424 | 管理员按批次、类型和状态分页查询差异摘要 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 差异领取、转交与解决 | A425 | 条件领取 / 接管、受控处置类型、权威事实复核、原子关闭差异并按需关闭批次 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 差异详情 | A426 | 返回比较规则、期望与实际事实、全部来源证据、领取/转交/处置和复核时间线 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | A421 以回调标识和请求指纹保证同一次投递幂等,以支付流水聚合一次支付尝试的多个时序信号,不要求客户端另造独立幂等语义;A422~A426 只向管理员开放。具体 HTTP 方法、路径、签名头、请求响应、状态码和错误码在接口阶段由本表派生。 @@ -319,7 +319,7 @@ A421 以回调标识和请求指纹保证同一次投递幂等,以支付流水 8. 支付成功来源必须区分小金库与受控模拟通道;同一订单最多一个成功来源,回调不得生成钱包流水。 9. 管理员差异处理不直接更新业务表;需要纠正时必须引用所属模块的受控操作结果,并由系统重跑原比较规则确认已经一致。 10. 对账归属只使用成功提交时间和固定 UTC 水位;领取转交、处置类型、复核失败和当前领取人权限必须由接口承载。 -11. 接口完成后从本文逐项复核鉴别、幂等、截止时间、乱序、事务、批次和差异状态,不能用现有 A421~A426 草案反向修改流程。 +11. A421~A426 已从本文逐项复核鉴别、幂等、截止时间、乱序、事务、批次和差异状态;后续数据库、OpenAPI 与实现继续承接,不能用旧接口草案反向修改流程。 ## 十四、验收证据清单 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" index e97bd32..b11b6a7 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" @@ -17,7 +17,7 @@ |---|---|---| | M05-01/F10 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 完整定义,已完成统稿校准 | 角色、状态、分支、事务边界和模块出入口已闭合,可作为下游设计输入 | -| A401~A408 接口 | 部分定义、未冻结 | 由流程派生并做映射;冲突记为接口待评审项 | +| A401~A408 接口 | 完整定义,待交叉评审 | 已按本文流程派生并闭合核心契约;后续评审不得反向改写业务语义 | | DB081~DB100 数据库设计 | 模板/占位 | 本文不发明表字段、枚举编码和索引 | | X04/C08 | 独立扩展 | 只登记接入点,不混入 F10 核心状态机 | @@ -210,7 +210,7 @@ flowchart TD | 查询本人支付记录 | A407 | 按当前买家隔离并分页返回支付记录 | 待交叉评审 | | 查询本人支付详情 | A408 | 仅返回当前买家可访问的单笔支付详情 | 待交叉评审 | -接口详细定义与实现必须承接上述流程结果。当前接口设计拟使用 `Idempotency-Key` 承载“防重复标识”,并需满足接口设计 1.12 的幂等与并发规则以及 4.6 的资金类持久化幂等约束;HTTP 状态码、请求字段和错误码不得反向写入业务图。 +接口详细定义与实现必须承接上述流程结果。当前接口设计使用 `Idempotency-Key` 承载“防重复标识”,并按接口设计 1.12、1.12.1 的通用规则和资金类专用保留期,把充值、支付及其确定结果持久化;HTTP 状态码、请求字段和错误码不得反向写入业务图。 ## 九、扩展接入边界 @@ -218,17 +218,17 @@ flowchart TD - C08 是不扣小金库的受控挑战模拟通道,不替换 F10 默认同步钱包路径。C08 成功回调与 F10、F09、C03 共同以订单状态和支付截止时间竞争:最多一方把待支付订单推进为已支付或已取消;回调先成功时后续钱包支付返回已有已支付结果且不扣款,钱包支付先成功时新的成功回调登记差异且不得重复入账。 - X03/M09 只消费支付事务提交后的支付成功事实;消息失败不能反向修改支付或订单状态。具体事件名和可靠投递机制由系统架构设计派生。 -## 十、由流程反查出的接口与数据待评审项 - -1. 已支付订单进入收银台时,流程要求返回已有确定结果;现有 A404 同时出现“非 `PendingPayment` 返回 409”和“已支付收银台仍可读”,接口语义需要按流程统一。 -2. 同 Key、同请求重放必须返回首次确定结果;现有 A405 对已支付订单定义为 `409 PAYMENT.ALREADY_PAID`,还需区分“原 Key 重放”和“新 Key 再次请求”并确认响应。 -3. 最终扣款金额必须来自 M04 持久化订单事实;A405 的 `expectedAmount` 只能承担客户端旧值冲突保护,不能成为扣款事实。 -4. 充值和支付幂等结果必须可恢复;A405 不得把 Redis 作为唯一幂等事实,接口 4.6.2 已要求资金类幂等记录使用数据库唯一约束。 -5. F10 同步核心流程不包含迟到回调;A406 的“取消订单收到迟到成功支付”属于 C08,`PAYMENT.NOT_FOUND` 也不能只根据订单是否为 `PendingPayment/Paid` 推断。 -6. 流程只要求每个买家拥有独立钱包且无钱包记录时余额语义确定;A401 尚未确认钱包是在注册时创建还是首次查询时按需初始化。 -7. 需求尚未定义充值记录和支付记录的 `Pending/Failed/Succeeded` 状态机;A403/A407 草案中的这些状态不能反向写进流程,需先完成业务确认。 -8. A404/A405 必须把支付截止时间作为服务端可支付条件;不能只看 `PendingPayment` 状态,否则 C03 扫描延迟时会放过过期支付。到期结果由 M04/C03 取消能力完成状态推进和原库存回补。 -9. DB081~DB100 尚未形成可实施的完整表定义,数据库字段、约束和索引必须由数据库设计任务另行确认。 +## 十、接口承接结果与剩余数据待评审项 + +1. A404 已按流程统一收银台结果:已支付订单返回现有确定支付结果;已取消或其他不可支付状态使用明确结果,不把“已支付”误报为普通状态冲突。 +2. A405 已区分同 Key、同请求重放和新 Key 再次支付:原请求重放首次确定结果;订单已有成功支付时返回既有支付结果且不重复扣款。 +3. A405 的最终扣款金额只来自 M04 持久化订单事实;`expectedAmount` 仅承担客户端旧值冲突保护,不能成为扣款事实。 +4. A402/A405 的幂等结果按接口设计 1.12.1 与资金事实一起持久化并长期保留;Redis 不保存唯一幂等事实。 +5. F10 同步钱包流程与 C08 回调已经分开:A406 只查询当前订单的确定支付结果;迟到回调、聚合和差异登记由 C08/A421~A426 承接。 +6. A401 已冻结无钱包记录的处理:返回逻辑零余额且 GET 不写库;首次充值或扣款才在对应写事务内按需创建钱包记录,并用买家唯一约束防止并发重复创建。 +7. A403 仅列出已成功提交的充值事实,A407 仅列出已确认支付事实;本期不虚构 `Pending/Failed` 资金记录状态机。 +8. A404/A405 已把数据库权威时间与固化的 `paymentDeadline` 纳入可支付条件;到期后即使 C03 尚未扫描也拒绝支付,并复用 M04/C03 的过期取消结果。 +9. DB081~DB100 尚未形成可实施的完整表定义,数据库字段、约束和索引仍由后续数据库设计任务按上述已确认流程与接口统一派生。 ## 十一、验收证据清单 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" index 1e46c73..5c9da37 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" @@ -17,7 +17,7 @@ M10 负责订单项售后资格、申请数量占用、商家审核、退货说 |---|---|---| | M10 / X04 需求 | 完整定义 | 作为业务语义事实源 | | M04 / M06-02 订单与履约边界 | 完整定义,已完成统稿校准 | 已冻结订单状态、指定商家、可履约数量和发货竞争 | -| M05 退款能力 | 业务边界完整,待接口契约承接 | 只承接稳定退款操作,不决定售后资格与库存 | +| M05 退款能力 | 应用契约已按本文重建、未冻结 | 只承接稳定退款操作,不决定售后资格与库存;待数据库、公开签名与交叉评审 | | M02 / C01 / C07 库存通道 | 完整定义 | 售后成功按原来源回补;普通库存触发 C07,秒杀原活动库存不触发 | | M09 通知 | 完整定义,固定接收矩阵已校准 | 只消费已经提交的售后事实 | | 本文业务流程 | 已重构、待交叉评审 | 作为接口与后续数据设计输入 | @@ -394,17 +394,17 @@ flowchart TD | 已确认业务能力 | 派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 售后资格预检 | A411 | 当前可选类型、截止时间、剩余可申请数量;明确预检不是提交承诺 | 待重建详细契约 | -| 提交售后申请 | A412 | 本人归属、最新订单状态、类型、数量、服务端金额、发货竞争和请求幂等 | 待重建详细契约 | -| 买家 / 商家申请列表 | A413 | 本人或 `assignedMerchantUserId` 授权范围、状态筛选和分页 | 待重建详细契约 | -| 申请详情 | A414 | 快照、金额、申请内容、审核意见、退货说明、退款结果和状态时间线 | 待重建详细契约 | -| 买家撤销 | A415 | 仅本人 `PendingReview` 可撤销;与审核竞争并释放数量 | 待重建详细契约 | -| 商家审核 | A416 | 指定商家、同意 / 拒绝、意见、状态竞争;仅退款同意后进入 `Refunding` | 待重建详细契约 | -| 商家确认收货 | A417 | 指定商家确认整笔申请数量,`PendingReceipt → Refunding` | 待重建详细契约 | -| 退款失败重试 | A419 | 仅 `RefundFailed`,复用同一退款操作,返回当前确定或在途状态 | 待重建详细契约 | -| 买家提交退货说明 | A434 | 仅本人 `PendingReturn`,同内容重放,提交后不得静默覆盖 | 待重建详细契约 | -| 小金库退款 | Payment 内部应用契约 | 一个售后申请一个退款操作;成功重放、未知核实、确定失败可安全重试 | 待按本流程重建 | -| 履约快照 | AfterSales 内部应用契约 | 非终态阻断、已退款数量、剩余可履约数量和同订单串行复核 | 待按本流程补齐 | +| 售后资格预检 | A411 | 当前可选类型、截止时间、剩余可申请数量;明确预检不是提交承诺 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 提交售后申请 | A412 | 本人归属、最新订单状态、类型、数量、服务端金额、发货竞争和请求幂等 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 买家 / 商家申请列表 | A413 | 本人或 `assignedMerchantUserId` 授权范围、状态筛选和分页 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 申请详情 | A414 | 快照、金额、申请内容、审核意见、退货说明、退款结果和状态时间线 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 买家撤销 | A415 | 仅本人 `PendingReview` 可撤销;与审核竞争并释放数量 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 商家审核 | A416 | 指定商家、同意 / 拒绝、意见、状态竞争;仅退款同意后进入 `Refunding` | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 商家确认收货 | A417 | 指定商家确认整笔申请数量,`PendingReceipt → Refunding` | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 退款失败重试 | A419 | 仅 `RefundFailed`,复用同一退款操作,返回当前确定或在途状态 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 买家提交退货说明 | A434 | 仅本人 `PendingReturn`,同内容重放,提交后不得静默覆盖 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 小金库退款 | Payment 内部应用契约 | 一个售后申请一个退款操作;成功重放、未知核实、确定失败可安全重试 | 已按流程重建,待数据库、公开签名与交叉评审 | +| 履约快照 | AfterSales 内部应用契约 | 非终态阻断、已退款数量、剩余可履约数量和同订单串行复核 | 已按流程重建,待数据库、公开签名与交叉评审 | 接口阶段必须特别修正: @@ -416,7 +416,7 @@ flowchart TD 6. A417 不接收任意“收到数量”改变申请金额;本期只确认整笔申请数量。 7. A434 不把运单号设为全局业务唯一键。 8. 所有改变状态或资金的动作都要有稳定请求身份;相同请求重放首次结果,换内容不得复用。 -9. 当前流程不派生独立的售后退款详情或退款列表入口:申请详情已经承载退款结果,钱包记录由 M05 承载。旧 A432、A433 应在接口阶段并入这些事实源或登记为取消历史,不得为了保留编号反向增加页面和流程。 +9. 当前流程不派生独立的售后审核时间线、公开退款命令、退款详情或退款列表入口:旧 A418 由 A414 的完整领域时间线承载,旧 A431 由 Payment 退款应用契约承载,旧 A432 由 A414 的退款操作摘要承载,旧 A433 由 A413/A414 与 M05 支付记录承载。四个编号均已取消并只保留历史追踪,不得为了保留编号反向增加页面和流程。 ## 十三、跨模块整合必须承接的事实 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" index fedf0b5..a2de9ab 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" @@ -141,15 +141,12 @@ stateDiagram-v2 state "草稿/未上架" as Draft state "已上架" as OnSale state "已下架" as OffSale - state "物理删除完成(仅无历史关联的终止结果)" as Deleted - [*] --> Draft: 创建并保存 Draft --> OnSale: 完整性校验通过并主动上架 OnSale --> OffSale: 商家主动下架 OffSale --> OnSale: 重新校验通过并上架 - Draft --> Deleted: 无历史关联且确认删除 - OffSale --> Deleted: 无历史关联且确认删除 - Deleted --> [*] + Draft --> [*]: 无历史关联且确认物理删除 + OffSale --> [*]: 无历史关联且确认物理删除 ``` #### 3.0.2 核心模块直接出入口 @@ -316,7 +313,7 @@ flowchart TD H -- "否" --> I["拒绝保存并保留表单内容"] H -- "是" --> J["保存草稿或保持当前销售状态"] J --> K{"主动上架、下架、删除或暂不变更状态?"} - K -- "上架" --> L{"必填信息完整且分类已启用?"} + K -- "上架" --> L{"必填信息完整且分类在购物端有效?"} L -- "否" --> M["拒绝上架并指出缺失项"] L -- "是" --> N["M02 状态变为已上架"] K -- "下架" --> O["状态变为已下架"] @@ -356,7 +353,8 @@ flowchart TD - 新建或编辑商品不会自动上架;只有商家主动上架且完整性校验通过后,状态才进入“已上架”。 - 公开列表和搜索只返回已上架商品;下架商品的旧链接只能显示不可售状态。 - 已上架但库存为 0 的商品仍可展示详情,但必须标记售罄并禁用购买。 -- 分类停用只使该分类退出购物端分类筛选入口,不自动下架或隐藏其既有已上架商品;商品仍按自身销售状态公开和参与购买校验。 +- 分类最多一层父子关系,购物端只展示自身及父级均启用的有效分类;停用顶级分类时整个子树退出筛选入口但不改写子分类存储状态,重新启用后仅恢复自身仍为启用的子分类。 +- 顶级分类筛选包含直接归属该分类及其有效直属子分类的已上架商品,子分类筛选只匹配直接归属该子分类的已上架商品;分类失效不自动下架或隐藏其既有已上架商品,商品仍可通过全部商品、关键词和详情入口按自身销售状态公开并参与购买校验。 - 商品下架不删除历史订单快照、购物车、收藏或浏览历史中的关联记录,但购买入口必须失效。 - 有历史订单关联的商品不得进行破坏性删除,应使用下架表达停售。 - 本期不引入多商家商品归属模型;后台以商家 Policy 控制入口,不在流程图中自行增加店铺或租户边界。 @@ -842,19 +840,19 @@ flowchart LR | 教师编号 | 核心状态或确定结果 | 直接入口 → 直接出口 | 本文流程 | 主责人 | 当前成熟度 | |---|---|---|---|---|---| -| F01 | 创建 `Normal` 买家账号,不自动登录 | 游客注册 → F02 登录 | [M01-01 用户注册流程](tyh/M01-01-用户注册流程.md)、3.1 | 唐宇昊 | 完整定义,已校准;待接口、数据库、实现和测试承接 | -| F02 | 有效 JWT + 服务端角色;退出后当前 JWT 失效 | M01 → 全部受保护入口 | [M01-02 用户登录与退出流程](tyh/M01-02-用户登录与退出流程.md)、3.1、3.8.1 | 唐宇昊 | 完整定义,已校准;待接口、数据库、实现和测试承接 | -| F03 | 本人资料与地址;敏感修改后全部旧凭证失效 | M01 Address → M04 地址快照 | [M01-03 个人信息与收货地址流程](tyh/M01-03-个人信息与收货地址流程.md)、3.2、3.8.1 | 唐宇昊 | 完整定义,已校准;待接口、数据库、实现和测试承接 | -| F04 | 只返回 `OnSale` 商品的稳定分页列表 | M02 → 购物端列表 | [M02 分类与商品流程](gxy/M02-分类与商品流程.md)、3.3 | 顾欣月 | 完整定义,已校准;待接口、数据库、实现和测试承接 | -| F05 | 强制公开过滤下的安全关键词与组合查询 | 查询条件 → M02/C04 → F04 列表 | [M02 分类与商品流程](gxy/M02-分类与商品流程.md)、[C04 中文搜索流程](gxy/C04-中文搜索流程.md)、3.3 | 顾欣月 | 完整定义,已校准;待接口、数据库、实现和测试承接 | -| F06 | 商品自身公开详情、最新价格库存和明确可售状态 | F04 → M02 详情 → M03/M07/M08 | [M02 分类与商品流程](gxy/M02-分类与商品流程.md)、3.3、3.8.1 | 顾欣月 | 完整定义,已校准;待接口、数据库、实现和测试承接 | -| F07 | 本人购物车;选中与可结算状态由服务端实时派生 | M01/M02 → M03 → M04 | [M03 购物车流程](zhh/M03-购物车流程.md)、3.4、3.8.1 | 朱惠惠 | 完整定义,已校准;待接口、数据库、实现和测试承接 | -| F08 | 唯一 `PendingPayment` 订单、快照、默认商家、固定截止时间、库存扣减与购物车清理 | M01/M02/M03 → M04 → M05 | [M04 订单流程](wqq/M04-订单流程.md)、3.4、3.8.2 | 韦乾强 | 完整定义,已校准;待接口、数据库、实现和测试承接 | -| F09 | 本人订单可查;合法到达 `Cancelled` 或 `Completed` | M04/M05/系统任务 → M04 | [M04 订单流程](wqq/M04-订单流程.md)、[C03 订单超时流程](wqq/C03-订单超时流程.md)、3.6、3.8.2~3.8.3 | 韦乾强 | 完整定义,已校准;待接口、数据库、实现和测试承接 | -| F10 | `Wallet` 或 `SimulatedChannel` 形成唯一确定支付事实并推进 `Paid` | M04 → M05/C08 → M04/M06-02 | [M05 支付流程](zhy/M05-支付流程.md)、[C08 支付回调与对账流程](zhy/C08-支付回调与对账流程.md)、3.5、3.8.2、3.8.10 | 张海洋 | 完整定义,已校准;待接口、数据库、实现和测试承接 | -| F11 | 商品处于 `Draft`、`OnSale`、`OffSale`,或满足约束后完成删除 | M06-01 → M02 → F04~F06/C07 | [M06-01 后台商品管理流程](gxy/M06-01-后台商品管理流程.md)、3.3、3.7、3.8.4 | 顾欣月 | 完整定义,已校准;待接口、数据库、实现和测试承接 | -| F12 | 责任商家经售后快照复核后合法 `Paid → Shipped` | M04/M10 → M06-02 → M04 | [M06-02 商家履约流程](wqq/M06-02-商家履约流程.md)、3.6、3.7、3.8.3 | 韦乾强 | 完整定义,已校准;待接口、数据库、实现和测试承接 | -| F13 | 买家/商家账号 `Normal ↔ Disabled`,旧凭证和商家责任结果明确 | M06-03 → M01/M04/M10/C01 → 全部受保护入口 | [M06-03 后台用户管理流程](tyh/M06-03-后台用户管理流程.md)、3.1、3.7、3.8.4 | 唐宇昊 | 完整定义,已校准;待接口、数据库、实现和测试承接 | +| F01 | 创建 `Normal` 买家账号,不自动登录 | 游客注册 → F02 登录 | [M01-01 用户注册流程](tyh/M01-01-用户注册流程.md)、3.1 | 唐宇昊 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| F02 | 有效 JWT + 服务端角色;退出后当前 JWT 失效 | M01 → 全部受保护入口 | [M01-02 用户登录与退出流程](tyh/M01-02-用户登录与退出流程.md)、3.1、3.8.1 | 唐宇昊 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| F03 | 本人资料与地址;敏感修改后全部旧凭证失效 | M01 Address → M04 地址快照 | [M01-03 个人信息与收货地址流程](tyh/M01-03-个人信息与收货地址流程.md)、3.2、3.8.1 | 唐宇昊 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| F04 | 只返回 `OnSale` 商品的稳定分页列表 | M02 → 购物端列表 | [M02 分类与商品流程](gxy/M02-分类与商品流程.md)、3.3 | 顾欣月 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| F05 | 强制公开过滤下的安全关键词与组合查询 | 查询条件 → M02/C04 → F04 列表 | [M02 分类与商品流程](gxy/M02-分类与商品流程.md)、[C04 中文搜索流程](gxy/C04-中文搜索流程.md)、3.3 | 顾欣月 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| F06 | 商品自身公开详情、最新价格库存和明确可售状态 | F04 → M02 详情 → M03/M07/M08 | [M02 分类与商品流程](gxy/M02-分类与商品流程.md)、3.3、3.8.1 | 顾欣月 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| F07 | 本人购物车;选中与可结算状态由服务端实时派生 | M01/M02 → M03 → M04 | [M03 购物车流程](zhh/M03-购物车流程.md)、3.4、3.8.1 | 朱惠惠 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| F08 | 唯一 `PendingPayment` 订单、快照、默认商家、固定截止时间、库存扣减与购物车清理 | M01/M02/M03 → M04 → M05 | [M04 订单流程](wqq/M04-订单流程.md)、3.4、3.8.2 | 韦乾强 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| F09 | 本人订单可查;合法到达 `Cancelled` 或 `Completed` | M04/M05/系统任务 → M04 | [M04 订单流程](wqq/M04-订单流程.md)、[C03 订单超时流程](wqq/C03-订单超时流程.md)、3.6、3.8.2~3.8.3 | 韦乾强 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| F10 | `Wallet` 或 `SimulatedChannel` 形成唯一确定支付事实并推进 `Paid` | M04 → M05/C08 → M04/M06-02 | [M05 支付流程](zhy/M05-支付流程.md)、[C08 支付回调与对账流程](zhy/C08-支付回调与对账流程.md)、3.5、3.8.2、3.8.10 | 张海洋 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| F11 | 商品处于 `Draft`、`OnSale`、`OffSale`,或满足约束后完成删除 | M06-01 → M02 → F04~F06/C07 | [M06-01 后台商品管理流程](gxy/M06-01-后台商品管理流程.md)、3.3、3.7、3.8.4 | 顾欣月 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| F12 | 责任商家经售后快照复核后合法 `Paid → Shipped` | M04/M10 → M06-02 → M04 | [M06-02 商家履约流程](wqq/M06-02-商家履约流程.md)、3.6、3.7、3.8.3 | 韦乾强 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| F13 | 买家/商家账号 `Normal ↔ Disabled`,旧凭证和商家责任结果明确 | M06-03 → M01/M04/M10/C01 → 全部受保护入口 | [M06-03 后台用户管理流程](tyh/M06-03-后台用户管理流程.md)、3.1、3.7、3.8.4 | 唐宇昊 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | ## 五、选做与挑战流程登记 @@ -862,17 +860,17 @@ flowchart LR | 编号 | 基础核心流程 | 直接扩展入口 → 出口 | 不可变核心结果 | 主责人 | 当前状态 | |---|---|---|---|---|---| -| X01 | F09、F06 | 本人 `Completed` 订单项 → 提交时资格重检 → 唯一公开评价 | 订单保持 `Completed`;不修改商品状态、价格、库存,不触发 C07 或 M09 | 顾欣月 | [M07 商品评价流程](gxy/M07-商品评价流程.md):完整定义,已校准;待下游承接 | -| X02 | F02、F06,展示衔接 F04 | 已登录买家查看详情 → 本人收藏/最近 200 条浏览记录 | 不修改商品事实;游客不产生个人记录;关闭历史只阻止未来写入 | 唐宇昊 | [M08 商品收藏与浏览历史流程](tyh/M08-商品收藏与浏览历史流程.md):完整定义,已校准;待下游承接 | -| X03 | F02、F13;F08、F09、F10、F12、X04 | 固定来源事实 → 整事件消息落库 → 查询/高水位已读/离线补查 | 消息和推送失败不回滚核心事务;接收人不能由调用方任意扩张 | 罗皓晨 | [M09 站内消息流程](lhc/M09-站内消息流程.md):完整定义,接收矩阵与原子性已冻结;待下游承接 | -| X04 | F09、F10、F12 | 本人合格订单项 → 独立售后状态 → 稳定退款操作 → 原库存通道回补 | 不覆盖订单核心状态和快照;退款不超实付且不重复入账/回补 | 张海洋 | [M10 售后流程](zhy/M10-售后流程.md):完整定义,履约竞争和退款闭环已校准;待下游承接 | -| C01 | F11 + F04/F06 → F08 → F10/F09/F12 | 发布时普通库存原子划转 → 独立库存扣减 → M04 统一 `PendingPayment` | 后续复用核心支付和履约;取消/售后只回原活动库存;活动创建人不决定履约商家 | 朱惠惠 | [C01 秒杀流程](zhh/C01-秒杀流程.md):完整定义,库存归属和默认商家已冻结;待下游承接 | -| C03 | F08、F10、F09 | 到达固定 `paymentDeadline` → Worker 复用 M04 统一取消 → `Cancelled` | 与 Wallet/C08 只能一个胜出;原库存通道回补原子且幂等 | 韦乾强 | [C03 订单超时流程](wqq/C03-订单超时流程.md):完整定义,固定截止时间已冻结;待下游承接 | -| C04 | F04、F05、F06 | 同一查询入口选择高级搜索 → 失败时安全回退基础搜索 | 只公开 `OnSale`;权限、强制筛选、下单重校验和性能口径不变 | 顾欣月 | [C04 中文搜索流程](gxy/C04-中文搜索流程.md):完整定义,降级与验收口径已校准;待下游承接 | -| C06 | F02、F13;经 X03 接入核心与售后事实 | M09 消息提交 → WebSocket 轻提示/固定重连 → M09 权威补查 | 推送失败不改变消息与业务事实;未知撤销状态必须关闭连接 | 罗皓晨 | [C06 实时推送流程](lhc/C06-实时推送流程.md):完整定义,传输与连接边界已冻结;待下游承接 | -| C07 | F04、F06、F11;普通库存变更扩展至 F08/F09/C01/X04 | 固定首页/A103 Cache-Aside;提交后立即及第 3 秒失效 | PostgreSQL 是事实源;范围、60/10 秒 TTL、2 秒回填窗、500 ms 等待和 62/12 秒旧值上限固定 | 罗皓晨、顾欣月 | [C07 缓存流程](lhc/C07-缓存流程.md):完整定义,范围、参数与失效矩阵已冻结;待下游承接 | -| C08 | F10、F09;退款对账关联 X04 | 受控回调 → 四种终态 → 每日固定范围对账 → 领取/举证/复核闭环 | 不扣 Wallet;迟到成功只形成 Difference;不重复支付、退款或关闭差异 | 张海洋 | [C08 支付回调与对账流程](zhy/C08-支付回调与对账流程.md):完整定义,通道竞争与差异闭环已冻结;待下游承接 | -| C10 | F01~F13 全部横切;支撑 C06/C07 与 Worker | Migrator → 全局就绪 → 能力级降级/恢复 → 有序停止 | 状态机和数据库结果不变;未知安全事实失败关闭;单实例切换不重复写或越权 | 罗皓晨 | [C10 高可用流程](lhc/C10-高可用流程.md):完整定义,迁移、就绪、降级、恢复和停止边界已冻结;待下游承接 | +| X01 | F09、F06 | 本人 `Completed` 订单项 → 提交时资格重检 → 唯一公开评价 | 订单保持 `Completed`;不修改商品状态、价格、库存,不触发 C07 或 M09 | 顾欣月 | [M07 商品评价流程](gxy/M07-商品评价流程.md):完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| X02 | F02、F06,展示衔接 F04 | 已登录买家查看详情 → 本人收藏/最近 200 条浏览记录 | 不修改商品事实;游客不产生个人记录;关闭历史只阻止未来写入 | 唐宇昊 | [M08 商品收藏与浏览历史流程](tyh/M08-商品收藏与浏览历史流程.md):完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| X03 | F02、F13;F08、F09、F10、F12、X04 | 固定来源事实 → 整事件消息落库 → 查询/高水位已读/离线补查 | 消息和推送失败不回滚核心事务;接收人不能由调用方任意扩张 | 罗皓晨 | [M09 站内消息流程](lhc/M09-站内消息流程.md):完整定义,接收矩阵与原子性已冻结;接口已按流程重建,待数据库、实现和测试承接 | +| X04 | F09、F10、F12 | 本人合格订单项 → 独立售后状态 → 稳定退款操作 → 原库存通道回补 | 不覆盖订单核心状态和快照;退款不超实付且不重复入账/回补 | 张海洋 | [M10 售后流程](zhy/M10-售后流程.md):完整定义,履约竞争和退款闭环已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| C01 | F11 + F04/F06 → F08 → F10/F09/F12 | 发布时普通库存原子划转 → 独立库存扣减 → M04 统一 `PendingPayment` | 后续复用核心支付和履约;取消/售后只回原活动库存;活动创建人不决定履约商家 | 朱惠惠 | [C01 秒杀流程](zhh/C01-秒杀流程.md):完整定义,库存归属和默认商家已冻结;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| C03 | F08、F10、F09 | 到达固定 `paymentDeadline` → Worker 复用 M04 统一取消 → `Cancelled` | 与 Wallet/C08 只能一个胜出;原库存通道回补原子且幂等 | 韦乾强 | [C03 订单超时流程](wqq/C03-订单超时流程.md):完整定义,固定截止时间已冻结;接口与 Worker 契约已按流程重建,待数据库、实现和测试承接 | +| C04 | F04、F05、F06 | 同一查询入口选择高级搜索 → 失败时安全回退基础搜索 | 只公开 `OnSale`;权限、强制筛选、下单重校验和性能口径不变 | 顾欣月 | [C04 中文搜索流程](gxy/C04-中文搜索流程.md):完整定义,降级与验收口径已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| C06 | F02、F13;经 X03 接入核心与售后事实 | M09 消息提交 → WebSocket 轻提示/固定重连 → M09 权威补查 | 推送失败不改变消息与业务事实;未知撤销状态必须关闭连接 | 罗皓晨 | [C06 实时推送流程](lhc/C06-实时推送流程.md):完整定义,传输与连接边界已冻结;Hub 与事件契约已按流程重建,待部署、实现和测试承接 | +| C07 | F04、F06、F11;普通库存变更扩展至 F08/F09/C01/X04 | 固定首页/A103 Cache-Aside;提交后立即及第 3 秒失效 | PostgreSQL 是事实源;范围、60/10 秒 TTL、2 秒回填窗、500 ms 等待和 62/12 秒旧值上限固定 | 罗皓晨、顾欣月 | [C07 缓存流程](lhc/C07-缓存流程.md):完整定义,范围、参数与失效矩阵已冻结;缓存契约已按流程重建,待实现和压测承接 | +| C08 | F10、F09;退款对账关联 X04 | 受控回调 → 四种终态 → 每日固定范围对账 → 领取/举证/复核闭环 | 不扣 Wallet;迟到成功只形成 Difference;不重复支付、退款或关闭差异 | 张海洋 | [C08 支付回调与对账流程](zhy/C08-支付回调与对账流程.md):完整定义,通道竞争与差异闭环已冻结;接口与 Worker 契约已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| C10 | F01~F13 全部横切;支撑 C06/C07 与 Worker | Migrator → 全局就绪 → 能力级降级/恢复 → 有序停止 | 状态机和数据库结果不变;未知安全事实失败关闭;单实例切换不重复写或越权 | 罗皓晨 | [C10 高可用流程](lhc/C10-高可用流程.md):完整定义,迁移、就绪、降级、恢复和停止边界已冻结;接口与运行契约已按流程重建,待部署、实现和验收承接 | 流程层已关闭的原阻断项: diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" index 163f259..ee771bb 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" @@ -390,7 +390,7 @@ AFTER_SALES.INVALID_STATUS 规则: - 客户端生成 UUID 作为幂等键,并在不确定首次请求结果时使用原键重试。 -- 服务端幂等范围至少包含“已认证用户或可信调用方 + 接口/业务动作 + 幂等键”。 +- 服务端幂等范围默认至少包含“已认证用户或可信调用方 + 接口/业务动作 + 幂等键”。A016/A017 是显式例外:二者共享“管理员 + 账号治理 + 幂等键”唯一范围,再把目标账号与禁用/启用动作绑定到请求指纹;同键换目标或换动作必须拒绝,不能因路径不同而分别接受。A301 也按 M04 上游规则采用订单创建事实内的 `(buyerId, Idempotency-Key)` 唯一范围,请求指纹绑定地址与购物车条目;该订单唯一范围不扩展为跨模块共享 Key。 - 完成身份、Header 和固定字段格式校验后,必须先查询持久化幂等结果,再读取库存、状态、资格、余额、限购、截止时间等会变化的事实;同键同指纹直接重放首次确定结果。 - 同一范围、同一幂等键、相同请求内容重复提交时,不重复执行副作用,返回首次已确认结果。 - 同一幂等键对应不同请求内容时返回 `409` 和 `IDEMPOTENCY.KEY_REUSED`。 @@ -399,6 +399,23 @@ AFTER_SALES.INVALID_STATUS - 客户端按钮置灰、防抖只能改善体验,不能替代服务端幂等、唯一约束和状态条件。 - A421 是唯一例外:完成 HMAC 与固定字段校验后,以 `callbackId + 规范请求指纹` 查询已处理结果;同 ID 不同指纹拒绝,同一支付流水允许不同 callbackId 表达乱序事实。 +本期具体保留与重放边界如下;“本期不自动清理”表示至少覆盖对应业务事实及幂等、回调或对账证据的完整留存期,数据库设计只能延长,不能缩短到使旧请求重新执行: + +| 接口 | 唯一范围 | 最低保留边界 | 重放范围 | +|---|---|---|---| +| A016/A017 | 共享 `(adminUserId, 账号治理, Idempotency-Key)`,指纹绑定目标账号与动作 | 账号治理与幂等事实留存期;本期不自动清理 | 首次确定的启用/禁用响应或确定失败 | +| A122/A142/A220 | 已认证用户 + 接口 + `Idempotency-Key`,并由商品、订单项评价或活动唯一事实兜底 | 对应资源与幂等事实留存期;本期不自动清理 | 首次创建响应、`Location`(存在时)及确定失败 | +| A201 | 买家 + A201 + `Idempotency-Key` | 首次确定结果提交后 24 小时 | 首次 HTTP 状态、响应体及确定失败;窗口内不得再次累加 | +| A204~A207 | 买家 + 具体接口 + `Idempotency-Key` | 首次确定结果提交后 24 小时 | 首次 204/200 或确定失败,不重复删除、清空或翻转 | +| A222/A223 | 商家 + 具体接口 + `Idempotency-Key` | 活动与幂等事实留存期;本期不自动清理 | 首次发布/取消响应及确定失败,不重复划拨或改写状态 | +| A228 | 买家 + A228 + `Idempotency-Key` | 秒杀订单与幂等事实留存期;本期不自动清理 | 首次秒杀下单响应及确定失败,不重复扣活动库存、占用限购或建单 | +| A301 | `(buyerId, Idempotency-Key)`,指纹绑定地址与购物车条目 | 订单与幂等事实留存期;本期不自动清理 | 首次普通订单响应及确定失败,不重复扣普通库存、建单或清理购物车 | +| A307 | 商家 + A307 + `Idempotency-Key` | 订单履约与幂等事实留存期;本期不自动清理 | 首次发货响应及确定失败,不重复发货 | +| A402/A405 | 买家 + 具体接口 + `Idempotency-Key` | 资金流水、支付与幂等事实留存期;本期不自动清理 | 首次充值/支付结果或确定失败,不重复入账或扣款 | +| A412/A415~A417/A419/A434 | 已认证用户 + 具体接口 + `Idempotency-Key` | 售后申请、退款操作、状态时间线与幂等事实留存期;本期不自动清理 | 首次申请/状态动作结果或确定失败,不重复占用数量、退款或写时间线 | +| A425 | 管理员 + A425 + `Idempotency-Key` | 对账批次、差异与证据留存期;本期不自动清理 | 首次处置结果或确定失败,不重复领取、解决或追加同一证据 | +| A421 | `callbackId + 规范请求指纹` | 回调聚合、支付与对账证据留存期;本期不自动清理 | 首次回调处理结果;同 ID 换指纹固定拒绝 | + #### 1.12.2 并发与状态竞争 - 库存扣减、余额扣减、订单支付、订单取消、发货、退款等操作必须在服务端使用事务、条件更新、唯一约束或等价并发控制。 @@ -678,14 +695,14 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 | 编号 | 模块/Tag | 需求 | 名称 | 方法 | 路径 | operationId | 鉴权 | 汇总状态 | |---|---|---|---|---|---|---|---|---| -| A301 | Ordering | F08 | 提交订单 | POST | `/api/orders` | `Ordering_CreateOrder` | BuyerOnly | 部分定义 | -| A302 | Ordering | F09、C01 | 查询订单列表 | GET | `/api/orders` | `Ordering_ListOrders` | BuyerOnly | 部分定义 | -| A303 | Ordering | F09、C01 | 查询订单详情 | GET | `/api/orders/{orderId}` | `Ordering_GetOrder` | BuyerOnly | 部分定义 | -| A304 | Ordering | F09 | 取消订单 | POST | `/api/orders/{orderId}/cancel` | `Ordering_CancelOrder` | BuyerOnly | 部分定义 | -| A305 | Ordering | F12 | 商家查询订单列表 | GET | `/api/merchant/orders` | `Ordering_ListMerchantOrders` | MerchantOnly | 部分定义 | -| A306 | Ordering | F12 | 商家查询订单详情 | GET | `/api/merchant/orders/{orderId}` | `Ordering_GetMerchantOrder` | MerchantOnly | 部分定义 | -| A307 | Ordering | F12 | 商家发货 | POST | `/api/merchant/orders/{orderId}/ship` | `Ordering_ShipOrder` | MerchantOnly | 部分定义 | -| A308 | Ordering | F09 | 买家确认收货 | POST | `/api/orders/{orderId}/confirm-receipt` | `Ordering_ConfirmReceipt` | BuyerOnly | 部分定义 | +| A301 | Ordering | F08 | 提交订单 | POST | `/api/orders` | `Ordering_CreateOrder` | BuyerOnly | 待交叉评审 | +| A302 | Ordering | F09、C01 | 查询订单列表 | GET | `/api/orders` | `Ordering_ListOrders` | BuyerOnly | 待交叉评审 | +| A303 | Ordering | F09、C01 | 查询订单详情 | GET | `/api/orders/{orderId}` | `Ordering_GetOrder` | BuyerOnly | 待交叉评审 | +| A304 | Ordering | F09 | 取消订单 | POST | `/api/orders/{orderId}/cancel` | `Ordering_CancelOrder` | BuyerOnly | 待交叉评审 | +| A305 | Ordering | F12 | 商家查询订单列表 | GET | `/api/merchant/orders` | `Ordering_ListMerchantOrders` | MerchantOnly | 待交叉评审 | +| A306 | Ordering | F12 | 商家查询订单详情 | GET | `/api/merchant/orders/{orderId}` | `Ordering_GetMerchantOrder` | MerchantOnly | 待交叉评审 | +| A307 | Ordering | F12 | 商家发货 | POST | `/api/merchant/orders/{orderId}/ship` | `Ordering_ShipOrder` | MerchantOnly | 待交叉评审 | +| A308 | Ordering | F09 | 买家确认收货 | POST | `/api/orders/{orderId}/confirm-receipt` | `Ordering_ConfirmReceipt` | BuyerOnly | 待交叉评审 | #### 张海洋(A401~A500) @@ -755,7 +772,7 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 - Route 参数:无 - Query 参数:无 -- Header:无强制要求 +- Header:无强制要求;若携带可验证且账号状态正常的 Bearer Token,服务端识别为已登录请求并拒绝创建新账号 - Body: ```text @@ -794,6 +811,7 @@ RegisteredUserResponse { |---:|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 手机号格式错误、密码强度不足或两次密码不一致 | | 400 | `COMMON.MALFORMED_JSON` | 请求体无法解析 | +| 409 | `AUTH.ALREADY_AUTHENTICATED` | 当前请求携带可验证且账号状态正常的现有登录凭证 | | 409 | `AUTH.PHONE_ALREADY_REGISTERED` | 手机号已存在有效账号 | | 409 | `AUTH.USERNAME_GENERATION_RETRY_EXHAUSTED` | 用户名生成冲突且超过重试上限 | | 500 | `COMMON.INTERNAL_ERROR` | 未处理的服务端错误 | @@ -804,6 +822,7 @@ RegisteredUserResponse { - 密码使用可靠的自适应哈希保存,具体算法按系统架构与实现统一确定;明文密码、确认密码和哈希结果均不得出现在响应、日志或 ProblemDetails 中。 - 手机号唯一性由 PostgreSQL 唯一约束 `ux_users_phone` 保证;并发注册同一手机号时仅一笔成功,其余返回 `409 / AUTH.PHONE_ALREADY_REGISTERED`。 - 账号、固定 Buyer 角色、唯一用户名和默认头像必须在同一事务内创建;任一步失败不留部分账号。公开注册不接受客户端角色。 +- A001 只执行游客注册动作。前端检测到已登录买家时进入个人中心,已登录商家或管理员回到其工作台;API 收到可验证且账号状态正常的现有登录凭证时返回 `409 / AUTH.ALREADY_AUTHENTICATED`,不得继续创建第二个 Buyer。缺失或无法建立有效登录态的可选凭证不替代手机号、密码等注册校验。 #### 缓存、事件或外部依赖 @@ -818,6 +837,7 @@ RegisteredUserResponse { - 已注册手机号 → 409 / `AUTH.PHONE_ALREADY_REGISTERED`,不暴露其他用户资料。 - 请求体注入 `role=Admin` → 400,且不创建账号。 - 并发注册同一手机号 → 仅一笔 201,另一笔 409。 +- 已登录买家、商家或管理员携带当前有效凭证调用 → 409 / `AUTH.ALREADY_AUTHENTICATED`,不创建账号。 ### A002 登录 @@ -943,12 +963,13 @@ LogoutResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| -| 401 | `AUTH.UNAUTHENTICATED` | 缺少或无效访问令牌 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少令牌,或令牌格式、签名、Issuer、Audience、有效期无效;已明确撤销的同一令牌重放按成功处理 | | 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态共享不可用 | #### 业务规则与并发 -- 当前 JWT 的 `jti` 被写入共享撤销事实,过期时间不晚于原 JWT 过期时间;没有刷新令牌需要处理。 +- 当前 JWT 的 `jti` 被写入共享撤销事实,撤销事实的过期时间不得早于原 JWT 的自然过期时间,至少覆盖令牌全部剩余有效期(通常与 `exp` 一致);没有刷新令牌需要处理。 +- A003 使用退出专用认证顺序:先验证令牌格式、签名、Issuer、Audience 和自然有效期并提取 `jti`,再读取撤销事实;同一 `jti` 已明确撤销时直接重放首次 `200` 与原 `revokedAt`,不在普通受保护接口的撤销拦截处提前返回 401。 - 撤销事实无法确认写入时返回 `503`,不得向客户端报告退出成功。 - 同一账号在其他设备的有效令牌不受影响。 - 退出后前端必须清理本地令牌和登录态;后续 `A004` 使用已退出的令牌必须返回 `401 / AUTH.TOKEN_REVOKED`。 @@ -960,6 +981,7 @@ LogoutResponse { #### 验证场景 - 已登录用户调用 → 200;同一令牌再次访问 `A004` 返回 401 / `AUTH.TOKEN_REVOKED`。 +- 同一仍处于自然有效期但已明确撤销的令牌再次调用 A003 → 200,返回与首次相同的 `revokedAt`;伪造、过期或撤销事实未知分别返回 401 / 503。 - 第二个设备登录后的令牌仍可正常使用。 ### A004 获取当前用户 @@ -1349,10 +1371,10 @@ AddressResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或格式错误 | +| 400 | `COMMON.INVALID_UUID` | `addressId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `RESOURCE.NOT_FOUND` | 地址不存在或不属于当前用户 | -| 404 | `RESOURCE.NOT_FOUND` | 地址不存在或不属于当前用户 | #### 业务规则与并发 @@ -1396,8 +1418,10 @@ AddressResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| +| 400 | `COMMON.INVALID_UUID` | `addressId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 404 | `RESOURCE.NOT_FOUND` | 地址不存在、已删除或不属于当前买家 | #### 业务规则与并发 @@ -1442,14 +1466,15 @@ AddressResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| +| 400 | `COMMON.INVALID_UUID` | `addressId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `RESOURCE.NOT_FOUND` | 地址不存在或不属于当前用户 | #### 业务规则与并发 -- 在同一事务内将该地址 `is_default=true`,其他地址 `is_default=false`;保证唯一性。 -- 并发设置多个默认地址时由数据库 `WHERE user_id = :uid AND is_default = true` 条件更新保证最终唯一。 +- 默认地址切换必须在同一事务内形成一个原子结果:目标地址设为默认、当前买家的其他地址取消默认;任一步失败整体不生效。 +- 并发设置不同默认地址时,必须通过“每个买家至多一个默认地址”的数据库唯一约束、按买家串行化或等效可执行机制兜底,并按已提交顺序返回最终结果;普通条件更新本身不作为唯一性证明。 #### 缓存、事件或外部依赖 @@ -1478,10 +1503,11 @@ AddressResponse { - Header:`Authorization: Bearer `(必填,角色 Admin) - Query 参数: - `page`(默认 1) - - `pageSize`(默认 10,上限 50) + - `pageSize`(默认 10,上限 100) + - `username`(可选,独立字段,完整用户名精确匹配) + - `phone`(可选,独立字段,完整 11 位手机号精确匹配;响应仍只返回掩码) - `role`(可选,`Buyer` / `Merchant`;不传表示全部非管理员账号) - `status`(可选,`Normal` / `Disabled`) - - `keyword`(可选,对用户名或手机号做模糊匹配) #### 成功响应 @@ -1496,6 +1522,17 @@ AdminUserListResponse { total: integer totalPages: integer } + +AdminUserResponse { + userId: uuid + username: string + phoneMasked: string + role: "Buyer" | "Merchant" + isDefaultMerchant: boolean + status: "Normal" | "Disabled" + createdAt: string + updatedAt: string +} ``` #### 失败响应 @@ -1509,6 +1546,7 @@ AdminUserListResponse { #### 业务规则与并发 - 永远不返回管理员账号;过滤条件 `role IN ('Buyer','Merchant')`。 +- `username` 与 `phone` 是互相独立的精确筛选字段;手机号输入必须满足账号手机号完整格式,查询参数化执行。即使按完整手机号筛选,响应也只返回 `phoneMasked`。 - 列表响应只返回管理操作所需字段;不返回密码哈希、内部审计、登录态。 - 手机号使用掩码 `138****8888` 形式。 - `AdminUserResponse.isDefaultMerchant` 仅用于说明单店默认运营账号及禁用按钮原因;买家固定为 `false`。 @@ -1521,6 +1559,7 @@ AdminUserListResponse { #### 验证场景 - 管理员查询全部买家 → 200,按注册时间倒序,手机号掩码。 +- 管理员分别按完整用户名或完整手机号筛选 → 只返回精确匹配账号;组合角色/状态时全部条件同时生效。 - 管理员传入 `role=Admin` → 400 / `COMMON.VALIDATION_FAILED`。 - 买家调用 → 403 / `AUTH.FORBIDDEN`。 @@ -1547,38 +1586,31 @@ AdminUserListResponse { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`AdminUserResponse` - -```text -AdminUserResponse { - userId: uuid - username: string - phoneMasked: string - role: "Buyer" | "Merchant" - isDefaultMerchant: boolean - status: "Normal" | "Disabled" - updatedAt: string -} -``` +- 响应 Schema:`AdminUserResponse`(统一定义见 A015) #### 失败响应 | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| +| 400 | `COMMON.INVALID_UUID` | `userId` 不是标准 UUID | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失或格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是管理员 | | 404 | `RESOURCE.NOT_FOUND` | 目标账号不存在或不是买家/商家 | | 409 | `IDENTITY.DEFAULT_MERCHANT_PROTECTED` | 目标是本期唯一默认商家运营账号,不允许直接禁用 | | 409 | `IDENTITY.MERCHANT_HAS_ACTIVE_WORK` | 非默认商家仍有关联待履约订单、售后窗口/申请或未结束秒杀活动 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一治理幂等键被用于不同目标或在禁用/启用动作间复用 | | 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态共享不可用 | +| 503 | `IDENTITY.MERCHANT_RESPONSIBILITY_UNAVAILABLE` | Ordering、AfterSales 或 Seckill 任一责任复核/受理门槛无法确认 | #### 业务规则与并发 -- 完成身份、路径与幂等键格式校验后先查询持久化结果;同键同目标重放,同键换目标返回 `409 IDEMPOTENCY.KEY_REUSED`。 +- A016 与 A017 共享账号治理幂等作用域 `(adminUserId, Idempotency-Key)`;持久结果同时绑定 `targetUserId + action`。同键、同目标、同动作重放首次确定结果;同键换目标或在禁用/启用间换动作返回 `409 IDEMPOTENCY.KEY_REUSED`,不得按两个 HTTP 端点分别接受。 - 条件更新:仅当目标为 `Normal` 时改为 `Disabled` 并提升 `tokenVersion`;目标已是 `Disabled` 时返回并绑定当前禁用结果,不再次提升版本号。 - Identity 必须配置且最多只能有一个 `isDefaultMerchant=true` 的启用商家账号;该账号负责普通订单默认归属,本期 A016 不提供默认账号迁移能力,因此直接禁用返回 409。 - 买家禁用不查询订单、支付、售后等业务责任,也不改写这些事实。 - 禁用非默认商家前,通过 Ordering、AfterSales、Seckill 公开应用契约检查固定清单:`PendingPayment`;仍有可履约数量的 `Paid`;`Shipped`;完成后 7 天窗口内 `Completed`;任一非终态售后;任一未结束秒杀活动。已全量退款、无剩余可履约数量且无非终态售后的 `Paid` 不单独阻断。 +- 任一责任存在时,`IDENTITY.MERCHANT_HAS_ACTIVE_WORK` 的 ProblemDetails `extensions.blockingReasons[]` 只允许 `PendingPaymentOrder`、`PaidOrderWithFulfillableQuantity`、`ShippedOrder`、`CompletedOrderWithinAfterSalesWindow`、`NonTerminalAfterSales`、`UnendedSeckillActivity`,不返回订单、申请或活动私人明细;任一责任契约不可用或结果未知时返回 `503 IDENTITY.MERCHANT_RESPONSIBILITY_UNAVAILABLE`,不得按“无责任”继续禁用。 - 商家责任检查与新的普通/秒杀订单、售后责任接收形成唯一提交顺序;禁用后不得并发写入新责任。 - 账号状态、全部旧凭证失效、追踪信息和幂等结果形成一个确定结果;任一部分无法确认时不返回成功。 @@ -1593,6 +1625,8 @@ AdminUserResponse { - 禁用管理员账号 → 404。 - 禁用默认商家 → 409 / `IDENTITY.DEFAULT_MERCHANT_PROTECTED`。 - 禁用仍有待处理业务的非默认商家 → 409 / `IDENTITY.MERCHANT_HAS_ACTIVE_WORK`。 +- 责任复核任一模块不可用 → 503 / `IDENTITY.MERCHANT_RESPONSIBILITY_UNAVAILABLE`,账号状态不变。 +- A016 使用某键后,A017 对同一或另一账号复用该键 → 409 / `IDEMPOTENCY.KEY_REUSED`。 - 禁用过程中 Redis 撤销不可用 → 503,不返回虚假成功。 ### A017 启用账号 @@ -1624,14 +1658,19 @@ AdminUserResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| +| 400 | `COMMON.INVALID_UUID` | `userId` 不是标准 UUID | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失或格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是管理员 | | 404 | `RESOURCE.NOT_FOUND` | 目标账号不存在或不是买家/商家 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一治理幂等键被用于不同目标或在禁用/启用动作间复用 | #### 业务规则与并发 +- 与 A016 共用 `(adminUserId, Idempotency-Key)` 治理作用域并绑定 `targetUserId + action`;同键同目标同动作重放首次结果,同键换目标或从禁用换成启用返回 `409 IDEMPOTENCY.KEY_REUSED`。 - 先查询持久化幂等结果;条件更新仅当目标为 `Disabled` 时改为 `Normal`,目标已是 `Normal` 时返回并绑定当前正常结果。 - 启用不回退 `tokenVersion`;禁用前签发的旧令牌仍不可用,需重新登录。 +- 账号状态、最小安全追踪信息(管理员、目标账号、动作、提交时间、`traceId`)与稳定幂等结果必须在同一 PostgreSQL 事务形成;任一步无法确认时不得返回启用成功。 #### 缓存、事件或外部依赖 @@ -1659,7 +1698,7 @@ AdminUserResponse { #### 请求 - Header:`Authorization: Bearer `(必填,角色 Buyer) -- Query 参数:`page`、`pageSize`、`sortBy`(仅允许 `createdAt`,默认 `createdAt desc`)、`sortOrder` +- Query 参数:`page`、`pageSize`;排序固定为 `createdAt desc, favoriteId desc`,不开放客户端排序字段和方向 #### 成功响应 @@ -1674,22 +1713,39 @@ FavoriteListResponse { total: integer totalPages: integer } + +FavoriteResponse { + productId: uuid + createdAt: string + product: EngagementProductSnapshot +} + +EngagementProductSnapshot { + productId: uuid + name: string? // 防御性缺失时为 null + mainImageUrl: string? // 防御性缺失或无图时为 null + currentPrice: number? // 商品实体缺失时为 null + salesStatus: "OnSale" | "OffSale" | "Missing" + stockStatus: "InStock" | "SoldOut" | "Unavailable" + isAvailable: boolean // 仅 OnSale 且有库存时为 true + unavailableReason: null | "OutOfStock" | "ProductOffSale" | "ProductMissing" +} ``` #### 失败响应 | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 分页或排序参数非法 | +| 400 | `COMMON.VALIDATION_FAILED` | 分页参数非法 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 商品批量快照暂时不可用;不得把整批商品伪装为 `Missing` | #### 业务规则与并发 - 严格按 `user_id = current_user_id` 过滤;不允许跨用户访问。 -- 排序白名单仅 `createdAt`,方向 `asc` / `desc`;非法字段返回 400。 - 默认按 `createdAt desc, favoriteId desc` 稳定分页。 -- 收藏商品摘要来自 Catalog 模块;商品已下架或防御性缺失时仍返回收藏占位,标记 `isAvailable=false`,不得丢失记录。 +- 收藏商品摘要来自 Catalog 模块;库存为零、商品已下架或防御性缺失时仍返回收藏占位,并分别标记 `OutOfStock`、`ProductOffSale`、`ProductMissing`,不得丢失记录或把实体缺失伪装成下架。 #### 缓存、事件或外部依赖 @@ -1728,15 +1784,7 @@ AddFavoriteRequest { #### 成功响应 - HTTP 状态:`201 Created`(新增)或 `200 OK`(幂等命中已存在) -- 响应 Schema:`FavoriteResponse` - -```text -FavoriteResponse { - productId: uuid - createdAt: string - productSummary: ProductSummaryResponse -} -``` +- 响应 Schema:`FavoriteResponse`(统一定义见 A018) #### 失败响应 @@ -1746,6 +1794,8 @@ FavoriteResponse { | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | +| 409 | `CATALOG.PRODUCT_NOT_ON_SALE` | 新建收藏时商品存在但当前不是 `OnSale` | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 新建收藏所需的 Catalog 商品公开事实暂时不可用 | #### 业务规则与并发 @@ -1754,13 +1804,14 @@ FavoriteResponse { #### 缓存、事件或外部依赖 -- 不缓存、不发布事件。 +- 不缓存、不发布事件;新建收藏通过 Catalog 公开应用契约确认商品存在且为 `OnSale`,依赖不可用时不得伪装成商品不存在或不可售。已有收藏的幂等重放不重复检查商品当前状态。 #### 验证场景 - 收藏存在商品 → 201/200。 - 重复收藏 → 200,已存在记录。 - 收藏不存在商品 → 404。 +- 收藏已下架商品 → 409 / `CATALOG.PRODUCT_NOT_ON_SALE`;已有收藏仍可由 A018 返回不可用占位。 ### A020 取消收藏 @@ -1790,6 +1841,7 @@ FavoriteResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| +| 400 | `COMMON.INVALID_UUID` | `productId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | @@ -1832,12 +1884,18 @@ FavoriteResponse { ```text BrowsingHistoryListResponse { - items: BrowsingHistoryResponse[] + items: BrowsingHistoryItemResponse[] page: integer pageSize: integer total: integer totalPages: integer } + +BrowsingHistoryItemResponse { + productId: uuid + viewedAt: string + product: EngagementProductSnapshot +} ``` #### 失败响应 @@ -1847,11 +1905,12 @@ BrowsingHistoryListResponse { | 400 | `COMMON.VALIDATION_FAILED` | 分页或排序参数非法 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 商品批量快照暂时不可用;不得把整批商品伪装为 `Missing` | #### 业务规则与并发 - 按 `user_id = current_user_id` 过滤并默认使用 `viewedAt desc, browsingHistoryId desc` 稳定分页。 -- 开关关闭只阻止未来写入,不能隐藏或删除已有历史;下架或防御性缺失商品仍以不可用占位返回。 +- 开关关闭只阻止未来写入,不能隐藏或删除已有历史;库存为零、下架或防御性缺失商品仍以 A018 定义的明确不可用占位返回,不能把“已不存在”伪装为“已下架”。 #### 缓存、事件或外部依赖 @@ -1892,7 +1951,7 @@ UpdateBrowsingHistorySettingRequest { - HTTP 状态:`200 OK` - 响应 Schema:`BrowsingHistorySettingResponse` -- 字段定义复用 A022 的同名响应 Schema,本接口不重复定义第二份结构。 +- 字段定义复用 A025 定义的 `BrowsingHistorySettingResponse`,本接口不重复定义第二份结构。 #### 失败响应 @@ -1905,7 +1964,8 @@ UpdateBrowsingHistorySettingRequest { #### 业务规则与并发 - 关闭开关不影响已有浏览记录;重新开启后恢复写入。 -- 开关切换与 A024 浏览写入按数据库确定提交顺序竞争:关闭先提交则后续写入返回 `recorded=false`;写入先提交则本次记录保留,随后关闭只影响未来请求。 +- A022 开关切换与 A024 浏览写入/裁剪必须经过同一买家级串行化门槛,可锁定稳定的设置/配额行、使用可重试的 Serializable 事务、事务级 advisory lock 或等效机制;设置记录尚不存在时也必须先安全建立默认 `enabled=true` 的稳定门槛,不能只做一次无锁读取。 +- 以该串行化门槛的数据库提交顺序裁决:关闭先提交则后续写入返回 `recorded=false`;写入先提交则本次记录保留,随后关闭只影响未来请求。 #### 缓存、事件或外部依赖 @@ -1915,6 +1975,7 @@ UpdateBrowsingHistorySettingRequest { - `enabled=false` → 200,后续访问商品不再写入历史。 - `enabled=true` → 200,重新开启写入。 +- A022 关闭与 A024 写入并发 → 只出现“写入先提交并保留”或“关闭先提交且 `recorded=false`”两种结果,不出现关闭已提交后补写。 ### A024 记录浏览历史 @@ -1946,10 +2007,10 @@ RecordBrowsingHistoryRequest { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`BrowsingHistoryResponse` +- 响应 Schema:`RecordBrowsingHistoryResponse` ```text -BrowsingHistoryResponse { +RecordBrowsingHistoryResponse { productId: uuid recorded: boolean // 开关关闭时为 false viewedAt: string? // recorded=true 时返回服务端生成的 UTC 时间 @@ -1962,17 +2023,18 @@ BrowsingHistoryResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或格式错误 | -| 401 | `AUTH.UNAUTHENTICATED` | 已登录用户令牌无效 | +| 401 | `AUTH.UNAUTHENTICATED` | 缺少或无效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在或未上架 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 商品资格校验暂时不可用 | #### 业务规则与并发 - 浏览记录开关关闭时返回 `200`、`recorded=false`,不写入记录;这属于用户偏好,不是权限错误。 - 游客身份不持久化记录;本接口仅 BuyerOnly;前端在游客访问商品详情时引导登录,登录后再调用本接口。 - 同一买家同一商品只保留一条记录;按 `(user_id, product_id)` 唯一约束写入或更新,`viewedAt` 始终取服务端时间。 -- 默认单买家最多保留 200 条记录;同一商品 upsert 与按 `viewedAt, browsingHistoryId` 裁剪最早记录必须在同一事务完成,并在 `trimmedCount` 返回本次清理数量。 -- A022 开关切换与本次写入由数据库提交顺序唯一裁决,不能先读开关后在其已关闭时继续写入。 +- A024 与 A022 共用同一买家级串行化门槛;取得门槛后才复核当前 `enabled`、校验本次商品资格、执行同一商品 upsert,并按 `viewedAt, browsingHistoryId` 裁剪最早记录。开关复核、写入和裁剪在同一事务提交,不能先读 `enabled=true` 后让关闭先提交、自己再补写。 +- 默认单买家最多保留 200 条记录;同一买家的并发写入被上述门槛串行化,任一提交后都必须满足总数 `≤ 200`,并在 `trimmedCount` 返回本次清理数量。 - 新写入仅接受当前已上架商品;商品后来下架时保留既有历史记录,并由列表标记为不可购买。 #### 缓存、事件或外部依赖 @@ -1985,8 +2047,9 @@ BrowsingHistoryResponse { - 已开启开关的买家查看新商品 → 200,`recorded=true`,记录新增。 - 同一买家再次查看该商品 → 200,仅更新时间,不重复创建。 - 关闭开关后调用 → 200,`recorded=false`,数据库不新增或更新记录。 -- 商家或游客调用 → 403 / `AUTH.FORBIDDEN`。 +- 游客未携带有效凭证 → 401 / `AUTH.UNAUTHENTICATED`;商家调用 → 403 / `AUTH.FORBIDDEN`。 - 达到 200 条上限后再记录 → 200,`trimmedCount` 大于 0。 +- 已有 199 条时两个不同商品并发写入 → 两次请求按同一买家门槛串行提交,最终记录数不超过 200。 - 商品不存在或已下架后调用 → 404;已有历史记录仍保留。 ### A025 查询浏览记录开关 @@ -2050,7 +2113,7 @@ BrowsingHistorySettingResponse { - 负责人:顾欣月 - 关联数据表:DB021 `categories` - 当前状态:待交叉评审 -- 用途:为购物端商品筛选提供当前启用的分类,供列表页分类入口使用。 +- 用途:为购物端商品筛选提供当前有效分类,供列表页分类入口使用。 - 方法与路径:`GET /api/categories` - operationId:`Catalog_ListCategories` - 请求 Schema:无 @@ -2065,12 +2128,12 @@ BrowsingHistorySettingResponse { - Query 参数:无。 - Header:`Accept: application/json`。 - Body:无。 -- 校验规则:只返回 `status = Enabled` 的分类;停用分类不作为购物端筛选入口,也不改变其下商品的销售状态。 +- 校验规则:只返回购物端有效分类,即分类自身 `status = Enabled`,且子分类的父分类也为 `Enabled`;不得返回父分类已停用的孤立子分类。分类失效不改变其下商品的销售状态。 #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`CategoryTreeResponse`,`data.items` 为分类数组,字段含 `categoryId`、`name`、`parentId`(可空)、`sortOrder`、`productCount`;`productCount` 只统计当前 `OnSale` 商品,不决定分类是否返回。 +- 响应 Schema:`CategoryTreeResponse`,`data.items` 为分类数组,字段含 `categoryId`、`name`、`parentId`(可空)、`sortOrder`、`productCount`。顶级分类的 `productCount` 统计直接归属自身及其有效直属子分类的去重 `OnSale` 商品,子分类只统计直接归属自身的 `OnSale` 商品;计数范围必须与 A102 选择同一分类时完全一致,计数为 0 不影响有效分类返回。 - 示例: ```json @@ -2093,7 +2156,8 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 -- 购物端只暴露启用分类;层级最多一层父子,`parentId` 为 `null` 表示顶级分类。 +- 分类层级最多一层父子,`parentId = null` 表示顶级分类;购物端只暴露自身及父级均启用的有效分类。 +- 停用顶级分类时,该分类及其直属子分类全部退出结果,但不改写子分类存储状态;重新启用顶级分类后,只有自身仍为 `Enabled` 的子分类恢复。 - 排序按 `sortOrder asc, categoryId asc` 稳定排序。 #### 缓存、事件或外部依赖 @@ -2102,7 +2166,8 @@ BrowsingHistorySettingResponse { #### 验证场景 -- 停用分类不出现在结果中;已启用但暂无 `OnSale` 商品的分类仍返回,`productCount=0`。 +- 停用顶级分类后结果中既没有该顶级分类,也没有其自身仍为启用的子分类;父分类恢复后,启用子分类重新出现。 +- 已有效但暂无 `OnSale` 商品的分类仍返回,`productCount=0`;顶级和子分类计数分别与 A102 对应筛选结果的 `total` 一致。 --- @@ -2132,7 +2197,7 @@ BrowsingHistorySettingResponse { | `page` | integer | 否 | 1 | ≥ 1 | | `pageSize` | integer | 否 | 10 | 1~100 | | `keyword` | string | 否 | — | 去首尾空白后长度 1~50;为空按未提供处理 | -| `categoryId` | uuid | 否 | — | 必须为存在且启用的分类 | +| `categoryId` | uuid | 否 | — | 必须为购物端有效分类(自身及父级均启用) | | `minPrice` | number | 否 | — | ≥ 0,最多两位小数 | | `maxPrice` | number | 否 | — | ≥ 0,且 ≥ `minPrice` | | `inStockOnly` | boolean | 否 | false | 为 `true` 时仅返回库存 > 0 | @@ -2167,16 +2232,20 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 分页、类型或枚举字段非法 | +| 400 | `COMMON.INVALID_UUID` | `categoryId` 不是标准 UUID | | 400 | `CATALOG.INVALID_PRICE_RANGE` | `minPrice > maxPrice` | | 400 | `CATALOG.INVALID_SORT_FIELD` | `sortBy` 不在白名单 | -| 404 | `CATALOG.CATEGORY_NOT_FOUND` | `categoryId` 不存在或已停用(按接口固定行为返回,不越权返回商品) | +| 404 | `CATALOG.CATEGORY_NOT_FOUND` | `categoryId` 不存在或不是购物端有效分类(自身或父级已停用) | +| 503 | `CATALOG.SEARCH_UNAVAILABLE` | 进阶搜索不可用且基础查询无法安全保证既定召回范围、强制过滤或参数安全 | #### 业务规则与并发 - 服务端始终附加“已上架(`OnSale`)”过滤;`Draft`、`OffSale` 和已物理删除商品不得泄露;不得因商品所属分类后来停用而额外隐藏仍为 `OnSale` 的商品。 +- 提供顶级 `categoryId` 时,匹配直接归属该顶级分类以及归属其有效直属子分类的商品;提供子分类时,只匹配直接归属该子分类的商品。未提供分类时不按分类有效状态过滤。 - 空结果为正常结果,返回空数组与真实分页元数据。 -- 稳定排序:业务排序字段相同时追加 `productId` 作为次级排序,避免翻页重复或遗漏。 -- C04:`keyword` 存在时走 `IProductSearch` 分词/倒排实现并支持 `relevance` 排序;进阶不可用时在保证“已上架过滤 + 参数安全”的前提下降级为 `ILIKE` 基础模糊查询并记录降级原因,返回口径不变。 +- 稳定排序:`relevance` 相同时依次按 Catalog 持有的上架时间倒序、`productId` 倒序;价格或 `createdAt` 相同时追加 `productId`,避免翻页重复或遗漏。 +- C04:`keyword` 存在时,进阶搜索必须对商品名称、所属分类名称和商品描述执行中文 N-gram 等效召回,并支持 `relevance` 排序;分类停用不阻止其下仍为 `OnSale` 的商品通过未指定分类的关键词命中。 +- 进阶能力不可用时,基础 `ILIKE` 降级仍须覆盖商品名称、所属分类名称和商品描述,并保持已上架过滤、全部组合筛选、参数化查询、稳定排序和分页;无法安全保证任一条件时返回 `503 CATALOG.SEARCH_UNAVAILABLE`,不得缩成“只搜名称”或返回未过滤候选。 #### 缓存、事件或外部依赖 @@ -2187,6 +2256,9 @@ BrowsingHistorySettingResponse { #### 验证场景 - 分类、关键词、价格区间、库存与白名单排序可独立与组合生效;翻页保持条件。 +- 顶级分类筛选结果包含其有效直属子分类商品且 `total` 与 A101 的 `productCount` 一致;子分类筛选只返回直接归属商品。 +- 父分类停用后使用其自身或子分类 ID 筛选均返回分类不可用;同一子树中的 `OnSale` 商品仍可通过未指定分类的全部商品与关键词搜索命中。 +- 中文关键词分别命中商品名称、分类名称或商品描述时均可召回;相关度相同按上架时间、商品 ID 稳定排序。 - 任意身份都搜索不到草稿/下架/已删除商品;非法排序字段返回 400。 --- @@ -2213,7 +2285,7 @@ BrowsingHistorySettingResponse { - Query 参数:无。 - Header:`Accept: application/json`。 - Body:无。 -- 校验规则:`productId` 格式校验;仅返回已上架(`OnSale`)商品;所属分类停用不影响已上架商品公开。 +- 校验规则:`productId` 格式校验;仅返回已上架(`OnSale`)商品;所属分类自身或父级停用不影响已上架商品公开。 #### 成功响应 @@ -2238,7 +2310,7 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | `productId` 格式非法 | +| 400 | `COMMON.INVALID_UUID` | `productId` 不是标准 UUID | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在或已物理删除 | | 404 | `CATALOG.PRODUCT_UNAVAILABLE` | 商品为 `Draft`、`OffSale` 或当前不可公开 | @@ -2267,7 +2339,7 @@ BrowsingHistorySettingResponse { - 负责人:顾欣月 - 关联数据表:DB021 `categories` - 当前状态:待交叉评审 -- 用途:商家维护商品时查看全部(含停用)分类及层级、排序与启停状态。 +- 用途:商家维护商品时查看全部(含停用)分类及层级、排序、存储启停状态和派生的购物端有效状态。 - 方法与路径:`GET /api/merchant/categories` - operationId:`Catalog_ListMerchantCategories` - 请求 Schema:无(Query) @@ -2287,8 +2359,8 @@ BrowsingHistorySettingResponse { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`MerchantCategoryListResponse`,元素含 `categoryId`、`name`、`parentId`、`sortOrder`、`status`(`Enabled`/`Disabled`)、`productCount`。 -- 示例:见 A101 结构,额外含 `status` 字段。 +- 响应 Schema:`MerchantCategoryListResponse`,元素为 `MerchantCategoryResponse`,含 `categoryId`、`name`、`parentId`、`sortOrder`、`status`(存储状态:`Enabled`/`Disabled`)、`isEffectiveForStorefront`(派生布尔值)和 `directProductCount`(直接归属该分类、尚未物理删除的全状态商品数)。 +- 示例:`{ "categoryId": "6f1d…", "name": "手机", "parentId": "08af…", "sortOrder": 1, "status": "Enabled", "isEffectiveForStorefront": false, "directProductCount": 12 }` 表示分类自身启用,但父分类停用。 #### 失败响应 @@ -2299,7 +2371,8 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 -- 与购物端 A101 分开:本接口返回全状态分类,购物端只返回启用分类。 +- 与购物端 A101 分开:本接口返回全状态分类;`status` 是持久化状态,`isEffectiveForStorefront = status == Enabled && (parentId == null || parent.status == Enabled)`,购物端只返回该派生值为 `true` 的分类。 +- 分类最多一层父子;后台不得为方便展示而把父分类停用时的子分类 `status` 批量改成 `Disabled`。 - 排序固定按 `sortOrder asc, categoryId asc`。 #### 缓存、事件或外部依赖 @@ -2309,6 +2382,7 @@ BrowsingHistorySettingResponse { #### 验证场景 - 买家/管理员调用返回 403;停用分类可在后台查询到。 +- 子分类自身为 `Enabled`、父分类为 `Disabled` 时,返回 `status=Enabled` 且 `isEffectiveForStorefront=false`。 --- @@ -2334,13 +2408,13 @@ BrowsingHistorySettingResponse { - Query 参数:无。 - Header:`Authorization`、`Content-Type: application/json`。 - Body:`CreateCategoryRequest`:`name`(string,必填,1~30,去首尾空白)、`parentId`(uuid,可空,最多一层)、`sortOrder`(integer,可选,默认 0,≥ 0)、`status`(string,必填,`Enabled`/`Disabled`)。 -- 校验规则:同一父级下 `name` 唯一;`parentId` 必须存在且为顶级分类(避免超过一层);`status` 只接受已确认的两态。 +- 校验规则:同一父级下 `name` 唯一;`parentId` 必须存在且为顶级分类(避免超过一层),父分类可以为 `Enabled` 或 `Disabled`;`status` 只接受已确认的两态。 #### 成功响应 - HTTP 状态:`201 Created` - 响应 Schema:`MerchantCategoryResponse`;本期没有单条分类读取接口,因此不返回不可解析的 `Location`。 -- 示例:`data` 含新 `categoryId`,并回显 `name`、`parentId`、`sortOrder`、`status`。 +- 示例:`data` 含新 `categoryId`,并回显 `name`、`parentId`、`sortOrder`、`status`、`isEffectiveForStorefront`、`directProductCount=0`。 #### 失败响应 @@ -2355,6 +2429,7 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 - 层级不超过一层;名称唯一约束由数据库保障。 +- 在停用父分类下创建 `status=Enabled` 的子分类是合法存储结果,但响应必须为 `isEffectiveForStorefront=false`,且 A101 不得返回该子分类。 #### 缓存、事件或外部依赖 @@ -2362,7 +2437,7 @@ BrowsingHistorySettingResponse { #### 验证场景 -- 重名返回 409;两层以上父级被拒绝。 +- 重名返回 409;两层以上父级被拒绝;停用父分类下创建启用子分类时存储状态与派生有效状态正确区分。 --- @@ -2385,7 +2460,7 @@ BrowsingHistorySettingResponse { - Route 参数:`categoryId`(uuid,必填)。 - Body:`UpdateCategoryRequest`:`name`、`parentId`(可空)、`sortOrder`;约束同 A111。 -- 校验规则:不允许将分类设为自身或其子级的子级;名称在同父级唯一。 +- 校验规则:不允许将分类设为自身或其子级的子级;新父级必须是顶级分类,允许其存储状态为 `Disabled`;当前分类存在直属子分类时,`parentId` 不得从 `null` 改为非空;名称在同父级唯一。 #### 成功响应 @@ -2396,16 +2471,20 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `categoryId` 不是标准 UUID | | 400 | `COMMON.VALIDATION_FAILED` | 字段非法 | | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | | 404 | `CATALOG.CATEGORY_NOT_FOUND` | 分类或父级不存在 | | 409 | `CATALOG.CATEGORY_NAME_CONFLICT` | 名称重复 | +| 409 | `CATALOG.CATEGORY_HIERARCHY_CONFLICT` | 仍有直属子分类的顶级分类被改为子分类,可能产生超过一层的层级 | #### 业务规则与并发 - 禁止形成环或超过一层层级。 +- 将顶级分类改为子分类前必须在同一已提交事实中确认其没有直属子分类;存在子分类时整体拒绝,不隐式迁移、删除或级联改绑子分类。 - 商品或历史引用不阻止分类名称、父级和排序等元数据编辑;`status` 不在 A112 修改,启用/停用分别走 A113/A114。 +- 子分类改绑到停用父分类后保留自身 `status`,但响应 `isEffectiveForStorefront=false`;从停用父分类移到启用父分类或顶级后按自身状态重新派生。 #### 缓存、事件或外部依赖 @@ -2413,7 +2492,7 @@ BrowsingHistorySettingResponse { #### 验证场景 -- 改名冲突返回 409;不存在分类返回 404。 +- 改名冲突返回 409;不存在分类返回 404;仍有直属子分类时改为子分类返回 `CATALOG.CATEGORY_HIERARCHY_CONFLICT`;合法改绑后 `isEffectiveForStorefront` 与父级状态一致。 --- @@ -2424,7 +2503,7 @@ BrowsingHistorySettingResponse { - 负责人:顾欣月 - 关联数据表:DB021 `categories` - 当前状态:待交叉评审 -- 用途:启用分类,使其可以重新作为购物端筛选入口和商品上架分类。 +- 用途:把分类存储状态设为启用;只有其父分类也启用时才成为购物端筛选入口和可用于商品新建、改绑、上架的有效分类。 - 方法与路径:`POST /api/merchant/categories/{categoryId}/enable` - operationId:`Catalog_EnableCategory` - 请求 Schema:无(Route) @@ -2439,12 +2518,13 @@ BrowsingHistorySettingResponse { #### 成功响应 -- HTTP 状态:`200 OK`,返回更新后 `status = Enabled`。 +- HTTP 状态:`200 OK`,返回更新后 `status = Enabled` 及重新派生的 `isEffectiveForStorefront`。 #### 失败响应 | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `categoryId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | | 404 | `CATALOG.CATEGORY_NOT_FOUND` | 分类不存在 | @@ -2452,6 +2532,7 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 - 启用不恢复已删除数据,也不自动上架该分类下的商品。 +- 启用子分类时若父分类仍为 `Disabled`,只保存子分类 `status=Enabled`,响应 `isEffectiveForStorefront=false`;启用顶级分类后,自身为 `Enabled` 的直属子分类自动恢复有效,无需批量改写。 #### 缓存、事件或外部依赖 @@ -2459,7 +2540,7 @@ BrowsingHistorySettingResponse { #### 验证场景 -- 启用后购物端分类列表包含该分类;重复启用幂等成功。 +- 有效分类启用后购物端分类列表包含该分类;父分类停用时启用子分类仍不进入 A101;重复启用幂等成功。 --- @@ -2470,7 +2551,7 @@ BrowsingHistorySettingResponse { - 负责人:顾欣月 - 关联数据表:DB021 `categories` - 当前状态:待交叉评审 -- 用途:停用分类,使其退出购物端筛选,并替代破坏性删除。 +- 用途:把分类存储状态设为停用;若为顶级分类,则其整个子树退出购物端筛选,并替代破坏性删除。 - 方法与路径:`POST /api/merchant/categories/{categoryId}/disable` - operationId:`Catalog_DisableCategory` - 请求 Schema:无(Route) @@ -2485,20 +2566,22 @@ BrowsingHistorySettingResponse { #### 成功响应 -- HTTP 状态:`200 OK`,返回更新后 `status = Disabled`。 +- HTTP 状态:`200 OK`,返回更新后 `status = Disabled`、`isEffectiveForStorefront=false`。 #### 失败响应 | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `categoryId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | | 404 | `CATALOG.CATEGORY_NOT_FOUND` | 分类不存在 | #### 业务规则与并发 -- 停用分类不做物理删除,不影响历史引用;停用后 A101/A102 不再以其作为筛选入口。 -- 停用不改变其下既有商品销售状态:已有 `OnSale` 商品继续在全部商品、关键词搜索和详情公开,并可修改不改变分类归属的字段;新建、改绑分类,以及 `Draft`/`OffSale` 商品重新上架时才要求分类为 `Enabled`(见 A122、A123、A125)。 +- 停用分类不做物理删除,不影响历史引用;停用顶级分类后 A101 不再返回该分类及其直属子分类,A102 也不接受该子树中的分类作为筛选条件。 +- 停用顶级分类只修改自身 `status`,不得批量改写子分类存储状态;再次启用后,自身仍为 `Enabled` 的直属子分类恢复有效。 +- 停用不改变其下既有商品销售状态:已有 `OnSale` 商品继续在全部商品、关键词搜索和详情公开,并可修改不改变分类归属的字段;新建、改绑分类,以及 `Draft`/`OffSale` 商品重新上架时才要求分类为购物端有效状态(见 A122、A123、A125)。 #### 缓存、事件或外部依赖 @@ -2506,7 +2589,7 @@ BrowsingHistorySettingResponse { #### 验证场景 -- 停用后购物端分类列表不含该分类;重复停用幂等成功。 +- 停用顶级分类后购物端分类列表不含其整个子树,子分类存储状态保持不变;重复停用幂等成功。 --- @@ -2540,7 +2623,7 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | `categoryId` 格式非法 | +| 400 | `COMMON.INVALID_UUID` | `categoryId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | | 404 | `CATALOG.CATEGORY_NOT_FOUND` | 分类不存在或已物理删除 | @@ -2579,7 +2662,7 @@ BrowsingHistorySettingResponse { #### 请求 -- Query 参数:`page`、`pageSize`(同通用分页)、`keyword`(可选)、`categoryId`(可选)、`status`(可选,多值:`Draft`/`OnSale`/`OffSale`)、`sortBy`(白名单:`createdAt`/`price`/`stock`,默认 `createdAt`)、`sortOrder`(默认 `desc`)。 +- Query 参数:`page`、`pageSize`(同通用分页)、`keyword`(可选)、`categoryId`(UUID,可选)、`status`(可选,多值:`Draft`/`OnSale`/`OffSale`)、`sortBy`(白名单:`createdAt`/`price`/`stock`,默认 `createdAt`)、`sortOrder`(默认 `desc`)。 - Header:`Authorization`、`Accept`。 - 校验规则:`status`、`sortBy` 白名单。 @@ -2593,6 +2676,7 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 参数非法 | +| 400 | `COMMON.INVALID_UUID` | `categoryId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | @@ -2613,7 +2697,7 @@ BrowsingHistorySettingResponse { ### A121 后台商品详情 -- 请求 Schema:—(Route) +- 请求 Schema:无(Route 参数见下) - 模块 / Tag:Catalog - 需求编号:M06-01-FR04、FR06 @@ -2641,6 +2725,7 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `productId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | @@ -2670,7 +2755,7 @@ BrowsingHistorySettingResponse { - 用途:商家录入商品基础信息,创建为草稿状态。 - 方法与路径:`POST /api/merchant/products` - operationId:`Catalog_CreateProduct` -- 请求 Schema:`multipart/form-data`(商品字段 + 首批图片) +- 请求 Schema:`CreateProductMultipartRequest`(`multipart/form-data`) - 响应 Schema:`MerchantProductDetailResponse` - 身份与 Policy:MerchantOnly。 - 幂等要求:必需 `Idempotency-Key`;相同键和请求指纹稳定重放首次确定结果,不重复创建商品或对象。 @@ -2683,14 +2768,14 @@ BrowsingHistorySettingResponse { | 字段 | 类型 | 必填 | 约束 | |---|---|---|---| | `name` | string | 是 | 1~100,去首尾空白 | -| `categoryId` | uuid | 是 | 存在且启用 | +| `categoryId` | uuid | 是 | 分类存在且在购物端有效(自身及父级均启用) | | `price` | number | 是 | ≥ 0,最多两位小数 | | `stock` | integer | 是 | ≥ 0 非负整数 | | `description` | string | 否 | ≤ 2000,受控内容 | | `images` | file[] | 是 | 1~8 张,第一张固定为主图 | | `altTexts` | string[] | 否 | 与 `images` 同序;每项 ≤ 100 | -- 校验规则:分类须启用;价格非负;库存非负整数;必须有 1~8 张图片;每张同时校验扩展名、声明 MIME、实际文件特征、大小和尺寸,仅接受 JPEG、PNG、WebP,单图 ≤ 5 MB,宽高均 400~4096 像素。 +- 校验规则:`CreateProductMultipartRequest` 由上述 form-data 字段构成;分类须在购物端有效;价格非负;库存非负整数;必须有 1~8 张图片;每张同时校验扩展名、声明 MIME、实际文件特征、大小和尺寸,仅接受 JPEG、PNG、WebP,单图 ≤ 5 MB,宽高均 400~4096 像素。 #### 成功响应 @@ -2701,14 +2786,15 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 字段校验失败 | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失/格式错误,或请求字段校验失败 | | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | | 404 | `CATALOG.CATEGORY_NOT_FOUND` | 分类不存在 | -| 409 | `CATALOG.CATEGORY_DISABLED` | 分类已停用,不能用于新建 | +| 409 | `CATALOG.CATEGORY_DISABLED` | 分类自身或父级已停用,当前不是购物端有效分类,不能用于新建 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同请求内容 | | 413 | `COMMON.PAYLOAD_TOO_LARGE` | 任一图片超过大小限制 | | 415 | `CATALOG.INVALID_IMAGE` | 图片格式或尺寸不合规 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 对象存储或数据库暂时不可用,未形成确定创建结果 | #### 业务规则与并发 @@ -2722,7 +2808,7 @@ BrowsingHistorySettingResponse { #### 验证场景 -- 缺必填、负价、停用分类被拒;成功后为草稿态且不出现在购物端。 +- 缺必填、负价、分类自身停用或父级停用均被拒;成功后为草稿态且不出现在购物端。 --- @@ -2745,7 +2831,7 @@ BrowsingHistorySettingResponse { - Route 参数:`productId`(uuid,必填)。 - Body:`UpdateProductRequest`:字段同 A122(`name`、`categoryId`、`price`、`stock`、`description`),另加必填 `version`(integer,来自 A121);图片新增和删除分别使用 A127、A128。 -- 校验规则:`version` 必填;仅当 `categoryId` 相对当前商品发生变化时,目标分类必须已启用。原分类后来停用时,仍允许修改名称、价格、库存、图片和描述等不改变分类归属的字段;其余字段规则同 A122。 +- 校验规则:`version` 必填;仅当 `categoryId` 相对当前商品发生变化时,目标分类必须在购物端有效(自身及父级均启用)。原分类自身或父级后来停用时,仍允许修改名称、价格、库存、图片和描述等不改变分类归属的字段;其余字段规则同 A122。 #### 成功响应 @@ -2756,11 +2842,12 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `productId` 不是标准 UUID | | 400 | `COMMON.VALIDATION_FAILED` | 字段非法或缺 `version` | | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | -| 409 | `CATALOG.CATEGORY_DISABLED` | 本次改绑的目标分类已停用 | +| 409 | `CATALOG.CATEGORY_DISABLED` | 本次改绑的目标分类自身或父级已停用,不是购物端有效分类 | | 409 | `CATALOG.PRODUCT_VERSION_CONFLICT` | 提交 `version` 与当前不一致(并发编辑冲突) | #### 业务规则与并发 @@ -2807,11 +2894,13 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `productId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在或已删除 | | 409 | `CATALOG.PRODUCT_MUST_BE_OFF_SALE` | 商品当前为 `OnSale`,必须先下架 | | 409 | `CATALOG.PRODUCT_HAS_REFERENCES` | 存在订单、购物车、收藏、浏览、评价、秒杀等任一历史关联,禁止物理删除 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 任一所属模块的历史引用检查暂时不可用,无法安全确认可删除 | #### 业务规则与并发 @@ -2858,17 +2947,18 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `productId` 不是标准 UUID | | 400 | `COMMON.VALIDATION_FAILED` | 缺少 `version` 或格式非法 | | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | | 409 | `CATALOG.PRODUCT_INCOMPLETE` | 必填项或主图缺失 | -| 409 | `CATALOG.CATEGORY_DISABLED` | 商品分类已停用 | +| 409 | `CATALOG.CATEGORY_DISABLED` | 商品分类自身或父级已停用,不是购物端有效分类 | | 409 | `CATALOG.PRODUCT_VERSION_CONFLICT` | 非目标态且提交 `version` 与当前版本不一致 | #### 业务规则与并发 -- 上架前必须满足:名称、启用分类、价格、库存以及至少一张主图;只允许 `Draft` 或 `OffSale` 进入 `OnSale`。 +- 上架前必须满足:名称、购物端有效分类(自身及父级均启用)、价格、库存以及至少一张主图;只允许 `Draft` 或 `OffSale` 进入 `OnSale`。 - 当前已为 `OnSale` 时直接返回当前结果;否则使用 `productId + version + 当前状态` 条件更新,确保编辑、上架和下架竞争只提交一个基于最新版本的结果。 #### 缓存、事件或外部依赖 @@ -2878,7 +2968,7 @@ BrowsingHistorySettingResponse { #### 验证场景 -- 缺主图或分类停用时返回 409;重复上架幂等成功。 +- 缺主图、分类自身停用或父级停用时返回 409;重复上架幂等成功。 --- @@ -2910,6 +3000,7 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `productId` 不是标准 UUID | | 400 | `COMMON.VALIDATION_FAILED` | 缺少 `version` 或格式非法 | | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | @@ -2942,7 +3033,7 @@ BrowsingHistorySettingResponse { - 用途:为商品上传图片到 S3 兼容对象存储,返回图片记录。 - 方法与路径:`POST /api/merchant/products/{productId}/images` - operationId:`Catalog_UploadProductImage` -- 请求 Schema:`multipart/form-data` +- 请求 Schema:`UploadProductImageRequest`(`multipart/form-data`) - 响应 Schema:`ProductImageResponse` - 身份与 Policy:MerchantOnly。 - 幂等要求:非幂等;每次上传生成新图片记录。 @@ -2951,7 +3042,7 @@ BrowsingHistorySettingResponse { - Route 参数:`productId`(uuid,必填)。 - Header:`Authorization`、`Content-Type: multipart/form-data`。 -- Body(form-data):`file`(图片文件,必填)、`isPrimary`(boolean,可选)、`altText`(string,可选)。 +- Body(form-data):`UploadProductImageRequest`,含 `file`(图片文件,必填)、`isPrimary`(boolean,可选)、`altText`(string,可选)。 - 校验规则:同时校验扩展名、声明 MIME、实际文件特征、大小与尺寸;仅接受 JPEG、PNG、WebP;单图 ≤ 5 MB;宽高均 400~4096 像素;单商品累计 ≤ 8 张。首图作为主图并生成方形缩略图。 #### 成功响应 @@ -2963,12 +3054,14 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `productId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | | 409 | `CATALOG.IMAGE_LIMIT_EXCEEDED` | 超过 8 张上限 | | 413 | `COMMON.PAYLOAD_TOO_LARGE` | 文件超过大小限制 | | 415 | `CATALOG.INVALID_IMAGE` | 格式或尺寸不符合要求 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 对象存储或数据库暂时不可用,未形成确定上传结果 | #### 业务规则与并发 @@ -3017,6 +3110,7 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `productId` 或 `imageId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 未认证 | | 403 | `AUTH.FORBIDDEN` | 非商家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | @@ -3087,7 +3181,9 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 参数非法 | +| 400 | `COMMON.INVALID_UUID` | `productId` 不是标准 UUID | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在或不可见 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 商品公开事实暂时不可用,无法安全判断评价是否可公开 | #### 业务规则与并发 @@ -3116,7 +3212,7 @@ BrowsingHistorySettingResponse { - 用途:买家在提交评价前逐张上传晒图,返回图片标识供 A142 引用。 - 方法与路径:`POST /api/reviews/images` - operationId:`Review_UploadReviewImage` -- 请求 Schema:`multipart/form-data` +- 请求 Schema:`UploadReviewImageRequest`(`multipart/form-data`) - 响应 Schema:`ReviewImageResponse` - 身份与 Policy:BuyerOnly。 - 幂等要求:非幂等;每次上传生成新的暂存图片。 @@ -3124,7 +3220,7 @@ BrowsingHistorySettingResponse { #### 请求 - Header:`Authorization`、`Content-Type: multipart/form-data`。 -- Body(form-data):`orderItemId`(uuid,必填)、`file`(图片文件,必填)。 +- Body(form-data):`UploadReviewImageRequest`,含 `orderItemId`(uuid,必填)、`file`(图片文件,必填)。 - 校验规则:先校验订单项属于当前买家、所属订单为 `Completed` 且尚无评价,再校验累计图片数不超过 6;仅接受 JPEG、PNG、WebP;单图 ≤ 5 MB;宽高均 200~4096 像素;同时校验扩展名、声明 MIME 与实际特征。该资格只用于允许上传,A142 正式提交时仍完整重检。 #### 成功响应 @@ -3144,6 +3240,7 @@ BrowsingHistorySettingResponse { | 409 | `REVIEW.IMAGE_LIMIT_EXCEEDED` | 该订单项累计可提交图片超过 6 张 | | 413 | `COMMON.PAYLOAD_TOO_LARGE` | 文件超限 | | 415 | `REVIEW.INVALID_IMAGE` | 格式或尺寸不符合要求 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Ordering 评价资格或对象存储暂时不可用 | #### 业务规则与并发 @@ -3201,7 +3298,7 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 评分、文字或图片字段非法 | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失/格式错误,或评分、文字、图片字段非法 | | 401 | `AUTH.UNAUTHENTICATED` | 未登录或登录失效 | | 403 | `AUTH.FORBIDDEN` | 非买家,或商家/管理员尝试提交 | | 404 | `REVIEW.ORDER_ITEM_NOT_FOUND` | 订单项不存在或不属于当前买家(不泄露归属) | @@ -3210,6 +3307,7 @@ BrowsingHistorySettingResponse { | 409 | `REVIEW.IMAGE_ALREADY_USED` | 任一图片已被其他评价使用 | | 409 | `REVIEW.IMAGE_LIMIT_EXCEEDED` | 图片数量超过 6 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同请求内容 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Ordering 订单项资格、Identity 展示名快照或图片关联依赖暂时不可用 | #### 业务规则与并发 @@ -3266,10 +3364,11 @@ BrowsingHistorySettingResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | `orderItemId` 非法 | +| 400 | `COMMON.INVALID_UUID` | `orderItemId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 未登录 | | 403 | `AUTH.FORBIDDEN` | 非买家 | | 404 | `REVIEW.ORDER_ITEM_NOT_FOUND` | 订单项不存在或不属于当前买家 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Ordering 订单项归属或订单状态暂时不可用,无法安全判断资格 | #### 业务规则与并发 @@ -3316,7 +3415,7 @@ AddCartItemRequest { - 校验规则: - `quantity` 必须为正整数;超过实时库存属于可变业务冲突,按 409 返回当前最大可设值。 - 服务端忽略请求中任何尝试指定 `userId`、`cartItemId`、`createdAt` 的字段;条目归属固定为当前买家。 - - 商品必须处于 `OnSale`;库存不足、商品下架或被禁用时拒绝。 + - 商品必须处于 `OnSale`;库存不足,或商品为 `Draft` / `OffSale` 时拒绝。所属分类停用不改变仍为 `OnSale` 商品的可加入资格。 #### 成功响应 @@ -3328,7 +3427,7 @@ AddCartItemRequest { CartItemResponse { cartItemId: uuid productId: uuid - productSummary: ProductSummaryResponse + productSummary: ProductSummary unitPrice: number // 服务端实时单价(decimal) quantity: integer subtotal: number // unitPrice × quantity,由服务端计算 @@ -3345,11 +3444,11 @@ CartItemResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | `quantity` 缺失、非整数或 ≤ 0 | +| 400 | `COMMON.VALIDATION_FAILED` | `productId` 缺失/格式错误,或 `quantity` 缺失、非整数、≤ 0 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | -| 409 | `CATALOG.PRODUCT_NOT_ON_SALE` | 商品不是 `OnSale` 或已被禁用 | +| 409 | `CATALOG.PRODUCT_NOT_ON_SALE` | 商品状态不是 `OnSale` | | 409 | `CART.QUANTITY_EXCEEDS_STOCK` | 累加后数量超过商品实时可售库存,返回当前最大允许值 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同请求内容 | | 429 | `COMMON.RATE_LIMITED` | 触发限流 | @@ -3361,9 +3460,9 @@ CartItemResponse { - 主键为 `(buyer_id, product_id)`;同一组合只能保留一条记录,重复加入时新数量累加到已有条目。 - 累加过程在同一数据库事务内完成:读取已有条目、加锁或条件更新、`quantity = quantity + :newQty`;影响行数为 0 即失败。 - 条目归属固定为当前买家;客户端传入的 `userId`、`cartItemId` 被忽略;越权访问他人条目返回 404。 -- 商品不可加时返回明确错误码与 `maxAllowedQuantity`;前端按此截断。 +- 商品状态不是 `OnSale` 时返回不可售错误;库存不足时返回含 `maxAllowedQuantity` 的数量错误,前端按此截断。所属分类停用不反向改变仍为 `OnSale` 的商品状态。 - `Idempotency-Key` 为可选;提供时,在固定身份、键格式和请求结构校验后先按买家、接口、键和请求指纹读取持久化结果,再读取商品、库存和购物车条目。同键同请求重放首次确定结果且不重复累加,同键换内容返回 `IDEMPOTENCY.KEY_REUSED`。 -- 提供幂等键时,条目变更与成功结果在同一 PostgreSQL 事务提交;商品不可售、库存不足等确定业务失败同样持久化后返回。数据库或依赖故障等未形成确定结果的瞬态失败不固化,允许原键重试;不发明固定分钟窗口。 +- 提供幂等键时,条目变更与成功结果在同一 PostgreSQL 事务提交;商品不可售、库存不足等确定业务失败同样持久化后返回。记录自首次确定结果提交起保留 24 小时,窗口内重放首次 HTTP 状态与响应体且不再次累加;到期后的同键按新请求重新校验当前事实。数据库或依赖故障等未形成确定结果的瞬态失败不固化,允许原键重试。 #### 缓存、事件或外部依赖 @@ -3375,11 +3474,13 @@ CartItemResponse { - 已上架商品、合法 `quantity` → 201,返回最新条目。 - 同一商品二次加入 → 200,条目数量累加,库存上限生效。 -- 数量 ≤ 0 或超过库存 → 400 / `COMMON.VALIDATION_FAILED`,附 `maxAllowedQuantity`。 +- 数量缺失、非整数或 ≤ 0 → 400 / `COMMON.VALIDATION_FAILED`。 +- 数量或累加后数量超过实时库存 → 409 / `CART.QUANTITY_EXCEEDS_STOCK`,附 `maxAllowedQuantity`,原条目不被超量写入。 - 商品已下架 → 409 / `CATALOG.PRODUCT_NOT_ON_SALE`,不创建条目。 -- 商品被禁用 → 409 / `CATALOG.PRODUCT_NOT_ON_SALE`。 +- 商品为 `Draft` 或 `OffSale` → 409 / `CATALOG.PRODUCT_NOT_ON_SALE`。 +- 所属分类停用但商品仍为 `OnSale` → 正常按实时库存加入购物车。 - 已存在购物车条目累加后超库存 → 409 / `CART.QUANTITY_EXCEEDS_STOCK`,原条目数量不超上限。 -- 同一 `Idempotency-Key` 重复提交 → 仅首次创建/累加,后续返回首次结果且 `quantity` 不再累加。 +- 同一 `Idempotency-Key` 在首次确定结果提交后 24 小时内重复提交 → 仅首次创建/累加,后续返回首次结果且 `quantity` 不再累加;窗口到期后按新请求重新校验。 ### A202 查看购物车 @@ -3431,12 +3532,13 @@ CartListResponse { | 400 | `COMMON.VALIDATION_FAILED` | 分页或筛选参数非法 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 商品状态、实时价格或库存暂时不可用,无法返回完整购物车事实 | #### 业务规则与并发 - 严格按 `buyer_id = current_user_id` 过滤;不允许跨用户查看。 - 排序固定按 `updatedAt desc, cartItemId desc`,避免同一商品事实变化或同时间戳导致翻页重复、遗漏。 -- 商品下架、库存归零或被禁用时,条目仍可见但标记 `isAvailable=false` 并附 `unavailableReason`;不参与 `selectedTotalAmount` 与 `availableSelectedCount` 计算。 +- 商品为 `Draft` / `OffSale` 或实时可售库存归零时,条目仍可见但标记 `isAvailable=false` 并附 `unavailableReason`;不参与 `selectedTotalAmount` 与 `availableSelectedCount` 计算。分类停用不反向改变仍为 `OnSale` 商品的可结算资格。 - 实时单价与库存来自 Catalog 模块;不接受客户端传入的价格或库存覆盖。 #### 缓存、事件或外部依赖 @@ -3487,18 +3589,20 @@ UpdateCartItemQuantityRequest { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| +| 400 | `COMMON.INVALID_UUID` | `cartItemId` 不是标准 UUID | | 400 | `COMMON.VALIDATION_FAILED` | `quantity` 缺失、非整数或 ≤ 0 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `RESOURCE.NOT_FOUND` | 条目不存在或不属于当前用户 | -| 409 | `CART.ITEM_UNAVAILABLE` | 商品已下架或被禁用,不允许调大 | +| 409 | `CART.ITEM_UNAVAILABLE` | 商品为 `Draft` / `OffSale`,不允许调大 | | 409 | `CART.QUANTITY_EXCEEDS_STOCK` | 调大后超过实时可售库存 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 商品状态或实时库存暂时不可用,无法安全完成并返回更新 | #### 业务规则与并发 - 严格按 `cart_item_id = :cartItemId AND buyer_id = current_user_id` 条件更新;不存在的条目返回 404。 - 新数量与旧数量相等时不产生写副作用,只按当前商品状态、价格和库存重新派生响应。 -- 调小不受实时库存上限约束;商品下架或被禁用时允许调小或删除,但保存后条目仍可能保持不可结算。只有调大才要求商品为 `OnSale` 且新数量不超过实时库存。 +- 调小不受实时库存上限约束;商品为 `Draft` / `OffSale` 时允许调小或删除,但保存后条目仍保持不可结算。只有商品为 `OnSale` 且新数量不超过实时库存时才允许调大;分类停用不单独阻止仍为 `OnSale` 商品的调大操作。 - 库存上限校验以 Catalog 模块实时库存为准;不允许客户端传入目标库存。 - 服务端不接受修改 `productId`、`isSelected`、`userId` 等字段;选中状态变更走 A206。 @@ -3543,6 +3647,8 @@ UpdateCartItemQuantityRequest { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| +| 400 | `COMMON.INVALID_UUID` | `cartItemId` 不是标准 UUID | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失或格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `CART.ITEM_NOT_FOUND` | 条目不存在或不属于当前买家 | @@ -3606,7 +3712,7 @@ BatchRemoveCartItemsResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | `cartItemIds` 缺失、为空、超过 100 个或包含非法 UUID | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失/格式错误,或 `cartItemIds` 缺失、为空、超过 100 个、包含非法 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `CART.ITEM_NOT_FOUND` | 任一条目不存在或不属于当前买家 | @@ -3664,12 +3770,13 @@ UpdateCartItemSelectionRequest { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | `mode` 非法、`cartItemIds` 缺失/超限或 `isSelected` 缺失 | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失/格式错误,或 `mode` 非法、`cartItemIds` 缺失/超限、`isSelected` 缺失 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `CART.ITEM_NOT_FOUND` | `SetExplicit` 中任一目标不存在或不属于当前买家 | | 409 | `CART.ITEM_UNAVAILABLE` | 尝试选中已下架或失效的条目 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同选择请求 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 本次动作需要判断可结算资格,但 Catalog 商品状态或库存暂时不可用 | #### 业务规则与并发 @@ -3720,6 +3827,7 @@ UpdateCartItemSelectionRequest { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失或格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同清空请求指纹 | @@ -3779,6 +3887,7 @@ CheckoutPreviewResponse { |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 商品状态、实时价格或库存暂时不可用,无法形成完整结算预览 | #### 业务规则与并发 @@ -3848,7 +3957,9 @@ SeckillActivityResponse { perBuyerLimit: integer startAt: string endAt: string - status: "Draft" + status: "Draft" | "Published" | "Ongoing" | "Ended" | "Cancelled" + cancelReason: string? // 仅 Cancelled 时非空 + cancelledAt: string? // 仅 Cancelled 时非空,UTC ISO 8601 createdAt: string updatedAt: string } @@ -3858,19 +3969,23 @@ SeckillActivityResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 字段缺失、格式错误或金额/数量/时间窗口非法 | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失/格式错误,或字段缺失、格式错误、金额/数量/时间窗口非法 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是商家 | +| 403 | `AUTH.ACCOUNT_DISABLED` | 创建提交前账号已被禁用或责任受理门槛拒绝 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | | 409 | `CATALOG.PRODUCT_NOT_ON_SALE` | 商品不是 `OnSale` | | 409 | `SECKILL.STOCK_EXCEEDS_AVAILABLE` | `plannedQuantity` 超过商品当前普通可售库存 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同活动草稿请求 | | 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 应用能力或数据库暂时不可用 | #### 业务规则与并发 +- 完成身份、幂等键格式和固定请求结构校验后先读取持久化结果;同键同请求重放首次草稿与 `Location`,同键换内容返回 `409 IDEMPOTENCY.KEY_REUSED`,不得创建第二个活动。 - 服务端以数据库权威时间校验 `startAt >= now()`、`endAt > startAt`;秒杀价只要求为正,不强制低于普通售价,也不发明最长 30 天或跨活动时段互斥规则。 - Draft 只保存 `plannedQuantity`,不写可抢 `remainingStock`、`soldCount` 或冻结量,不扣普通库存;发布前这些字段为 `null`/不适用。 - 本项目是单店 B2C 统一经营目录,不按商家账号隔离商品。`createdByMerchantUserId` 只决定活动维护权限,绝不决定秒杀订单的 `assignedMerchantUserId`。 +- 创建 Draft 会形成“未结束秒杀活动”的商家责任。A220 必须与 A016 通过 4.2 的责任受理门槛形成唯一提交顺序:Draft 先提交时 A016 必须看到阻断责任;禁用先提交时 A220 返回 `403 AUTH.ACCOUNT_DISABLED` 且不创建活动。仅在认证入口读取一次账号状态不足以证明该竞争。 #### 缓存、事件或外部依赖 @@ -3879,6 +3994,7 @@ SeckillActivityResponse { #### 验证场景 - 合法参数创建 → 201,状态 `Draft`,仅有 `plannedQuantity`,没有已划拨/剩余/已售库存。 +- 与管理员禁用同一商家并发 → 只有 Draft 创建成功并阻断禁用,或禁用先成功且创建被拒绝两种结果。 - `plannedQuantity` 超过商品普通库存 → 409 / `SECKILL.STOCK_EXCEEDS_AVAILABLE`。 - `startAt < now()` → 400 / `COMMON.VALIDATION_FAILED`。 - 同商品活动时间重叠不由本期接口额外拒绝。 @@ -3907,15 +4023,18 @@ SeckillActivityResponse { ```text UpdateSeckillActivityRequest { productId?: uuid // 可选;变更后仍须是 OnSale 商品 - activityName?: string // 可选 - seckillPrice?: number // 可选 - plannedQuantity?: integer // 可选;正整数,可调大或调小 - perBuyerLimit?: integer // 可选 + activityName?: string // 可选;去除首尾空白后 1~50 字 + seckillPrice?: number // 可选;> 0,最多两位小数 + plannedQuantity?: integer // 可选;>= 1,可调大或调小 + perBuyerLimit?: integer // 可选;>= 1,且不得超过最终 plannedQuantity startAt?: string // 可选;不得早于 now() endAt?: string // 可选 } ``` +- 至少提交一个可修改字段;空对象或全部字段均缺失返回 `400 COMMON.VALIDATION_FAILED`。 +- 未提交字段保持原值;提交后的完整活动计划必须重新满足 A220 的名称、价格、数量、限购和时间窗口约束。 + #### 成功响应 - HTTP 状态:`200 OK` @@ -3926,22 +4045,25 @@ UpdateSeckillActivityRequest { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 字段格式或时间窗口非法 | +| 400 | `COMMON.INVALID_UUID` | `activityId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是商家 | | 404 | `SECKILL.ACTIVITY_NOT_FOUND` | 活动不存在或不是当前商家创建 | +| 404 | `CATALOG.PRODUCT_NOT_FOUND` | 提交后的目标商品不存在 | | 409 | `SECKILL.INVALID_STATUS` | 活动不是 `Draft` | | 409 | `CATALOG.PRODUCT_NOT_ON_SALE` | 目标商品不是 `OnSale` | | 409 | `SECKILL.STOCK_EXCEEDS_AVAILABLE` | `plannedQuantity` 超过当前普通可售库存 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 商品资格或普通库存快照暂时不可用 | #### 业务规则与并发 - 仅允许 `status=Draft` 且 `createdByMerchantUserId=currentUserId` 的活动更新;活动不存在或非创建人统一 404。 -- `productId`、价格、时间、`plannedQuantity` 和限购均可在草稿中修改;计划量可调大或调小,但必须为正、不得超过目标商品当前普通可售库存,单用户限购不得超过计划量。 +- `productId`、名称、价格、时间、`plannedQuantity` 和限购均可在草稿中修改;计划量可调大或调小,但必须为正、不得超过目标商品当前普通可售库存,单用户限购不得超过计划量。 - 修改后仍满足 `startAt >= databaseNow` 与 `endAt > startAt`;不维护 `remaining + sold + frozen`,因为 Draft 尚未形成已划拨库存,本期也没有冻结量。 #### 缓存、事件或外部依赖 -- Draft 不公开且不缓存;更新只写 PostgreSQL 活动计划。 +- Draft 不公开且不缓存;活动计划写入 Seckill 自有 PostgreSQL,商品资格与普通库存快照通过 Catalog 公开应用契约取得,不直读 Catalog 内部表。 #### 验证场景 @@ -3979,6 +4101,8 @@ UpdateSeckillActivityRequest { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| +| 400 | `COMMON.INVALID_UUID` | `activityId` 不是标准 UUID | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失或格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是商家 | | 404 | `SECKILL.ACTIVITY_NOT_FOUND` | 活动不存在或不是当前商家创建 | @@ -4024,7 +4148,7 @@ UpdateSeckillActivityRequest { #### 请求 - Route 参数:`activityId: uuid` -- Header:`Authorization: Bearer `(必填,角色 Merchant) +- Header:`Authorization: Bearer `(必填,角色 Merchant)、`Idempotency-Key: `(必填) - Body: ```text @@ -4042,17 +4166,18 @@ CancelSeckillActivityRequest { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 字段格式错误 | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失/格式错误,或请求字段格式错误 | +| 400 | `COMMON.INVALID_UUID` | `activityId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是商家 | | 404 | `SECKILL.ACTIVITY_NOT_FOUND` | 活动不存在或不是当前商家创建 | -| 409 | `SECKILL.INVALID_STATUS` | 活动已 `Ended` 或已 `Cancelled` | +| 409 | `SECKILL.INVALID_STATUS` | 活动已 `Ended` / `Cancelled`,或数据库权威时间已达到 `endAt` | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同取消请求 | #### 业务规则与并发 - 完成固定校验后先读取持久化幂等结果;同键同请求重放首次取消结果,新键对 `Ended`/`Cancelled` 请求返回 409。 -- 条件更新:`status IN ('Draft','Published','Ongoing') → 'Cancelled'`;状态、`cancelReason`、`cancelledAt` 与确定幂等结果在同一事务提交。 +- 新请求的条件更新必须同时满足 `status IN ('Draft','Published','Ongoing')` 与 `databaseNow < endAt`;权威时间已达到 `endAt` 时先形成或视为自然 `Ended`,取消返回 `409 SECKILL.INVALID_STATUS`。状态、`cancelReason`、`cancelledAt` 与确定幂等结果在同一事务提交。 - Draft 取消时没有已划拨库存;`Published`/`Ongoing` 取消后,未售的 `remainingStock` 继续隔离留在原活动且不可售,不存在 `frozenCount`,也不回到普通库存。 - 已存在秒杀订单沿用 M04 状态机;C03 超时取消时回补到原秒杀库存通道。 @@ -4063,6 +4188,7 @@ CancelSeckillActivityRequest { #### 验证场景 - `Ongoing` 活动取消 → 200,状态 `Cancelled`,抢购入口立即失效。 +- Worker 尚未把状态推进为 `Ended`、但数据库权威时间已达到 `endAt` → 409 / `SECKILL.INVALID_STATUS`,不得误取消自然结束活动。 - 重复取消 → 409 / `SECKILL.INVALID_STATUS`。 - 已取消活动 → 409。 @@ -4115,6 +4241,7 @@ SeckillActivityListResponse { #### 业务规则与并发 +- 列表先按数据库权威时间幂等追赶或计算 `Published → Ongoing → Ended` 的等价有效状态;返回的 `status` 与状态筛选都使用有效状态,Worker 延迟不得让已开始活动仍按 `Published` 筛选,也不得让已结束活动仍显示 `Ongoing`。 - 严格按 `created_by_merchant_user_id = current_user_id` 过滤;创建人是活动操作归属,不代表商品租户隔离。 - 排序默认按 `startAt desc`;相同 `startAt` 时按 `activityId` 稳定排序。 @@ -4157,8 +4284,6 @@ SeckillActivityListResponse { SeckillActivityDetailResponse { activity: SeckillActivityResponse orderStats: SeckillOrderStatsResponse - cancelReason: string? - cancelledAt: string? } SeckillOrderStatsResponse { @@ -4173,20 +4298,23 @@ SeckillOrderStatsResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| +| 400 | `COMMON.INVALID_UUID` | `activityId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是商家 | | 404 | `SECKILL.ACTIVITY_NOT_FOUND` | 活动不存在或不是当前商家创建 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Ordering 活动订单统计契约暂时不可用 | #### 业务规则与并发 +- 详情先按数据库权威时间幂等追赶或计算 `Published → Ongoing → Ended` 的等价有效状态;`activity.status` 必须返回有效状态,不能暴露 Worker 延迟形成的旧生命周期值。 - 严格按 `created_by_merchant_user_id = current_user_id` 过滤;非创建人访问返回 404,避免泄露活动存在性。 - 订单统计通过 Ordering 公开查询契约取得;Seckill 不读取 Ordering 内部订单表,也不维护平行订单事实。 -- `cancelReason` 与 `cancelledAt` 仅在 `status=Cancelled` 时返回。 +- `activity.cancelReason` 与 `activity.cancelledAt` 仅在 `activity.status=Cancelled` 时非空;详情顶层不重复表达这两个事实。 - 活动自身 `plannedQuantity`、`allocatedQuantity`、`remainingStock`、`soldCount` 是库存权威:Draft 只有计划量,发布后才有已划拨/剩余/已售数量;`orderStats` 只是附加汇总,不能用订单笔数替代按数量扣减、已售数量或买家限购占用。 #### 缓存、事件或外部依赖 -- 不缓存;订单统计每次实时计算。 +- 不缓存;活动事实读取 Seckill 自有 PostgreSQL,订单统计每次通过 Ordering 公开应用契约实时计算,不直读 Ordering 内部表。 #### 验证场景 @@ -4219,23 +4347,55 @@ SeckillOrderStatsResponse { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`PublicSeckillActivityListResponse`,元素固定包含 `activityId`、`product`(`productId`、`name`、`mainImageUrl`)、`status`(`Published`/`Ongoing`)、`seckillPrice`、`originalPrice`、`startAt`、`endAt`、`remainingStock`、`soldCount`、`isSoldOut`、`serverTime`、`resultVersion`。 +- 响应 Schema:`PublicSeckillActivityListResponse` + +```text +PublicSeckillProductResponse { + productId: uuid + name: string + mainImageUrl: string? +} + +PublicSeckillActivityResponse { + activityId: uuid + product: PublicSeckillProductResponse + status: "Published" | "Ongoing" + seckillPrice: number + originalPrice: number + startAt: string + endAt: string + remainingStock: integer + soldCount: integer + isSoldOut: boolean + serverTime: string + resultVersion: integer +} + +PublicSeckillActivityListResponse { + items: PublicSeckillActivityResponse[] + page: integer + pageSize: integer + total: integer + totalPages: integer +} +``` #### 失败响应 | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 分页或 `window` 参数非法 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 商品公开快照暂时不可用 | #### 业务规则与并发 -- 仅返回 `status IN ('Published','Ongoing')` 的活动;已结束、已取消或草稿活动不公开。 +- 查询先按数据库权威时间对 `Published → Ongoing → Ended` 做幂等追赶或计算等价有效状态;只返回当前有效状态为 `Published` 且 `databaseNow < startAt`,或有效状态为 `Ongoing` 且 `startAt <= databaseNow < endAt` 的活动。Worker 扫描滞后不得让已结束活动继续公开。 - 排序默认按 `startAt asc`(即将开始优先),相同 `startAt` 时按 `activityId` 稳定排序。 - `remainingStock`、`soldCount` 和 `isSoldOut` 从同一已提交库存事实派生;`serverTime` 使用服务端权威 UTC 时间,`resultVersion` 随活动/库存结果单调变化,前端不得用旧结果覆盖新结果。 #### 缓存、事件或外部依赖 -- A226 每次直读 PostgreSQL 活动与库存事实;秒杀列表、状态、剩余库存、已售数量和售罄结果不进入 Redis/C07。 +- A226 每次直读 Seckill 自有 PostgreSQL 活动与库存事实,并通过 Catalog 批量公开应用契约取得商品名称与主图;秒杀列表、状态、剩余库存、已售数量和售罄结果不进入 Redis/C07。 #### 验证场景 @@ -4266,25 +4426,35 @@ SeckillOrderStatsResponse { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`PublicSeckillActivityDetailResponse`,固定包含 A226 单项全部公开字段;不复用含 `orderStats`、`cancelReason` 或创建人信息的商家详情 Schema。 -- 若携带可验证且状态正常的 Buyer JWT,额外返回 `currentBuyerOccupiedQuantity` 与 `currentBuyerRemainingQuantity`;未携带或可选 Token 无效/过期/禁用时按游客返回,不返回私人限购字段。 +- 响应 Schema:`PublicSeckillActivityDetailResponse`,继承 A226 的 `PublicSeckillActivityResponse` 全部公开字段,不复用含 `orderStats`、`cancelReason` 或创建人信息的商家详情 Schema。 + +```text +PublicSeckillActivityDetailResponse extends PublicSeckillActivityResponse { + currentBuyerOccupiedQuantity?: integer // 仅有效 Buyer JWT 时返回,≥ 0 + currentBuyerRemainingQuantity?: integer // 仅有效 Buyer JWT 时返回,≥ 0 +} +``` + +- 未携带可选 Token,或可选 Token 无效、过期、账号禁用时按游客返回,并省略两个私人限购字段;不得以 `0` 冒充未知买家事实。 #### 失败响应 | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| +| 400 | `COMMON.INVALID_UUID` | `activityId` 不是标准 UUID | | 404 | `RESOURCE.NOT_FOUND` | 活动不存在或未公开 | | 410 | `SECKILL.ACTIVITY_GONE` | 活动已结束或已取消 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 商品公开快照暂时不可用 | #### 业务规则与并发 -- 仅返回 `status IN ('Published','Ongoing')` 的活动;其他状态返回 410。 +- 查询先按数据库权威时间对 `Published → Ongoing → Ended` 做幂等追赶或计算等价有效状态;只有当前有效状态为 `Published` 且 `databaseNow < startAt`,或为 `Ongoing` 且 `startAt <= databaseNow < endAt` 时返回。已到 `endAt` 即使 Worker 尚未落库 `Ended` 也返回 410;草稿或已取消同样不公开。 - 已登录买家提示按 `(activity_id, buyer_id)` 的当前有效占用数量计算,不按订单笔数计算;待支付和已支付订单按购买数量占用,取消成功才按数量释放。 - `remainingStock`、`soldCount`、`isSoldOut`、`serverTime` 与 `resultVersion` 均来自权威 PostgreSQL 结果,客户端按版本应用更新。 #### 缓存、事件或外部依赖 -- A227 每次直读 PostgreSQL;不缓存活动详情或限购数量,C07 只服务普通商品固定首页和 A103 商品详情。 +- A227 每次直读 Seckill 自有 PostgreSQL 活动与限购事实,并通过 Catalog 公开应用契约取得商品名称与主图;不缓存活动详情或限购数量,C07 只服务普通商品固定首页和 A103 商品详情。 #### 验证场景 @@ -4346,7 +4516,7 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或格式错误 | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失/格式错误,或字段缺失、格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `SECKILL.ACTIVITY_NOT_FOUND` | 活动不存在 | @@ -4367,11 +4537,11 @@ PlaceSeckillOrderResponse { - 秒杀下单为高风险操作,必须使用 `Idempotency-Key`;缺失时返回 400 / `COMMON.VALIDATION_FAILED`。 - 完成认证和固定请求字段校验后,先读取 PostgreSQL 幂等记录:相同 Key + 相同请求指纹直接重放首次结果,不再经过限流、时间、库存或限购校验;相同 Key + 不同指纹立即返回 `409 / IDEMPOTENCY.KEY_REUSED`。只有全新 Key 才进入后续可变业务校验。 -- 全新请求在幂等检查后才执行正式限流,并读取活动、地址归属、买家限购占用和 Identity 的唯一启用默认商家;A228 不再次用 Catalog 当前上下架状态推翻已发布活动的独立库存资格。 +- 全新请求在幂等检查后才执行正式限流,并读取活动、秒杀库存和买家限购占用;随后按数据库权威时间幂等追赶或计算 `Published → Ongoing → Ended` 的等价有效状态,Worker 稍有延迟不得让已开始活动误报未开始,也不得让已结束活动继续成交。A228 不读取 Identity 地址表或默认商家,也不再次用 Catalog 当前上下架状态推翻已发布活动的独立库存资格。地址归属和唯一启用默认商家统一交给 Ordering 订单创建契约校验。 - 同一数据库事务内顺序: - 1. 按 `Ongoing + 权威时间窗口 + remaining >= quantity` 条件扣减秒杀库存并增加 `soldCount`;影响行数为 0 时不产生部分结果。 + 1. 在同一事务中先按数据库权威时间追赶活动有效状态,再按 `Ongoing + startAt <= databaseNow < endAt + remaining >= quantity` 条件扣减秒杀库存并增加 `soldCount`;影响行数为 0 时不产生部分结果,并按追赶后的状态返回未开始、已结束、已取消或售罄。 2. 按 `(activityId, buyerId)` 对当前有效占用数量执行原子条件更新,保证累加后不超过 `perBuyerLimit`;普通聚合查询或 Redis 不能作为限购正确性边界。取消成功按 `orderId` 和原数量最多释放一次。 - 3. 通过 Ordering 统一创建 DB061/DB062 共享订单事实,使用 Identity 解析出的唯一启用默认商家写入 `assignedMerchantUserId`,保存地址快照、订单项与成交价快照、`Seckill` 来源、`seckillActivityId` 和固定 `paymentDeadline`。活动 `createdByMerchantUserId` 只决定活动管理权,绝不参与订单分配。 + 3. 调用 Ordering 统一订单创建契约,由 Ordering 校验 `buyerId + addressId` 并形成地址快照、解析唯一启用默认商家并写入 `assignedMerchantUserId`,随后保存 DB061/DB062 订单与订单项、成交价快照、`Seckill` 来源、`seckillActivityId` 和固定 `paymentDeadline`。Seckill 只提供活动、商品、数量、成交价和库存来源输入;活动 `createdByMerchantUserId` 只决定活动管理权,绝不参与订单分配。 4. 秒杀库存、限购占用、共享订单与快照、可靠待发布订单事实和确定幂等结果整体提交;任一步失败全部回滚,Seckill 不建立第二套订单状态机或事件。 - 不写入普通商品库存;`products.stock` 不受秒杀下单影响。 - 成功、未开始、已结束、已取消、售罄、超限、地址无效、默认商家不可用和已正式返回的 429 都是可重放的确定结果,必须与本次 Key 持久绑定后再返回;数据库断连、事务提交未知或依赖中断等未形成确定结果的失败不固化,客户端用原 Key 重试。 @@ -4390,6 +4560,7 @@ PlaceSeckillOrderResponse { - 同一幂等键重复提交 → 第二次返回首次成功订单号,不重复扣减。 - 活动未开始 → 409 / `SECKILL.NOT_STARTED`。 - 活动已结束 → 409 / `SECKILL.ALREADY_ENDED`。 +- Worker 尚未落库 `Ongoing`、但数据库权威时间已进入活动窗口 → 按有效 `Ongoing` 正常参与库存竞争,不误报未开始。 - 活动发布后普通商品下架不反向改写已划拨秒杀库存资格;A228 仍按活动状态、时间、库存和限购裁决。 - 地址不属于当前买家 → 404 / `IDENTITY.ADDRESS_NOT_FOUND`,不泄露地址存在性。 - 默认商家配置缺失、重复或禁用 → 503 / `ORDER.DEFAULT_MERCHANT_UNAVAILABLE`,不创建无人负责订单。 @@ -4402,7 +4573,7 @@ PlaceSeckillOrderResponse { - **需求编号**:F08 - **负责人**:韦乾强 - **关联数据表**:DB061(orders)、DB062(order_items) -- **当前状态**:部分定义 +- **当前状态**:待交叉评审 - **用途**:买家选择购物车商品和收货地址提交订单,系统原子扣减库存并返回订单号 - **方法与路径**:`POST /api/orders` - **operationId**:`Ordering_CreateOrder` @@ -4410,7 +4581,7 @@ PlaceSeckillOrderResponse { - **响应Schema**:`CreateOrderResponse` - **身份与Policy**:BuyerOnly - **资源归属**:订单归属于当前登录买家 -- **幂等要求**:客户端生成幂等键 `Idempotency-Key`,服务端以 `(buyerId, idempotencyKey)` 保证幂等 +- **幂等要求**:客户端生成幂等键 `Idempotency-Key`,服务端以订单创建事实内的 `(buyerId, Idempotency-Key)` 唯一范围保证幂等 #### 请求 @@ -4420,7 +4591,16 @@ PlaceSeckillOrderResponse { - `Authorization: Bearer `(必需) - `Idempotency-Key: `(必需) - `Content-Type: application/json` -- **Body**: +- **请求 Schema**: + +```text +CreateOrderRequest { + addressId: uuid + cartItemIds: uuid[] // 非空,元素唯一 +} +``` + +- **Body 示例**: ```json { "addressId": "uuid", @@ -4429,14 +4609,25 @@ PlaceSeckillOrderResponse { ``` - **校验规则**: - `addressId`:必填,UUID格式,必须属于当前买家 - - `cartItemIds`:必填,非空、元素唯一,每个元素为 UUID;全部条目必须存在且属于当前买家 + - `cartItemIds`:必填,非空、元素唯一,每个元素为 UUID;全部条目必须存在、属于当前买家且在服务端当前仍为已选中 - `Idempotency-Key` 只从 Header 读取,Body 不重复传递 #### 成功响应 - **HTTP状态**:`201 Created` - **Response Header**:`Location: /api/orders/{orderId}`(A303) -- **响应Schema**:`CreateOrderResponse` +- **响应 Schema**: + +```text +CreateOrderResponse { + orderId: uuid + totalAmount: decimal // 服务端重算,> 0,保留两位小数 + status: "PendingPayment" + createdAt: datetime + paymentDeadline: datetime // createdAt + 创建时固化的有效等待时长 +} +``` + - **示例**: ```json { @@ -4456,25 +4647,30 @@ PlaceSeckillOrderResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失或格式错误 | | 400 | ORDER.INVALID_PARAM | 参数格式错误 | | 400 | ORDER.EMPTY_CART_ITEMS | 购物车商品列表为空 | | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 403 | AUTH.FORBIDDEN | 当前账号不是买家 | | 404 | IDENTITY.ADDRESS_NOT_FOUND | 地址不存在或不属于当前买家 | | 404 | CART.ITEM_NOT_FOUND | 任一购物车条目不存在或不属于当前买家 | +| 409 | CART.ITEM_NOT_SELECTED | 任一请求条目在服务端当前不是已选中状态 | | 409 | ORDER.STOCK_INSUFFICIENT | 商品库存不足 | | 409 | ORDER.ITEM_NOT_AVAILABLE | 商品已下架或不可售 | +| 409 | ORDER.TOTAL_MUST_BE_POSITIVE | 服务端按实时成交价重算的订单总额不大于 0 | | 409 | IDEMPOTENCY.KEY_REUSED | 同一幂等键被用于不同请求内容 | | 503 | ORDER.DEFAULT_MERCHANT_UNAVAILABLE | Identity 未能解析唯一且启用的默认商家运营账号 | +| 503 | COMMON.DEPENDENCY_UNAVAILABLE | Identity 地址、Cart 条目、Catalog 商品/库存或数据库依赖暂时不可用,无法安全创建订单 | #### 业务规则与并发 -1. 完成身份、幂等键格式和固定请求结构校验后,先按买家、接口、键和请求指纹读取 PostgreSQL 幂等结果,再读取地址、购物车、商品、库存和默认商家等可变事实;同键同请求重放首次完整结果,同键换内容返回 409。 -2. 服务端从购物车重读每项商品 ID、数量和选中/归属事实,从 Catalog 重读 `OnSale`、实时价格与普通库存;不接受客户端传入数量、价格、金额或商家。 -3. 普通库存扣减使用条件更新 `WHERE stock >= quantity`,避免超卖;订单金额由服务端按已确认实时价格计算。 +1. 完成身份、幂等键格式和固定请求结构校验后,先按 `(buyerId, Idempotency-Key)` 与请求指纹读取 PostgreSQL 订单创建幂等结果,再读取地址、购物车、商品、库存和默认商家等可变事实;同键同请求重放首次完整结果,同键换内容返回 409。 +2. 服务端从购物车重读每项商品 ID、数量、已选中状态和归属事实;任一条目未选中则整单拒绝。从 Catalog 重读 `OnSale`、实时价格与普通库存;不接受客户端传入数量、价格、金额或商家。 +3. 普通库存扣减使用条件更新 `WHERE stock >= quantity`,避免超卖;订单金额由服务端按已确认实时价格计算且必须大于 0,否则整单拒绝。 4. 本期使用 UUID `orderId` 作为对外订单号,不生成暴露业务量的顺序型 `ORD...` 编号。订单项保存 `orderItemId`、商品 ID、名称、图片、成交单价和数量快照,地址保存完整下单时快照。 5. 普通订单和秒杀订单都通过 Identity 解析同一唯一启用默认商家并写入 `assignedMerchantUserId`;未配置、重复、禁用或与下单并发失效时整单失败,不创建无人处理订单。 -6. 商品/库存/默认商家不可用等已经正式裁决的业务失败可与幂等键持久绑定并稳定重放;数据库连接中断、事务提交未知等未形成确定结果的失败不固化。 +6. 只有已经形成确定裁决的业务结果才可与幂等键持久绑定并稳定重放,包括成功结果、库存不足、商品不可售、总额不合法等 409 结果,以及默认商家配置确定不可用的 `ORDER.DEFAULT_MERCHANT_UNAVAILABLE`。`COMMON.DEPENDENCY_UNAVAILABLE`、数据库连接中断、事务提交结果未知等未形成确定结果的失败一律不固化;客户端可使用同一键安全重试。 +7. 正式环境的支付截止时间固定为订单 `createdAt + 30 分钟`;演示环境只能通过可追踪配置缩短等待,配置值在订单创建时固化为 `paymentDeadline`,不得追溯修改历史订单或把演示值写成正式规则。 #### 缓存、事件或外部依赖 @@ -4487,7 +4683,8 @@ PlaceSeckillOrderResponse { 1. 正常提交订单:返回201,订单号 2. 库存不足:返回409,订单未创建 3. 地址无效或任一购物车条目越权:返回404且整单不创建 -4. 幂等键重复:返回原订单号,不重复扣库存 +4. 任一条目已取消选中,或服务端重算总额不大于 0:返回对应 409,整单不创建 +5. 幂等键重复:返回原订单号,不重复扣库存 --- @@ -4497,7 +4694,7 @@ PlaceSeckillOrderResponse { - **需求编号**:F09 - **负责人**:韦乾强 - **关联数据表**:DB061(orders)、DB062(order_items) -- **当前状态**:部分定义 +- **当前状态**:待交叉评审 - **用途**:买家分页查询自己的普通或秒杀订单,支持按状态、订单来源和秒杀活动筛选 - **方法与路径**:`GET /api/orders` - **operationId**:`Ordering_ListOrders` @@ -4518,6 +4715,7 @@ PlaceSeckillOrderResponse { - `seckillActivityId`(可选,UUID):按秒杀活动筛选;传入时 `orderType` 固定按 `Seckill` 处理 - **Header**:`Authorization: Bearer `(必需) - **Body**:无 +- **校验规则**:`status` 只接受五种订单状态,`orderType` 只接受 `Normal` / `Seckill`;非法枚举返回字段级参数错误,不得静默按“全部”查询。 #### 成功响应 @@ -4557,6 +4755,8 @@ PlaceSeckillOrderResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| | 400 | ORDER.INVALID_PAGE_PARAM | 分页参数非法 | +| 400 | `COMMON.VALIDATION_FAILED` | `status` 或 `orderType` 不是受控枚举 | +| 400 | `COMMON.INVALID_UUID` | `seckillActivityId` 不是标准 UUID | | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 403 | AUTH.FORBIDDEN | 当前账号不是买家 | @@ -4575,7 +4775,8 @@ PlaceSeckillOrderResponse { 1. 正常查询:返回订单列表 2. 分页参数非法:返回400 -3. 无订单:返回空列表 +3. 状态筛选非法:返回 400 字段级错误,不按全部状态查询 +4. 无订单:返回空列表 --- @@ -4585,7 +4786,7 @@ PlaceSeckillOrderResponse { - **需求编号**:F09 - **负责人**:韦乾强 - **关联数据表**:DB061(orders)、DB062(order_items) -- **当前状态**:部分定义 +- **当前状态**:待交叉评审 - **用途**:买家查看单个订单的完整详情 - **方法与路径**:`GET /api/orders/{orderId}` - **operationId**:`Ordering_GetOrder` @@ -4621,7 +4822,7 @@ PlaceSeckillOrderResponse { "seckillUnitPrice": 199.00 }, "status": "PendingPayment", - "totalAmount": 299.00, + "totalAmount": 199.00, "createdAt": "2026-07-24T10:00:00Z", "paymentDeadline": "2026-07-24T10:30:00Z", "canPay": true, @@ -4656,7 +4857,7 @@ PlaceSeckillOrderResponse { "statusHistory": [ {"status": "PendingPayment", "time": "2026-07-24T10:00:00Z"} ], - "availableActions": ["cancel"] + "availableActions": ["pay", "cancel"] } } ``` @@ -4665,8 +4866,10 @@ PlaceSeckillOrderResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `orderId` 不是标准 UUID | | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 404 | ORDER.NOT_FOUND | 订单不存在或不属于当前买家 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Payment 或 AfterSales 已提交摘要暂时不可用,无法返回完整订单详情 | #### 业务规则与并发 @@ -4696,7 +4899,7 @@ PlaceSeckillOrderResponse { - **需求编号**:F09 - **负责人**:韦乾强 - **关联数据表**:DB061(orders)、DB062(order_items) -- **当前状态**:部分定义 +- **当前状态**:待交叉评审 - **用途**:买家取消自己待支付的订单,触发库存回补 - **方法与路径**:`POST /api/orders/{orderId}/cancel` - **operationId**:`Ordering_CancelOrder` @@ -4735,9 +4938,11 @@ PlaceSeckillOrderResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `orderId` 不是标准 UUID | | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 404 | ORDER.NOT_FOUND | 订单不存在或不属于当前买家 | | 409 | ORDER.INVALID_STATUS | 支付或后续状态已经胜出;响应包含当前订单状态,已取消则返回现有成功结果 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 或 Seckill 原库存通道暂时不可用,取消事务整体未提交 | #### 业务规则与并发 @@ -4766,7 +4971,7 @@ PlaceSeckillOrderResponse { - **需求编号**:F12 - **负责人**:韦乾强 - **关联数据表**:DB061(orders)、DB062(order_items) -- **当前状态**:部分定义 +- **当前状态**:待交叉评审 - **用途**:商家分页查询分配给当前运营账号的订单,支持按状态筛选 - **方法与路径**:`GET /api/merchant/orders` - **operationId**:`Ordering_ListMerchantOrders` @@ -4850,7 +5055,7 @@ PlaceSeckillOrderResponse { - **需求编号**:F12 - **负责人**:韦乾强 - **关联数据表**:DB061(orders)、DB062(order_items) -- **当前状态**:部分定义 +- **当前状态**:待交叉评审 - **用途**:商家查看分配给当前运营账号的订单详情 - **方法与路径**:`GET /api/merchant/orders/{orderId}` - **operationId**:`Ordering_GetMerchantOrder` @@ -4927,8 +5132,10 @@ PlaceSeckillOrderResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `orderId` 不是标准 UUID | | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 404 | ORDER.NOT_FOUND | 订单不存在或未分配给当前 `assignedMerchantUserId` | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | AfterSales 履约阻断或退款数量快照暂时不可用,无法返回完整详情 | #### 业务规则与并发 @@ -4956,7 +5163,7 @@ PlaceSeckillOrderResponse { - **需求编号**:F12 - **负责人**:韦乾强 - **关联数据表**:DB061(orders)、DB062(order_items) -- **当前状态**:部分定义 +- **当前状态**:待交叉评审 - **用途**:商家对已支付订单执行发货操作 - **方法与路径**:`POST /api/merchant/orders/{orderId}/ship` - **operationId**:`Ordering_ShipOrder` @@ -4964,7 +5171,7 @@ PlaceSeckillOrderResponse { - **响应Schema**:`ShipOrderResponse` - **身份与Policy**:MerchantOnly - **资源归属**:订单的 `assignedMerchantUserId` 必须等于当前账号 -- **幂等要求**:必需 `Idempotency-Key`;同键同请求稳定重放首次确定结果,同键换内容返回 409 +- **幂等要求**:必需 `Idempotency-Key`;同键同请求稳定重放首次确定结果,同键换内容返回 409;订单已 `Shipped` 时,同一规范化发货动作即使来自新 Key 也返回既有发货结果 #### 请求 @@ -5009,17 +5216,21 @@ PlaceSeckillOrderResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | COMMON.INVALID_UUID | `orderId` 不是标准 UUID | +| 400 | COMMON.VALIDATION_FAILED | `Idempotency-Key` 缺失/格式错误,或 `note` 格式错误 | | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | +| 403 | AUTH.FORBIDDEN | 当前账号不是商家 | | 404 | ORDER.NOT_FOUND | 订单不存在或未分配给当前 `assignedMerchantUserId` | -| 409 | ORDER.INVALID_STATUS | 订单状态不允许发货(只有已支付可发货) | +| 409 | ORDER.INVALID_STATUS | 订单状态不允许发货,或订单已发货但本次规范化动作与既有发货事实不同;返回安全的 `currentStatus`,已发货时同时返回 `shippedAt` | | 409 | ORDER.AFTER_SALES_IN_PROGRESS | 订单存在会影响履约的处理中售后申请 | | 409 | ORDER.NO_FULFILLABLE_ITEMS | 全部订单项均已退款,没有剩余可发货数量 | | 409 | IDEMPOTENCY.KEY_REUSED | 同一幂等键用于不同发货请求 | +| 503 | COMMON.DEPENDENCY_UNAVAILABLE | AfterSales 履约快照契约或数据库暂时不可用,无法安全判定可发货数量 | #### 业务规则与并发 1. 完成身份、幂等键格式、路由和请求结构校验后先读取持久化幂等结果;同键同请求重放首次结果,不重新检查当前状态,同键换内容返回 409。 -2. 新请求只允许当前 `assignedMerchantUserId` 对 `Paid` 订单发货;条件更新未命中后读取最新状态,确定失败与幂等键绑定,瞬态未知失败不固化。 +2. 新请求只允许当前 `assignedMerchantUserId` 对 `Paid` 订单首次发货;条件更新未命中后读取最新状态。若订单已 `Shipped` 且规范化 `note` 与既有发货动作一致,则直接返回首次 `ShipOrderResponse`,即使来自另一设备或新 Key 也不重复发货;内容不同则返回 409,并只附当前状态、首次发货时间等安全事实,不能修改已经提交的发货记录。其他确定状态失败可与幂等键绑定,瞬态未知失败不固化。 3. 发货前通过 AfterSales 公开应用契约取得履约快照。`PendingReview`、`PendingReturn`、`PendingReceipt`、`Refunding`、`RefundFailed` 等仍可能改变履约结果的申请阻断发货;`Rejected`、`Cancelled` 不阻断。 4. 服务端以购买数量减去已退款及处理中占用量计算每项实际发货数量;仍有剩余时只发出剩余可履约数量,全部无剩余时拒绝。客户端不能指定数量。 5. A307 与 A412 必须在同一 PostgreSQL 事务协调入口锁定同一订单行并取得最新履约快照;若发货先提交,售后按已发货规则重判;若售后先提交,发货必须看到占用结果。 @@ -5046,7 +5257,7 @@ PlaceSeckillOrderResponse { - **需求编号**:F09 - **负责人**:韦乾强 - **关联数据表**:DB061(orders) -- **当前状态**:部分定义 +- **当前状态**:待交叉评审 - **用途**:买家确认已收到商品,将订单状态从 `Shipped` 变更为 `Completed` - **方法与路径**:`POST /api/orders/{orderId}/confirm-receipt` - **operationId**:`Ordering_ConfirmReceipt` @@ -5085,9 +5296,10 @@ PlaceSeckillOrderResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `orderId` 不是标准 UUID | | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 404 | ORDER.NOT_FOUND | 订单不存在或不属于当前买家 | -| 409 | ORDER.INVALID_STATUS | 订单状态不允许确认收货(只有已发货可确认) | +| 409 | ORDER.INVALID_STATUS | 订单既非首次可确认的 `Shipped`,也非可返回当前结果的 `Completed` | #### 业务规则与并发 @@ -5241,7 +5453,7 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 字段格式错误或 0/负数 | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失/格式错误,或字段格式错误、金额为 0/负数 | | 400 | `PAYMENT.TOPUP_EXCEEDS_LIMIT` | 单笔金额 > 10000.00 或小数 > 2 位 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | @@ -5416,10 +5628,12 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `orderId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 订单不存在或非本人 | | 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 订单已取消或处于其他不可支付且无成功支付记录的状态 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Ordering 订单状态、应付金额或截止时间暂时不可用,无法安全形成收银台结果 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 @@ -5482,6 +5696,20 @@ PlaceSeckillOrderResponse { - **HTTP 状态**:`200 OK` - **响应 Schema**:`PaymentResultResponse` + +```text +PaymentResultResponse { + paymentId: uuid + orderId: uuid + amount: number + currency: "CNY" + status: "Succeeded" + source: "Wallet" | "SimulatedChannel" + walletBalanceAfter: number? // Wallet 成功后为余额;SimulatedChannel 为 null + paidAt: string // UTC ISO 8601 +} +``` + - **示例**: ```json { @@ -5504,16 +5732,17 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 字段格式错误 | +| 400 | `COMMON.INVALID_UUID` | `orderId` 不是标准 UUID | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失/格式错误,或请求字段格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 订单不存在或非本人 | -| 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 订单状态非 `PendingPayment` 或已取消 | +| 409 | `PAYMENT.ORDER_NOT_PAYABLE` | 未找到既有成功支付事实,且订单状态非 `PendingPayment` 或已取消 | | 409 | `PAYMENT.DEADLINE_EXPIRED` | 已达到订单固定支付截止时间;不再允许支付 | | 409 | `PAYMENT.AMOUNT_MISMATCH` | `expectedAmount` 与订单金额不一致 | | 409 | `PAYMENT.INSUFFICIENT_BALANCE` | 钱包余额不足 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | -| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 数据库不可用 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Ordering 应付快照契约或数据库暂时不可用 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 @@ -5555,7 +5784,7 @@ PlaceSeckillOrderResponse { - **方法与路径**:`GET /api/payment/orders/{orderId}` - **operationId**:`Payment_GetPaymentByOrder` - **请求 Schema**:(无) -- **响应 Schema**:`PaymentResultResponse` +- **响应 Schema**:`OrderPaymentLookupResponse` - **身份与 Policy**:JWT Bearer + `BuyerOnly` - **资源归属**:当前 buyerId 订单 - **幂等要求**:GET 天然幂等 @@ -5573,28 +5802,40 @@ PlaceSeckillOrderResponse { #### 成功响应 - **HTTP 状态**:`200 OK` -- **响应 Schema**:`OrderPaymentLookupResponse`,含 `orderId`、`orderStatus`、`paymentResult`(`PaymentResultResponse`,可空) +- **响应 Schema**:`OrderPaymentLookupResponse` + +```text +OrderPaymentLookupResponse { + orderId: uuid + orderStatus: "PendingPayment" | "Paid" | "Shipped" | "Completed" | "Cancelled" + paymentResult: PaymentResultResponse? // 尚无已确定支付事实时为 null +} +``` + - **示例**:没有已确定支付事实时返回 `200` 与 `"paymentResult": null`;有记录时返回本人已确认的支付结果 #### 失败响应 | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `orderId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 订单不存在或非本人 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Ordering 订单归属/状态契约或 Payment 数据库暂时不可用 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 - 只返回当前订单已经确定的 Wallet 或 `SimulatedChannel` 支付事实;没有记录时明确返回 `paymentResult=null`,不得根据订单是否 `PendingPayment`、`Paid` 或 `Cancelled` 猜测。 - C08 迟到成功回调只形成 `Difference` 来源,不伪造成功支付记录;因此已取消且无确定支付事实的订单仍返回 `paymentResult=null`。 +- 订单归属与 `orderStatus` 通过 4.2 的 Ordering→Payment 公开应用契约取得;Payment 不直接读取 Ordering 内部订单表。 #### 缓存、事件或外部依赖 - 缓存:不缓存 - 事件:无 -- 外部依赖:PostgreSQL +- 外部依赖:Ordering 公开订单支付快照/归属查询契约;Payment 自有 PostgreSQL 支付事实 #### 验证场景 @@ -5624,7 +5865,7 @@ PlaceSeckillOrderResponse { - **Route 参数**:(无) - **Query 参数**: - - `orderId`(可选):按订单过滤 + - `orderId`(UUID,可选):按订单过滤 - `createdFrom` / `createdTo`(可选):时间范围 - `page` / `pageSize` / `sortBy` / `sortOrder`(标准分页) - **Header**:`Authorization: Bearer ` @@ -5666,6 +5907,7 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 参数错误 | +| 400 | `COMMON.INVALID_UUID` | `orderId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | @@ -5740,6 +5982,7 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `paymentId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 支付不存在或非本人 | @@ -5817,9 +6060,11 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `orderId` 或 `orderItemId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 订单或订单项不存在 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Ordering 归属、履约状态或剩余可售后数量暂时不可用,不能返回伪造的 `eligible=false` | #### 业务规则与并发 @@ -5915,13 +6160,14 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失/格式错误,或请求字段错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 订单或订单项不存在 | | 409 | `AFTER_SALES.NOT_ELIGIBLE` | 订单不满足售后条件 | | 409 | `AFTER_SALES.QUANTITY_EXCEEDS_AVAILABLE` | 申请数量超过该订单项剩余可售后数量,或并发申请已占用额度 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Ordering 归属、履约状态或剩余可售后数量暂时不可用,申请未提交 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 @@ -6120,6 +6366,7 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `requestId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer / Merchant | | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | @@ -6175,7 +6422,7 @@ PlaceSeckillOrderResponse { ``` - **校验规则**: - 申请归属当前 buyerId - - 申请状态必须为 `PendingReview`,否则 409 + `AFTER_SALES.INVALID_STATUS` + - 首次撤销时申请状态必须为 `PendingReview`;本人申请已经为 `Cancelled` 时返回当前取消详情 - `Idempotency-Key` 必填 #### 成功响应 @@ -6188,10 +6435,12 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `requestId` 不是标准 UUID | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 或请求字段缺失、格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或非本人 | -| 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `PendingReview` | +| 409 | `AFTER_SALES.INVALID_STATUS` | 申请既非首次可撤销的 `PendingReview`,也非可返回当前结果的 `Cancelled` | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | @@ -6247,7 +6496,7 @@ PlaceSeckillOrderResponse { ``` - **校验规则**: - 申请关联订单分配给当前商家账号 - - 首次审核时申请状态必须为 `PendingReview`;幂等重放先于状态校验并返回首次已提交结果 + - 首次审核时申请状态必须为 `PendingReview`;幂等重放或当前申请已经存在规范化内容相同的审核结果时返回最新已提交结果 - `decision` 枚举:`Approve` / `Reject` - `Idempotency-Key` 必填 @@ -6261,17 +6510,20 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 400 | `COMMON.INVALID_UUID` | `requestId` 不是标准 UUID | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失/格式错误,或请求字段错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Merchant | | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | -| 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `PendingReview` | +| 409 | `AFTER_SALES.INVALID_STATUS` | 未命中幂等或相同审核结果,且申请状态非首次可审核的 `PendingReview` | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 审核状态提交前必需的 Ordering、Payment 或原库存通道依赖暂时不可用 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 - 完成固定字段校验后先查询幂等结果;命中同请求时返回首次已提交结果或该退款操作可确认的当前结果,不再按当前状态重复审核。 +- 当前申请已存在同一商家提交、且 `decision` 与规范化 `auditNote` 均相同的审核事实时,即使使用新 Key,也返回当前状态;相反决定或不同审核内容不得覆盖首次审核结果。 - 商家不能修改买家原始申请内容(业务规则)。 - 状态条件更新同时校验关联订单 `assigned_merchant_user_id = currentUserId`;买家撤销与审核竞争时只有一个状态迁移提交。 - 审核结果由申请类型决定,客户端不能通过布尔字段选择是否退款: @@ -6298,7 +6550,7 @@ PlaceSeckillOrderResponse { - 异常:退款结果未知 → 保持并返回 `Refunding`,不创建第二笔退款 - 异常:买家角色调用 → 403 + `AUTH.FORBIDDEN` - 异常:他人商家申请 → 404 + `RESOURCE.NOT_FOUND` -- 异常:状态已非 `PendingReview` → 409 + `AFTER_SALES.INVALID_STATUS` +- 异常:未命中幂等或相同审核结果,且状态已非 `PendingReview` → 409 + `AFTER_SALES.INVALID_STATUS` --- @@ -6347,18 +6599,21 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 400 | `COMMON.INVALID_UUID` | `requestId` 不是标准 UUID | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失/格式错误,或请求字段错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Merchant | | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | -| 409 | `AFTER_SALES.INVALID_STATUS` | 状态非 `PendingReceipt` | +| 409 | `AFTER_SALES.INVALID_STATUS` | 申请既非首次可确认的 `PendingReceipt`,也非同一退款操作可返回当前结果的 `Refunding` / `Refunded` / `RefundFailed` | | 409 | `AFTER_SALES.WRONG_TYPE` | 申请类型非 `ReturnAndRefund` | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 收货状态提交前必需的 Payment 或原库存通道依赖暂时不可用 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 - 完成固定字段校验后先查询幂等结果;相同 Key 同请求即使申请已进入 `Refunding` / `Refunded` / `RefundFailed`,也返回同一退款操作的当前结果,不重复确认或退款。 +- 仅在收货与 `Refunding` 状态尚未可靠提交、且必需依赖无法安全确认时返回 503;状态已提交后依赖暂时不可用仍返回 200 与同一退款操作当前结果。 - 状态条件更新同时校验关联订单 `assigned_merchant_user_id = currentUserId`。 - 首次确认时,收货事实、该申请唯一退款操作和 `PendingReceipt → Refunding` 状态时间线必须原子提交;退款失败也不得回到 `PendingReturn` 或抹去已确认收货事实。 - 退款执行器根据 Ordering 快照的 `orderType` 与 `seckillActivityId`,在退款成功原子结果中按原通道回补整笔申请数量;本期不支持部分收货或部分退款。 @@ -6375,7 +6630,7 @@ PlaceSeckillOrderResponse { - 正常:商家确认收货 → 原子进入 `Refunding`,后续按正确库存通道形成一次完整退款结果 - 正常:同 Key 重放 → 返回同一退款操作当前状态,不重复退款或回补 - 异常:申请类型为 `RefundOnly` → 409 + `AFTER_SALES.WRONG_TYPE` -- 异常:状态非 `PendingReceipt` → 409 + `AFTER_SALES.INVALID_STATUS` +- 异常:申请未处于 `PendingReceipt`,且没有可重放的 `Refunding` / `Refunded` / `RefundFailed` 同一退款操作 → 409 + `AFTER_SALES.INVALID_STATUS` - 异常:退款结果未知 → 保持 `Refunding`,不误报失败 --- @@ -6422,16 +6677,20 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `requestId` 不是标准 UUID | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 或请求字段缺失、格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Merchant | | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | | 409 | `AFTER_SALES.INVALID_STATUS` | 状态既非可重试的 `RefundFailed`,也不是同一退款操作可返回的 `Refunding` / `Refunded` | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 新重试状态提交前必需的 Ordering、Payment 或原库存通道依赖暂时不可用 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 - 完成固定字段校验后先查询幂等结果;命中同 Key 同请求时返回原调用结果或同一退款操作的当前确定状态,不因状态已经变化而重新报错。 +- 仅在新的重试尚未可靠提交、且必需依赖无法安全确认时返回 503;已经进入 `Refunding` 后返回 200 与同一退款操作当前结果,不因依赖瞬态中断发起第二次退款。 - 状态条件更新同时校验关联订单 `assigned_merchant_user_id = currentUserId`。 - 首次人工重试只能把 `RefundFailed → Refunding`,并继续使用原 `refundOperationId`、金额、收款人、申请数量和库存通道;不得重新审核或创建第二笔业务退款。 - 商家人工重试与系统恢复任务竞争时,最多一个执行器取得同一退款操作的执行权,其他调用返回 `Refunding` 或已确定结果。 @@ -6451,7 +6710,7 @@ PlaceSeckillOrderResponse { - 正常:原退款已成功 → 重放当前 `Refunded`,不重复入账或回补 - 正常:再次得到确定失败 → 返回可查询的 `RefundFailed` - 异常:原尝试结果未知 → 返回 `Refunding` 并继续核实 -- 异常:状态非 `RefundFailed` → 409 + `AFTER_SALES.INVALID_STATUS` +- 异常:状态既非首次可重试的 `RefundFailed`,也非可返回当前结果的 `Refunding` / `Refunded` → 409 + `AFTER_SALES.INVALID_STATUS` --- @@ -6528,6 +6787,7 @@ PlaceSeckillOrderResponse { | 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | | 401 | `PAYMENT.CALLBACK_INVALID_SIGNATURE` | 签名验证失败 | | 409 | `PAYMENT.CALLBACK_ID_REUSED` | 同一 `callbackId` 被用于不同规范请求内容 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Ordering 权威订单状态、应付金额或支付截止时间暂时不可用,未形成确定回调裁决 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 @@ -6719,6 +6979,7 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `batchId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Admin | | 404 | `PAYMENT.RECONCILIATION_BATCH_NOT_FOUND` | 批次不存在 | @@ -6812,6 +7073,7 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `batchId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Admin | | 404 | `PAYMENT.RECONCILIATION_BATCH_NOT_FOUND` | 批次不存在 | @@ -6923,7 +7185,8 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 400 | `COMMON.INVALID_UUID` | `differenceId` 不是标准 UUID | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失/格式错误,或请求字段错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Admin | | 404 | `PAYMENT.RECONCILIATION_DIFFERENCE_NOT_FOUND` | 差异不存在 | @@ -7055,6 +7318,7 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.INVALID_UUID` | `differenceId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Admin | | 404 | `PAYMENT.RECONCILIATION_DIFFERENCE_NOT_FOUND` | 差异不存在 | @@ -7159,11 +7423,12 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 字段错误(trackingNumber 格式、shippedAt 未来时间) | +| 400 | `COMMON.INVALID_UUID` | `requestId` 不是标准 UUID | +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失/格式错误,或请求字段错误(trackingNumber 格式、shippedAt 未来时间) | | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Buyer | | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或非本人 | -| 409 | `AFTER_SALES.INVALID_STATUS` | 申请状态非 `PendingReturn` | +| 409 | `AFTER_SALES.INVALID_STATUS` | 申请既非首次可提交的 `PendingReturn`,也未保存可按相同规范化内容返回的退货信息 | | 409 | `AFTER_SALES.WRONG_TYPE` | 申请类型非 `ReturnAndRefund` | | 409 | `AFTER_SALES.RETURN_INFO_CONFLICT` | 该申请已保存退货信息,但本次规范化内容不同 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | @@ -7192,7 +7457,7 @@ PlaceSeckillOrderResponse { - 冲突:新 Key + 不同退货内容 → 409 + `AFTER_SALES.RETURN_INFO_CONFLICT` - 异常:他人申请 → 404 + `RESOURCE.NOT_FOUND` - 异常:申请类型为 `RefundOnly` → 409 + `AFTER_SALES.WRONG_TYPE` -- 异常:状态非 `PendingReturn` → 409 + `AFTER_SALES.INVALID_STATUS` +- 异常:状态非 `PendingReturn` 且没有可按相同内容重放的既有退货信息 → 409 + `AFTER_SALES.INVALID_STATUS` --- @@ -7208,7 +7473,7 @@ PlaceSeckillOrderResponse { - 用途:按创建时间倒序分页查询当前用户自己的消息。 - 方法与路径:`GET /api/messages` - operationId:`Messaging_ListMessages` -- 请求 Schema:Query 参数 +- 请求 Schema:无(Query 参数见下) - 响应 Schema:`MessageListResponse` - 身份与 Policy:`BuyerOnly / MerchantOnly` - 资源归属:接收用户必须等于当前认证用户;服务端不接收 `userId` @@ -7311,7 +7576,7 @@ PlaceSeckillOrderResponse { - 用途:查询当前用户拥有的一条完整站内消息。 - 方法与路径:`GET /api/messages/{messageId}` - operationId:`Messaging_GetMessage` -- 请求 Schema:Route 参数 +- 请求 Schema:无(Route 参数见下) - 响应 Schema:`MessageDetailResponse` - 身份与 Policy:`BuyerOnly / MerchantOnly` - 资源归属:消息接收用户必须等于当前认证用户 @@ -7358,7 +7623,7 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | `messageId` 格式非法 | +| 400 | `COMMON.INVALID_UUID` | `messageId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | JWT 缺失或无效 | | 403 | `AUTH.FORBIDDEN` | 当前身份不是买家或商家 | | 403 | `AUTH.ACCOUNT_DISABLED` | 当前账号已禁用,不读取消息 | @@ -7456,7 +7721,7 @@ PlaceSeckillOrderResponse { - 用途:幂等地记录当前用户一条消息的首次已读时间。 - 方法与路径:`POST /api/messages/{messageId}/read` - operationId:`Messaging_MarkMessageRead` -- 请求 Schema:Route 参数 +- 请求 Schema:无(Route 参数见下) - 响应 Schema:`MarkMessageReadResponse` - 身份与 Policy:`BuyerOnly / MerchantOnly` - 资源归属:消息接收用户必须等于当前认证用户 @@ -7492,7 +7757,7 @@ PlaceSeckillOrderResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | `messageId` 格式非法 | +| 400 | `COMMON.INVALID_UUID` | `messageId` 不是标准 UUID | | 401 | `AUTH.UNAUTHENTICATED` | JWT 缺失或无效 | | 403 | `AUTH.FORBIDDEN` | 当前身份不是买家或商家 | | 403 | `AUTH.ACCOUNT_DISABLED` | 当前账号已禁用,不修改消息 | @@ -7783,16 +8048,23 @@ PlaceSeckillOrderResponse { | Identity | Messaging | 解析并校验事件派生接收身份 | `userId + expectedRole -> exists + actualRole + accountStatus`;账号禁用但身份和归属仍有效时返回可保存,不要求账号启用 | Identity 拥有账号身份与角色;Messaging 用于整事件校验和按角色生成文案,不跨模块读账号表 | | Ordering、AfterSales、Seckill | Identity | 校验非默认商家能否禁用并约束新责任受理 | `merchantUserId -> blockingReason[]`,精确覆盖 `PendingPayment`、仍有可履约数量的 `Paid`、`Shipped`、完成后 7 天售后窗口、任一非终态售后申请、任一未结束秒杀活动;新责任受理同时校验账号状态 | 各业务模块拥有责任事实;Identity 只在全部允许时改变账号状态 | | Catalog | Cart、Ordering、AfterSales | 查询可售商品快照、条件扣减及幂等回补普通库存 | 商品/订单项/数量/原因/稳定操作标识 -> 商品快照或库存结果 | Catalog 拥有商品与普通库存;AfterSales 仅在退款成功完整结果中按原通道回补 | +| Catalog | Engagement | 批量返回收藏/历史商品展示快照并校验新写入可售性 | `productId[] -> name/mainImage/currentPrice/salesStatus/stockStatus`,实体缺失与下架分别返回稳定占位;新收藏和新浏览记录按单商品 `OnSale` 重检 | Catalog 拥有商品事实;Engagement 只保存个人关联与时间,不直读 Catalog 表或调用 A103 HTTP | +| Catalog | Review | 校验公开评价目标商品当前可见性 | `productId -> exists + isPubliclyVisible`;不存在或不可公开映射为 A140 的稳定 404,不泄露内部状态 | Catalog 拥有商品公开状态;Review 拥有评价事实 | +| Catalog | Seckill | 返回活动维护与公开展示所需的统一经营目录商品快照 | `productId[] -> exists + name + mainImageUrl + salesStatus + currentPrice + ordinaryAvailableStock`;A220/A221 按单商品校验 `OnSale` 与计划量,A226/A227 批量取得公开展示字段 | Catalog 拥有商品与普通库存事实;所有正常商家共享同一经营目录,Seckill 不按商家隔离商品 | | Catalog | Seckill | 发布活动时原子划转普通库存到秒杀配额 | 商品、活动、数量、幂等键 -> 划转结果 | Catalog 扣减普通库存;Seckill 拥有已划转配额 | | Cart | Ordering | 读取本人已选条目并在下单成功后清理 | `buyerId + cartItemIds -> CheckoutItems` | Cart 拥有购物车 | -| Ordering | Payment | 查询支付快照并按状态条件标记已支付 | `buyerId + orderId -> amount + currency + assignedMerchantUserId + orderStatus + paymentDeadline` | Ordering 拥有订单状态、截止时间与处理商家归属 | +| Ordering | Payment(M05) | 查询本人支付快照并按状态条件推进已支付 | `buyerId + orderId -> amount + currency + assignedMerchantUserId + orderStatus + paymentDeadline`;Wallet 成功与订单 `PendingPayment → Paid` 形成同一确定结果 | Ordering 拥有订单状态、截止时间与处理商家归属;Payment 拥有 Wallet 资金事实 | +| Ordering | C08 回调处理 | 在受信回调上下文中条件推进订单已支付 | `orderId + paymentId + SimulatedChannel + amount + currency -> committed Paid / current status + paymentDeadline`;Ordering 在同一条件推进中自行取得数据库权威时间,不接收或复用调用方预读时间;无需买家 JWT,仍校验金额、截止时间、既有成功来源并与取消条件竞争 | Ordering 拥有订单状态;Payment/C08 拥有回调与支付聚合,双方在受控共享事务中只形成一个成功来源 | | Payment | Ordering、M05、C08 | 查询订单已有成功支付来源 | `orderId -> existingPaymentSummary? { paymentId, source, amount, paidAt }` | Payment 拥有 `Wallet` / `SimulatedChannel` 成功支付事实;同一订单最多一个成功来源 | | Ordering | AfterSales | 在共享事务中锁定订单履约变更并返回售后校验快照 | `orderId + buyerId + orderItemId -> locked order status + 实付 + orderType + seckillActivityId + assignedMerchantUserId` | Ordering 拥有订单行和履约状态;行锁保持到调用方事务提交 | | AfterSales | Ordering | 查询发货阻断与剩余可履约数量 | `orderId -> hasBlockingRequest + item[{orderItemId, refundedQuantity}]` | AfterSales 拥有申请状态和已退款数量;Ordering 计算并固化实际发货数量 | +| Ordering | M06-02 / A307 | 原子执行责任商家发货 | `merchantUserId + orderId + idempotencyKey + note -> Shipped + shippedAt + shippedItems[]`;内部先锁定订单并读取 AfterSales 履约快照,同键重放首次确定结果 | Ordering 拥有归属校验、`Paid → Shipped`、实际发货数量、幂等结果和可靠发货事实;M06-02 不直写订单表 | | Ordering | Review | 校验评价资格并返回订单项快照 | `buyerId + orderItemId -> Completed + product/order snapshot` | Ordering 拥有订单完成与订单项归属事实 | | Ordering | Catalog | 判断商品是否存在历史订单关联 | `productId -> hasHistoricalOrders` | Ordering 拥有历史订单关联;Catalog 据此保护删除 | -| Ordering | Seckill | 在秒杀库存条件扣减成功后创建共享订单事实 | 活动/商品/买家/地址/价格快照 -> 订单结果 | Ordering 是唯一订单事实来源 | -| Seckill | Ordering、AfterSales | 幂等回补原活动库存并释放买家限购额度 | 活动/订单项/买家/数量/原因/幂等键 -> 回补结果 | Seckill 拥有活动库存与买家配额 | +| Ordering | Seckill | 在秒杀库存条件扣减成功后创建共享订单事实 | `activity/product/buyer/addressId/quantity/seckillPrice/库存来源 -> 订单结果`;Ordering 校验地址并解析唯一启用默认商家 | Ordering 是唯一订单事实来源;Seckill 不读取 Identity 地址或默认商家事实 | +| Ordering | Seckill | 查询活动对应共享订单统计 | `seckillActivityId -> totalOrders + paidOrders + cancelledOrders + totalSoldAmount`;由 Seckill 先完成活动创建人授权,Ordering 只按活动来源汇总,不返回买家或订单私人明细 | Ordering 拥有共享订单与状态事实;Seckill 只在 A225 展示经营汇总,不维护平行订单统计事实 | +| Seckill | Ordering、C03 | 待支付订单取消时幂等回补原活动库存并释放限购占用 | 活动/订单项/买家/数量/取消原因/稳定操作标识 -> `remainingStock + soldCount + buyerOccupiedQuantity` | Seckill 拥有活动库存与买家当前有效限购占用;只在订单首次取消完整结果中按原数量释放 | +| Seckill | AfterSales | 已支付退款时按 M10 矩阵幂等回补原活动库存 | 活动/订单项/买家/退款数量/稳定退款操作标识 -> `remainingStock + soldCount`;不释放买家既有购买限购额度 | Seckill 拥有活动库存;已支付购买量仍计入买家活动限购,防止退款后再次突破上限 | | Payment | AfterSales | 执行或核实同一退款操作 | `ExecuteRefundCommand -> RefundExecutionResult`,稳定 `refundOperationId`,结果为 `Succeeded` / `DefiniteFailure` / `Unknown` | Payment 拥有钱包、退款和资金流水;AfterSales 拥有申请状态 | | Ordering、Payment、AfterSales | C08 对账 Worker | 按固定 UTC 范围和一致水位读取已提交比较事实 | `businessDate + rangeFrom + rangeTo + watermarkAt -> 订单支付终态、成功支付与回调聚合、售后终态、退款操作和钱包入账比较单元` | 各来源模块拥有原事实;C08 只保存批次、差异、证据和处置时间线,不跨模块直接读表 | | Ordering、Payment、AfterSales | Messaging | 发布已提交业务事实与归属快照 | 标准事件 Envelope + 稳定 `eventId` + 业务归属字段 + 最小资源快照;不接收 `recipients[]` | 来源模块拥有业务事实与归属;Messaging 按 4.3.6 固定矩阵派生接收人并持久化 | @@ -8106,18 +8378,19 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 ##### 业务规则 -1. M04 创建每张订单时按当时生效且可追踪的配置固化 `paymentDeadline`;后续配置变化不追溯修改历史订单。 +1. 正式环境固定以 `createdAt + 30 分钟` 生成 `paymentDeadline`。演示环境只能通过明确、可追踪的配置缩短等待;当时生效值随订单固化,后续配置变化不追溯修改历史订单,演示值也不得替代正式规则或影响生产环境。 2. Worker 只扫描 `PendingPayment` 且 `paymentDeadline <= 权威数据库时间` 的订单。达到截止时间后即使尚未扫描,M05 与 C08 也必须拒绝支付。 -3. Worker 通过受信任 Ordering 内部过期取消契约调用 M04,不复用买家 JWT 或把 A304 HTTP 当内部接口;系统原因固定为 `PaymentExpired`。 +3. Worker、M05 到期裁决和 A421/C08 回调处理都通过同一受信任 Ordering 内部过期取消契约调用 M04,不复用买家 JWT 或把 A304 HTTP 当内部接口;系统原因固定为 `PaymentExpired`。 4. 首次成功必须原子形成 `PendingPayment → Cancelled`、`cancelledAt`、`cancelReason=PaymentExpired`、普通/秒杀原通道库存回补、秒杀限购释放和买家取消通知可靠事实。 5. 普通库存回补在提交后触发 C07 精确失效;秒杀原活动库存回补不触发 C07,活动已结束或取消也不把库存转回普通通道或重开活动。 -6. Worker、M05 到期触发和买家到期后取消复用同一结果;多次扫描、重启和消息重投都不得重复取消、回补或通知。 +6. Worker、M05 到期触发、A421/C08 到期回调和买家到期后取消复用同一结果;多次扫描、重启、回调重放和消息重投都不得重复取消、回补或通知。 ##### 任务触发 - `Mall.Worker` 按稳定顺序领取有界批次的到期待支付订单;批量大小、扫描间隔和单轮重试次数是后续可观测配置,不在业务契约中写死。 - 多实例可重复发现候选,但最终由 M04 条件竞争保证一个取消完整结果;进程内集合或单机锁不作为唯一正确性保障。 -- 暂时失败时记录订单标识与 `traceId` 并按退避策略进入后续重试;订单保持“已过期但尚待取消”的 `PendingPayment`,仍不可支付。 +- 每次领取和执行必须持久保留独立运维任务记录,最少包含 `orderId`、`attemptCount`、`lastAttemptAt`、`lastOutcome`(本次结果)、`executionStatus`(`PendingRetry` / `Finalized`)、可空 `finalOutcome` 与 `traceId`;只有 `executionStatus=Finalized` 时才写入 `finalOutcome`(`Cancelled` / `AlreadyResolvedByCompetitor`)。该记录只用于恢复、业务追踪和告警,不成为订单业务状态。 +- 暂时失败时更新 `lastOutcome` 并保持 `executionStatus=PendingRetry`,按退避策略进入后续重试;订单保持“已过期但尚待取消”的 `PendingPayment`,仍不可支付。首次完整取消或确认订单已由竞争方推进后改为 `Finalized` 并写入确定 `finalOutcome`,后续重扫复用该结果而不重复回补或通知。 - Worker 重启后重新扫描 PostgreSQL 共享事实,不依赖内存定时器或未持久化队列保存唯一到期责任。 ##### 事件发布 @@ -8208,7 +8481,7 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 ### 5.1 当前成熟度 -本次已把六份最新个人原稿综合到唯一主接口文档,并解决已知的缺口、重复端点、模块命名和明显语义冲突。当前状态仍为 **部分定义,未冻结**:接口清单已经闭合,但 DBxxx、真实 OpenAPI、跨模块实现签名和交叉评审证据尚未完成。 +本次已把六份最新个人原稿综合到唯一主接口文档,并按已确认流程闭合有效接口的请求、响应、错误、鉴权、幂等、并发和依赖边界。当前状态为 **完整定义,待交叉评审,尚未冻结**:本文件可作为后续数据库设计与 OpenAPI 的输入,但 DBxxx、真实 OpenAPI、跨模块实现签名和交叉评审证据仍未完成。 | 负责人 | 追踪编号 | 有效 HTTP | 本次已闭合 | 仍需确认 | |---|---:|---:|---|---| @@ -8228,7 +8501,7 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 4. A431 已取消并只保留历史追踪编号,退款通过不占 Axxx 的 Payment 公开应用契约完成。 5. A305~A307 归属 Ordering;商家身份只影响路径和 Policy,不新增 Merchant 业务模块。 6. F03 的 A006~A014 统一为 BuyerOnly;商家资料维护不在本期范围。 -7. 商品先创建草稿再上传图片;评价图片先进入当前买家暂存区,再由评价提交关联。 +7. 商品通过 A122 将草稿与首批图片原子创建,后续图片再由 A127/A128 增删;评价图片先进入当前买家暂存区,再由评价提交关联。 8. C04 使用 PostgreSQL `pg_trgm`/GIN 同步索引,不通过 Outbox/Worker 复制搜索索引。 9. 本期为单店 B2C;订单以 `assignedMerchantUserId` 指定处理账号,不建设多商户商品归属、拆单或结算模型。 10. C01 活动推进、C03 超时取消、M04-04 自动完成和 C08 每日对账均由 `Mall.Worker` 扫描 PostgreSQL 事实并幂等执行。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" index 058d6db..1272a21 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" @@ -2,7 +2,7 @@ > 组别:24级1班第7组 编写人:罗皓晨 编写日期:2026-07-22 版本:v0.2 > -> 文档状态:已按业务流程 v1.0 完成架构承接校准,待接口、数据库、实现和全组技术评审后冻结 +> 文档状态:已按业务流程 v1.0 完成架构承接校准,接口已按流程重建,待数据库、实现和全组技术评审后冻结 > 截止:第 1 周周五 ## 修订记录 -- Gitee From da3b76067f2c7f7d42c8f4f4cbcab050e99c2e4c Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Sat, 25 Jul 2026 01:01:10 +0800 Subject: [PATCH 110/118] =?UTF-8?q?docs(docs):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E4=BF=AE=E8=AE=A2=E8=AE=B0=E5=BD=95=EF=BC=9B?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E9=9C=80=E6=B1=82=E4=B8=8E=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E4=B8=AD=E7=9A=84=E4=BF=AE=E8=AE=A2=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E7=AB=A0=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 19 ------------------- .../interface/interface-gxy.md" | 6 ------ .../interface/interface-lhc.md" | 6 ------ .../interface/interface-tyh.md" | 6 ------ .../interface/interface-wqq.md" | 6 ------ .../interface/interface-zhh.md" | 6 ------ .../interface/interface-zhy.md" | 6 ------ ...01\347\250\213\350\256\276\350\256\241.md" | 9 --------- ...75\345\220\215\350\247\204\350\214\203.md" | 6 ------ ...45\345\217\243\350\256\276\350\256\241.md" | 7 ------- ...56\345\272\223\350\256\276\350\256\241.md" | 6 ------ ...66\346\236\204\350\256\276\350\256\241.md" | 7 ------- 12 files changed, 90 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 6b37c68..4b10f21 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -5,25 +5,6 @@ > 文档状态:内容和格式已完成内部统一,待全组与指导教师评审后冻结 > 截止:第 1 周周三提交初稿 -## 修订记录 - -| 版本 | 日期 | 修改人 | 修改说明 | -|---|---|---|---| -| v0.1 | 2026-07-24 | 全体成员(罗皓晨统稿) | 形成并统一需求规格,明确四类角色、必做功能、4 项选做、7 项挑战、单店 B2C 边界、核心状态与六人模块职责 | -| v0.2 | 2026-07-24 | 罗皓晨 | 消解 C01 库存展示绝对一致与“不预占库存”的冲突,统一为数据库权威快照、旧结果防覆盖及并发失败后同一交互刷新 | -| v0.3 | 2026-07-24 | 罗皓晨 | 冻结 F10 同步钱包与 C08 受控模拟回调的互斥入账边界,并明确回调同样受订单状态和支付截止时间约束 | -| v0.4 | 2026-07-24 | 罗皓晨 | 冻结商品三态、分类停用、售罄展示与 C07 缓存范围,明确统一经营目录及商品列表、搜索不进入本期缓存 | -| v0.5 | 2026-07-24 | 罗皓晨 | 冻结 X01 评价提交入口、提交时资格重检和公开计分口径,明确评价数据不进入 C07 商品详情缓存 | -| v0.6 | 2026-07-24 | 罗皓晨 | 冻结 C04 关键词分流、统一过滤与降级口径,明确搜索不缓存且正式 60 秒性能采样不包含预热 | -| v0.7 | 2026-07-24 | 罗皓晨 | 冻结 F01~F03 注册后登录、单一登录凭证、全部旧凭证失效、资料字段和地址默认切换边界 | -| v0.8 | 2026-07-24 | 罗皓晨 | 冻结 F13 买家与商家分流、非默认商家责任清单、禁用竞争顺序和全部旧凭证失效边界 | -| v0.9 | 2026-07-24 | 罗皓晨 | 冻结 X02 收藏幂等、浏览历史默认开启与最近 200 条上限,明确关闭记录不隐藏旧历史并取消清空历史能力 | -| v0.10 | 2026-07-24 | 罗皓晨 | 冻结 X03 事件接收人、整事件消息原子性和全部已读水位,并统一 C06 WebSocket、凭证失效、角标补查和固定重连边界 | -| v0.11 | 2026-07-24 | 罗皓晨 | 冻结 C07 固定首页、详情缓存、TTL、跨实例填充、二次失效和普通/秒杀库存失效矩阵 | -| v0.12 | 2026-07-24 | 罗皓晨 | 冻结 C10 一次性 Migrator、全局就绪与能力降级、Redis 安全恢复、WebSocket 落点和优雅停止边界 | -| v0.13 | 2026-07-24 | 罗皓晨 | 冻结分类存储状态与购物端有效状态、父子层级、顶级分类筛选范围及商品计数口径 | -| v0.14 | 2026-07-24 | 罗皓晨 | 冻结下单确定结果与瞬态失败的幂等边界,并统一订单状态筛选非法值处理 | - ## 业务流程设计入口 本文件是正式需求的唯一事实源,不再按负责人拆分个人需求文件。跨角色、跨模块和包含状态变化的详细图统一维护在 [`../02-设计文档/process/业务流程设计.md`](../02-设计文档/process/业务流程设计.md);流程图只负责可视化本文件已经确认的业务语义,不替代功能需求、异常规则和验收标准。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" index f21332d..9317d3d 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" @@ -3,12 +3,6 @@ > 组别:24级1班第7组 编写人:顾欣月 编写日期:2026-07-24 版本:v0.1 > 编号区间:`A101`~`A200` 负责模块:Catalog(商品目录)、Review(评价) -## 修订记录 - -| 版本 | 日期 | 修改人 | 修改说明 | -|------|------|--------|----------| -| v0.1 | 2026-07-24 | 顾欣月 | 建立并完善 Catalog、Review 接口清单、A101~A144 详细定义与枚举附录;补齐单条评价读取并统一商品、评价图片暂存顺序 | - ## 一、说明与约定引用 - 本文件是《[接口设计.md](../接口设计.md)》第二章要求的个人协作文件,只登记本人 `A101`~`A200` 区间和本人负责模块的 HTTP 接口。汇总后继续保留用于贡献与评审追踪;实现、OpenAPI 和联调一律以总《接口设计》为准。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" index 6640b65..da1231a 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" @@ -7,12 +7,6 @@ > 编写日期:2026-07-24 > 版本:v0.1 -## 修订记录 - -| 版本 | 日期 | 修改人 | 修改说明 | -|---|---|---|---| -| v0.1 | 2026-07-24 | 罗皓晨 | 建立并完善 A501~A507 消息与健康检查接口,并登记 Messaging 集成事件、实时推送和断线补偿契约 | - ## 一、范围与设计结论 本文件只登记罗皓晨负责的 Messaging HTTP 接口和 M00 公共健康检查接口。全部接口遵循[《接口设计》](../接口设计.md)第一章通用约定;本文未重复定义的认证、响应包装、ProblemDetails、分页和安全规则均以该文档为准。汇总后本文件继续保留用于贡献与评审追踪,但不得覆盖总文档中的最终契约。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" index 453995c..da13199 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" @@ -6,12 +6,6 @@ > 当前状态:部分定义;清单与详细定义已完成,待数据库、OpenAPI 和交叉评审 > 本文件为协作与评审追踪材料;汇总后继续保留,实现、OpenAPI 和联调以 `../接口设计.md` 为准 -## 修订记录 - -| 版本 | 日期 | 修改人 | 修改说明 | -|---|---|---|---| -| v0.1 | 2026-07-24 | 唐宇昊 | 建立并完善 `A001`~`A025` 接口清单与详细定义;补齐浏览记录写入和设置查询,统一 F03 买家权限、令牌撤销及账号状态幂等语义 | - ## 一、接口清单 | 编号 | 模块 | 需求编号 | 名称 | 方法 | 路径 | operationId | 请求 Schema | 响应 Schema | 鉴权 Policy | 关联 DBxxx | 当前状态 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" index 1148d33..64c4ac8 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" @@ -7,12 +7,6 @@ > 版本:v0.1 > 本文件为协作与评审追踪材料;汇总后继续保留,实现、OpenAPI 和联调以 `../接口设计.md` 为准 -## 修订记录 - -| 版本 | 日期 | 修改人 | 修改说明 | -|---|---|---|---| -| v0.1 | 2026-07-24 | 韦乾强 | 建立并完善 A301~A308 订单接口;统一 Ordering 模块命名、确认收货、秒杀订单查询复用与幂等规则 | - ## 接口清单 | 编号 | 模块 | 需求编号 | 名称 | 方法 | 路径 | operationId | 请求Schema | 响应Schema | 鉴权 | 关联DB | 状态 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" index 0e82710..1a6529e 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" @@ -6,12 +6,6 @@ > 当前状态:部分定义;清单与详细定义已完成,待数据库、OpenAPI 和交叉评审 > 本文件为协作与评审追踪材料;汇总后继续保留,实现、OpenAPI 和联调以 `../接口设计.md` 为准 -## 修订记录 - -| 版本 | 日期 | 修改人 | 修改说明 | -|---|---|---|---| -| v0.1 | 2026-07-24 | 朱惠惠 | 建立并完善 `A201`~`A230` 接口清单与详细定义;A229/A230 作为历史占号取消,秒杀订单查询复用 A302/A303 | - ## 一、接口清单 | 编号 | 模块 | 需求编号 | 名称 | 方法 | 路径 | operationId | 请求 Schema | 响应 Schema | 鉴权 Policy | 关联 DBxxx | 当前状态 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" index ceeb82f..b42eeaa 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" @@ -12,12 +12,6 @@ --- -## 修订记录 - -| 版本 | 日期 | 修改人 | 修改说明 | -|---|---|---|---| -| v0.1 | 2026-07-24 | 张海洋 | 建立并完善 A401~A434 支付、售后与对账接口;取消 A418/A431 历史 HTTP 编号,并补齐退货信息 A434 与退款应用契约 | - ## 0. 阅读须知 1. 每个接口按《接口设计.md》1.20 节模板逐项填写。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" index a2de9ab..025c6a5 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" @@ -6,15 +6,6 @@ > > 当前状态:完整定义,已完成统稿校准;F01~F13、X01~X04 及已选 C01/C03/C04/C06/C07/C08/C10 均有模块流程承接,可作为接口、数据库和架构设计输入;正式团队确认、实现和测试状态分别在对应事实源中记录 -## 修订记录 - -| 版本 | 日期 | 修改人 | 修改说明 | -|---|---|---|---| -| v0.1 | 2026-07-24 | 全体成员(罗皓晨统稿) | 建立集中式业务流程设计,覆盖核心主链路,并对照 F01~F13 需求与验收校准状态、模块交接、X/C 扩展点和核心结果保护规则 | -| v0.2 | 2026-07-24 | 罗皓晨 | 补充 M09、C06、C07、C10 个人流程入口,新增消息、缓存和单 API 实例故障的直接交接图,并更新扩展流程成熟度 | -| v0.3 | 2026-07-24 | 唐宇昊 | 在 tyh/ 新增 M01-01、M01-02、M01-03、M06-03、M08 五份个人流程文档,登记 F01/F02/F03/F13 和 X02 追踪矩阵链接 | -| v1.0 | 2026-07-24 | 全体成员(罗皓晨统稿) | 汇总 21 份模块流程,统一唯一履约商家、固定支付截止时间、售后履约竞争、消息接收人、缓存一致性、支付回调对账和 C10 运行边界,关闭流程层阻断项 | - ## 一、文档定位与事实来源 本文档集中维护跨角色、跨模块、包含状态或异常分支的业务流程图,用于避免在主需求正文中堆叠复杂图示。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" index 051c625..519c794 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" @@ -4,12 +4,6 @@ > > 适用对象:全体开发成员、Code Reviewer、AI 编码代理和文档代理 -## 修订记录 - -| 版本 | 日期 | 修改人 | 修改说明 | -|---|---|---|---| -| v0.1 | 2026-07-24 | 罗皓晨 | 建立跨技术栈统一命名规范,并明确 Axxx 接口编号和 DBxxx 表编号格式 | - ## 一、目标、优先级与 AI 执行规则 ### 1.1 目标 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" index ee771bb..8398c0c 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" @@ -5,13 +5,6 @@ > > 当前状态:已按业务流程 v1.0 重建统一契约;共 109 个追踪编号,其中 99 个活动 HTTP 定义、10 个历史取消编号;待 OpenAPI、数据库、实现、测试和正式交叉评审承接 -## 修订记录 - -| 版本 | 日期 | 修改人 | 修改说明 | -|------|------|--------|----------| -| v0.1 | 2026-07-24 | 罗皓晨、各模块负责人 | 建立通用约定,综合六份最新个人接口原稿,形成 107 个追踪编号、103 个有效 HTTP 契约及非 HTTP 协作边界,并更新冻结条件 | -| v1.0 | 2026-07-24 | 全体成员(罗皓晨统稿) | 以 21 份已校准业务流程为输入,隔离 10 个历史取消编号、补出 A115/A426,统一 99 个活动 HTTP 定义及消息、缓存、Worker、回调、退款和运行契约 | - ## 一、通用约定 ### 1.1 适用范围与基础信息 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241.md" index f63d913..d0a0ed5 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241.md" @@ -3,12 +3,6 @@ > 组别:24级1班第7组 编写人:罗皓晨 编写日期:2026-07-24 版本:v0.1 > 截止:第 1 周周五 -## 修订记录 - -| 版本 | 日期 | 修改人 | 修改说明 | -|---|---|---|---| -| v0.1 | 2026-07-24 | 罗皓晨 | 明确 DBxxx 分工、个人原稿、单表模板、汇总和冻结规则 | - ## 一、设计说明 - 数据库使用 PostgreSQL,通过 EF Core 10 与 Npgsql 访问。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" index 1272a21..7f471f0 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" @@ -5,13 +5,6 @@ > 文档状态:已按业务流程 v1.0 完成架构承接校准,接口已按流程重建,待数据库、实现和全组技术评审后冻结 > 截止:第 1 周周五 -## 修订记录 - -| 版本 | 日期 | 修改人 | 修改说明 | -|---|---|---|---| -| v0.1 | 2026-07-24 | 罗皓晨 | 形成并完善系统架构,明确技术选型、分层依赖、模块边界、角色权限、事件、Worker、库存与售后履约协作及六人纵向职责 | -| v0.2 | 2026-07-24 | 罗皓晨 | 按流程 v1.0 统一默认履约商家、固定支付截止时间、售后竞争、消息矩阵、C07 参数、C08 通道和 C10 Migrator/降级/停止边界 | - ## 一、架构目标与约束 ### 1.1 架构目标 -- Gitee From d1938ba5fc29ead54cf0259400772311d5003b65 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Sat, 25 Jul 2026 02:30:42 +0800 Subject: [PATCH 111/118] =?UTF-8?q?docs(database):=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=95=B0=E6=8D=AE=E5=BA=93=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=EF=BC=9B=E6=8C=89=E6=B5=81=E7=A8=8B=E4=B8=8E=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=86=BB=E7=BB=9344=E5=BC=A0=E8=A1=A8=E5=8F=8A=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E7=BA=A6=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...56\345\272\223\350\256\276\350\256\241.md" | 2320 ++++++++++++++++- 1 file changed, 2238 insertions(+), 82 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241.md" index d0a0ed5..52308f1 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241.md" @@ -1,133 +1,2289 @@ # 数据库设计 -> 组别:24级1班第7组 编写人:罗皓晨 编写日期:2026-07-24 版本:v0.1 -> 截止:第 1 周周五 +> 组别:24级1班第7组 统一设计与统稿:罗皓晨 编写日期:2026-07-25 版本:v1.0 +> +> 文档状态:**完整定义,已确认,可作为 EF Core 实体、映射、初始 Migration、Seed 与数据库测试的实施事实源;当前仓库尚未实现实体、DbContext 或 Migration。** -## 一、设计说明 +## 一、设计依据与结论 -- 数据库使用 PostgreSQL,通过 EF Core 10 与 Npgsql 访问。 -- PostgreSQL 是业务事实来源;Redis、RabbitMQ、对象存储和进程内存不登记为业务表。 -- [《命名规范》](命名规范.md)第六章是表、字段、约束、索引和 Migration 命名的唯一事实源,本文件不重复命名规则。 -- [《接口设计》](接口设计.md)是 Axxx、字段和状态输入来源;接口未确认时,关联表必须标记“待接口确认”。 -- `数据库设计.md` 是 EF Core 实体、映射、Migration、初始化和测试的唯一数据库设计事实源。 -- `database/database-<姓名拼音首字母>.md` 是个人贡献原稿,汇总后继续保留,但不能覆盖主文档。 -- 当前尚未汇总完整表定义,文档成熟度为 **模板/占位,未冻结**。 +### 1.1 事实优先级 -## 二、DBxxx 分工与个人原稿 +本设计从零建立,不以旧表、旧接口草稿或预想代码反推业务。设计顺序固定为: -### 2.1 编号分配 +```text +教师要求与已确认需求 +→ 已确认业务流程、状态、异常和模块出入口 +→ 主接口契约 +→ 数据库实体、关系、约束、索引和事务 +→ 后续 EF Core、Migration、Seed 与测试 +``` -DBxxx 只用于文档追踪和分工,不进入真实表名、实体、DbSet、约束、索引或 Migration 名。 +发生冲突时,按“业务流程优先、接口随流程调整、数据库同时满足两者”的原则处理。本文直接承接: -| 负责人 | 负责模块 | DBxxx 范围 | 个人原稿 | -|---|---|---|---| -| 唐宇昊 | Identity、Engagement | `DB001`~`DB020` | `database/database-tyh.md` | -| 顾欣月 | Catalog、Review | `DB021`~`DB040` | `database/database-gxy.md` | -| 朱惠惠 | Cart、Seckill | `DB041`~`DB060` | `database/database-zhh.md` | -| 韦乾强 | Ordering | `DB061`~`DB080` | `database/database-wqq.md` | -| 张海洋 | Payment、AfterSales | `DB081`~`DB100` | `database/database-zhy.md` | -| 罗皓晨 | Messaging、可靠事件公共表 | `DB101`~`DB120` | `database/database-lhc.md` | +- `F01`~`F13`、`X01`~`X04`; +- `C01`、`C03`、`C04`、`C06`、`C07`、`C08`、`C10`; +- 单店 B2C、统一经营目录、唯一启用默认商家、普通库存与秒杀库存分通道; +- PostgreSQL 业务事实、Redis 可恢复加速、RabbitMQ 事务后传输、S3 兼容对象存储只保存文件; +- 接口中的 99 个活动 HTTP 契约和已确认的内部应用契约。 + +### 1.2 统一设计结论 + +1. `docs/02-设计文档/数据库设计.md` 是唯一数据库设计事实源,不再拆分、等待或同步六份个人数据库原稿。 +2. DBxxx 只用于文档追踪,不进入表名、实体名、DbSet、约束、索引或 Migration 名。 +3. 共设计 **44 张业务与可靠性表**;所有表均给出字段、约束、索引、关系、状态和事务边界。 +4. PostgreSQL 18.4 是唯一业务事实来源;Redis、RabbitMQ、对象存储和进程内存均不得保存不可恢复的唯一业务事实。 +5. 模块化单体共用同一数据库,但跨模块只能通过公开应用能力编排;共享事务不等于允许调用方直接访问其他模块的 DbSet 或仓储。 +6. 不建设店铺、租户、拆单、结算、刷新令牌、数据库会话、平行秒杀订单、通用软删除或通用业务审计大表。 +7. 订单、支付、钱包流水、已进入交易链的库存流水、售后、退款、回调、对账、消息、Outbox/Inbox 本期不物理删除;尚无任何外部业务引用且满足 A124 的商品被合法物理删除时,可一并删除其 Catalog 内部图片关联、搜索投影和普通库存维护流水。 + +### 1.3 技术基线 + +| 项目 | 决策 | +|---|---| +| 数据库 | PostgreSQL 18.4,默认 `public` schema,UTF-8 | +| 数据访问 | EF Core 10 + Npgsql | +| 主键 | 对外业务表使用应用侧生成的 UUIDv7;接口继续把 UUID 当作不透明标识 | +| 时间 | `timestamptz`,统一 UTC;资格与截止判断使用锁定目标事实后取得的数据库 `clock_timestamp()` | +| 金额 | `numeric(18,2)`;金额字段不得使用浮点;币种 `char(3)`,本期固定 `CNY` | +| 状态 | 数据库存 lower_snake_case;API 层映射为已确认的 PascalCase | +| 并发版本 | 显式 `version bigint`,不把 PostgreSQL `xmin` 暴露为公开并发版本 | +| 搜索 | `pg_trgm` 扩展 + GIN;1~2 字关键词参数化 `ILIKE` 保召回,3~50 字使用 trigram 候选与相关度 | +| JSONB | 只用于幂等响应、事件 Envelope、安全对账快照和处置详情,不替代稳定业务字段 | +| 删除 | 同聚合从表可按明确规则级联;跨模块外键统一 `RESTRICT/NO ACTION`,禁止跨模块级联 | +| 迁移 | 只允许一次性 Migrator 执行;API、Worker 不自动迁移 | + +## 二、全局建模规则 + +### 2.1 公共字段与数据库时间 + +- 可变聚合根通常包含 `id`、`created_at`、`updated_at`、`version`。 +- 不可变流水只保存 `created_at` 或业务发生时间,不伪造 `updated_at`。 +- `created_at` 默认 `CURRENT_TIMESTAMP`;应用更新可变记录时必须同时写 `updated_at = clock_timestamp()`。 +- 通用 `version` 创建时使用各表初值;每次成功改变该聚合的任一可变业务事实,必须在同一 `UPDATE` 中执行 `version = version + 1`。纯读取、校验失败、条件更新未命中和幂等重放不递增;钱包 version=0 初态及活动 `result_version` 等特殊初值按各表更严格规则执行。 +- 订单支付、活动抢购、超时取消等请求可能等待行锁;必须在取得目标行锁后调用一次 `clock_timestamp()` 形成 `decision_time`,后续同一事务复用该值。不得用锁等待前取得的应用时间或事务开始时的旧时间越过截止边界。 +- 数据库默认值不能代替接口要求的服务端计算。例如 `payment_deadline`、`auto_complete_at`、图片过期时间必须由用例按已追踪配置显式写入。 + +### 2.2 金额、数量与快照 + +- 所有金额最多两位小数;单价、总额和余额使用 `numeric(18,2)`。 +- 所有业务数量使用 `integer`,并以 Check 保证非负或正数。 +- 订单保存地址、商品名称、主图对象 Key、成交单价、原价、活动名称和买家安全展示名快照;历史展示不得回查当前商品或地址覆盖快照。 +- 对象 URL 不作为长期事实。数据库保存 `object_key`、`media_type`、尺寸和顺序,API 通过对象存储适配器生成受控 URL。 +- 对象 Key 遵守命名规范 `//.`,并用互斥资源前缀冻结命名空间:商品原图 `products/{productId}/...`、缩略图 `product-thumbnails/{productId}/...`、评价图 `reviews/{orderItemId}/...`。Key 创建后不可改、不复用,Check 校验前缀与所属资源 ID,避免不同列或对象类型碰撞。 + +### 2.3 外键与删除 + +- 用户不提供物理删除;账号只在 `normal` / `disabled` 间转换。 +- 分类、商品仅在已确认流程允许且不存在引用时物理删除;所有历史引用外键均为 `RESTRICT`。 +- 地址、购物车条目可物理删除;订单保存快照且不依赖地址原记录继续存在。 +- 商品图片、评价图片的数据库关联删除不代表对象已删除,必须在同一数据库事务登记 `object_cleanup_tasks`。 +- 多态业务引用(消息关联资源、Outbox 聚合、幂等作用域、对账证据)不建立伪外键,使用受控类型 + UUID/字符串并由所属模块校验。 + +### 2.4 敏感数据 + +- `password_hash` 只保存 ASP.NET Core PasswordHasher 或等价自适应哈希输出;明文密码、确认密码、HMAC 密钥和 JWT 不落库。 +- 手机号与收货信息仅由必要接口读取;日志、遥测、ProblemDetails、消息正文和对账安全快照必须脱敏。 +- 回调表保存 Key 标识与请求哈希,不保存 HMAC Secret 或原始签名。 +- 数据库备份、磁盘卷和对象存储在部署层加密;本期不把密钥放入数据库,也不引入依赖数据库会话变量的 RLS。 + +### 2.5 跨模块共享事务 + +需要跨模块原子提交时,由应用编排层开启同一 Npgsql 连接与事务,各模块公开应用能力加入该事务: + +- 普通下单:Cart + Catalog + Ordering + Outbox + Idempotency; +- 秒杀发布:Catalog 普通库存 + Seckill 活动库存 + 双侧库存流水 + Idempotency; +- 秒杀下单:Seckill 库存/限购 + Ordering + Outbox + Idempotency; +- 支付:Payment 钱包/支付 + Ordering 状态 + Outbox + Idempotency; +- 取消:Ordering + 原 Catalog/Seckill 库存通道 + Outbox; +- 退款成功:Payment 钱包/退款 + AfterSales + 原库存通道 + Outbox; +- 消息消费:Inbox + 同一事件全部接收人消息。 +- C07 失效:Catalog/库存来源事务 + Immediate Outbox;消费者首次 DEL 后以 Inbox + Delayed Outbox 原子登记第二阶段责任。 + +共享事务只解决原子性,不改变数据所有权。 + +### 2.6 数据库对象与约束命名 + +- 表、列、索引、约束使用 `snake_case`;DBxxx 只用于本文追踪,不进入实际对象名。 +- 主键、唯一、外键、Check、普通索引分别固定为 `pk_`、`ux_
_`、`fk_
__`、`ck_
_`、`ix_
_`。 +- 本文简写为“PK/FK/Unique/Check”的每一项,在 Migration 中都必须展开为有名字的对象;不得依赖提供程序生成不可预测的匿名名称。 +- SHA-256 指纹统一保存为 64 位小写十六进制 `char(64)`,对应 Check 使用 `^[0-9a-f]{64}$`。 +- JSONB 空对象默认值统一写成 `'{}'::jsonb`,需要对象结构的列增加 `jsonb_typeof(column)='object'` Check;不得把稳定关系字段藏入 JSONB。 + +## 三、统一表清单 + +| DBxxx | 表名 | 模块 | 核心用途 | 状态 | +|---|---|---|---|---| +| DB001 | `users` | Identity | 账号、角色、状态、凭证版本、默认商家 | 已确认 | +| DB002 | `user_status_histories` | Identity | 管理员启停账号领域历史 | 已确认 | +| DB003 | `addresses` | Identity | 买家收货地址与默认地址 | 已确认 | +| DB004 | `revoked_access_tokens` | Identity | JWT `jti` 撤销的 PostgreSQL 权威事实 | 已确认 | +| DB005 | `favorites` | Engagement | 买家商品收藏 | 已确认 | +| DB006 | `browsing_history` | Engagement | 每买家最近 200 条浏览事实 | 已确认 | +| DB007 | `browsing_history_settings` | Engagement | 浏览记录开关 | 已确认 | +| DB021 | `categories` | Catalog | 两级分类及启停状态 | 已确认 | +| DB022 | `products` | Catalog | 商品、价格、普通库存与三态 | 已确认 | +| DB023 | `product_images` | Catalog | 商品图片上传、主图和排序 | 已确认 | +| DB024 | `reviews` | Review | 订单项唯一评价与买家展示快照 | 已确认 | +| DB025 | `review_images` | Review | 评价前暂存及评价图片 | 已确认 | +| DB026 | `catalog_inventory_movements` | Catalog | 普通库存不可变变动链与回补去重 | 已确认 | +| DB027 | `product_search_documents` | Catalog | 可重建的 PostgreSQL 搜索投影 | 已确认 | +| DB041 | `cart_items` | Cart | 买家购物车条目与选中状态 | 已确认 | +| DB042 | `seckill_activities` | Seckill | 活动生命周期及独立库存 | 已确认 | +| DB043 | `seckill_buyer_quotas` | Seckill | 买家当前有效限购占用 | 已确认 | +| DB044 | `seckill_inventory_movements` | Seckill | 划拨、抢购、取消、退款回补审计 | 已确认 | +| DB061 | `orders` | Ordering | 普通/秒杀共享订单与核心状态 | 已确认 | +| DB062 | `order_items` | Ordering | 商品、价格、库存来源和履约快照 | 已确认 | +| DB063 | `order_lifecycle_tasks` | Ordering | 超时取消、自动完成的运维任务事实 | 已确认 | +| DB081 | `wallet_accounts` | Payment | 买家 CNY 钱包余额 | 已确认 | +| DB082 | `wallet_topups` | Payment | 成功模拟充值事实 | 已确认 | +| DB083 | `wallet_transactions` | Payment | 不可变资金流水 | 已确认 | +| DB084 | `payment_channel_attempts` | Payment | 模拟通道流水号不可变绑定与聚合 | 已确认 | +| DB085 | `payments` | Payment | 每订单唯一成功支付事实 | 已确认 | +| DB086 | `after_sales_requests` | AfterSales | 分次售后申请、金额与状态 | 已确认 | +| DB087 | `after_sales_status_histories` | AfterSales | 可循环售后状态时间线 | 已确认 | +| DB088 | `refund_operations` | Payment | 每售后申请唯一业务退款 | 已确认 | +| DB089 | `payment_callbacks` | Payment | 回调幂等、四终态与差异来源 | 已确认 | +| DB090 | `reconciliation_batches` | Payment | 每日一致水位对账批次 | 已确认 | +| DB091 | `reconciliation_differences` | Payment | 唯一比较单元与当前处置状态 | 已确认 | +| DB092 | `after_sales_return_shipments` | AfterSales | 一次性退货物流事实 | 已确认 | +| DB093 | `refund_attempts` | Payment | 同一退款操作的多次执行尝试 | 已确认 | +| DB094 | `reconciliation_evidence` | Payment | 差异多来源证据 | 已确认 | +| DB095 | `reconciliation_actions` | Payment | 领取、释放、接管、复核和解决时间线 | 已确认 | +| DB096 | `financial_posting_sequences` | Payment | 严格财务提交水位 | 已确认 | +| DB101 | `messages` | Messaging | 用户可查询、可已读的站内消息 | 已确认 | +| DB102 | `outbox_messages` | M00 | 来源事务可靠待发布事件 | 已确认 | +| DB103 | `inbox_messages` | M00 | 消费者整事件成功防重事实 | 已确认 | +| DB104 | `idempotency_records` | M00 | 全模块确定结果稳定重放 | 已确认 | +| DB105 | `object_cleanup_tasks` | M00 | 对象删除补偿与过期上传清理 | 已确认 | +| DB106 | `outbox_delivery_attempts` | M00 | Outbox 每次发布结果审计 | 已确认 | +| DB107 | `worker_job_runs` | M00 | 多实例定时任务调度与恢复 | 已确认 | + +## 四、Identity 与 Engagement + +### 4.1 DB001 `users` + +用途:F01~F03、F13 的账号事实;不保存刷新令牌或服务器会话。 + +| 字段 | PostgreSQL 类型 | 允许空 | 默认值 | 说明 | +|---|---|---:|---|---| +| `id` | `uuid` | 否 | 应用 UUIDv7 | 用户 ID | +| `phone` | `varchar(11)` | 否 | — | 规范化中国大陆手机号 | +| `username` | `varchar(32)` | 否 | — | 服务端生成 `u_` + 8 位字符 | +| `password_hash` | `varchar(512)` | 否 | — | 自适应密码哈希 | +| `avatar_code` | `varchar(32)` | 否 | `'default_01'` | 默认头像代码,URL 由配置生成 | +| `role` | `varchar(16)` | 否 | — | `buyer/merchant/admin` | +| `status` | `varchar(16)` | 否 | `'normal'` | `normal/disabled` | +| `token_version` | `bigint` | 否 | `1` | 全部旧 JWT 失效版本 | +| `username_reset_count` | `smallint` | 否 | `0` | 买家自助重置次数 | +| `is_default_merchant` | `boolean` | 否 | `false` | 唯一订单责任商家 | +| `status_changed_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 最近启停时间 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 创建时间 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 更新时间 | +| `version` | `bigint` | 否 | `1` | 乐观并发版本 | + +约束: + +- `pk_users(id)`。 +- `ux_users_phone(phone)`、`ux_users_username(username)`。 +- `ux_users_id_role(id,role)` 供需要固化接收角色的复合 FK。 +- `ux_users_default_merchant(is_default_merchant) WHERE is_default_merchant`,保证最多一个默认商家。 +- `ck_users_phone`:`phone ~ '^1[3-9][0-9]{9}$'`。 +- `ck_users_role`、`ck_users_status`。 +- `ck_users_token_version`:`token_version >= 1`。 +- `ck_users_username_reset_count`:`username_reset_count BETWEEN 0 AND 1`;非 Buyer 固定为 0。 +- `ck_users_default_merchant`:默认标记只能属于 `role='merchant' AND status='normal'`。 +- 用户名重置、手机号修改和账号启停均执行 `version = version + 1`;手机号修改和禁用同时执行 `token_version = token_version + 1`。 + +索引: + +| 索引 | 字段 | 用途 | +|---|---|---| +| `ix_users_role_status_created_at` | `role, status, created_at DESC, id DESC` | A015 稳定分页 | +| `ix_users_status_changed_at` | `status, status_changed_at DESC` | 安全事实重建与运维 | + +关系与删除:被业务表引用后永久保留,所有外键 `RESTRICT`。A016 对默认商家由 Check 最终保护;非默认商家责任阻断由各模块公开能力在同一事务复核。 + +### 4.2 DB002 `user_status_histories` + +用途:保存 A016/A017 的领域状态变化,不与通用技术日志混用。 + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | UUIDv7 | +| `user_id` | `uuid` | 否 | 被操作账号 | +| `from_status` | `varchar(16)` | 否 | `normal/disabled` | +| `to_status` | `varchar(16)` | 否 | `normal/disabled` | +| `reason` | `varchar(500)` | 否 | 管理原因或稳定默认原因 | +| `actor_admin_user_id` | `uuid` | 否 | 操作管理员 | +| `token_version_after` | `bigint` | 否 | 变化后的凭证版本 | +| `operation_id` | `uuid` | 否 | 本次唯一状态变更操作 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | + +约束与索引: + +- PK `pk_user_status_histories`。 +- FK `user_id`、`actor_admin_user_id` → `users.id ON DELETE RESTRICT`。 +- Unique `ux_user_status_histories_operation_id(operation_id)`。 +- Check `from_status <> to_status`、状态白名单、`token_version_after >= 1`。 +- `ix_user_status_histories_user_id_created_at(user_id, created_at DESC, id DESC)`。 +- 记录与 `users` 状态修改、DB104 幂等结果在同一事务提交;表不可更新、不可删除。 + +### 4.3 DB003 `addresses` + +| 字段 | 类型 | 空 | 默认 | 说明 | +|---|---|---:|---|---| +| `id` | `uuid` | 否 | UUIDv7 | 地址 ID | +| `buyer_id` | `uuid` | 否 | — | 地址所有者 | +| `receiver_name` | `varchar(50)` | 否 | — | 收件人 | +| `receiver_phone` | `varchar(11)` | 否 | — | 收货手机号 | +| `province` | `varchar(100)` | 否 | — | 省 | +| `city` | `varchar(100)` | 否 | — | 市 | +| `district` | `varchar(100)` | 否 | — | 区县 | +| `detail_address` | `varchar(120)` | 否 | — | 详细地址 | +| `is_default` | `boolean` | 否 | `false` | 默认地址 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 创建时间 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 更新时间 | +| `version` | `bigint` | 否 | `1` | 并发版本 | + +约束与索引: + +- PK `pk_addresses`;FK `buyer_id → users.id ON DELETE RESTRICT`。 +- `ux_addresses_buyer_id_default(buyer_id) WHERE is_default`,每个买家最多一个默认地址。 +- `ck_addresses_receiver_phone`:`receiver_phone ~ '^1[3-9][0-9]{9}$'`;收件人去空白后 1~50 字,详细地址 5~120 字,省/市/区县去空白后非空。 +- `ix_addresses_buyer_id_created_at(buyer_id, created_at DESC, id DESC)`。 +- A011 固定创建非默认;A014 锁定买家账号行后先清旧默认再设新默认,同一事务提交。 +- A013 允许物理删除;删除默认地址不自动选择其他地址。订单只保留 `source_address_id` 弱引用和完整地址快照,不对地址建立订单外键。 + +### 4.4 DB004 `revoked_access_tokens` + +用途:PostgreSQL 保存 JWT 撤销权威事实,Redis `auth:revoked:{jti}` 只是可重建镜像。 + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | UUIDv7 | +| `jti` | `uuid` | 否 | JWT 唯一标识 | +| `user_id` | `uuid` | 否 | 令牌账号 | +| `token_version` | `bigint` | 否 | JWT 内版本 | +| `reason` | `varchar(32)` | 否 | `logout/security_invalidation` | +| `revoked_at` | `timestamptz` | 否 | 首次撤销时间 | +| `expires_at` | `timestamptz` | 否 | 不早于 JWT 自然过期 | + +约束与索引: + +- PK `pk_revoked_access_tokens`;FK `user_id → users.id ON DELETE RESTRICT`。 +- Unique `ux_revoked_access_tokens_jti(jti)`。 +- Check `expires_at > revoked_at`、`token_version >= 1`、原因白名单。 +- `ix_revoked_access_tokens_expires_at(expires_at)` 用于过期清理。 +- `ix_revoked_access_tokens_user_id_expires_at(user_id, expires_at)` 用于安全重建。 +- A003 使用 `INSERT ... ON CONFLICT (jti) DO NOTHING`,重复退出返回首次 `revoked_at`。 +- 账号级全部旧凭证失效以 `users.token_version` 为权威事实,不批量展开每个未知 JTI。 + +### 4.5 DB005 `favorites` + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | UUIDv7 | +| `buyer_id` | `uuid` | 否 | 买家 | +| `product_id` | `uuid` | 否 | 商品 | +| `created_at` | `timestamptz` | 否 | 收藏时间 | + +- PK `pk_favorites`;FK 分别指向 `users`、`products`,均 `RESTRICT`。 +- Unique `ux_favorites_buyer_id_product_id(buyer_id, product_id)`。 +- `ix_favorites_buyer_id_created_at(buyer_id, created_at DESC, id DESC)` 服务 A018。 +- `ix_favorites_product_id(product_id)` 服务商品删除引用检查。 +- A019 `ON CONFLICT` 返回既有记录;A020 按买家 + 商品物理删除,不存在也返回未收藏结果。 +- 不保存商品名称、价格、上下架或库存;列表通过 Catalog 公开能力取得当前快照,商品已下架仍保留收藏占位。 + +### 4.6 DB006 `browsing_history` + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | UUIDv7 | +| `buyer_id` | `uuid` | 否 | 买家 | +| `product_id` | `uuid` | 否 | 商品 | +| `viewed_at` | `timestamptz` | 否 | 最近浏览时间 | +| `created_at` | `timestamptz` | 否 | 首次记录时间 | + +- PK `pk_browsing_history`;FK `buyer_id → users`、`product_id → products`,均 `RESTRICT`。 +- Unique `ux_browsing_history_buyer_id_product_id`,重复浏览更新 `viewed_at`,不新增重复行。 +- `ix_browsing_history_buyer_id_viewed_at(buyer_id, viewed_at DESC, id DESC)` 服务 A021 与最近 200 条裁剪。 +- `ix_browsing_history_product_id(product_id)` 服务商品删除引用检查。 +- A024 在同一事务取得买家级 advisory lock,重检 DB007 开关、UPSERT 当前商品,再删除该买家排序第 201 条以后的记录。 +- 关闭记录不删除、不隐藏旧历史;本期没有清空历史能力。 + +### 4.7 DB007 `browsing_history_settings` + +| 字段 | 类型 | 空 | 默认 | 说明 | +|---|---|---:|---|---| +| `buyer_id` | `uuid` | 否 | — | PK/FK | +| `is_enabled` | `boolean` | 否 | `true` | 后续浏览是否写入 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 修改时间 | +| `version` | `bigint` | 否 | `1` | 并发版本 | + +- PK `pk_browsing_history_settings(buyer_id)`;FK → `users.id ON DELETE RESTRICT`。 +- 没有记录时 A025 返回逻辑默认 `true` 且 GET 不写库;A022 首次修改时 UPSERT。 +- A022 与 A024 对同一买家使用相同事务锁,保证“关闭先提交则不记录,记录先提交则本次已记录”的唯一顺序。 + +## 五、Catalog 与 Review + +### 5.1 DB021 `categories` + +| 字段 | 类型 | 空 | 默认 | 说明 | +|---|---|---:|---|---| +| `id` | `uuid` | 否 | UUIDv7 | 分类 ID | +| `parent_id` | `uuid` | 是 | — | 根分类为空;只允许指向根分类 | +| `name` | `varchar(30)` | 否 | — | 展示名称 | +| `normalized_name` | `varchar(30)` | 否 | — | 去空白并统一大小写后的唯一名称 | +| `sort_order` | `integer` | 否 | `0` | 同层排序 | +| `status` | `varchar(16)` | 否 | `'enabled'` | `enabled/disabled` | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 创建时间 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 更新时间 | +| `version` | `bigint` | 否 | `1` | 并发版本 | + +约束: + +- PK `pk_categories`;自 FK `parent_id → categories.id ON DELETE RESTRICT`。 +- `ux_categories_parent_id_normalized_name` 使用 PostgreSQL `UNIQUE NULLS NOT DISTINCT(parent_id, normalized_name)`,保证根级和同父子级名称分别唯一。 +- `ck_categories_parent_id`:`parent_id IS NULL OR parent_id <> id`。 +- `ck_categories_status`、`ck_categories_sort_order(sort_order >= 0)`、名称非空。 +- 最多两级不是普通行 Check 能表达。A111/A112 先按 ID 稳定顺序锁定目标分类和候选父分类,确认候选父 `parent_id IS NULL`,并确认有子分类的根分类不允许改为子分类后再写入。 + +索引: + +| 索引 | 字段 | 用途 | +|---|---|---| +| `ix_categories_parent_id_sort_order` | `parent_id, sort_order, id` | A101/A110 树形列表 | +| `ix_categories_status_parent_id_sort_order` | `status, parent_id, sort_order, id` | 有效分类查询 | +| `ix_categories_normalized_name_trgm` | `normalized_name gin_trgm_ops` | C04 分类名匹配 | + +有效性规则:存储状态不级联修改。子分类购物端有效 = 自身 `enabled` 且父级 `enabled`;停用父级只让子树暂时不可筛选,不改写子级状态或商品状态。 + +删除:A115 只允许无子分类、无商品、无其他历史引用时物理删除;FK `RESTRICT` 是最终保护。 + +### 5.2 DB022 `products` + +| 字段 | 类型 | 空 | 默认 | 说明 | +|---|---|---:|---|---| +| `id` | `uuid` | 否 | UUIDv7 | 商品 ID | +| `category_id` | `uuid` | 否 | — | 当前分类 | +| `name` | `varchar(100)` | 否 | — | 商品名 | +| `normalized_name` | `varchar(100)` | 否 | — | 搜索规范化名称 | +| `description` | `varchar(2000)` | 是 | — | 受控描述 | +| `price` | `numeric(18,2)` | 否 | — | 当前普通售价 | +| `stock` | `integer` | 否 | — | 普通库存,不含秒杀已划拨库存 | +| `status` | `varchar(16)` | 否 | `'draft'` | `draft/on_sale/off_sale` | +| `on_sale_at` | `timestamptz` | 是 | — | 最近一次真实上架时间 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 创建时间 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 更新时间 | +| `version` | `bigint` | 否 | `1` | 内容与库存并发版本 | + +约束: + +- PK `pk_products`;FK `category_id → categories.id ON DELETE RESTRICT`。 +- `ck_products_price(price >= 0)`、`ck_products_stock(stock >= 0)`、状态白名单、名称非空、`version >= 1`。 +- `status='on_sale'` 时 `on_sale_at IS NOT NULL`;草稿可为空;下架保留最近上架时间。 +- A123 修改库存时必须写 DB026;订单、发布秒杀、取消和退款也只通过库存公开能力修改 `stock`。 +- A125 必须在锁定商品行后确认商品至少一张已完成主图、当前分类有效、价格和库存合法;真实 `draft/off_sale → on_sale` 时更新 `on_sale_at`,重复上架不改时间。 + +索引: + +| 索引 | 字段 | 用途 | +|---|---|---| +| `ix_products_on_sale_created_at` | `created_at DESC, id DESC WHERE status='on_sale'` | C07 固定首页、默认列表 | +| `ix_products_on_sale_at` | `on_sale_at DESC, id DESC WHERE status='on_sale'` | 搜索同分排序 | +| `ix_products_category_on_sale_created_at` | `category_id, created_at DESC, id DESC WHERE status='on_sale'` | 分类筛选 | +| `ix_products_on_sale_price` | `price, id WHERE status='on_sale'` | 价格排序/区间 | +| `ix_products_status_created_at` | `status, created_at DESC, id DESC` | A120 后台列表 | +| `ix_products_category_id` | `category_id` | 分类引用与删除保护 | +| `ix_products_on_sale_stock` | `stock, id WHERE status='on_sale' AND stock > 0` | 仅看有货 | + +删除:仅 `draft/off_sale` 且 Cart、Favorite、History、OrderItem、Review、Seckill 等全部引用不存在时物理删除。公开引用检查用于友好错误,跨模块 FK `RESTRICT` 负责最终竞争保护。 + +### 5.3 DB023 `product_images` + +| 字段 | 类型 | 空 | 默认 | 说明 | +|---|---|---:|---|---| +| `id` | `uuid` | 否 | UUIDv7 | 图片 ID | +| `product_id` | `uuid` | 否 | — | 所属商品 | +| `object_key` | `varchar(500)` | 否 | — | 原图对象 Key | +| `thumbnail_object_key` | `varchar(500)` | 否 | — | 预生成的方形缩略图 Key | +| `media_type` | `varchar(32)` | 是 | — | `image/jpeg/png/webp` | +| `byte_size` | `bigint` | 是 | — | 文件大小 | +| `width` | `integer` | 是 | — | 像素宽 | +| `height` | `integer` | 是 | — | 像素高 | +| `status` | `varchar(16)` | 否 | `'uploading'` | `uploading/attached` | +| `sort_order` | `smallint` | 是 | — | 已关联图片 1~8 | +| `is_primary` | `boolean` | 否 | `false` | 主图 | +| `alt_text` | `varchar(100)` | 是 | — | 替代文本 | +| `upload_expires_at` | `timestamptz` | 是 | — | 上传预留过期时间 | +| `attached_at` | `timestamptz` | 是 | — | 关联完成时间 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 创建时间 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 更新时间 | + +约束: + +- PK `pk_product_images`;FK `product_id → products.id ON DELETE RESTRICT`。 +- Unique `ux_product_images_object_key(object_key)`、`ux_product_images_thumbnail_object_key(thumbnail_object_key)`。 +- Check 原图 Key 匹配 `products/{product_id}/...`、缩略图匹配 `product-thumbnails/{product_id}/...` 且二者不等;互斥前缀与列内唯一共同保证全局不复用。 +- `ux_product_images_product_id_sort_order(product_id, sort_order) WHERE status='attached'`。 +- `ux_product_images_product_id_primary(product_id) WHERE status='attached' AND is_primary`。 +- `uploading`:原图与缩略图 Key 已预生成并持久化;媒体元数据、排序、主图、关联时间为空/false,`upload_expires_at` 非空。 +- `attached`:媒体元数据、`sort_order`、`attached_at` 非空,过期时间为空;类型、5 MB、400~4096 像素和顺序范围满足接口。 + +索引: + +- `ix_product_images_product_id_sort_order(product_id, sort_order, id) WHERE status='attached'`。 +- `ix_product_images_upload_expires_at(upload_expires_at) WHERE status='uploading'`。 + +上传协议: + +1. A127 先锁商品行并把“已关联 + 未过期上传预留”控制在 8 条内,预生成原图和缩略图全部不可变 Key,提交一条 `uploading` 预留; +2. 写对象与缩略图; +3. 新事务再次锁商品,补齐元数据并切换 `attached`、主图和排序; +4. 尚未写入任何对象时才可直接删除失败预留;任一对象已经写入但业务关联未提交时,必须先在数据库事务中为每个已写对象登记 DB105,再删除预留。数据库不可用时保留原预留,不得丢弃最后一份持久化 Key 清单。 + +A122 使用 DB104 的持久化工作清单预生成全部图片 ID、原图 Key 和缩略图 Key,对象成功后在一个数据库事务直接创建 `attached` 图片、Draft 商品和完成幂等结果。任一部分对象已写而商品事务未完成时同样先登记 DB105,或保留 Processing 工作清单等待恢复;A128 删除关联时先在同一事务登记 DB105,再物理删除图片行,删除主图时按 `sort_order,id` 提升下一张。 + +### 5.4 DB024 `reviews` + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | 评价 ID | +| `order_id` | `uuid` | 否 | 订单引用 | +| `order_item_id` | `uuid` | 否 | 唯一评价资格 | +| `buyer_id` | `uuid` | 否 | 评价买家 | +| `product_id` | `uuid` | 否 | 被评商品 | +| `rating` | `smallint` | 否 | 1~5 | +| `content` | `varchar(500)` | 否 | 评价正文 | +| `buyer_display_name_snapshot` | `varchar(50)` | 否 | 已脱敏展示名 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | + +约束与索引: + +- PK `pk_reviews`。 +- 复合 FK `(order_id,buyer_id) → orders(id,buyer_id)`、`(order_item_id,order_id,product_id) → order_items(id,order_id,product_id)`;用户、商品删除关系均 `RESTRICT`。订单、买家、订单项和商品引用因此不能被合法但互不归属的 ID 拼接。 +- Unique `ux_reviews_order_item_id(order_item_id)`。 +- Unique `ux_reviews_id_buyer_order_item(id,buyer_id,order_id,order_item_id)` 供评价图片复合归属。 +- Check `rating BETWEEN 1 AND 5`,正文去空白后 1~500 字。 +- `ix_reviews_product_id_created_at(product_id, created_at DESC, id DESC) INCLUDE (rating)` 同时服务 A140 分页与实时评分汇总。 +- `ix_reviews_buyer_id_created_at(buyer_id, created_at DESC, id DESC)` 用于归属验证与审计。 + +当前没有评价编辑、隐藏、删除或审核能力,因此评价提交后不可变且立即公开;不建冗余评分汇总表,A140 对索引覆盖的已提交事实做 `COUNT/AVG`。 + +### 5.5 DB025 `review_images` + +| 字段 | 类型 | 空 | 默认 | 说明 | +|---|---|---:|---|---| +| `id` | `uuid` | 否 | UUIDv7 | 图片 ID | +| `buyer_id` | `uuid` | 否 | — | 上传买家 | +| `order_id` | `uuid` | 否 | — | 由订单项派生的订单 | +| `order_item_id` | `uuid` | 否 | — | 上传资格绑定 | +| `review_id` | `uuid` | 是 | — | 正式评价 | +| `object_key` | `varchar(500)` | 否 | — | 对象 Key | +| `media_type` | `varchar(32)` | 是 | — | 图片 MIME | +| `byte_size` | `bigint` | 是 | — | 文件大小 | +| `width` | `integer` | 是 | — | 像素宽 | +| `height` | `integer` | 是 | — | 像素高 | +| `status` | `varchar(16)` | 否 | `'uploading'` | `uploading/pending/attached` | +| `sort_order` | `smallint` | 是 | — | 正式评价内 1~6 | +| `upload_expires_at` | `timestamptz` | 否 | — | 未关联图片过期时间 | +| `attached_at` | `timestamptz` | 是 | — | 正式关联时间 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 创建时间 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 更新时间 | + +约束与索引: + +- PK `pk_review_images`;复合 FK `(order_id,buyer_id) → orders(id,buyer_id)`、`(order_item_id,order_id) → order_items(id,order_id)` 均 `RESTRICT`;可空复合 FK `(review_id,buyer_id,order_id,order_item_id) → reviews(id,buyer_id,order_id,order_item_id)`,保证暂存和正式关联阶段的图片、评价、买家、订单与订单项始终一致。`order_id` 由服务端从已锁定订单项派生,不接受客户端自由组合。 +- Unique `ux_review_images_object_key(object_key)`。 +- Check `object_key` 匹配 `reviews/{order_item_id}/...`;与商品/缩略图互斥命名空间保证全局不复用。 +- `ux_review_images_review_id_sort_order(review_id, sort_order) WHERE status='attached'`。 +- 状态组合 Check: + - `uploading/pending` 时 `review_id/sort_order/attached_at` 为空; + - `attached` 时三者非空; + - 媒体元数据在 `pending/attached` 时完整,符合 5 MB、200~4096 像素和类型白名单。 +- `ix_review_images_buyer_order_item_pending(buyer_id, order_item_id, upload_expires_at) WHERE status IN ('uploading','pending')`。 +- `ix_review_images_review_id_sort_order(review_id, sort_order, id) WHERE status='attached'`。 +- `ix_review_images_upload_expires_at(upload_expires_at) WHERE status <> 'attached'`。 + +A141 按 `(buyer_id,order_item_id)` 取得事务 advisory lock,连同未过期上传预留计数,保证并发第 7 张失败。上传完成进入 `pending`,默认可引用期 **24 小时**;A142 在同一锁下重检资格、图片归属和未过期状态,创建评价并把最多 6 张图片切换为 `attached`。过期图片不能再引用,由 Worker 登记 DB105 后清理。 + +### 5.6 DB026 `catalog_inventory_movements` + +用途:每次普通库存变化的不可变审计与幂等保护;`products.stock` 仍是当前权威余额。 + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | 流水 ID | +| `product_id` | `uuid` | 否 | 商品 | +| `movement_type` | `varchar(32)` | 否 | 变动类型 | +| `operation_id` | `uuid` | 否 | 跨重试稳定操作 ID | +| `order_id` | `uuid` | 是 | 订单引用 | +| `order_item_id` | `uuid` | 是 | 订单项引用 | +| `quantity_delta` | `integer` | 否 | 正数增加、负数减少 | +| `stock_before` | `integer` | 否 | 变动前 | +| `stock_after` | `integer` | 否 | 变动后 | +| `reason` | `varchar(100)` | 是 | 受控说明 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | + +约束: + +- PK `pk_catalog_inventory_movements`;FK `product_id → products.id ON DELETE CASCADE`,属于 Catalog 商品聚合内部历史;只有商品已通过所有跨模块引用检查后才可能触发。 +- Unique `ux_catalog_inventory_movements_type_operation_id(movement_type, operation_id)`。 +- Check `quantity_delta <> 0`、`stock_before >= 0`、`stock_after >= 0`、`stock_after = stock_before + quantity_delta`。 +- 类型:`product_created/product_adjusted/seckill_allocated/order_debited/order_cancel_returned/after_sales_returned`。 +- 稳定操作 ID:正库存商品创建使用 `product_id`;后台库存调整使用本次受控调整 ID;秒杀划拨使用 `activity_id`;下单扣减和待支付取消使用 `order_item_id`;售后回补使用 `refund_operation_id`。 +- 类型组合 Check: + - `product_created` 只在初始库存大于 0 时写入,`stock_before=0`、`quantity_delta=stock_after>0`,订单引用为空;初始库存为 0 时不写零变化流水; + - `product_adjusted` 可正可负,订单引用为空; + - `seckill_allocated/order_debited` 的 `quantity_delta < 0`; + - `order_cancel_returned/after_sales_returned` 的 `quantity_delta > 0`; + - 订单相关类型要求订单/订单项非空。 +- 订单相关行使用复合 FK `(order_item_id,order_id,product_id) → order_items(id,order_id,product_id) ON DELETE RESTRICT`;写入能力必须从已锁定订单项派生三者,禁止调用方分别传入可能不一致的引用。因表创建顺序产生的跨模块 FK 在初始 Migration 后段追加。 + +索引: + +- `ix_catalog_inventory_movements_product_id_created_at(product_id, created_at, id)`。 +- `ix_catalog_inventory_movements_order_item_id(order_item_id) WHERE order_item_id IS NOT NULL`。 + +所有库存修改先锁商品或使用 `WHERE stock >= :quantity` 条件更新,取得前后值后同事务插入流水。A122 创建正库存商品时也写入 `product_created`,使审计链从 0 开始。取消/退款先尝试插入稳定 `operation_id` 流水;唯一冲突表示已处理,不能再次增加库存。 + +### 5.7 DB027 `product_search_documents` + +用途:C04 在同一 PostgreSQL 内保存可重建、同步更新的搜索投影,不是第二份业务事实。 + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `product_id` | `uuid` | 否 | PK/FK | +| `product_name_text` | `text` | 否 | 规范化商品名 | +| `category_name_text` | `text` | 否 | 当前分类名 | +| `description_text` | `text` | 否 | 规范化描述,空描述写空串 | +| `search_text` | `text` | 否 | 三字段拼接 | +| `source_hash` | `char(64)` | 否 | 三个规范化来源字段的 SHA-256 | +| `updated_at` | `timestamptz` | 否 | 同步时间 | + +约束与索引: + +- PK `pk_product_search_documents(product_id)`;FK `product_id → products.id ON DELETE CASCADE`,属于 Catalog 内部派生投影。 +- Check `source_hash` 为 64 位小写十六进制、`search_text` 等于按固定规范拼接的三个来源字段。 +- `ix_product_search_documents_search_text_trgm USING GIN(search_text gin_trgm_ops)`。 +- 相关度在查询时对商品名、分类名和描述分别计算 `similarity` 后组合,不把相关度持久化;具体权重属于 A102 查询策略,不改变表结构,所有策略都以 `on_sale_at DESC, product_id DESC` 作为同分稳定次序。 + +维护规则: + +- 商品创建、改名、改描述、改分类和分类改名在同一 Catalog 事务同步 UPSERT;不用 Outbox/Worker 异步复制。 +- 1~2 字关键词使用覆盖三个字段的参数化 `ILIKE`;3~50 字使用 GIN 候选并计算相关度;`pg_trgm` 固定 trigram,N=3,不存在另行配置的最大 N。 +- 商品状态、价格、库存、分类筛选仍从 `products/categories` 权威表读取;投影只负责关键词候选与排名。 +- `source_hash` 对 `product_name_text + U+001F + category_name_text + U+001F + description_text` 的 UTF-8 字节计算 SHA-256;由应用与重建工具使用同一规范生成,不依赖 `products.version`。库存、价格或状态变化不会制造搜索投影“假性落后”。 +- Migrator/运维可从 `products + categories` 全量重建并核对 `source_hash`,投影损坏不得回写业务表。 + +## 六、Cart、Seckill 与 Ordering + +### 6.1 DB041 `cart_items` + +| 字段 | 类型 | 空 | 默认 | 说明 | +|---|---|---:|---|---| +| `id` | `uuid` | 否 | UUIDv7 | API `cartItemId` | +| `buyer_id` | `uuid` | 否 | — | 买家 | +| `product_id` | `uuid` | 否 | — | 商品 | +| `quantity` | `integer` | 否 | — | 购买意向数量 | +| `is_selected` | `boolean` | 否 | `true` | 持久化选择状态 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 创建时间 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 最近修改时间 | +| `version` | `bigint` | 否 | `1` | 并发版本 | + +约束与索引: + +- PK `pk_cart_items`;FK `buyer_id → users`、`product_id → products`,均 `RESTRICT`。 +- Unique `ux_cart_items_buyer_id_product_id(buyer_id, product_id)`;这是业务唯一键,不替代物理 PK。 +- Check `quantity > 0`、`version >= 1`。 +- `ix_cart_items_buyer_id_updated_at(buyer_id, updated_at DESC, id DESC)` 服务 A202。 +- `ix_cart_items_buyer_id_selected(buyer_id, id) WHERE is_selected` 服务 A208/A301。 +- `ix_cart_items_product_id(product_id)` 服务商品删除引用检查。 规则: -1. 一张 PostgreSQL 持久化表占一个 DBxxx;不得为了占满区间拆表。 -2. 每人只使用本人区间,只设计本人模块拥有的数据。 -3. 编号进入主文档后不得复用;拆表使用新编号,废弃表保留原编号并标记。 -4. 跨模块业务事实只由事实所有者登记,其他模块只能保存稳定引用或必要快照。 +- 新条目默认选中;重复加购锁定既有行并累加,不能生成平行条目。 +- 不保存价格、库存、商品状态、`is_available` 或失败原因;每次查询、选择和结算从 Catalog 取得当前事实。 +- GET 不产生写操作。已选商品后来失效时可返回 `isSelected=true/isAvailable=false`,但不计入可结算金额;A206 处理选择动作时必须把失效条目保持未选中。 +- A301 成功时精确删除请求中的已选条目;订单失败则购物车原样保留。 -### 2.2 编写与汇总 +### 6.2 DB042 `seckill_activities` -1. 每人只修改本人原稿,先登记表清单,再按第三章补齐完整定义。 -2. 个人原稿阶段不同时修改主文档,避免六人冲突。 -3. 六份原稿分别交叉评审并合入 `dev` 后,由罗皓晨在独立整合分支汇总本文件。 -4. 首次汇总后,个人原稿继续保留;后续变更必须在同一任务中同步个人原稿和主文档。 -5. 分支、提交、PR 和评审流程统一遵循[《Git 团队协作流程》](Git团队协作流程.md),本文件不重复规定。 +| 字段 | 类型 | 空 | 默认 | 说明 | +|---|---|---:|---|---| +| `id` | `uuid` | 否 | UUIDv7 | 活动 ID | +| `product_id` | `uuid` | 否 | — | 统一经营目录商品 | +| `created_by_merchant_user_id` | `uuid` | 否 | — | 活动管理人,不是订单责任商家 | +| `activity_name` | `varchar(50)` | 否 | — | 活动名 | +| `seckill_price` | `numeric(18,2)` | 否 | — | 秒杀成交价 | +| `original_unit_price` | `numeric(18,2)` | 是 | — | 发布时固化普通价 | +| `planned_quantity` | `integer` | 否 | — | 草稿计划量 | +| `allocated_quantity` | `integer` | 是 | — | 发布后实际划拨总量 | +| `remaining_stock` | `integer` | 是 | — | 活动独立剩余库存 | +| `sold_count` | `integer` | 是 | — | 当前未取消/未退回数量 | +| `per_buyer_limit` | `integer` | 否 | — | 每买家限购 | +| `start_at` | `timestamptz` | 否 | — | 开始时间 | +| `end_at` | `timestamptz` | 否 | — | 结束时间 | +| `status` | `varchar(16)` | 否 | `'draft'` | 五态 | +| `published_at` | `timestamptz` | 是 | — | 划拨提交时间 | +| `cancel_reason` | `varchar(200)` | 是 | — | 取消原因;未填使用稳定默认值 | +| `cancelled_at` | `timestamptz` | 是 | — | 取消时间 | +| `status_changed_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 状态最近变化 | +| `result_version` | `bigint` | 否 | `1` | 生命周期/库存响应版本 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 创建时间 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 更新时间 | -## 三、个人原稿内容 +约束: -### 3.1 表登记清单 +- PK `pk_seckill_activities`;FK `product_id → products`、`created_by_merchant_user_id → users`,均 `RESTRICT`。 +- Unique `ux_seckill_activities_id_product_id(id,product_id)` 供订单项复合校验活动商品。 +- Check: + - `seckill_price > 0`; + - `planned_quantity > 0`; + - `per_buyer_limit BETWEEN 1 AND planned_quantity`; + - `start_at < end_at`; + - 状态为 `draft/published/ongoing/ended/cancelled`; + - `result_version >= 1`。 +- 未划拨的 Draft 或由 Draft 取消:`original_unit_price/allocated_quantity/remaining_stock/sold_count/published_at` 全为空。 +- 已划拨:上述五个字段全非空、金额和数量非负,且 `allocated_quantity = planned_quantity`、`remaining_stock + sold_count = allocated_quantity`。发布后 `planned_quantity/allocated_quantity` 均不可修改。 +- `published/ongoing/ended` 必须已划拨;`cancelled` 可来自未划拨 Draft 或已划拨状态。 +- `cancelled` 时 `cancel_reason/cancelled_at` 非空;其他状态二者为空。可选原因未提交时保存 `merchant_cancelled`,避免响应宣称非空而数据库为空。 +- 本期没有 `frozen_count`,也没有时间段互斥约束。 -每份个人原稿先登记本人实际需要的表,不要求占满编号: +索引: -| DBxxx | 真实表名 | 表用途 | 关联需求 | 关联接口 | 状态 | -|---|---|---|---|---|---| -| DBxxx | `table_name` | 说明唯一业务事实 | Mxx / Fxx / Xxx / Cxx | Axxx / 待接口确认 | 待评审 | +| 索引 | 字段 | 用途 | +|---|---|---| +| `ix_seckill_activities_merchant_created_at` | `created_by_merchant_user_id, created_at DESC, id DESC` | A224 | +| `ix_seckill_activities_status_start_at` | `status, start_at, id` | 生命周期 Worker/公开列表 | +| `ix_seckill_activities_status_end_at` | `status, end_at, id` | 自然结束 | +| `ix_seckill_activities_product_id` | `product_id` | 商品删除保护 | +| `ix_seckill_activities_name_trgm` | `activity_name gin_trgm_ops` | A224 关键词 | + +状态: + +```text +draft → published → ongoing → ended +draft / published / ongoing → cancelled +``` + +- Draft 无论计划时间是否已经过去都允许创建人取消,避免“过期草稿既不自动结束又无法取消”的死状态。 +- Published/Ongoing 取消时若锁后数据库时间已达到 `end_at`,先按自然结束处理并返回状态冲突。 +- 发布后商品、活动名称、价格、计划量、限购和时间全部不可编辑。 +- 每次已提交的活动内容修改、发布、生命周期推进、取消、抢购扣减、待支付取消回补或售后回补都必须在同一条件更新中执行 `result_version = result_version + 1`;只读时间推导不改版本。任何写入路径不得绕过该规则,A226/A227/A228 以此拒绝旧结果覆盖新快照。 +- 已结束/取消活动的剩余库存仍属于原活动,不回普通库存,也不重新开放抢购;历史订单取消/退款仍可回补该封闭库存。 + +### 6.3 DB043 `seckill_buyer_quotas` + +| 字段 | 类型 | 空 | 默认 | 说明 | +|---|---|---:|---|---| +| `id` | `uuid` | 否 | UUIDv7 | 配额行 | +| `activity_id` | `uuid` | 否 | — | 活动 | +| `buyer_id` | `uuid` | 否 | — | 买家 | +| `occupied_quantity` | `integer` | 否 | `0` | 当前未取消购买占用 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 创建时间 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 修改时间 | +| `version` | `bigint` | 否 | `1` | 并发版本 | + +- PK `pk_seckill_buyer_quotas`;FK `activity_id → seckill_activities`、`buyer_id → users`,均 `RESTRICT`。 +- Unique `ux_seckill_buyer_quotas_activity_id_buyer_id`。 +- Check `occupied_quantity >= 0`、`version >= 1`。 +- `ix_seckill_buyer_quotas_buyer_id(buyer_id)` 服务用户引用检查。 +- 上限跨表,A228 必须先锁活动行取得 `per_buyer_limit`,再条件 UPSERT `occupied_quantity + :quantity <= :locked_limit`。 +- 待支付订单首次取消时减少占用;已支付后的售后退款 **不释放** 买家限购额度。 +- 数量降到 0 后保留行,避免并发删除/重建产生 ABA 问题。 +- 每次占用增加或待支付取消释放都在同一条件 UPSERT/UPDATE 中执行 `version = version + 1`;创建行从 `version=1` 开始。 + +### 6.4 DB044 `seckill_inventory_movements` + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | 流水 ID | +| `activity_id` | `uuid` | 否 | 活动 | +| `movement_type` | `varchar(32)` | 否 | 变动类型 | +| `operation_id` | `uuid` | 否 | 稳定业务操作 | +| `order_id` | `uuid` | 是 | 订单 | +| `order_item_id` | `uuid` | 是 | 订单项 | +| `buyer_id` | `uuid` | 是 | 买家 | +| `quantity_delta` | `integer` | 否 | 活动库存增减 | +| `stock_before` | `integer` | 否 | 变动前 | +| `stock_after` | `integer` | 否 | 变动后 | +| `quota_before` | `integer` | 是 | 限购占用变动前 | +| `quota_after` | `integer` | 是 | 限购占用变动后 | +| `sold_count_before` | `integer` | 是 | 已售量变动前 | +| `sold_count_after` | `integer` | 是 | 已售量变动后 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | + +约束与索引: -### 3.2 单表详细定义 +- PK `pk_seckill_inventory_movements`;FK `activity_id → seckill_activities`、可空 `buyer_id → users` 均 `ON DELETE RESTRICT`。 +- 订单相关行使用复合 FK `(order_item_id,order_id,activity_id) → order_items(id,order_id,seckill_activity_id)` 与 `(order_id,buyer_id) → orders(id,buyer_id)`,均 `ON DELETE RESTRICT`;跨模块 FK 在初始 Migration 后段追加。 +- Unique `ux_seckill_inventory_movements_type_operation_id(movement_type, operation_id)`。 +- 类型:`catalog_allocated/order_debited/order_cancel_returned/after_sales_returned`。 +- Check `quantity_delta <> 0`、库存及所有非空配额/已售前后值均非负、`stock_after = stock_before + quantity_delta`。 +- `catalog_allocated` 的 `quantity_delta > 0`、`operation_id=activity_id`,订单/订单项/买家/配额/已售前后字段全空。 +- `catalog_allocated` 还要求 `stock_before=0 AND stock_after=quantity_delta`,使活动库存从零开始形成完整审计链。 +- `order_debited` 的 `quantity_delta < 0`;`order_cancel_returned/after_sales_returned` 的 `quantity_delta > 0`。 +- `order_debited/order_cancel_returned` 要求订单、订单项、买家、配额和已售前后值非空,`operation_id=order_item_id`,且: + - `quota_after = quota_before - quantity_delta`; + - `sold_count_after = sold_count_before - quantity_delta`。 + 因此抢购扣库存时占用/已售增加,待支付取消时二者减少。 +- `after_sales_returned` 的 `operation_id=refund_operation_id`,要求订单、订单项、买家和已售前后值非空,且 `sold_count_after = sold_count_before - quantity_delta`;它只增加活动库存并减少已售量,不修改限购,配额字段为空。 +- 订单相关引用必须从已锁定订单项、订单和买家事实派生,不接受调用方自由组合。 +- `ix_seckill_inventory_movements_activity_created_at(activity_id, created_at, id)`。 +- `ix_seckill_inventory_movements_order_item_id(order_item_id) WHERE order_item_id IS NOT NULL`。 -```markdown -#### DBxxx 中文表名(real_table_name) +发布活动时,DB026 `seckill_allocated` 负流水和本表 `catalog_allocated` 正流水使用同一 `operation_id=activity_id`,与活动划拨在一个事务提交。 -- 负责人: -- 所属模块: -- 表用途: -- 关联需求: -- 关联接口: -- 当前状态:待评审 +### 6.5 DB061 `orders` -| 字段名 | PostgreSQL 类型 | 允许空 | 默认值 | 说明与校验 | +| 字段 | 类型 | 空 | 默认 | 说明 | |---|---|---:|---|---| +| `id` | `uuid` | 否 | UUIDv7 | 订单 ID/对外订单号 | +| `buyer_id` | `uuid` | 否 | — | 买家 | +| `buyer_display_name_snapshot` | `varchar(50)` | 否 | — | 商家列表安全展示名 | +| `assigned_merchant_user_id` | `uuid` | 否 | — | 唯一订单责任商家 | +| `order_type` | `varchar(16)` | 否 | `'normal'` | `normal/seckill` | +| `seckill_activity_id` | `uuid` | 是 | — | 秒杀来源 | +| `seckill_activity_name_snapshot` | `varchar(50)` | 是 | — | 活动名快照 | +| `source_address_id` | `uuid` | 是 | — | 下单地址追踪弱引用,不建 FK | +| `receiver_name_snapshot` | `varchar(50)` | 否 | — | 收件人快照 | +| `receiver_phone_snapshot` | `varchar(11)` | 否 | — | 收货电话快照 | +| `province_snapshot` | `varchar(100)` | 否 | — | 省快照 | +| `city_snapshot` | `varchar(100)` | 否 | — | 市快照 | +| `district_snapshot` | `varchar(100)` | 否 | — | 区县快照 | +| `detail_address_snapshot` | `varchar(120)` | 否 | — | 详细地址快照 | +| `total_amount` | `numeric(18,2)` | 否 | — | 服务端重算总额 | +| `currency` | `char(3)` | 否 | `'CNY'` | 币种 | +| `status` | `varchar(32)` | 否 | `'pending_payment'` | 五态 | +| `payment_deadline` | `timestamptz` | 否 | — | 创建时固化 | +| `paid_at` | `timestamptz` | 是 | — | 支付提交时间 | +| `payment_posting_sequence` | `bigint` | 是 | — | C08 一致水位 | +| `shipped_at` | `timestamptz` | 是 | — | 发货时间 | +| `shipment_note` | `varchar(200)` | 是 | — | 发货说明 | +| `shipment_request_hash` | `char(64)` | 是 | — | 新 Key 相同内容重放判断 | +| `auto_complete_at` | `timestamptz` | 是 | — | 发货时固化 | +| `completed_at` | `timestamptz` | 是 | — | 完成时间 | +| `completed_by` | `varchar(24)` | 是 | — | `buyer_confirmed/auto_completed` | +| `cancelled_at` | `timestamptz` | 是 | — | 取消时间 | +| `cancel_reason` | `varchar(24)` | 是 | — | `buyer_requested/payment_expired` | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 创建时间 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 更新时间 | +| `version` | `bigint` | 否 | `1` | 状态并发版本 | 约束: -| 约束名 | 类型 | 字段 | 说明 | -|---|---|---|---| +- PK `pk_orders`;FK `buyer_id`、`assigned_merchant_user_id → users.id ON DELETE RESTRICT`。 +- Unique `ux_orders_id_buyer_id(id,buyer_id)` 供买家归属复合 FK;Unique `ux_orders_payment_identity(id,buyer_id,assigned_merchant_user_id,total_amount,currency)` 供支付精确绑定。 +- Unique `ux_orders_id_order_type(id,order_type)` 供售后冻结订单类型。 +- Unique `ux_orders_id_completed_at(id,completed_at)` 供 Completed 售后资格绑定真实完成时间;PostgreSQL 对空值不冲突,不影响未完成订单。 +- 秒杀活动 FK `seckill_activity_id → seckill_activities.id ON DELETE RESTRICT`;普通订单活动字段均空,秒杀订单活动 ID/名称均非空。 +- Check `total_amount > 0`、`currency='CNY'`、`payment_deadline > created_at`、状态白名单、`version >= 1`。 +- 状态字段组合: + - `pending_payment`:`paid_at/payment_posting_sequence`、发货、完成、取消字段全空; + - `paid`:`paid_at/payment_posting_sequence` 非空,发货、完成、取消字段全空; + - `shipped`:支付字段、`shipped_at/auto_complete_at/shipment_request_hash` 非空,完成、取消字段为空,`shipment_note` 可空; + - `completed`:支付字段、发货三个必需字段、`completed_at/completed_by` 非空,取消字段为空,`shipment_note` 可空; + - `cancelled`:`cancelled_at/cancel_reason` 非空,支付、发货、完成字段全空。 +- `shipment_request_hash` 非空时必须为 64 位小写十六进制;地址快照满足与 DB003 相同的字段长度、手机号和非空规则。 +- `auto_complete_at > shipped_at`;正式配置为发货后 7 天,演示参数只影响新发货订单。 +- 时间顺序 Check: + - 非空 `paid_at` 必须满足 `created_at <= paid_at AND paid_at < payment_deadline`; + - 非空 `shipped_at` 必须 `shipped_at >= paid_at`,非空 `completed_at` 必须 `completed_at >= shipped_at`; + - `completed_by='auto_completed'` 时还必须 `completed_at >= auto_complete_at`; + - Cancelled 必须 `cancelled_at >= created_at`;`buyer_requested` 要求 `cancelled_at < payment_deadline`,`payment_expired` 要求 `cancelled_at >= payment_deadline`。 + +索引: + +| 索引 | 字段 | 用途 | +|---|---|---| +| `ix_orders_buyer_id_created_at` | `buyer_id, created_at DESC, id DESC` | A302 | +| `ix_orders_buyer_id_status_created_at` | `buyer_id, status, created_at DESC, id DESC` | 买家状态筛选 | +| `ix_orders_merchant_id_created_at` | `assigned_merchant_user_id, created_at DESC, id DESC` | A305 | +| `ix_orders_merchant_id_status_created_at` | `assigned_merchant_user_id, status, created_at DESC, id DESC` | 商家状态筛选 | +| `ix_orders_payment_deadline_pending` | `payment_deadline, id WHERE status='pending_payment'` | C03 | +| `ix_orders_auto_complete_at_shipped` | `auto_complete_at, id WHERE status='shipped'` | 自动完成 | +| `ix_orders_seckill_activity_status` | `seckill_activity_id, status, created_at WHERE seckill_activity_id IS NOT NULL` | A225 统计 | +| `ux_orders_payment_posting_sequence` | `UNIQUE payment_posting_sequence WHERE payment_posting_sequence IS NOT NULL` | C08 水位唯一 | + +订单状态严格线性且没有回退或循环,A303 `statusHistory` 由 `created_at/paid_at/shipped_at/completed_at/cancelled_at` 这些唯一真实时间点确定生成,不重复建设订单状态历史表。来源事务 Outbox 保存对外事件证据。 + +### 6.6 DB062 `order_items` + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | 订单项 ID | +| `order_id` | `uuid` | 否 | 所属订单 | +| `product_id` | `uuid` | 否 | 历史商品引用 | +| `product_name_snapshot` | `varchar(100)` | 否 | 名称快照 | +| `product_image_object_key_snapshot` | `varchar(500)` | 否 | 主图 Key 快照 | +| `unit_price` | `numeric(18,2)` | 否 | 实际成交单价 | +| `original_unit_price` | `numeric(18,2)` | 否 | 普通价/秒杀原价 | +| `quantity` | `integer` | 否 | 购买数量 | +| `subtotal` | `numeric(18,2)` | 否 | 单项小计 | +| `inventory_source` | `varchar(16)` | 否 | `catalog/seckill` | +| `seckill_activity_id` | `uuid` | 是 | 秒杀原库存通道 | +| `shipped_quantity` | `integer` | 否 | 实际已发货数量,默认 `0` | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | + +约束与索引: + +- PK `pk_order_items`;FK `order_id → orders`、`product_id → products`、可空 `seckill_activity_id → seckill_activities`,均 `RESTRICT`。 +- Unique `ux_order_items_order_id_product_id(order_id, product_id)`。 +- Unique `ux_order_items_id_order_id(id,order_id)` 供上传资格绑定。 +- Unique `ux_order_items_identity(id,order_id,product_id)`、`ux_order_items_seckill_identity(id,order_id,seckill_activity_id)` 供库存流水复合归属。 +- Unique `ux_order_items_after_sales_snapshot(id,order_id,product_id,unit_price,quantity,inventory_source)`,供售后不可改写成交单价、购买量和库存来源。 +- 可空复合 FK `(seckill_activity_id,product_id) → seckill_activities(id,product_id)`,保证秒杀订单项商品就是活动商品。 +- Check: + - `unit_price >= 0`、`original_unit_price >= 0`; + - `quantity > 0`; + - `subtotal = unit_price * quantity`; + - `0 <= shipped_quantity <= quantity`; + - Catalog 来源活动 ID 为空且 `original_unit_price=unit_price`;Seckill 来源活动 ID 非空,原价使用发布快照且不要求高于秒杀价。 +- `ix_order_items_order_id(order_id, id)`。 +- `ix_order_items_product_id(product_id)`。 +- `ix_order_items_seckill_activity_id(seckill_activity_id) WHERE seckill_activity_id IS NOT NULL`。 + +库存回补始终读取 `inventory_source + seckill_activity_id + quantity` 快照。售后处理中/已退款数量由 AfterSales 提供,不在订单项冗余维护;A307 有任何非终态售后时整单阻断,否则 `shipped_quantity = quantity - refunded_quantity`。 + +订单头与明细属于跨行不变量:Normal 订单全部明细必须为 `catalog` 且活动为空;Seckill 订单必须恰有一条明细,明细为 `seckill`,并与订单头保存同一 `seckill_activity_id`。Ordering 在同一创建事务中生成全部明细并在提交前校验,数据库集成测试必须构造非法组合证明不能经公开写入能力落库;该规则不能伪装成单行 Check。 + +### 6.7 DB063 `order_lifecycle_tasks` + +用途:记录 C03 超时取消与发货后自动完成的尝试、退避和最终结果;订单行仍是唯一业务状态。 + +| 字段 | 类型 | 空 | 默认 | 说明 | +|---|---|---:|---|---| +| `id` | `uuid` | 否 | UUIDv7 | 任务 ID | +| `order_id` | `uuid` | 否 | — | 订单 | +| `task_type` | `varchar(32)` | 否 | — | `payment_expiration/auto_completion` | +| `due_at` | `timestamptz` | 否 | — | 固化到期时间 | +| `execution_status` | `varchar(16)` | 否 | `'pending_retry'` | `pending_retry/finalized` | +| `attempt_count` | `integer` | 否 | `0` | 尝试数 | +| `next_attempt_at` | `timestamptz` | 是 | — | 下一次处理;终态为空 | +| `last_attempt_at` | `timestamptz` | 是 | — | 最近尝试 | +| `last_outcome` | `varchar(64)` | 是 | — | 运维结果 | +| `last_error_code` | `varchar(100)` | 是 | — | 安全错误码 | +| `final_outcome` | `varchar(48)` | 是 | — | 最终结果 | +| `lease_owner` | `varchar(100)` | 是 | — | Worker 实例 | +| `lease_token` | `uuid` | 是 | — | 领取令牌 | +| `lease_expires_at` | `timestamptz` | 是 | — | 租约过期 | +| `trace_id` | `varchar(64)` | 是 | — | 最近链路 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 创建时间 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 更新时间 | + +约束与索引: + +- PK `pk_order_lifecycle_tasks`;FK `order_id → orders.id ON DELETE RESTRICT`。 +- Unique `ux_order_lifecycle_tasks_order_id_task_type(order_id, task_type)`。 +- Check `attempt_count >= 0`、状态和任务类型白名单;`attempt_count=0` 时 `last_attempt_at/last_outcome/last_error_code` 为空,正数时最近尝试时间非空。 +- `last_outcome` 只允许 `cancelled/completed/already_resolved_by_competitor/retryable_failure`。 +- `pending_retry`:`next_attempt_at` 非空、`final_outcome` 为空,三个 lease 字段全空或全非空。 +- `finalized`:`next_attempt_at`、错误和三个 lease 字段全空,`final_outcome` 非空: + - `payment_expiration` 只允许 `cancelled/already_resolved_by_competitor`; + - `auto_completion` 只允许 `completed/already_resolved_by_competitor`。 +- `ix_order_lifecycle_tasks_status_next_attempt(execution_status, next_attempt_at, lease_expires_at, id)`。 + +跨表不变量:创建支付过期任务时 `due_at=orders.payment_deadline`,创建自动完成任务时 `due_at=orders.auto_complete_at`,两类任务首次 `next_attempt_at=due_at`。创建订单/发货与对应任务分别同事务提交;该相等关系不能伪装成普通 Check,必须由集成测试验证。 + +Worker 同时扫描订单部分索引,以权威订单时间为准 `INSERT ... ON CONFLICT DO UPDATE` 修复缺失或错误调度时间;执行时再次锁订单并按订单字段重检,绝不能仅凭任务 `due_at` 提前改变业务状态。租约和 `SKIP LOCKED` 只减少重复工作,最终正确性仍由订单状态条件和库存流水唯一约束保证。 + +### 6.8 核心事务与锁顺序 + +#### A301 普通下单 + +1. 取得 DB104 幂等范围锁并重查确定结果。 +2. 通过 Identity 锁定唯一启用默认商家责任门槛,读取买家安全展示名与地址快照。 +3. 按 ID 稳定顺序锁定请求中的本人、已选购物车条目。 +4. 按商品 ID 稳定顺序锁定并重检 Catalog 普通库存;锁定全部资格事实后取得一次 `order_created_at=clock_timestamp()`,显式写订单 `created_at` 并由它计算固化 `payment_deadline`,再预生成订单/订单项 ID,写 DB061/DB062。 +5. 按相同顺序条件扣减普通库存并写引用已存在订单项的 DB026,再写 DB063 支付过期任务、OrderCreated 事件与 C07 Immediate 失效事件。 +6. 精确删除本次购物车条目并核对行数。 +7. 写 DB104 成功结果,一次提交。 + +任一步失败整体回滚。库存不足、商品不可售、默认商家确定缺失等确定失败使用 Savepoint 回滚业务副作用后保存 DB104;连接中断和提交未知不保存结果。 + +#### A222 秒杀发布 + +1. 锁 DB104 幂等范围、商家账号责任门槛、活动行和商品行。 +2. 重检 Draft、商品 OnSale、计划量、当前普通库存与时间。 +3. Catalog 条件扣减普通库存,写 DB026; +4. 固化原价,令 `allocated_quantity=planned_quantity`、`remaining_stock=allocated_quantity`、`sold_count=0`,写发布时间和 `published`;DB044 首条 `catalog_allocated` 的数量和前后库存与这些值完全一致; +5. 为普通库存变化写 DB102 C07 Immediate 失效事件; +6. 完成 DB104 后提交。发布后不再编辑。 + +#### A228 秒杀下单 + +1. 在应用准入限流前先识别格式有效的幂等键;边缘/Nginx 429 是瞬态结果,不承诺数据库重放,只有进入应用幂等范围后的确定 429 才可保存。 +2. 取得 DB104 幂等锁、默认商家责任门槛、活动行和买家配额行。 +3. 锁后取得 `decision_time`,按时间推导最新有效状态;只有 Ongoing 且 `start_at <= decision_time < end_at` 可继续。 +4. 条件扣减活动库存、增加占用;以锁后 `decision_time` 同时作为显式 `order_created_at` 并计算支付截止,预生成订单/订单项 ID,由 Ordering 写共享订单、订单项、支付过期任务和 Outbox;A228 不读购物车。 +5. 写引用已存在订单项的 DB044 抢购流水。 +6. 写 DB104 确定结果后提交。 + +#### 取消与原路回补 + +1. 锁订单,使用 `WHERE status='pending_payment'` 竞争唯一取消。 +2. 按订单项 ID 稳定顺序处理: + - Catalog:插入 DB026 唯一回补流水并增加普通库存; + - Seckill:插入 DB044 唯一回补流水,增加原活动库存、减少 `sold_count`;待支付取消再减少 DB043 占用。 +3. 写订单取消字段、完成 DB063 任务、写 OrderCancelled Outbox;Catalog 回补同时写 C07 Immediate 失效事件。 +4. 同一事务提交;任一项失败整单回滚。 + +#### 发货与售后竞争 + +锁顺序固定为: + +```text +orders +→ AfterSales 目标事实 +→ order_items +→ 本模块结果 / Outbox / Idempotency +``` + +A307 与 A412 都先通过 Ordering 公开能力锁同一订单行并保持至提交。售后先提交时非终态申请阻断整单发货;发货先提交时 A412 按最新已发货规则重算资格。 + +A307 在锁内按订单项重算已退款数量,要求不存在任何非终态售后且 `SUM(order_item.quantity - refunded_quantity) > 0`;全部数量已退款时必须拒绝,不能把零数量订单推进为 Shipped。成功发货时每项 `shipped_quantity` 精确写为 `quantity - refunded_quantity`,所有订单项与订单 `paid → shipped`、任务、Outbox 和 DB104 在同一事务提交。 + +## 七、Payment 与 AfterSales + +### 7.1 DB081 `wallet_accounts` + +| 字段 | 类型 | 空 | 默认 | 说明 | +|---|---|---:|---|---| +| `id` | `uuid` | 否 | UUIDv7 | 钱包 ID | +| `buyer_id` | `uuid` | 否 | — | 买家 | +| `currency` | `char(3)` | 否 | `'CNY'` | 币种 | +| `balance_amount` | `numeric(18,2)` | 否 | `0` | 当前余额 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 创建时间 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 最近余额变更时间 | +| `version` | `bigint` | 否 | `0` | 已提交资金分录序号;0 为未入账初态 | + +约束与索引: + +- PK `pk_wallet_accounts`;FK `buyer_id → users.id ON DELETE RESTRICT`。 +- Unique `ux_wallet_accounts_buyer_id(buyer_id)`。 +- 额外 Unique `ux_wallet_accounts_id_buyer_id(id,buyer_id)`,供流水复合归属外键使用。 +- Check `currency='CNY'`、`balance_amount >= 0`、`version >= 0`,且 `version=0` 时 `balance_amount=0`。 + +A401 没有钱包行时返回逻辑零余额且不写库。首次入账可先在同一事务 UPSERT `balance=0,version=0` 的初态,再把首笔贷记提交为 `account_version=1`;不存在钱包的扣款失败必须回滚初态行,不留下空钱包。扣款使用 `WHERE balance_amount >= :amount AND version=:version` 条件更新;余额与不可变流水必须同事务形成。 + +### 7.2 DB082 `wallet_topups` + +每行只代表已成功到账充值,不建设 Pending/Failed 充值状态。 + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | `topupId` | +| `wallet_account_id` | `uuid` | 否 | 钱包 | +| `buyer_id` | `uuid` | 否 | 买家 | +| `amount` | `numeric(18,2)` | 否 | 充值额 | +| `currency` | `char(3)` | 否 | `CNY` | +| `balance_after_amount` | `numeric(18,2)` | 否 | 到账后余额 | +| `posting_sequence` | `bigint` | 否 | 财务提交水位 | +| `credited_at` | `timestamptz` | 否 | 到账时间 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | + +- PK `pk_wallet_topups`。 +- 复合 FK `(wallet_account_id,buyer_id) → wallet_accounts(id,buyer_id) ON DELETE RESTRICT`。 +- Unique `ux_wallet_topups_source_identity(id,wallet_account_id,buyer_id,amount,currency,balance_after_amount,posting_sequence)` 供资金流水复合来源 FK。 +- Check `amount > 0 AND amount <= 10000.00`、`balance_after_amount >= 0`、`currency='CNY'`、`posting_sequence > 0`。 +- `ix_wallet_topups_buyer_id_credited_at(buyer_id, credited_at DESC, id DESC)` 服务 A403。 +- Unique `ux_wallet_topups_posting_sequence(posting_sequence)`。 +- A402 的钱包增加、DB082、DB083、DB104 与同一 posting sequence 在一个事务提交。 + +### 7.3 DB083 `wallet_transactions` + +不可变资金账本,只追加、不更新、不删除。 + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | 流水 ID | +| `wallet_account_id` | `uuid` | 否 | 钱包 | +| `buyer_id` | `uuid` | 否 | 买家 | +| `account_version` | `bigint` | 否 | 入账后钱包版本 | +| `entry_type` | `varchar(32)` | 否 | 三种分录 | +| `amount` | `numeric(18,2)` | 否 | 始终为正 | +| `balance_before_amount` | `numeric(18,2)` | 否 | 变动前 | +| `balance_after_amount` | `numeric(18,2)` | 否 | 变动后 | +| `currency` | `char(3)` | 否 | `CNY` | +| `topup_id` | `uuid` | 是 | 充值来源 | +| `payment_id` | `uuid` | 是 | 支付来源 | +| `refund_operation_id` | `uuid` | 是 | 退款来源 | +| `posting_sequence` | `bigint` | 否 | 财务水位 | +| `trace_id` | `varchar(64)` | 是 | 链路 | +| `created_at` | `timestamptz` | 否 | 资金变动时间 | + +约束: + +- PK `pk_wallet_transactions`。 +- 复合 FK 钱包归属;来源按分录类型使用以下复合 FK,均 `RESTRICT`,初始 Migration 在目标表创建后追加: + - `(topup_id,wallet_account_id,buyer_id,amount,currency,balance_after_amount,posting_sequence) → wallet_topups(id,wallet_account_id,buyer_id,amount,currency,balance_after_amount,posting_sequence)`; + - `(payment_id,wallet_account_id,buyer_id,amount,currency,posting_sequence) → payments(id,wallet_account_id,buyer_id,amount,currency,posting_sequence)`; + - `(refund_operation_id,buyer_id,amount,currency,posting_sequence) → refund_operations(id,buyer_id,amount,currency,posting_sequence)`。 +- Unique `ux_wallet_transactions_account_version(wallet_account_id,account_version)`。 +- 三个来源字段严格一选一,并与: + - `topup_credit`; + - `order_payment_debit`; + - `after_sales_refund_credit` + 对应。 +- 三个来源各建非空部分唯一索引,保证每个充值、支付、退款最多一条资金流水。 +- Check `account_version >= 1`、`amount > 0`、余额非负、币种和 posting sequence;贷记 `after=before+amount`,借记 `after=before-amount`。 +- 每次资金变更先锁钱包,流水固定使用 `account_version = wallet_accounts.version + 1`,并把钱包更新为同一版本;首笔实际分录必须是 `balance_before_amount=0, account_version=1`,版本 0 永远没有流水。 索引: -| 索引名 | 字段与顺序 | 是否唯一 | 服务的接口或查询 | +- `ix_wallet_transactions_buyer_id_created_at(buyer_id, created_at DESC, id DESC)`。 +- Unique `ux_wallet_transactions_posting_sequence(posting_sequence)`。 + +### 7.4 DB084 `payment_channel_attempts` + +用途:一个模拟通道 `paymentSerialNumber` 的不可变绑定;允许同一流水收到多个乱序 callbackId。 + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | 通道尝试 ID | +| `payment_serial_number` | `varchar(100)` | 否 | 模拟通道流水号 | +| `bound_order_id` | `uuid` | 否 | 首次绑定订单;故意不建 FK | +| `bound_amount` | `numeric(18,2)` | 否 | 首次绑定金额 | +| `bound_currency` | `char(3)` | 否 | 首次绑定币种 | +| `first_success_callback_id` | `uuid` | 是 | 首个 Success 信号弱引用 | +| `last_callback_id` | `uuid` | 是 | 最近回调弱引用 | +| `first_seen_at` | `timestamptz` | 否 | 首次看到 | +| `last_processed_at` | `timestamptz` | 否 | 最近处理 | +| `version` | `bigint` | 否 | 聚合版本,初始 `1` | + +- PK `pk_payment_channel_attempts`。 +- Unique `ux_payment_channel_attempts_serial(payment_serial_number)`。 +- Unique `ux_payment_channel_attempts_binding(id,bound_order_id,bound_amount,bound_currency)` 供成功支付复合绑定。 +- Check 金额正数、币种 `CNY`、`version >= 1`、时间顺序。 +- `ix_payment_channel_attempts_bound_order_id(bound_order_id,last_processed_at,id)` 服务按订单聚合回调与对账;该索引不把不存在订单误认为合法引用。 +- `bound_order_id` 不建硬 FK,因为“流水绑定不存在订单”正是必须保存的差异证据。 +- A421 按流水号锁定本行;后续回调改变订单/金额/币种时不覆盖绑定,而把回调裁决为 Difference。 + +### 7.5 DB085 `payments` + +每行只代表已确认成功支付;不保存 Pending、Failed 或伪成功状态。 + +| 字段 | 类型 | 空 | 说明 | |---|---|---:|---| +| `id` | `uuid` | 否 | 支付 ID | +| `order_id` | `uuid` | 否 | 订单 | +| `buyer_id` | `uuid` | 否 | 买家 | +| `assigned_merchant_user_id` | `uuid` | 否 | 订单责任商家快照 | +| `source` | `varchar(32)` | 否 | `wallet/simulated_channel` | +| `amount` | `numeric(18,2)` | 否 | 实付金额 | +| `currency` | `char(3)` | 否 | `CNY` | +| `wallet_account_id` | `uuid` | 是 | Wallet 来源 | +| `channel_attempt_id` | `uuid` | 是 | 模拟回调来源 | +| `refunded_amount` | `numeric(18,2)` | 否 | 累计成功退款,默认 `0` | +| `posting_sequence` | `bigint` | 否 | 支付原子结果水位 | +| `paid_at` | `timestamptz` | 否 | 支付提交时间 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | -关系与删除行为: +约束与索引: -状态值与转换: +- PK `pk_payments`。 +- 复合 FK `(order_id,buyer_id,assigned_merchant_user_id,amount,currency) → orders(id,buyer_id,assigned_merchant_user_id,total_amount,currency)`,保证支付归属与应付金额不能拼接。 +- Wallet 来源复合 FK `(wallet_account_id,buyer_id) → wallet_accounts(id,buyer_id)`;SimulatedChannel 来源复合 FK `(channel_attempt_id,order_id,amount,currency) → payment_channel_attempts(id,bound_order_id,bound_amount,bound_currency)`,均 `RESTRICT`。 +- Unique `ux_payments_order_id(order_id)`,数据库层保证同步钱包与模拟回调互斥成功。 +- `ux_payments_channel_attempt_id(channel_attempt_id) WHERE channel_attempt_id IS NOT NULL`。 +- Unique `ux_payments_id_order_id(id,order_id)` 供既有成功来源绑定。 +- Unique `ux_payments_after_sales_identity(id,order_id,buyer_id,assigned_merchant_user_id)`、`ux_payments_wallet_source_identity(id,wallet_account_id,buyer_id,amount,currency,posting_sequence)`、`ux_payments_callback_identity(id,order_id,channel_attempt_id,amount,currency)`,供售后、账本与成功回调复合 FK。 +- 来源组合 Check:Wallet 只允许钱包非空;SimulatedChannel 只允许通道尝试非空。 +- Check `amount > 0`、`0 <= refunded_amount <= amount`、`currency='CNY'`、`posting_sequence > 0`。 +- `ix_payments_buyer_id_created_at(buyer_id, created_at DESC, id DESC)` 服务 A407。 +- Unique `ux_payments_posting_sequence(posting_sequence)` 服务 C08 并阻止同表复用水位。 -并发、幂等与事务: +A405 原子结果:钱包条件扣减、DB083、DB085、订单 `pending_payment → paid`、订单/支付相同 posting sequence、DB102、DB104 一次提交。A421 成功回调相同,但绝不修改钱包。 -敏感数据与脱敏: +### 7.6 DB086 `after_sales_requests` -验证场景: +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | 售后申请 ID | +| `order_id` | `uuid` | 否 | 订单 | +| `order_item_id` | `uuid` | 否 | 订单项 | +| `buyer_id` | `uuid` | 否 | 买家 | +| `assigned_merchant_user_id` | `uuid` | 否 | 责任商家 | +| `original_payment_id` | `uuid` | 否 | 原成功支付 | +| `product_id` | `uuid` | 否 | 商品引用 | +| `product_name_snapshot` | `varchar(100)` | 否 | 商品名快照 | +| `product_image_object_key_snapshot` | `varchar(500)` | 否 | 主图快照 | +| `unit_price_snapshot` | `numeric(18,2)` | 否 | 实付单价 | +| `purchased_quantity_snapshot` | `integer` | 否 | 原购买量 | +| `order_type_snapshot` | `varchar(16)` | 否 | `normal/seckill` | +| `seckill_activity_id` | `uuid` | 是 | 秒杀来源 | +| `inventory_source` | `varchar(16)` | 否 | `catalog/seckill` | +| `order_status_at_request` | `varchar(32)` | 否 | 提交时订单状态 | +| `order_completed_at` | `timestamptz` | 是 | 完成时间快照 | +| `eligibility_deadline` | `timestamptz` | 是 | 完成订单的售后截止 | +| `request_type` | `varchar(32)` | 否 | `refund_only/return_and_refund` | +| `quantity` | `integer` | 否 | 本次申请量 | +| `calculated_amount` | `numeric(18,2)` | 否 | 服务端计算金额 | +| `currency` | `char(3)` | 否 | `CNY` | +| `reason` | `varchar(32)` | 否 | `damaged/quality_issue/wrong_item/not_as_described/other` | +| `reason_note` | `varchar(500)` | 是 | 补充说明 | +| `stock_return_policy` | `varchar(32)` | 否 | 成功退款是否及向何处回补 | +| `status` | `varchar(32)` | 否 | 八态,默认 `pending_review` | +| `audited_by_user_id` | `uuid` | 是 | 审核商家 | +| `audit_decision` | `varchar(16)` | 是 | `approve/reject` | +| `audit_note` | `varchar(500)` | 是 | 审核意见 | +| `audited_at` | `timestamptz` | 是 | 审核时间 | +| `cancel_reason` | `varchar(500)` | 是 | 买家撤销原因 | +| `cancelled_at` | `timestamptz` | 是 | 撤销时间 | +| `receipt_confirmed_by_user_id` | `uuid` | 是 | 确认收货商家 | +| `receipt_note` | `varchar(500)` | 是 | 收货说明 | +| `receipt_confirmed_at` | `timestamptz` | 是 | 确认时间 | +| `refund_posting_sequence` | `bigint` | 是 | 完整退款成功水位 | +| `closed_at` | `timestamptz` | 是 | 终态时间 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | +| `version` | `bigint` | 否 | 并发版本,初始 `1` | + +状态: + +```text +pending_review +→ cancelled / rejected / refunding / pending_return +pending_return → pending_receipt +pending_receipt → refunding +refunding → refunded / refund_failed +refund_failed → refunding ``` -只有表名或字段列表不算完整定义。字段类型、空值、默认值、主外键、唯一/Check 约束、索引用途、删除行为和状态转换均必须明确。 +非终态:`pending_review/pending_return/pending_receipt/refunding/refund_failed`。终态:`cancelled/rejected/refunded`。 -## 四、主文档汇总 +库存回补策略在申请创建时按锁定的订单事实冻结: -### 4.1 统一表清单 +- Paid 且尚未发货的 RefundOnly:按原来源 `return_to_catalog/return_to_seckill`; +- Shipped/Completed 的 RefundOnly:`none`; +- ReturnAndRefund:商家确认收到整笔退货并退款成功时按原来源回补; +- 任一策略都不能由客户端传入或在审核后改写。 -| DBxxx | 真实表名 | 所属模块 | 负责人 | 表用途 | 关联需求/接口 | 状态 | -|---|---|---|---|---|---|---| -| 待汇总 | 待汇总 | 待汇总 | 待汇总 | 六份个人原稿尚未完成 | 待汇总 | 模板/占位 | +约束: -### 4.2 跨模块关系 +- PK `pk_after_sales_requests`;复合 FK: + - `(order_id,buyer_id) → orders(id,buyer_id)`; + - `(order_item_id,order_id,product_id,unit_price_snapshot,purchased_quantity_snapshot,inventory_source) → order_items(id,order_id,product_id,unit_price,quantity,inventory_source)`; + - `(order_id,order_type_snapshot) → orders(id,order_type)`; + - 可空 `(order_id,order_completed_at) → orders(id,completed_at)`; + - `(original_payment_id,order_id,buyer_id,assigned_merchant_user_id) → payments(id,order_id,buyer_id,assigned_merchant_user_id)`; + - 可空 `(seckill_activity_id,product_id) → seckill_activities(id,product_id)`。 + 所有删除行为均 `RESTRICT`,合法但彼此不归属的订单、订单项、买家、商家、支付、商品和活动 ID 不能拼接成申请。 +- Unique `ux_after_sales_requests_id_buyer_id(id,buyer_id)` 供退货物流复合归属;Unique `ux_after_sales_requests_refund_identity(id,original_payment_id,buyer_id,calculated_amount,currency)` 供退款金额绑定。 +- Check: + - `quantity > 0 AND quantity <= purchased_quantity_snapshot`; + - `calculated_amount = unit_price_snapshot * quantity`; + - 金额、币种、订单类型、库存来源、申请类型、原因、状态白名单; + - `reason='other'` 时 `reason_note` 去空白后非空,其他原因可空; + - Normal/Catalog 时活动为空;Seckill 时活动非空; + - `stock_return_policy` 为 `none/return_to_catalog/return_to_seckill` 且与快照一致; + - `order_status_at_request='paid'` 时只允许 `refund_only`,完成快照/截止均空; + - `order_status_at_request='shipped'` 时允许两类申请,完成快照/截止均空; + - `order_status_at_request='completed'` 时两类均可,`order_completed_at/eligibility_deadline` 非空,`eligibility_deadline = order_completed_at + interval '7 days'` 且 `created_at < eligibility_deadline`。 -跨模块关系由双方评审,不能由引用方单方面决定对方表结构: +逐状态字段矩阵: -| 引用方 | 事实所有者 | 引用内容 | 引用方式 | 快照要求 | 状态 | +| 状态 | 审核字段 | 撤销字段 | 退货/收货字段 | 退款水位 | `closed_at` | |---|---|---|---|---|---| -| Cart、Engagement、Review | Identity、Catalog | 用户 ID、商品 ID | 稳定 ID | 无 | 待确认 | -| Ordering | Identity、Catalog | 买家、商品、地址、成交信息 | 稳定 ID + 订单快照 | 必须 | 待确认 | -| Payment、AfterSales | Ordering | 订单、订单项、实付和状态 | 应用契约 + 稳定 ID | 按金额事实确认 | 待确认 | -| Messaging | Ordering、Payment、AfterSales | 接收人和业务目标 | 集成事件 + 稳定 ID | 消息展示摘要 | 待确认 | +| `pending_review` | 全空 | 全空 | 全空 | 空 | 空 | +| `cancelled` | 全空 | `cancelled_at` 必填,原因可空 | 全空 | 空 | 必填且等于 `cancelled_at` | +| `rejected` | 审核人、`reject`、审核时间必填,意见可空 | 全空 | 全空 | 空 | 必填且等于 `audited_at` | +| `pending_return` | 审核人、`approve`、审核时间必填 | 全空 | DB092 尚不存在,收货字段全空 | 空 | 空 | +| `pending_receipt` | 同意审核字段保留 | 全空 | 必须存在一条 DB092,收货字段全空 | 空 | 空 | +| `refunding` | 同意审核字段保留 | 全空 | `refund_only` 全空;`return_and_refund` 必须已有 DB092,确认收货人/时间必填、说明可空 | 空 | 空 | +| `refund_failed` | 与进入退款时一致 | 全空 | 与 `refunding` 一致 | 空 | 空 | +| `refunded` | 与进入退款时一致 | 全空 | 与 `refunding` 一致 | 必填 | 必填 | + +- `audited_by_user_id`、`receipt_confirmed_by_user_id` 非空时必须等于 `assigned_merchant_user_id`。 +- `pending_return/pending_receipt` 只允许 `return_and_refund`;`refunding/refund_failed/refunded` 必须存在同一申请唯一 DB088。DB092/DB088 的“必须存在”属于共享事务跨表不变量,不能伪装成单行 Check,必须用同事务写入与数据库集成测试证明。 + +索引: + +| 索引 | 字段 | 用途 | +|---|---|---| +| `ix_after_sales_requests_buyer_status_created_at` | `buyer_id, status, created_at DESC, id DESC` | A413 买家 | +| `ix_after_sales_requests_merchant_status_created_at` | `assigned_merchant_user_id, status, created_at DESC, id DESC` | A413 商家 | +| `ix_after_sales_requests_order_item_status` | `order_item_id, status` | 数量汇总 | +| `ix_after_sales_requests_order_non_terminal` | `order_id, order_item_id WHERE status IN ('pending_review','pending_return','pending_receipt','refunding','refund_failed')` | A307 发货阻断 | +| `ix_after_sales_requests_refund_recovery` | `updated_at, id WHERE status IN ('refunding','refund_failed')` | 退款恢复 | +| `ix_after_sales_requests_original_payment_id` | `original_payment_id` | 金额上限与对账 | +| `ux_after_sales_requests_refund_posting_sequence` | `UNIQUE refund_posting_sequence WHERE refund_posting_sequence IS NOT NULL` | C08 水位唯一 | +| `ix_after_sales_requests_product_id` | `product_id` | 商品删除保护 | +| `ix_after_sales_requests_seckill_activity_id` | `seckill_activity_id WHERE seckill_activity_id IS NOT NULL` | 活动删除保护 | + +部分申请并发: + +- 处理中数量 = 五个非终态状态的 `SUM(quantity)`; +- 已退款数量 = `status='refunded'` 的 `SUM(quantity)`; +- 剩余 = 购买量 - 处理中 - 已退款。 + +这是一项跨行聚合,不能伪装成 Check。A412、A415、A416、A417、A419、退款最终提交与 A307 全部先锁同一 `orders` 行,再基于上述索引重算和写入,保证同一订单的数量与履约串行。由订单行承担统一锁后,不再增加可能与申请明细漂移的冗余“售后余额表”。 + +A412 必须在取得订单锁后调用一次 `decision_time=clock_timestamp()`,用它判断 Completed 的 7 天资格并显式写 `after_sales_requests.created_at=decision_time`;不得使用事务开始时的 `CURRENT_TIMESTAMP` 默认值越过截止点。该截止只限制新申请提交,后续审核、寄回、确认收货和退款不再受此时间阻断。 + +### 7.7 DB087 `after_sales_status_histories` + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | 历史 ID | +| `after_sales_request_id` | `uuid` | 否 | 申请 | +| `sequence` | `integer` | 否 | 申请内递增序号 | +| `from_status` | `varchar(32)` | 是 | 首条为空 | +| `to_status` | `varchar(32)` | 否 | 新状态 | +| `action` | `varchar(32)` | 否 | 一次状态迁移的稳定动作 | +| `actor_type` | `varchar(16)` | 否 | `buyer/merchant/system` | +| `actor_user_id` | `uuid` | 是 | 系统动作为空 | +| `actor_display_name_snapshot` | `varchar(50)` | 是 | 安全展示 | +| `note` | `varchar(1000)` | 是 | 领域说明 | +| `metadata` | `jsonb` | 否 | 安全补充数据,默认 `'{}'::jsonb` | +| `occurred_at` | `timestamptz` | 否 | 发生时间 | +| `trace_id` | `varchar(64)` | 是 | 链路 | + +- PK `pk_after_sales_status_histories`;FK 申请/操作者 `RESTRICT`。 +- Unique `ux_after_sales_status_histories_request_sequence(after_sales_request_id,sequence)`。 +- `ix_after_sales_status_histories_request_occurred_at(after_sales_request_id, occurred_at, sequence)`。 +- Check `metadata` 为 JSON object;系统动作的 `actor_display_name_snapshot` 为空,用户动作保存当时安全展示名快照。 +- 一次迁移只写一行,动作与边固定为: + - `submitted`:`NULL → pending_review`; + - `buyer_cancelled`:`pending_review → cancelled`; + - `audit_approved`:`pending_review → pending_return/refunding`; + - `audit_rejected`:`pending_review → rejected`; + - `return_submitted`:`pending_return → pending_receipt`; + - `receipt_confirmed`:`pending_receipt → refunding`; + - `refund_succeeded`:`refunding → refunded`; + - `refund_failed`:`refunding → refund_failed`; + - `refund_retried`:`refund_failed → refunding`。 +- 动作与操作者同时冻结: + - `submitted/buyer_cancelled/return_submitted` 只能由 `buyer` 发起,`actor_user_id` 必须等于申请买家; + - `audit_approved/audit_rejected/receipt_confirmed` 只能由 `merchant` 发起,`actor_user_id` 必须等于责任商家; + - 人工 A419 `refund_retried` 只能由责任商家发起;系统恢复重试使用 `system` 且用户为空; + - `refund_succeeded/refund_failed` 由退款执行结果生成,固定为 `system` 且用户为空。 +- 只追加;每次状态迁移锁申请行,使用 `MAX(sequence)+1` 写入。它是 A414 领域时间线,不建泛化后台审计表。 + +### 7.8 DB088 `refund_operations` + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | 稳定 `refundOperationId` | +| `after_sales_request_id` | `uuid` | 否 | 售后申请 | +| `original_payment_id` | `uuid` | 否 | 原支付 | +| `buyer_id` | `uuid` | 否 | 入账买家 | +| `amount` | `numeric(18,2)` | 否 | 不可变退款额 | +| `currency` | `char(3)` | 否 | `CNY` | +| `status` | `varchar(24)` | 否 | 三态,默认 `processing` | +| `attempt_count` | `integer` | 否 | 已建立尝试数,默认 `0` | +| `last_failure_code` | `varchar(100)` | 是 | 确定失败码 | +| `posting_sequence` | `bigint` | 是 | 成功财务水位 | +| `started_at` | `timestamptz` | 否 | 开始时间 | +| `completed_at` | `timestamptz` | 是 | 成功时间 | +| `failed_at` | `timestamptz` | 是 | 最近确定失败 | +| `next_recovery_at` | `timestamptz` | 是 | 未知结果下次核实时间 | +| `last_verified_at` | `timestamptz` | 是 | 最近核实时间 | +| `verification_count` | `integer` | 否 | 核实次数,初始 0 | +| `updated_at` | `timestamptz` | 否 | 最近变化 | +| `version` | `bigint` | 否 | 并发版本,默认 `1` | + +约束与索引: + +- PK `pk_refund_operations`。 +- Unique `ux_refund_operations_after_sales_request_id(after_sales_request_id)`,每申请一个业务退款。 +- 复合 FK `(after_sales_request_id,original_payment_id,buyer_id,amount,currency) → after_sales_requests(id,original_payment_id,buyer_id,calculated_amount,currency) ON DELETE RESTRICT`。 +- Unique `ux_refund_operations_wallet_source_identity(id,buyer_id,amount,currency,posting_sequence)` 供钱包流水复合来源 FK。 +- Check 金额正数、币种、尝试数和状态: + - `verification_count >= 0`; + - `processing` 无成功/确定失败终态时间,`next_recovery_at` 非空; + - `succeeded` 有 `posting_sequence/completed_at`,失败与恢复字段为空; + - `definite_failure` 有 `last_failure_code/failed_at`,成功水位/完成时间/恢复时间为空。 +- `ix_refund_operations_recovery(status,next_recovery_at,id) WHERE status='processing'` 服务 Unknown 核实与崩溃恢复。 +- Unique `ux_refund_operations_posting_sequence(posting_sequence) WHERE posting_sequence IS NOT NULL` 服务 C08。 +- `definite_failure → processing` 只复用同一 ID、金额、买家和支付;`succeeded` 永久终态。 + +### 7.9 DB089 `payment_callbacks` + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | 直接使用 callbackId | +| `server_sequence` | `bigint` | 否 | Identity,内部稳定顺序 | +| `channel_attempt_id` | `uuid` | 否 | 规范流水绑定 | +| `request_fingerprint` | `char(64)` | 否 | 规范请求 SHA-256 hex | +| `claimed_order_id` | `uuid` | 否 | 回调声明订单;不建 FK | +| `callback_result` | `varchar(16)` | 否 | `success/failed` | +| `amount` | `numeric(18,2)` | 否 | 声明金额 | +| `currency` | `char(3)` | 否 | 声明币种 | +| `occurred_at` | `timestamptz` | 否 | 渠道发生时间,仅追踪 | +| `callback_key_id` | `varchar(100)` | 否 | 验签 Key 标识 | +| `callback_timestamp` | `timestamptz` | 否 | 签名时间 | +| `status` | `varchar(32)` | 否 | 四种确定终态 | +| `disposition_code` | `varchar(100)` | 否 | 稳定裁决原因 | +| `order_status_snapshot` | `varchar(32)` | 是 | 裁决时订单状态 | +| `payment_deadline_snapshot` | `timestamptz` | 是 | 裁决时截止 | +| `created_payment_id` | `uuid` | 是 | 本回调创建的支付 | +| `existing_payment_id` | `uuid` | 是 | 已存在成功来源 | +| `response_http_status` | `smallint` | 否 | 首次回执状态 | +| `response_body` | `jsonb` | 否 | 首次安全响应 | +| `posting_sequence` | `bigint` | 否 | 回调/支付一致水位 | +| `processed_at` | `timestamptz` | 否 | 服务端处理时间 | +| `trace_id` | `varchar(64)` | 是 | 链路 | + +约束与索引: + +- PK `pk_payment_callbacks`;Unique `ux_payment_callbacks_server_sequence(server_sequence)`,列定义 `GENERATED ALWAYS AS IDENTITY`。 +- FK `channel_attempt_id → payment_channel_attempts`;可空复合 FK `(existing_payment_id,claimed_order_id) → payments(id,order_id)`,保证既有成功来源属于声明订单;`processed_success` 使用复合 FK `(created_payment_id,claimed_order_id,channel_attempt_id,amount,currency) → payments(id,order_id,channel_attempt_id,amount,currency)`,保证回调创建的支付与声明订单、流水、金额、币种一致,均 `ON DELETE RESTRICT`。 +- 状态只允许 `processed_success/processed_failure/ignored/difference`;不持久化 Processing。 +- Check 指纹格式、金额、币种、HTTP 范围、posting sequence、`response_body` 为 JSON object;两个支付引用不得同时非空。字段矩阵: + - `processed_success` 必须且只能有 `created_payment_id`; + - `processed_failure` 两个支付引用均空; + - `ignored/difference` 的 `created_payment_id` 为空,`existing_payment_id` 按裁决码决定; + - `order_not_found` 的两个支付引用、`order_status_snapshot/payment_deadline_snapshot` 全空; + - 除 `order_not_found` 外均已锁定并识别订单,两个订单裁决快照必须非空。 +- `disposition_code` 按以下固定结果保存,不允许自由文本: + - `success_applied` → `processed_success`; + - `failure_recorded_before_deadline`、`failure_recorded_after_deadline` → `processed_failure`; + - `paid_failure_ignored`、`terminal_failure_ignored`、`duplicate_success_ignored` → `ignored`; + - `expired_success`、`cancelled_success`、`other_payment_source_succeeded`、`order_not_found`、`serial_binding_mismatch`、`amount_mismatch`、`currency_mismatch` → `difference`。 +- `paid_failure_ignored`、`duplicate_success_ignored`、`other_payment_source_succeeded` 必须有 `existing_payment_id`;其他 `ignored/difference` 仅在裁决时确实找到既有成功支付才允许保存该引用,不能伪造占位 ID。 +- 同时命中多个 Difference 原因时按 `order_not_found → serial_binding_mismatch → amount_mismatch → currency_mismatch → other_payment_source_succeeded → cancelled_success → expired_success` 选择主裁决码;全部命中事实进入安全响应和后续对账证据,不能因单一主码丢失。 +- `ix_payment_callbacks_attempt_sequence(channel_attempt_id,server_sequence)`。 +- `ix_payment_callbacks_order_processed_at(claimed_order_id,processed_at,id)`。 +- `ix_payment_callbacks_status_posting_sequence(status,posting_sequence)`。 +- Unique `ux_payment_callbacks_posting_sequence(posting_sequence)`。 +- `ix_payment_callbacks_difference(posting_sequence,id) WHERE status='difference'`。 + +同 callbackId 先用 advisory lock,再查 PK:同指纹精确重放 `response_http_status/response_body`,不同指纹返回冲突。签名失败、字段格式错误和基础设施未知不写业务回调表。 + +### 7.10 DB090 `reconciliation_batches` + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | 批次 ID | +| `business_date` | `date` | 否 | 前一完整 UTC 业务日 | +| `range_from` | `timestamptz` | 否 | 半开区间起 | +| `range_to` | `timestamptz` | 否 | 半开区间止 | +| `watermark_at` | `timestamptz` | 否 | 展示水位时间 | +| `watermark_sequence` | `bigint` | 否 | 严格已提交财务水位 | +| `payment_unit_count` | `integer` | 否 | 支付比较单元 | +| `refund_unit_count` | `integer` | 否 | 退款比较单元 | +| `total_count` | `integer` | 否 | 总比较单元 | +| `matched_count` | `integer` | 否 | 匹配数 | +| `difference_count` | `integer` | 否 | 差异数 | +| `status` | `varchar(24)` | 否 | `matched/has_differences/resolved` | +| `created_at` | `timestamptz` | 否 | 创建时间 | +| `resolved_at` | `timestamptz` | 是 | 全部闭环时间 | + +约束与索引: + +- PK `pk_reconciliation_batches`。 +- Unique `ux_reconciliation_batches_business_date(business_date)`;同一 UTC 日只有一个权威批次,重复任务返回既有批次。 +- Check `range_from = business_date::timestamp AT TIME ZONE 'UTC'`、`range_to = range_from + interval '1 day'`、`watermark_at >= range_to`、`watermark_sequence >= 0`、所有计数非负;因此批次只能覆盖上一完整 UTC 自然日,不能用同一 `business_date` 抢占任意时间范围: + - `total_count = payment_unit_count + refund_unit_count`; + - `total_count = matched_count + difference_count`; + - Matched 要求差异 0、解决时间空; + - HasDifferences 要求差异 >0、解决时间空; + - Resolved 要求差异 >0、解决时间非空。 +- `ix_reconciliation_batches_status_business_date(status,business_date DESC,id DESC)`。 + +### 7.11 DB091 `reconciliation_differences` + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | 差异 ID | +| `batch_id` | `uuid` | 否 | 批次 | +| `difference_type` | `varchar(64)` | 否 | 已确认 12 类之一 | +| `status` | `varchar(24)` | 否 | `pending/in_progress/resolved` | +| `subject_type` | `varchar(32)` | 否 | 比较主体类型 | +| `subject_id` | `uuid` | 否 | 主体 ID | +| `comparison_rule_code` | `varchar(100)` | 否 | 稳定规则码 | +| `comparison_rule_version` | `smallint` | 否 | 规则版本 | +| `comparison_rule_text` | `varchar(500)` | 否 | 安全说明 | +| `expected_facts` | `jsonb` | 否 | 固定安全快照 | +| `actual_facts` | `jsonb` | 否 | 固定安全快照 | +| `order_id` | `uuid` | 是 | 可空弱引用 | +| `payment_id` | `uuid` | 是 | 可空弱引用 | +| `refund_operation_id` | `uuid` | 是 | 可空弱引用 | +| `callback_id` | `uuid` | 是 | 首个回调弱引用 | +| `current_assignee_user_id` | `uuid` | 是 | 当前处理人 | +| `claimed_at` | `timestamptz` | 是 | 领取时间 | +| `claim_expires_at` | `timestamptz` | 是 | 领取到期 | +| `resolution_type` | `varchar(48)` | 是 | 两种受控解决类型 | +| `resolution_note` | `varchar(1000)` | 是 | 解决说明 | +| `controlled_action_ref` | `varchar(500)` | 是 | 已提交纠正动作 | +| `resolved_by_user_id` | `uuid` | 是 | 处理管理员 | +| `resolved_at` | `timestamptz` | 是 | 解决时间 | +| `last_verified_at` | `timestamptz` | 是 | 最近复核 | +| `verification_result` | `varchar(24)` | 是 | `matched/still_mismatched` | +| `remaining_mismatch_reason` | `varchar(1000)` | 是 | 仍不一致原因 | +| `created_at` | `timestamptz` | 否 | 创建时间 | +| `updated_at` | `timestamptz` | 否 | 最近变化 | +| `version` | `bigint` | 否 | 并发版本,初始 `1` | + +约束: + +- PK `pk_reconciliation_differences`;FK `fk_reconciliation_differences_batches_batch`、`fk_reconciliation_differences_users_assignee`、`fk_reconciliation_differences_users_resolver` 分别约束批次、当前领取人和解决人,均 `ON DELETE RESTRICT`。 +- `difference_type` 固定使用: + - `payment_succeeded_order_not_updated`; + - `order_paid_payment_missing`; + - `multiple_successful_payment_sources`; + - `late_success_callback`; + - `callback_binding_mismatch`; + - `processed_success_payment_missing`; + - `refunded_operation_missing`; + - `duplicate_refund_operation`; + - `refund_succeeded_after_sales_not_updated`; + - `refund_succeeded_wallet_credit_missing`; + - `duplicate_wallet_credit`; + - `refund_amount_mismatch`。 +- 业务对象引用故意不统一建立 FK,因为“对象缺失”本身可能是差异;已存在引用由生成/解决规则校验。 +- `expected_facts/actual_facts` 必须为 JSON object,`comparison_rule_version >= 1`、`version >= 1`。 +- Unique `ux_reconciliation_differences_unit(batch_id,subject_type,subject_id,comparison_rule_code)`,同一比较单元累积证据而不重复计数。 +- 状态组合: + - Pending 无领取/解决; + - InProgress 可有有效领取人,也允许 Release 后领取字段全空; + - Resolved 解决字段、处理人、解决时间和 `verification_result='matched'` 非空,当前领取三字段必须清空;领取历史保存在 DB095。 +- 领取三字段全空或全非空;领取有效期晚于领取时间。 +- `resolution_type` 只允许 `corrected_by_controlled_action/confirmed_no_business_impact`;前者必须有受控动作引用,后者必须为空。 +- 复核失败保持 `in_progress`,固定 `verification_result='still_mismatched'` 且 `remaining_mismatch_reason` 非空;复核成功进入 Resolved 后该原因必须为空。 + +索引: + +- `ix_reconciliation_differences_batch_status_created_at(batch_id,status,created_at DESC,id DESC)`。 +- `ix_reconciliation_differences_assignee_claim(current_assignee_user_id,claim_expires_at) WHERE status='in_progress'`。 +- `ix_reconciliation_differences_subject(subject_type,subject_id)`。 + +### 7.12 DB092 `after_sales_return_shipments` + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | 退货物流 ID | +| `after_sales_request_id` | `uuid` | 否 | 售后申请 | +| `carrier` | `varchar(50)` | 否 | 承运方 | +| `tracking_number` | `varchar(50)` | 否 | 运单号 | +| `shipped_at` | `timestamptz` | 否 | 买家寄回时间 | +| `note` | `varchar(500)` | 是 | 说明 | +| `content_fingerprint` | `char(64)` | 否 | 规范内容指纹 | +| `submitted_by_user_id` | `uuid` | 否 | 买家 | +| `submitted_at` | `timestamptz` | 否 | 提交时间 | + +- PK `pk_after_sales_return_shipments`;Unique `ux_after_sales_return_shipments_request(after_sales_request_id)`。 +- 复合 FK `(after_sales_request_id,submitted_by_user_id) → after_sales_requests(id,buyer_id) ON DELETE RESTRICT`。 +- Check `btrim(carrier) <> ''`、`btrim(tracking_number) <> ''`、指纹为 64 位小写十六进制、`shipped_at <= submitted_at`。 +- 不对运单号做全局唯一,同一包裹可承载多份申请。 +- 内容提交后不可修改;新 Key + 同指纹返回既有结果,新 Key + 不同指纹返回冲突。 +- A434 在锁定申请后取得服务端提交时间并显式写 `submitted_at`;缺省 `shippedAt` 使用同一时间,客户端时间晚于它时拒绝。 + +### 7.13 DB093 `refund_attempts` + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | 尝试 ID | +| `refund_operation_id` | `uuid` | 否 | 稳定业务退款 | +| `attempt_number` | `integer` | 否 | 从 1 递增 | +| `status` | `varchar(24)` | 否 | `executing/unknown/succeeded/definite_failure` | +| `executor_kind` | `varchar(24)` | 否 | `merchant/system_recovery` | +| `executor_user_id` | `uuid` | 是 | 商家操作者 | +| `worker_instance_id` | `varchar(100)` | 是 | 系统恢复实例 | +| `execution_token` | `uuid` | 否 | 本次执行身份 | +| `lease_expires_at` | `timestamptz` | 是 | 执行租约 | +| `failure_code` | `varchar(100)` | 是 | 安全失败码 | +| `verification_count` | `integer` | 否 | Unknown 核实次数,初始 0 | +| `last_verified_at` | `timestamptz` | 是 | 最近核实 | +| `started_at` | `timestamptz` | 否 | 开始 | +| `finished_at` | `timestamptz` | 是 | 确定结果时间 | +| `trace_id` | `varchar(64)` | 是 | 链路 | + +- PK `pk_refund_attempts`;FK `refund_operation_id → refund_operations`、可空 `executor_user_id → users`,均 `ON DELETE RESTRICT`。 +- Unique `ux_refund_attempts_operation_number(refund_operation_id,attempt_number)`、Unique `ux_refund_attempts_execution_token(execution_token)`。 +- 部分 Unique `ux_refund_attempts_unresolved(refund_operation_id) WHERE status IN ('executing','unknown')`,每个退款最多一个未决尝试。 +- 建立新尝试时锁 DB088,先令 `refund_operations.attempt_count = attempt_count + 1`,新 DB093 `attempt_number` 必须等于该值;Unknown 核实不增加编号。 +- `merchant` 要求 `executor_user_id` 非空且 Worker 为空;`system_recovery` 要求 Worker 非空且用户为空。 +- `executor_kind='merchant'` 时,A419 锁定 DB088 后继续锁其所属 DB086,校验 `executor_user_id = after_sales_requests.assigned_merchant_user_id`;该跨表归属由共享事务与集成测试保证,不能接受任意 Merchant ID。 +- `executing` 要求租约非空、结束时间和失败码为空;`unknown` 要求结束时间非空、租约/失败码为空;`succeeded` 要求结束时间非空且失败码为空;`definite_failure` 要求结束时间和失败码非空。核实次数非负,正数时最近核实时间非空。 +- Unknown 不新建下一次尝试,恢复任务继续核实原尝试:确认完整原子结果已提交则同一尝试收敛为 Succeeded;只有确认资金、支付累计、库存和售后状态均无副作用后才收敛为 DefiniteFailure;仍无法判定时保持 Unknown 并更新核实计数。 + +### 7.14 DB094 `reconciliation_evidence` + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | 证据 ID | +| `difference_id` | `uuid` | 否 | 差异 | +| `phase` | `varchar(16)` | 否 | `detection/resolution` | +| `source_type` | `varchar(64)` | 否 | 来源类型 | +| `source_id` | `varchar(200)` | 否 | 来源标识 | +| `observation_hash` | `char(64)` | 否 | 去重哈希 | +| `safe_snapshot` | `jsonb` | 否 | 脱敏证据 | +| `observed_at` | `timestamptz` | 否 | 观察时间 | +| `created_at` | `timestamptz` | 否 | 落库时间 | + +- PK `pk_reconciliation_evidence`;FK `fk_reconciliation_evidence_differences_difference` 约束差异且 `ON DELETE RESTRICT`。 +- Unique `ux_reconciliation_evidence_observation(difference_id,source_type,source_id,observation_hash)`。 +- Check `safe_snapshot` 为 JSON object、`observation_hash` 为 64 位小写十六进制。 +- `ix_reconciliation_evidence_difference_created_at(difference_id,created_at,id)`。 +- 不保存签名、密钥、Token、连接字符串和完整异常堆栈。 + +### 7.15 DB095 `reconciliation_actions` + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | 动作 ID | +| `difference_id` | `uuid` | 否 | 差异 | +| `sequence` | `integer` | 否 | 差异内序号 | +| `action` | `varchar(32)` | 否 | `created/claimed/released/taken_over/verification_failed/resolved` | +| `from_status` | `varchar(24)` | 是 | 之前状态 | +| `to_status` | `varchar(24)` | 否 | 之后状态 | +| `actor_type` | `varchar(16)` | 否 | `system/admin` | +| `actor_user_id` | `uuid` | 是 | 管理员 | +| `note` | `varchar(1000)` | 是 | 原因/处置 | +| `resolution_type` | `varchar(48)` | 是 | 解决类型 | +| `controlled_action_ref` | `varchar(500)` | 是 | 受控动作 | +| `verification_result` | `varchar(24)` | 是 | 复核结果 | +| `remaining_mismatch_reason` | `varchar(1000)` | 是 | 仍不一致 | +| `occurred_at` | `timestamptz` | 否 | 时间 | +| `trace_id` | `varchar(64)` | 是 | 链路 | + +- PK `pk_reconciliation_actions`;FK `fk_reconciliation_actions_differences_difference`、`fk_reconciliation_actions_users_actor` 分别约束差异和管理员且 `ON DELETE RESTRICT`。 +- Unique `ux_reconciliation_actions_difference_sequence(difference_id,sequence)`。 +- `ix_reconciliation_actions_difference_occurred_at(difference_id,occurred_at,sequence)`。 +- 动作边固定为:`created: NULL→pending`、`claimed: pending/in_progress→in_progress`、`released/taken_over/verification_failed: in_progress→in_progress`、`resolved: in_progress→resolved`。 +- `created` 只能为 `system` 且用户为空;其余动作只能为 `admin` 且用户非空。只有 `resolved` 保存解决类型和可选受控动作引用;`verification_failed` 固定保存 `still_mismatched` 与非空剩余原因,其他动作不得伪造解决/复核字段。 +- 只追加。A425 的领取、释放、接管、复核失败和解决都必须留痕。 + +### 7.16 DB096 `financial_posting_sequences` + +用途:冻结 C08 的严格已提交集合,解决仅靠 `watermarkAt` 无法排除“早取时间、晚提交”事务的问题。 + +| 字段 | 类型 | 空 | 默认 | 说明 | +|---|---|---:|---|---| +| `id` | `smallint` | 否 | `1` | 单例 PK | +| `last_sequence` | `bigint` | 否 | `0` | 最近财务提交序号 | +| `last_posted_at` | `timestamptz` | 是 | — | 最近提交时间 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 更新时间 | + +- PK `pk_financial_posting_sequences`;Check `id=1`、`last_sequence >= 0`。 +- Seed 必须且只能插入一行。 +- 钱包充值、成功支付、有效回调裁决和完整退款结果在事务末段对单例行取得 `FOR UPDATE`,递增一次,并把同一 sequence 写入该原子结果涉及的全部表;行锁持有至提交,故序号顺序与这些事务的提交顺序一致。 +- 只有取得水位写锁后才调用一次 `posting_time = clock_timestamp()`;它是本系统的权威财务业务时间,而不是 PostgreSQL 物理 COMMIT instant。同一原子结果的业务时间及各表 posting sequence 使用本次提交阶段时间/序号,不得在取得水位锁前预取。极端跨 UTC 零点事务仍按该 `posting_time` 归属,日批次的共享锁会等待它提交后再取水位,因此不会漏账;不启用 `track_commit_timestamp`,也不声称两者时间完全相等。 +- 对账统一 `posted_at` 映射并要求等于该原子结果的 `posting_time`:DB061/DB085 为 `paid_at`,DB082 为 `credited_at`,DB083 为 `created_at`,DB086 仅 Refunded 行为 `closed_at`,DB088 为 `completed_at`,DB089 为 `processed_at`。渠道 `occurred_at` 仍只作外部追踪,不能决定业务日。 +- 对账启动时在短事务内对单例行取得 `FOR SHARE`,等待此前写事务提交并阻止后续写事务越过屏障;在同一屏障内读取 `watermark_sequence=last_sequence` 与 `watermark_at=clock_timestamp()` 后立即提交。后续财务事务取得写锁后的 `posting_time` 必然晚于该屏障。 +- 对账比较单元必须同时满足 `posted_at >= range_from AND posted_at < range_to` 与 `posting_sequence <= watermark_sequence`。在批次取水位后才取得 posting sequence 的事务自然进入下一业务日/后续批次,不会因事务开始时间较早而漏记。 +- 同一 posting sequence 在每张参与事实表内最多出现一次;跨表可以且必须为同一原子结果复用。DB061、DB082、DB083、DB085、DB086、DB088、DB089 均用非空部分 Unique/Unique 约束落实这一点。 + +### 7.17 支付、退款与对账事务 + +#### Wallet 支付 + +锁顺序: + +```text +orders +→ payments(order_id 唯一检查) +→ wallet_accounts +→ financial_posting_sequences +→ wallet_transactions / payments / outbox / idempotency +``` + +锁订单后取得 `decision_time`;只有 `pending_payment AND decision_time < payment_deadline` 可支付。余额、流水、支付、订单、Outbox、DB104 共用一个 posting sequence 和事务。 + +#### 回调处理 + +1. 验签和固定字段通过后按 callbackId advisory lock;重查 DB089。 +2. 锁 DB084 流水绑定,再锁订单和支付唯一事实。 +3. 锁后取得 `decision_time`,按 A421 固定矩阵裁决。 +4. 四种合法业务终态都写 DB089;只有 ProcessedSuccess 同事务写 DB085、订单 Paid 和 Outbox。 +5. 锁 DB096 分配 posting sequence,完成回执后提交。 +6. 若裁决为 `expired_success` 或 `failure_recorded_after_deadline`,在回调确定终态提交后立即尽力调用 M04/C03 统一过期取消能力:复用既有 DB063、DB026/DB044 与 OrderCancelled Outbox,C08 不直接改订单或库存。取消暂时失败不得回滚已经确定的回调终态,由下单时预建的 due task 继续恢复;若竞争方已推进订单则按统一取消结果收敛。 + +同一流水可有多个 callbackId;绝不对 DB089 的流水号建唯一约束。 + +#### 售后退款 + +统一锁顺序: + +```text +orders +→ after_sales_requests +→ refund_operations +→ payments +→ wallet_accounts +→ Catalog/Seckill 原库存聚合 +→ financial_posting_sequences +→ 明细流水、历史、Outbox +``` + +成功是不可拆分原子结果: + +- 钱包入账与唯一 DB083; +- `payments.refunded_amount` 条件增加且不超过支付额; +- 按 `stock_return_policy` 写 DB026 或 DB044 并回补; +- DB088 Succeeded、DB086 Refunded 和数量从处理中转已退款; +- DB087 时间线、DB102 买家消息事件;`return_to_catalog` 时同事务另写 C07 Immediate 失效事件; +- 同一 posting sequence。 + +结果未知保持 Refunding + unresolved DB093;确定失败只有在确认资金、库存均无副作用后进入 RefundFailed。 + +#### A425 对账处置 + +Claim 使用条件更新;Release 后仍为 InProgress 但清空当前领取;Takeover 只在无有效领取人时成功。Resolve 必须重跑原比较规则: + +- 仍不一致:写 DB095 `verification_failed`,差异保持 InProgress; +- 已一致:写证据和 Resolved;若批次最后一条未决差异同时更新批次 Resolved。 + +DB091、DB094、DB095、DB090 和 DB104 在一个事务提交;C08 不直接修改订单、支付、退款或钱包业务表。 + +## 八、Messaging 与 M00 可靠性 + +### 8.1 DB101 `messages` + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | `messageId` | +| `server_sequence` | `bigint` | 否 | 接收人内提交串行序号 | +| `inbox_id` | `uuid` | 否 | 成功消费事实 | +| `source_event_id` | `uuid` | 否 | 来源事件 | +| `recipient_user_id` | `uuid` | 否 | 接收人 | +| `recipient_role` | `varchar(16)` | 否 | `buyer/merchant` | +| `message_type` | `varchar(64)` | 否 | 固定事件矩阵类型 | +| `title` | `varchar(100)` | 否 | 标题 | +| `summary` | `varchar(200)` | 否 | 列表摘要 | +| `body` | `varchar(2000)` | 否 | 安全正文 | +| `related_resource_type` | `varchar(32)` | 是 | 关联资源类型 | +| `related_resource_id` | `uuid` | 是 | 关联资源 | +| `action_target` | `varchar(32)` | 是 | 前端动作类型 | +| `action_resource_id` | `uuid` | 是 | 动作资源 | +| `source_occurred_at` | `timestamptz` | 否 | 业务事件发生时间 | +| `read_at` | `timestamptz` | 是 | 首次已读时间 | +| `created_at` | `timestamptz` | 否 | 消息落库时间 | + +约束: + +- PK `pk_messages`;Unique `ux_messages_recipient_server_sequence(recipient_user_id,server_sequence)`,Check `server_sequence >= 1`。 +- `inbox_messages` 提供 `ux_inbox_messages_id_event_id(id,event_id)` 备用键;复合 FK `(inbox_id,source_event_id) → inbox_messages(id,event_id)` 保证消息没有挂到其他事件的 Inbox。 +- 复合 FK `(recipient_user_id,recipient_role) → users(id,role) ON DELETE RESTRICT`,数据库直接保证接收角色一致。 +- Unique `ux_messages_event_recipient_type(source_event_id,recipient_user_id,message_type)`。 +- `message_type` 固定为 `order_created/order_cancelled/payment_succeeded/order_shipped/order_completed/after_sales_submitted/after_sales_reviewed/after_sales_pending_return/after_sales_return_submitted/refund_succeeded/refund_failed`;API 映射为已确认的 PascalCase。 +- `related_resource_type` 只允许 `order/payment/after_sales`;类型与 ID 同空同非空。 +- `recipient_role` 和接收人实际角色必须由 Identity 公开能力在消费事务重检;禁用账号仍保留合法身份和消息,只是不能登录。 +- `action_target` 只允许 `order_detail/payment_detail/after_sales_detail`;动作字段全空或全非空。动作目标与 `action_resource_id` 必须按固定消息矩阵派生,接口映射为 `OrderDetail/PaymentDetail/AfterSalesDetail`,不能保存前端路由字符串。 +- 标题、摘要和正文去空白后非空。 +- 消息业务字段在插入后不可变,只允许 `read_at` 从空写入一次;Check `read_at IS NULL OR read_at >= created_at`。 +- 不保存冗余 `is_read`,API 由 `read_at IS NOT NULL` 派生;首次已读时间不可覆盖。 + +索引: + +| 索引 | 字段 | 用途 | +|---|---|---| +| `ix_messages_recipient_created_at` | `recipient_user_id, created_at DESC, id DESC` | A501 稳定分页 | +| `ix_messages_recipient_type_created_at` | `recipient_user_id, message_type, created_at DESC, id DESC` | 类型筛选 | +| `ix_messages_recipient_server_sequence` | `recipient_user_id, server_sequence DESC` | 水位读取 | +| `ix_messages_recipient_unread_sequence` | `recipient_user_id, server_sequence WHERE read_at IS NULL` | A503/A505 | +| `ix_messages_inbox_id` | `inbox_id` | Inbox 追踪与删除保护 | + +消息事务按 `recipient_user_id` 稳定顺序取得事务 advisory lock,在锁内从该用户当前 `MAX(server_sequence)+1` 分配连续序号,并持锁至消息与 Inbox 一起提交;不得使用 PostgreSQL Identity/Sequence 冒充提交顺序。A504 使用 `UPDATE ... WHERE recipient_user_id=:me AND id=:id AND read_at IS NULL`,已读重放返回原时间。A505 取得同一用户锁后,在一个事务中捕获当前 `MAX(server_sequence)`,再以同一 `read_at` 更新 `<= watermark` 的未读消息并提交;后续消息只能在该锁释放后取得更大序号,因此保持未读。 + +### 8.2 DB102 `outbox_messages` + +| 字段 | 类型 | 空 | 默认 | 说明 | +|---|---|---:|---|---| +| `id` | `uuid` | 否 | UUIDv7 | 同时作为 eventId 和 RabbitMQ MessageId | +| `event_type` | `varchar(150)` | 否 | — | 已确认事件类型 | +| `schema_version` | `smallint` | 否 | `1` | Envelope 版本 | +| `aggregate_type` | `varchar(64)` | 否 | — | 来源聚合 | +| `aggregate_id` | `uuid` | 否 | — | 来源聚合 ID | +| `deduplication_key` | `varchar(200)` | 是 | — | 需要跨事务派生事件时的稳定去重键 | +| `routing_key` | `varchar(200)` | 否 | — | 受控路由 | +| `payload` | `jsonb` | 否 | — | 最小事件快照,不含 recipients[] | +| `headers` | `jsonb` | 否 | `'{}'::jsonb` | 安全头 | +| `occurred_at` | `timestamptz` | 否 | — | 业务发生时间 | +| `correlation_id` | `varchar(64)` | 是 | — | 链路关联 | +| `status` | `varchar(24)` | 否 | `'pending'` | 投递状态 | +| `attempt_count` | `integer` | 否 | `0` | 尝试数 | +| `available_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 首次可投递 | +| `next_attempt_at` | `timestamptz` | 是 | `CURRENT_TIMESTAMP` | 下次尝试;运行/终态为空 | +| `lease_owner` | `varchar(100)` | 是 | — | Worker | +| `lease_token` | `uuid` | 是 | — | 领取令牌 | +| `lease_expires_at` | `timestamptz` | 是 | — | 租约到期 | +| `last_error_code` | `varchar(100)` | 是 | — | 安全错误 | +| `published_at` | `timestamptz` | 是 | — | Broker Confirm 时间 | +| `dead_lettered_at` | `timestamptz` | 是 | — | 人工介入时间 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 落库时间 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 投递状态更新时间 | + +约束: + +- PK `pk_outbox_messages`;事件 ID 不再另建 `messageId`。 +- Unique `ux_outbox_messages_event_deduplication(event_type,deduplication_key) WHERE deduplication_key IS NOT NULL`;普通来源事务事件可为空,跨事务派生的延迟失效事件必须使用稳定键。 +- 状态:`pending/publishing/published/dead_lettered`。 +- Envelope 字段、payload、headers、occurred_at 创建后不可变;只允许修改投递字段。 +- `payload/headers` 必须为 JSON object,`attempt_count >= 0`、schema version 正数。 +- 首次创建固定 `next_attempt_at=available_at`;重试时 `next_attempt_at >= available_at`,未来可用事件不得提前发布。 +- 状态组合: + - `pending`:`next_attempt_at` 非空,lease 与两个终态时间为空; + - `publishing`:三个 lease 字段非空,`next_attempt_at` 与两个终态时间为空; + - `published`:`published_at` 非空,`next_attempt_at`、lease、`dead_lettered_at`、错误码为空; + - `dead_lettered`:`dead_lettered_at/last_error_code` 非空,`next_attempt_at`、lease、`published_at` 为空。 + +索引: + +- `ix_outbox_messages_pending(next_attempt_at,id) WHERE status='pending'`。 +- `ix_outbox_messages_expired_lease(lease_expires_at,id) WHERE status='publishing'`。 +- `ix_outbox_messages_aggregate(aggregate_type,aggregate_id,occurred_at)`。 + +Worker 用 `FOR UPDATE SKIP LOCKED` 小批量领取,等待 Publisher Confirm。Broker 已确认但数据库标记前崩溃会重复发布,这是允许的故障窗口,由 Inbox 防重。 + +DB102 同时承接两类已确认的可靠事件,不新增 C07 业务表: + +- 业务集成事件:由 RabbitMQ 路由到 Messaging 等消费者; +- `CatalogCacheInvalidationRequestedV1`:只携带稳定 `operationId`、`phase=immediate/delayed`、受影响 `productIds`、是否失效固定首页和结构版本,不保存 Redis Key 或缓存值。来源业务事务只写 `immediate` 事件,去重键固定 `cache:{operationId}:immediate`。 +- Immediate 消费者先对配置规则派生的范围执行一次幂等 DEL,再在数据库事务中写 Processed DB103 与 `available_at=clock_timestamp()+interval '3 seconds'` 的 delayed DB102,延迟事件去重键固定 `cache:{operationId}:delayed`;提交后才确认原 Broker 消息。若删除后、事务提交前崩溃,Broker 重投只会安全重删;若事务提交后、确认前崩溃,唯一键保证不重复建立延迟事件。 +- Delayed 消费者到期后再次幂等 DEL,写 Processed DB103 后确认。这样二次删除相对首次成功删除延迟至少 3 秒,不占用消费者线程等待,也不长期持有未确认消息;任一阶段中断均可由 Broker + Inbox 恢复。 + +C07 Immediate 生产矩阵固定为:A112~A114 分类有效性/名称变化;A123~A128 已完成的商品内容、状态、图片或删除变化;A222 普通库存划拨;A301 普通库存扣减;A304/C03 普通库存取消回补;售后成功 `return_to_catalog`。A122 只创建不可公开的 Draft,不为不存在的公开缓存制造事件;评价不进入 C07 缓存。 + +### 8.3 DB103 `inbox_messages` + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | Inbox ID | +| `event_id` | `uuid` | 否 | 来源 eventId | +| `consumer_name` | `varchar(100)` | 否 | 稳定消费者名 | +| `event_type` | `varchar(150)` | 否 | 事件类型 | +| `schema_version` | `smallint` | 否 | 版本 | +| `payload_hash` | `char(64)` | 否 | Envelope/payload 指纹 | +| `outcome` | `varchar(24)` | 否 | `processed/rejected` | +| `message_count` | `integer` | 否 | 本事件生成消息数 | +| `result_code` | `varchar(100)` | 否 | 稳定结果码 | +| `received_at` | `timestamptz` | 否 | 收到时间 | +| `completed_at` | `timestamptz` | 否 | 完成时间 | +| `trace_id` | `varchar(64)` | 是 | 链路 | + +- PK `pk_inbox_messages`。 +- Unique `ux_inbox_messages_consumer_event(consumer_name,event_id)`。 +- Unique `ux_inbox_messages_id_event_id(id,event_id)` 供消息复合 FK。 +- Check schema version 正数、哈希格式、`message_count >= 0`。`rejected` 固定为 0;`processed` 对消息事件必须等于固定矩阵派生的接收人数,对已登记的明确无消息事实或 C07 失效命令允许为 0 并保存稳定结果码。 +- Rejected 结果码固定为 `unsupported_event_type/unsupported_schema_version/invalid_event_fields/recipient_missing/recipient_role_mismatch/resource_ownership_mismatch`;不得把瞬态依赖失败记录成确定拒绝。 +- `ix_inbox_messages_completed_at(completed_at,id)`。 + +Messaging 消费时先判断事件是否属于消息矩阵:明确无消息事实写一条 `processed/no_message/0` Inbox;消息事件先校验全部接收人,再在一个事务中写 Processed Inbox 和本事件全部消息。账号 Disabled 仍是合法接收人,只影响登录。 + +- 字段非法、必需接收人确定不存在、角色错误或业务归属确定错误:同事务写 Rejected/0/稳定结果码并告警,随后对原消息执行“不重新入队”的拒绝,由 RabbitMQ 路由到死信队列,避免毒消息无限重试; +- Identity/业务归属暂时无法确认、数据库写失败或其他基础设施未知:整体回滚并让 Broker 重投,不能留下 Rejected; +- 永久不支持的版本/事件同样写 Rejected 后进入死信;重复投递读取同一既有结果; +- 相同 eventId 不同 payload hash 是安全冲突,不得当重复成功,保持已有 Inbox 并告警、死信当前冲突消息。 + +### 8.4 DB104 `idempotency_records` + +一张公共表承接所有模块的确定结果,不为每个模块复制幂等表。 + +| 字段 | 类型 | 空 | 默认 | 说明 | +|---|---|---:|---|---| +| `id` | `uuid` | 否 | UUIDv7 | 记录 ID | +| `operation_code` | `varchar(64)` | 否 | — | Axxx 或稳定内部动作 | +| `scope_key` | `varchar(300)` | 否 | — | 规范作用域,不含秘密 | +| `actor_user_id` | `uuid` | 是 | — | 当前操作者 | +| `idempotency_key` | `uuid` | 否 | — | 客户端 Key | +| `request_hash` | `char(64)` | 否 | — | 规范请求指纹 | +| `status` | `varchar(16)` | 否 | `'processing'` | `processing/completed` | +| `replay_mode` | `varchar(24)` | 否 | `'exact_snapshot'` | `exact_snapshot/resource_current` | +| `work_payload` | `jsonb` | 否 | `'{}'::jsonb` | 可恢复工作清单 | +| `lease_owner` | `varchar(100)` | 是 | — | 跨对象写入恢复者 | +| `lease_token` | `uuid` | 是 | — | 不可复用 fencing token | +| `lease_expires_at` | `timestamptz` | 是 | — | 工作租约 | +| `work_expires_at` | `timestamptz` | 是 | — | 外部工作恢复/清理边界 | +| `attempt_count` | `integer` | 否 | `0` | 外部工作尝试数 | +| `last_error_code` | `varchar(100)` | 是 | — | 安全恢复原因 | +| `outcome_type` | `varchar(24)` | 是 | — | `success/business_failure` | +| `http_status` | `smallint` | 是 | — | 首次确定状态 | +| `response_headers` | `jsonb` | 是 | — | 首次响应头 | +| `response_body` | `jsonb` | 是 | — | 首次安全响应 | +| `error_code` | `varchar(100)` | 是 | — | 业务错误码 | +| `resource_type` | `varchar(64)` | 是 | — | 当前资源重放类型 | +| `resource_id` | `uuid` | 是 | — | 当前资源重放 ID | +| `trace_id` | `varchar(64)` | 是 | — | 首次链路 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 占用时间 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 最近变化 | +| `completed_at` | `timestamptz` | 是 | — | 确定结果时间 | +| `expires_at` | `timestamptz` | 是 | — | 允许自动过期的窗口 | + +约束: + +- PK `pk_idempotency_records`;Unique `ux_idempotency_records_operation_scope_key(operation_code,scope_key,idempotency_key)`。 +- `actor_user_id` 存在时 FK → users `RESTRICT`。 +- Check `request_hash` 格式、`attempt_count >= 0`、HTTP 状态范围、`work_payload` 为 JSON object,非空 `response_headers/response_body` 也必须为 JSON object。 +- Processing:结果字段、完成时间和 `expires_at` 为空;普通纯数据库操作的工作/租约字段全空,A122 跨对象工作要求工作清单非空、`work_expires_at` 与三个 lease 字段全非空。 +- Completed:`outcome_type/http_status/response_headers/completed_at` 非空;无响应体的 204 等结果以 SQL NULL 明确表示,其他响应体非空。工作清单重置为空对象,错误与工作租约字段和 `work_expires_at` 清空;`expires_at` 为空或严格晚于 `completed_at`,且只能从 `completed_at` 计算。 +- `success` 只保存 2xx 且 `error_code` 为空;`business_failure` 只保存已确认 4xx 且 `error_code` 非空。瞬态、依赖未知和提交未知不得伪装为 Completed。 +- 三个 lease 字段全空或全非空;`resource_type/resource_id` 同空同非空。 +- `resource_current` 必须有资源类型/ID;用于 A416/A417/A419 重放稳定命令身份并读取同一退款操作当前状态,其余接口使用 `exact_snapshot`。 +- `expires_at` 为空表示本期不自动清理;Processing 永远不能按该字段删除,外部工作只按 `work_expires_at` 进入受控恢复/补偿。 +- `ix_idempotency_records_processing_lease(status,lease_expires_at,id) WHERE status='processing'`。 +- `ix_idempotency_records_expires_at(expires_at) WHERE expires_at IS NOT NULL`。 +- `ix_idempotency_records_resource(resource_type,resource_id) WHERE resource_id IS NOT NULL`。 + +并发协议: + +1. 对 `operation_code + scope_key + key` 取得事务 advisory lock并重查唯一记录。 +2. 同 Key 不同请求哈希返回 `IDEMPOTENCY.KEY_REUSED`。 +3. 普通纯数据库操作在一个外层事务建立 Processing,执行副作用,完成后才提交 Completed;连接中断/提交未知整体回滚,不留下处理态。 +4. 确定业务失败使用 Savepoint 回滚副作用,再在外层事务保存 BusinessFailure。 +5. A122 是例外的跨对象写入:先提交 Processing + 预生成商品/图片 ID、全部原图/缩略图 Key、文件哈希清单、工作期限与新 lease token;上传后最终事务必须以 `WHERE id=:id AND status='processing' AND lease_token=:token` 完成。接管会换发新 token,旧执行者永远不能完成或清理新执行者的工作。 +6. 失败或崩溃时同 Key 按工作清单续传/清理,不重复生成对象;任一对象已写但业务关联未提交时,先登记 DB105 或保留 Processing,不丢弃最后持久化清单。 +7. 24 小时 Completed 记录到期但清理延迟时,同作用域请求可在锁内确认 `status='completed' AND expires_at <= decision_time` 后删除旧记录并重新占用,不能被旧行永久阻塞。 + +`scope_key` 固定采用不含秘密的规范字符串,例如 `buyer:{buyerId}`、`merchant:{merchantId}:order:{orderId}`、`admin:{adminId}:user:{targetUserId}`;同一 operation 不允许多个模块自行发明不同格式。公开 HTTP 通常使用对应 `Axxx` 作为 `operation_code`,内部动作使用稳定 lower_snake_case 名。A016/A017 是已确认例外:共同使用 `operation_code='account_governance'`、`scope_key='admin:{adminUserId}'`,目标账号与启用/禁用动作进入请求指纹,保证两接口共享治理幂等范围。 + +保留范围: + +| 接口 | 保留 | +|---|---| +| A016/A017 | 管理员 + 目标账号治理事实,本期不自动清理 | +| A122/A142/A220/A222/A223/A228/A301/A307 | 至少覆盖资源完整生命周期,本期不自动清理 | +| A201、A204~A207 | 首次确定结果起 24 小时 | +| A402/A405 | 资金事实完整生命周期,本期不自动清理 | +| A412/A415~A417/A419/A434 | 售后事实完整生命周期,本期不自动清理 | +| A425 | 对账差异完整生命周期,本期不自动清理 | + +A421 使用 DB089 的 callbackId 幂等,不重复写本表。 + +### 8.5 DB105 `object_cleanup_tasks` + +| 字段 | 类型 | 空 | 默认 | 说明 | +|---|---|---:|---|---| +| `id` | `uuid` | 否 | UUIDv7 | 任务 ID | +| `object_key` | `varchar(500)` | 否 | — | 待删除对象 | +| `object_kind` | `varchar(32)` | 否 | — | `product_image/thumbnail/review_image` | +| `source_type` | `varchar(64)` | 否 | — | 来源记录类型 | +| `source_id` | `uuid` | 否 | — | 来源 ID 弱引用 | +| `reason` | `varchar(64)` | 否 | — | 删除/过期/补偿原因 | +| `status` | `varchar(24)` | 否 | `'pending'` | `pending/running/retry_wait/succeeded/dead_lettered` | +| `attempt_count` | `integer` | 否 | `0` | 尝试数 | +| `next_attempt_at` | `timestamptz` | 是 | `CURRENT_TIMESTAMP` | 下次处理;运行/终态为空 | +| `lease_owner` | `varchar(100)` | 是 | — | Worker | +| `lease_token` | `uuid` | 是 | — | 租约令牌 | +| `lease_expires_at` | `timestamptz` | 是 | — | 租约到期 | +| `last_error_code` | `varchar(100)` | 是 | — | 安全错误 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 创建 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 更新 | +| `completed_at` | `timestamptz` | 是 | — | 删除确认 | +| `dead_lettered_at` | `timestamptz` | 是 | — | 转人工时间 | + +- PK `pk_object_cleanup_tasks`;Unique `ux_object_cleanup_tasks_object_key(object_key)`,重复清理合并为同一任务。 +- 状态增加 `running`,并冻结: + - `pending/retry_wait`:`next_attempt_at` 非空,lease 与终态时间为空; + - `running`:三个 lease 字段非空,`next_attempt_at` 与终态时间为空; + - `succeeded`:`completed_at` 非空,其余调度/lease/死信字段为空; + - `dead_lettered`:`dead_lettered_at/last_error_code` 非空,其余调度/lease/完成字段为空。 +- Check 尝试数非负、Key 和受控类型非空。 +- `object_kind` 与 Key 前缀必须一致:`product_image → products/`、`thumbnail → product-thumbnails/`、`review_image → reviews/`,清理任务不能伪造跨类型对象。 +- `ix_object_cleanup_tasks_due(status,next_attempt_at,lease_expires_at,id)`。 +- 数据库关联删除前在同一事务登记任务;Worker 把对象 NotFound 视为幂等成功。DeadLettered 进入告警但不能恢复已删除业务关联。 + +### 8.6 DB106 `outbox_delivery_attempts` + +| 字段 | 类型 | 空 | 说明 | +|---|---|---:|---| +| `id` | `uuid` | 否 | 尝试 ID | +| `outbox_message_id` | `uuid` | 否 | Outbox | +| `attempt_number` | `integer` | 否 | 从 1 递增 | +| `worker_instance_id` | `varchar(100)` | 否 | Worker | +| `lease_token` | `uuid` | 否 | 对应领取 | +| `status` | `varchar(16)` | 否 | `executing/confirmed/failed/unknown` | +| `broker_message_id` | `uuid` | 否 | 等于 eventId | +| `error_code` | `varchar(100)` | 是 | 安全错误 | +| `started_at` | `timestamptz` | 否 | 开始 | +| `finished_at` | `timestamptz` | 是 | 结束 | + +- PK `pk_outbox_delivery_attempts`;FK `outbox_message_id → outbox_messages ON DELETE RESTRICT`。 +- Unique `ux_outbox_delivery_attempts_message_number(outbox_message_id,attempt_number)`。 +- Check `attempt_number >= 1`、`broker_message_id=outbox_message_id`;`executing` 的结束时间/错误为空,三个终态 `finished_at >= started_at`,`confirmed` 错误为空,`failed/unknown` 保存稳定安全原因。 +- `ix_outbox_delivery_attempts_message_started_at(outbox_message_id,started_at,attempt_number)`。 +- 领取 DB102 的同一事务先执行 `outbox_messages.attempt_count + 1` 并以新值作为 DB106 `attempt_number`,再插入 `executing`。本表不是“完成后才追加”的结果表:Broker 调用后只允许以 `WHERE status='executing' AND lease_token=:token` 一次更新为终态,之后整行不可变。崩溃遗留的 Executing 由租约接管者在到期后转 Unknown,再建立下一尝试;这保留每次真实调用或未决调用的审计证据,最终投递状态仍在 DB102。 + +### 8.7 DB107 `worker_job_runs` + +用途:记录跨实例定时任务的调度与恢复;不替代任何业务唯一约束。 + +| 字段 | 类型 | 空 | 默认 | 说明 | +|---|---|---:|---|---| +| `id` | `uuid` | 否 | UUIDv7 | 运行 ID | +| `job_name` | `varchar(100)` | 否 | — | 稳定任务名 | +| `run_key` | `varchar(200)` | 否 | — | 日期/调度窗口等稳定键 | +| `status` | `varchar(24)` | 否 | `'pending'` | `pending/running/succeeded/retry_wait/dead_lettered` | +| `attempt_count` | `integer` | 否 | `0` | 尝试数 | +| `due_at` | `timestamptz` | 否 | — | 应执行时间 | +| `next_attempt_at` | `timestamptz` | 是 | — | 下次尝试;运行/终态为空 | +| `checkpoint` | `jsonb` | 否 | `'{}'::jsonb` | 非业务唯一事实进度 | +| `lease_owner` | `varchar(100)` | 是 | — | 实例 | +| `lease_token` | `uuid` | 是 | — | 租约 | +| `lease_expires_at` | `timestamptz` | 是 | — | 租约到期 | +| `result_summary` | `jsonb` | 是 | — | 安全统计 | +| `last_error_code` | `varchar(100)` | 是 | — | 安全错误 | +| `started_at` | `timestamptz` | 是 | — | 开始 | +| `completed_at` | `timestamptz` | 是 | — | 完成 | +| `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 创建 | +| `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 更新 | + +- PK `pk_worker_job_runs`;Unique `ux_worker_job_runs_job_name_run_key(job_name,run_key)`。 +- Check `attempt_count >= 0`、checkpoint/result summary 为 JSON object(后者可空)。 +- 状态组合: + - `pending`:`next_attempt_at` 非空,开始/完成/lease 为空; + - `running`:开始时间和三个 lease 字段非空,`next_attempt_at/completed_at` 为空; + - `retry_wait`:开始时间、`next_attempt_at/last_error_code` 非空,完成/lease 为空; + - `succeeded`:开始/完成时间非空,调度/lease/错误为空; + - `dead_lettered`:开始/完成时间和错误非空,调度/lease 为空。 +- `ix_worker_job_runs_due(status,next_attempt_at,lease_expires_at,id)`。 +- C08 `run_key=UTC business_date`;活动生命周期、过期上传清理、对象清理等使用稳定调度窗口。订单逐笔责任仍由 DB063,Outbox 逐笔责任仍由 DB102,退款责任仍由 DB093。 + +### 8.8 C06/C07/C10 不建业务表的内容 + +- SignalR 连接与 Redis Backplane 不落 PostgreSQL;消息可查询事实只在 DB101。 +- C07 Cache Key、填充锁、缓存值和 TTL 不落 PostgreSQL;商品事实仍在 DB021~DB027,但来源事务与两阶段可靠失效责任由 DB102/DB103 持久化。 +- Redis 撤销镜像从 DB001/DB004 重建;重建完成前受保护鉴权与 Hub 失败关闭。 +- EF Core 自带 `__EFMigrationsHistory` 是框架元数据,不分配 DBxxx;A507 只比较目标 Migration 版本和依赖能力,不伪造健康业务表。 + +## 九、跨模块关系与全局 ER + +### 9.1 数据所有权 + +| 事实 | 唯一所有者 | 其他模块保存内容 | +|---|---|---| +| 账号、角色、默认商家、地址 | Identity | 稳定用户 ID;Ordering 保存地址/展示名快照 | +| 分类、商品、普通库存 | Catalog | 商品 ID;订单/评价保存必要快照 | +| 购物车 | Cart | Ordering 只在创建事务成功后调用公开能力删除 | +| 秒杀活动、独立库存、限购 | Seckill | Ordering 保存活动 ID/名称和库存来源快照 | +| 订单、订单项、履约状态 | Ordering | Payment/AfterSales 保存稳定引用和必要金额/来源快照 | +| 钱包、资金流水、成功支付、回调、退款 | Payment | Ordering 只保存支付时间/水位;AfterSales 保存原支付引用 | +| 售后资格、申请状态、领域时间线 | AfterSales | Ordering 通过公开快照计算能否发货 | +| 用户消息与已读 | Messaging | 来源模块只写 Outbox 事件,不写消息表 | +| 幂等、Outbox/Inbox、对象补偿、Worker 运行 | M00 | 模块提供受控作用域、事件和业务引用 | + +物理 FK 只保证引用完整与删除阻断,不授权跨模块直接读写。应用代码仍必须通过公开 Application/Contracts 能力。 + +### 9.2 主要关系 + +```mermaid +erDiagram + USERS ||--o{ ADDRESSES : owns + USERS ||--o{ FAVORITES : owns + USERS ||--o{ BROWSING_HISTORY : owns + USERS ||--o| BROWSING_HISTORY_SETTINGS : configures + + CATEGORIES ||--o{ CATEGORIES : contains + CATEGORIES ||--o{ PRODUCTS : classifies + PRODUCTS ||--o{ PRODUCT_IMAGES : has + PRODUCTS ||--|| PRODUCT_SEARCH_DOCUMENTS : projects + PRODUCTS ||--o{ CATALOG_INVENTORY_MOVEMENTS : records + PRODUCTS ||--o{ CART_ITEMS : appears_in + + PRODUCTS ||--o{ SECKILL_ACTIVITIES : participates + SECKILL_ACTIVITIES ||--o{ SECKILL_BUYER_QUOTAS : limits + SECKILL_ACTIVITIES ||--o{ SECKILL_INVENTORY_MOVEMENTS : records + + USERS ||--o{ ORDERS : places + ORDERS ||--|{ ORDER_ITEMS : contains + ORDERS ||--o{ ORDER_LIFECYCLE_TASKS : schedules + ORDER_ITEMS ||--o| REVIEWS : receives + REVIEWS ||--o{ REVIEW_IMAGES : has + + USERS ||--o| WALLET_ACCOUNTS : owns + WALLET_ACCOUNTS ||--o{ WALLET_TRANSACTIONS : posts + ORDERS ||--o| PAYMENTS : settles + PAYMENT_CHANNEL_ATTEMPTS ||--o{ PAYMENT_CALLBACKS : receives + PAYMENT_CHANNEL_ATTEMPTS ||--o| PAYMENTS : creates + + ORDER_ITEMS ||--o{ AFTER_SALES_REQUESTS : requests + AFTER_SALES_REQUESTS ||--o{ AFTER_SALES_STATUS_HISTORIES : records + AFTER_SALES_REQUESTS ||--o| AFTER_SALES_RETURN_SHIPMENTS : returns + AFTER_SALES_REQUESTS ||--o| REFUND_OPERATIONS : refunds + REFUND_OPERATIONS ||--o{ REFUND_ATTEMPTS : attempts + + RECONCILIATION_BATCHES ||--o{ RECONCILIATION_DIFFERENCES : contains + RECONCILIATION_DIFFERENCES ||--o{ RECONCILIATION_EVIDENCE : evidences + RECONCILIATION_DIFFERENCES ||--o{ RECONCILIATION_ACTIONS : tracks + + INBOX_MESSAGES ||--o{ MESSAGES : creates + OUTBOX_MESSAGES ||--o{ OUTBOX_DELIVERY_ATTEMPTS : attempts +``` + +### 9.3 物理删除矩阵 + +| 主记录 | 从记录 | 行为 | 原因 | +|---|---|---|---| +| 用户 | 所有业务引用 | `RESTRICT` | 用户不物理删除 | +| 分类 | 子分类/商品 | `RESTRICT` | 先证明无引用 | +| 商品 | 图片 | 应用先登记对象清理并删图片 | 不让数据库级联跳过对象补偿 | +| 商品 | 搜索投影/普通库存维护流水 | `CASCADE` | Catalog 聚合内部派生/历史 | +| 商品 | Cart/Favorite/History/Order/Review/Seckill | `RESTRICT` | A124 竞争最终保护 | +| 订单 | 订单项/任务/支付/售后 | `RESTRICT` | 交易事实永久保留 | +| 售后 | 时间线/物流/退款 | `RESTRICT` | 售后事实永久保留 | +| Outbox/Inbox | 尝试/消息 | `RESTRICT` | 保留可靠性追踪 | + +## 十、接口、流程与表追踪 + +### 10.1 Identity、Engagement、Catalog、Review + +| 接口 | 主要表 | 说明 | +|---|---|---| +| A001 | DB001 | 原子创建 Buyer | +| A002 | DB001 | 凭据、状态、token version | +| A003 | DB001、DB004 | 当前 JTI 撤销 | +| A004 | DB001、DB004 | 账号与撤销权威事实 | +| A006 | DB001 | 手机号 + token version | +| A007、A008 | DB001 | 用户名重置/资料 | +| A010~A014 | DB003 | 地址及默认切换 | +| A015 | DB001 | 后台账号列表 | +| A016、A017 | DB001、DB002、DB104 | 启停、历史、稳定结果;责任通过各模块公开能力复核 | +| A018~A020 | DB005 | 收藏;展示实时取 Catalog | +| A021 | DB006 | 历史列表 | +| A022、A025 | DB007 | 浏览开关 | +| A024 | DB006、DB007 | 开关复核、UPSERT、最近 200 条 | +| A101 | DB021、DB022 | 有效分类与商品计数 | +| A102 | DB021~DB023、DB027 | 分类/商品/主图/搜索 | +| A103 | DB021~DB023 | 公开详情 | +| A110 | DB021、DB022 | 全状态分类和计数 | +| A111 | DB021 | 新建两级分类 | +| A112 | DB021、DB022、DB027、DB102 | 分类修改、搜索投影同步与受影响详情失效 | +| A113、A114 | DB021、DB102 | 启停分类与受影响首页/详情失效 | +| A115 | DB021、DB022及所有历史引用 | 受约束物理删除 | +| A120、A121 | DB021~DB023 | 后台商品列表/详情 | +| A122 | DB021~DB023、DB027、DB104,正库存时 DB026,失败时 DB105 | 幂等 multipart 创建 | +| A123 | DB021~DB023、DB026、DB027、DB102 | 编辑、库存流水、搜索同步与缓存失效 | +| A124 | DB022、DB023、DB026、DB027、DB102、DB105及跨模块引用 | 受约束删除与对象补偿 | +| A125 | DB021~DB023、DB102 | 上架完整性与缓存失效 | +| A126 | DB022、DB102 | 下架与缓存失效 | +| A127、A128 | DB022、DB023、DB102、DB105 | 图片上传/删除与缓存失效 | +| A140 | DB024、DB025 | 评价列表/实时汇总 | +| A141 | DB025、失败/过期时 DB105 | 评价图片暂存 | +| A142 | DB024、DB025、DB104 | 唯一评价和图片绑定 | +| A143 | DB024 | 评价资格/既有结果 | + +### 10.2 Cart、Seckill、Ordering + +| 接口/任务 | 主要表 | +|---|---| +| A201 | DB041;提供 Key 时 DB104 | +| A202、A203、A208 | DB041 + Catalog 实时快照 | +| A204~A207 | DB041、DB104 | +| A220 | DB042、DB104 | +| A221 | DB042 | +| A222 | DB042、DB044、DB026、DB102、DB104 | +| A223 | DB042、DB104 | +| A224 | DB042 | +| A225 | DB042;订单统计通过 DB061/DB062 公开能力 | +| A226 | DB042 + Catalog 展示快照 | +| A227 | DB042;已登录买家另读 DB043 | +| A228 | DB042~DB044、DB061~DB063、DB102、DB104 | +| A301 | DB041、DB026、DB061~DB063、DB102、DB104 | +| A302 | DB061、DB062 | +| A303 | DB061、DB062 + Payment/AfterSales 公开摘要 | +| A304 | DB061、DB062、DB026 或 DB044、DB102 | +| A305 | DB061、DB062 | +| A306 | DB061、DB062 + AfterSales 公开摘要 | +| A307 | DB061~DB063、DB102、DB104 + AfterSales 履约快照 | +| A308 | DB061、DB102 | +| C01 生命周期 | DB042、DB107 | +| C03 超时取消 | DB061~DB063、原库存流水、DB102 | +| 自动完成 | DB061、DB063、DB102 | + +### 10.3 Payment、AfterSales、Messaging、运行能力 + +| 接口/任务 | 主要表 | +|---|---| +| A401 | DB081 | +| A402 | DB081~DB083、DB096、DB104 | +| A403 | DB082 | +| A404 | DB061、DB081、DB085 | +| A405 | DB061、DB081、DB083、DB085、DB096、DB102、DB104 | +| A406 | DB061、DB085 | +| A407、A408 | DB085 | +| A411 | DB061、DB062、DB085、DB086 | +| A412 | DB061、DB062、DB085~DB087、DB102、DB104 | +| A413 | DB086 | +| A414 | DB086~DB088、DB092、DB093 | +| A415 | DB086、DB087、DB104 | +| A416 | DB086~DB088、DB093、DB102、DB104;成功退款另用钱包/库存/DB096 | +| A417 | DB086~DB088、DB092、DB093、DB102、DB104 | +| A419 | DB086~DB088、DB093、DB102、DB104 | +| A421 | DB061、DB084、DB085、DB089、DB096、DB102;到期裁决另触发 DB063 与 DB026/DB044 统一取消 | +| A422 | DB090 | +| A423 | DB090、DB091 | +| A424 | DB091、DB094 | +| A425 | DB090、DB091、DB094、DB095、DB104 | +| A426 | DB091、DB094、DB095 | +| A434 | DB086、DB087、DB092、DB102、DB104 | +| A501~A505 | DB101 | +| 消息事件消费 | DB103、DB101 | +| 全部可靠事件生产者 | DB102;投递尝试 DB106 | +| A506 | 无业务表 | +| A507 | `__EFMigrationsHistory` + 依赖检查,无伪业务表 | +| 对账 Worker | DB096、DB090~DB095、DB107 | +| 对象清理 Worker | DB105、DB107 | + +已取消的 A005、A009、A023、A144、A229、A230、A418、A431、A432、A433 不建表、不建字段、不重新分配。 + +## 十一、数据保留、归档与清理 + +| 数据 | 保留规则 | +|---|---| +| 用户、账号状态历史 | 本期永久保留;无用户删除能力 | +| JWT 撤销 | `expires_at` 后加安全余量才可清理;清理不影响 `token_version` | +| 地址、购物车、收藏 | 按明确业务动作物理删除 | +| 浏览历史 | 每买家最多 200 条;A024 同事务裁剪 | +| 商品/分类 | 仅按受约束删除;合法商品删除先登记对象清理 | +| 上传中商品图 | 预留超时后清理;不公开 | +| 未关联评价图 | 24 小时后不可引用并进入清理 | +| 订单、订单项、支付、钱包、售后、退款、回调、对账、消息 | 本期不自动清理 | +| DB104 | 仅 Cart 指定操作 24 小时后可过期;其余本期不自动清理 | +| Outbox/Inbox/投递尝试 | 本期不自动清理;未来归档也必须保留 eventId 防重墓碑 | +| Worker 运行、对象清理成功记录 | 本期保留用于验收;未来策略必须晚于最大重试/审计窗口 | + +任何归档都不能破坏: + +- 订单/售后历史展示; +- 库存和资金追溯; +- 支付、回调、退款对账; +- 幂等重放有效期; +- Outbox/Inbox 去重。 + +## 十二、初始 Migration 与 Seed 落地方案 + +> 本章只定义后续实施方式;当前文档任务不创建 Migration、不运行 SQL、不初始化真实数据库。 + +### 12.1 初始 Migration + +后续只创建一份统一初始 Migration,名称固定为 `InitialEshopSchema`,由一次性 Migrator 执行: + +1. 创建 `pg_trgm` 扩展。 +2. 创建 Identity 基础表。 +3. 创建 Catalog 商品聚合。 +4. 创建 Engagement、Cart、Seckill。 +5. 创建 Ordering。 +6. 创建 Review。 +7. 创建 Payment 与 AfterSales。 +8. 创建 Messaging/M00 可靠性表。 +9. 追加因创建顺序暂缓的交叉 FK(如钱包流水对支付/退款的来源 FK)。 +10. 创建部分索引、GIN 索引和所有 Check。 +11. 插入 DB096 单例水位行。 +12. 由 EF 维护 `__EFMigrationsHistory`。 + +Migration 必须在事务可支持范围内原子执行;扩展权限或不可事务步骤失败时 Migrator 整体失败,API/Worker 不进入就绪。不得把六人 SQL 或六份 Migration 依次拼接为初始库。 + +### 12.2 Seed + +Seed 只提供可重复演示事实: + +- 1 个 Admin; +- 1 个且仅 1 个启用默认 Merchant; +- 至少 1 个可登录 Buyer; +- 至少 4 个有效分类,包含根分类与子分类; +- 至少 30 个商品,覆盖 OnSale、有货、售罄、Draft、OffSale; +- 商品主图对象使用可重复准备的演示对象 Key,不保存外部永久 URL; +- 需要展示的订单、支付、消息、售后只能通过受控初始化器按真实约束生成,不直接绕过流水和状态组合随意插行。 + +账号密码只以哈希写入,明文演示凭据通过受控开发配置提供;不得把真实手机号、个人地址、Token、密钥或生产连接信息写入 Seed。Seed 必须幂等,同一环境重复执行不增加第二个默认商家或重复业务样例。 + +## 十三、数据库账号与权限 + +部署角色固定按以下模型建立: + +| 角色 | 权限 | +|---|---| +| `eshop_owner` | `NOLOGIN`,拥有 schema/对象 | +| `eshop_migrator` | 仅 Migrator 使用,可创建/变更对象 | +| `eshop_app` | API/Worker 使用,只授予所需 DML、序列和函数执行,无 DDL | +| `eshop_readonly` | 可选诊断账号,只读且不得无控制导出敏感列 | + +- 运行账号不得是数据库超级用户或对象 Owner。 +- API 和 Worker 使用同一逻辑应用权限,但代码层继续遵守模块公开边界。 +- 不使用依赖连接池会话变量的 RLS 代替服务端资源归属校验。 +- Migration、备份和诊断凭据只通过环境 Secret/部署平台提供,不进仓库。 + +## 十四、完整性、并发与验收检查 + +### 14.1 必须成立的不变量 + +1. 数据库约束保证默认商家最多一个且只能是 Normal Merchant;初始化器原子创建一个,应用禁止删除、禁用或改角色,因此全局就绪环境运行期恰为一个。 +2. 每买家最多一个默认地址。 +3. 分类最多两级;根转子分类时没有子分类。 +4. Product 普通库存、活动库存、钱包余额永不为负。 +5. 已划拨活动始终 `remaining_stock + sold_count = allocated_quantity`。 +6. 买家秒杀占用不超过锁定活动限购;退款不释放限购,待支付取消释放。 +7. 订单总额等于订单项小计之和。 +8. 每订单最多一条 Payment;Paid 订单存在同 posting sequence 的唯一成功 Payment。 +9. 每个 `account_version >= 1` 的已提交资金版本恰有一条账本分录,前后余额链连续;版本 0 只允许零余额且没有分录。 +10. 每售后申请最多一个 RefundOperation;成功退款只有一条钱包贷记和一次原路库存回补。 +11. 每订单项处理中 + 已退款数量不超过购买量。 +12. 一个事件对固定接收人/类型最多一条消息;Inbox 与全部消息同成同败。 +13. Completed 幂等记录必有完整可重放结果;未知基础设施结果不伪造 Completed。 +14. 搜索投影 `source_hash` 必须等于商品名、分类名和描述的规范哈希;它可以从权威表重建,库存、价格和状态版本不参与比较。 +15. Normal 订单全部订单项都来自 Catalog;Seckill 订单恰有一项,且订单、订单项、活动与商品来源一致。 +16. 同一原子财务结果在参与表复用同一 posting sequence 和 posting_time;每张参与事实表内该 sequence 最多一行。 +17. Shipped/Completed 订单的订单项已发货量等于购买量减已退款量,且整单已发货总量大于 0;全部数量已退款的订单不能发货。 + +### 14.2 后续实现必须覆盖的数据库测试 + +- 唯一约束:手机号、用户名、默认商家、默认地址、收藏、购物车、评价、订单支付、售后退款、回调 ID、幂等作用域。 +- Check:状态字段组合、金额/库存非负、活动数量守恒、钱包借贷平衡、售后状态字段。 +- 跨表不变量:发布初始划拨与 DB044 对齐、生命周期任务到期时间绑定、订单头/订单项类型一致、发货量与售后退款量一致且非零、Completed 售后完成时间绑定、状态历史 actor 归属、退款执行人归属、posting sequence 时间与序号一致。 +- 并发: + - 同手机号注册; + - 默认地址切换; + - 商品版本编辑与库存扣减; + - 普通/秒杀超卖; + - 秒杀限购; + - 支付/主动取消/超时取消; + - 发货/售后申请; + - 部分售后并发; + - 退款重试与恢复; + - 回调乱序和双支付来源; + - 消息整事件消费; + - 确定非法消息进入 Rejected/死信、瞬态校验失败重投; + - 对账领取/接管/解决; + - C07 Immediate/Delayed 重投、延迟事件唯一去重与不阻塞双删。 +- 故障恢复:对象写入后数据库失败、Outbox Confirm 后标记前崩溃、Worker lease 过期、退款 Unknown、Redis 丢失后的安全事实重建。 -跨模块物理外键如需建立,必须由双方确认删除行为;禁止跨模块级联删除,也不得借外键直接读写其他模块内部表。 +## 十五、流程与接口审计中采用的最优解 -### 4.3 ER 图 +| 问题 | 最终决策 | +|---|---| +| 个人 DB 原稿与主文档 | 取消分成员设计;只维护本主文档 | +| 购物车“主键 `(buyer,product)`”与 `cartItemId` 冲突 | `id` 为物理 PK,二元组为唯一业务键 | +| 浏览历史与设置混在 DB006 | 独立 DB007;无记录逻辑默认开启 | +| Redis 丢失后令牌撤销无来源 | DB004 为 PostgreSQL 权威事实,Redis 只作镜像 | +| 分类最多两级无法普通 Check | 目标/父级稳定行锁 + 自 FK/唯一约束 | +| 商品/评价并发图片上限无法普通 Check | 商品行或订单项 advisory lock + 计数 + 唯一主图/顺序索引 | +| A122 跨数据库与对象存储 | DB104 持久化 Processing 工作清单并可恢复;完成结果仍原子落库 | +| C04 “N-gram 最小/最大”未定 | 选型固定 `pg_trgm`,N=3;短关键词 ILIKE 保召回 | +| 秒杀 Draft 到期后既不结束又不能取消 | Draft 任何时间可取消;已发布活动到期自然结束 | +| A223 可选 reason 但响应要求非空 | 缺省写稳定 `merchant_cancelled` | +| 秒杀 `frozenCount` 旧说法 | 本期不建;活动库存只维护划拨、剩余、已售守恒 | +| 回调 ID 与渠道流水都被误当唯一回调键 | DB084 流水唯一,DB089 允许一流水多 callbackId | +| 仅 `watermarkAt` 可能漏并发晚提交 | DB096 严格 posting sequence + 展示 watermarkAt | +| 财务时间无法在事务内取得物理 COMMIT instant | 以持 DB096 写锁后取得的 posting_time 作为权威财务业务时间;共享水位屏障保证不漏账 | +| 订单状态历史是否另建表 | 五态线性且不回退,直接由唯一时间字段生成;不重复建表 | +| 售后状态历史是否另建表 | 存在 RefundFailed→Refunding 循环,必须有 DB087 | +| 部分售后是否建冗余余额表 | 统一锁 orders + 索引聚合,避免冗余计数漂移 | +| A416/A417/A419 重放首次命令又要显示退款进展 | DB104 `resource_current` 固定资源身份,再读同一 RefundOperation 当前态 | +| Outbox 发布恰好一次 | 接受至少一次发布窗口;DB103 Inbox 保证业务效果一次 | +| C07 双删若让消费者等待 3 秒会阻塞吞吐 | Immediate 消费后登记唯一 Delayed Outbox;两阶段分别确认、重投和幂等删除 | +| 跨模块原子性与模块边界 | 公开应用能力加入同一 Npgsql 事务;调用方仍不得取其他模块 DbSet/仓储 | +| RLS 是否作为权限方案 | 不采用;服务端 Policy/资源归属校验 + 最小数据库账号权限 | -六份原稿通过评审后,由罗皓晨生成全局 ER 图。ER 图覆盖已确认的主键、主要外键和关系基数,但不替代字段、约束、索引和状态定义。 +## 十六、实施边界与冻结条件 -当前状态:**待汇总**。 +本文已经完成表、字段、类型、状态、关系、约束、索引、并发、幂等、可靠事件、财务水位、对象补偿、清理、Migration 与 Seed 的完整设计,可作为后续实现事实源。 -## 五、冻结与 Migration +当前明确未完成的是: -主文档达到以下条件后才能冻结: +- EF Core 实体、配置、DbContext; +- `InitialEshopSchema` Migration; +- 初始化器与演示对象; +- OpenAPI、后端、前端和真实测试; +- 生产容量参数、备份周期与压测后的索引复核。 -- 六份个人原稿均完成自审和交叉评审。 -- 统一表清单不再包含“待汇总”占位。 -- 每张表的字段、约束、索引、关系、状态和关联接口完整。 -- DBxxx、表名、约束名和索引名无重复。 -- 跨模块引用、订单快照和数据所有权已经双方确认。 -- 全局 ER 图和表创建依赖顺序完整。 -- [《接口设计》](接口设计.md)第五章中会影响表结构的阻塞项已解决,或相关表继续标记为“部分定义”。 +后续开始真实数据库实施前必须: -只有在主文档中标记为“已确认”的表才能创建 EF Core 实体和 Migration。初始化与 Seed 必须遵守已确认约束及教师演示数据要求,不得使用真实个人敏感信息。 +1. 主接口的“关联数据表”全部同步到本文; +2. 流程/需求中“待数据库设计”的成熟度说明改为“数据库设计已完成,待实现”; +3. 实体与映射逐表对应本文,不新增未评审表或字段; +4. 先生成 Migration SQL 并审查扩展、约束、索引、FK 和删除行为; +5. 用真实 PostgreSQL 执行映射测试、Migration 正反向验证和并发集成测试; +6. 只有验证通过后,才把“数据库实现”标记为完成。 -- Gitee From 0d8a19ff85e324d42d7c57e090358af662e22c6f Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Sat, 25 Jul 2026 16:57:56 +0800 Subject: [PATCH 112/118] =?UTF-8?q?docs(process):=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E8=B4=AD=E7=89=A9=E9=97=AD=E7=8E=AF=E6=95=B4=E4=BD=93=E5=AE=A1?= =?UTF-8?q?=E6=9F=A5=EF=BC=9B=E8=A1=A5=E9=BD=90=E5=9F=BA=E7=A1=80=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E4=B8=8E=E5=BC=82=E5=B8=B8=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...74\350\257\264\346\230\216\344\271\246.md" | 416 ++++++++++-------- ...34\347\264\242\346\265\201\347\250\213.md" | 10 +- ...06\345\223\201\346\265\201\347\250\213.md" | 26 +- ...41\347\220\206\346\265\201\347\250\213.md" | 39 +- ...04\344\273\267\346\265\201\347\250\213.md" | 37 +- ...50\351\200\201\346\265\201\347\250\213.md" | 134 ++++-- ...23\345\255\230\346\265\201\347\250\213.md" | 145 +++--- ...57\347\224\250\346\265\201\347\250\213.md" | 88 ++-- ...10\346\201\257\346\265\201\347\250\213.md" | 198 ++++++--- ...50\345\206\214\346\265\201\347\250\213.md" | 4 +- ...00\345\207\272\346\265\201\347\250\213.md" | 63 ++- ...60\345\235\200\346\265\201\347\250\213.md" | 43 +- ...41\347\220\206\346\265\201\347\250\213.md" | 8 +- ...06\345\217\262\346\265\201\347\250\213.md" | 16 +- ...05\346\227\266\346\265\201\347\250\213.md" | 12 +- ...42\345\215\225\346\265\201\347\250\213.md" | 180 ++++++-- ...45\347\272\246\346\265\201\347\250\213.md" | 46 +- ...22\346\235\200\346\265\201\347\250\213.md" | 80 ++-- ...51\350\275\246\346\265\201\347\250\213.md" | 126 +++--- ...71\350\264\246\346\265\201\347\250\213.md" | 200 ++++++--- ...57\344\273\230\346\265\201\347\250\213.md" | 60 ++- ...56\345\220\216\346\265\201\347\250\213.md" | 162 +++++-- ...01\347\250\213\350\256\276\350\256\241.md" | 287 ++++++++---- 23 files changed, 1590 insertions(+), 790 deletions(-) diff --git "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" index 4b10f21..4da4e9c 100644 --- "a/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" +++ "b/docs/01-\351\234\200\346\261\202\346\226\207\346\241\243/\351\234\200\346\261\202\350\247\204\346\240\274\350\257\264\346\230\216\344\271\246.md" @@ -340,19 +340,20 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | M01-02-FR01 | 登录表单 | 只接收手机号和密码,并提供必要格式校验 | | M01-02-FR02 | 凭据校验 | 通过手机号定位账号并安全验证密码;账号不存在或密码错误统一返回“账号或密码错误” | | M01-02-FR03 | 状态校验 | 禁用账号不得登录;错误文案应说明账号停用但不暴露内部信息 | -| M01-02-FR04 | 登录凭证 | 登录成功后只签发一个有明确有效期的 JWT 登录凭证,不提供刷新凭证,并保证多实例访问结果一致 | +| M01-02-FR04 | 登录凭证 | 登录成功后只签发一个有明确有效期的 JWT 登录凭证,不提供刷新凭证,并保证多实例访问结果一致。JWT 有效期校验固定 `ClockSkew=0`,令牌在 `exp` 到达时立即失效;两个 API、Hub 与健康检查使用同一认证配置摘要,禁止沿用框架默认偏差 | | M01-02-FR05 | 登录态恢复 | 页面刷新后根据有效令牌恢复账号摘要和正确路由 | | M01-02-FR06 | 角色路由 | 登录成功后按服务端确认的角色进入购物端、商家端或管理端 | -| M01-02-FR07 | 安全退出 | 前端立即停止发起受保护请求并清理本地凭证,服务端使当前令牌在自然过期前不可继续使用;退出只影响当前令牌,撤销结果无法确认时不得显示退出成功 | +| M01-02-FR07 | 安全退出 | 前端立即停止发起受保护请求并清理本地凭证;服务端用一次 PostgreSQL `decisionTime` 裁决:`decisionTime < JWT exp` 时原子撤销当前令牌,`decisionTime >= JWT exp` 时按已自然失效确定收敛且不伪造撤销记录。退出只影响当前令牌;仍需撤销但提交结果无法确认时不得显示退出成功 | | M01-02-FR08 | 用户反馈 | 登录、恢复和退出过程均有明确状态,重复点击不得产生混乱结果 | +| M01-02-FR09 | 安全意图与返回目标 | PC Web 在当前浏览器会话分别保存最多一个 `actionIntent` 和一个 `returnDestination`,二者不得合并为 URL。游客从商品详情触发收藏或加购时,`actionIntent` 只含 `action + productId + quantity? + actionKey`;访问登录后页面时,`returnDestination` 只含页面白名单枚举及该页面必要的一个规范 UUID。Buyer 登录后先按白名单恢复目标页;有动作意图时再重读 A103,并以 `Favorite → A019`、`AddToCart → A201 且 Idempotency-Key=actionKey` 恢复原动作。动作只有在确定 `2xx` 或确定业务 `4xx` 后才消费;网络中断、超时、`503` 或提交结果未知时保留同一意图和 `actionKey`。商家/管理员登录、显式退出、字段非法时清除两类状态。 | #### 4. 主流程 1. 用户输入手机号和密码并提交。 2. 服务端校验格式、账号状态和密码哈希。 3. 校验成功后签发 JWT,返回账号摘要和角色。 -4. 前端保存登录态,按角色进入对应端并展示用户名与头像。 -5. 用户刷新页面时验证令牌并恢复状态;退出时前端立即停止受保护请求、清理本地凭证,并由服务端登记当前令牌失效。 +4. 前端保存登录态:存在合法白名单意图且角色为 Buyer 时,先回到目标商品并按最新事实恢复收藏或加购;否则按角色进入对应端并展示用户名与头像。 +5. 用户刷新页面时验证令牌并恢复状态;退出时前端立即停止受保护请求、清理本地凭证,服务端在令牌尚未自然到期时登记当前令牌撤销,在数据库裁决时已经到达 `exp` 则直接确认其自然失效。 #### 5. 业务规则与权限 @@ -360,10 +361,15 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, - 被禁用账号返回明确停用提示,并禁止签发新令牌。 - 401 表示未认证或登录已失效,403 表示身份有效但无权执行当前操作。 - 退出只影响当前令牌;多设备退出范围如需扩大,必须另行确认并写入接口设计。 +- A003 进入处理前仍按 `ClockSkew=0` 校验自然有效期;通过后必须在数据库事务内只读取一次 `decisionTime`。若该时点仍早于 `exp`,撤销事实与安全广播责任必须原子提交;若该时点已达到 `exp`,令牌已经由签名有效期确定失效,固定返回“已自然失效”,不得倒填早于当前时间的撤销时间、放宽约束或创建无意义广播。 - 手机号修改或账号禁用会使该账号此前签发的全部登录凭证失效;重新启用账号只允许重新登录,不恢复任何旧凭证。 -- 本期不提供刷新凭证;登录凭证到期、退出或失效后必须重新提交手机号和密码登录。 +- 本期不提供刷新凭证;JWT 验证固定 `ClockSkew=0`,到达 `exp` 即失效;登录凭证到期、退出或失效后必须重新提交手机号和密码登录。两个 API 和 SignalR Hub 的 Issuer、Audience、签名材料标识、有效期规则、ClockSkew 与令牌版本规则必须形成同一脱敏配置摘要,摘要不一致的实例不得承接受保护流量。 - 系统无法确认登录凭证是否已经失效时不得继续放行,必须明确提示服务暂不可用。 - 前端路由不能代替后端授权,角色和资源归属均以服务端为准。 +- `actionIntent` 只允许 `Favorite`、`AddToCart` 两种动作以及标准 UUID 商品 ID、加购时的正整数数量和客户端预生成的动作幂等键,不接受服务端路径、外部 URL、角色、价格、金额、库存或用户 ID。恢复时必须重新调用 A103;收藏调用天然幂等的 A019,加购调用 A201 并把原 `actionKey` 原样作为 `Idempotency-Key`。确定 `2xx` 后清除;商品下架、售罄、数量超限等确定 `4xx` 时停在商品详情、展示最新原因后清除;网络中断、超时、`503` 或提交结果未知时保留并复用同一键,不能另造键重复加购。 +- `actionIntent` 只允许从同一商品详情的收藏/加购登录门创建;它存在时,`returnDestination` 必须为空或精确为 `ProductDetail(actionIntent.productId)`。两者商品不一致,或动作意图与 Checkout、Cashier、PaymentResult、Orders、AfterSales 等其他目标并存时,整组状态视为非法并在登录后清除,不导航到可疑目标、不重放动作。这样既保留动作恢复,也不会在结算、支付或订单页面后台改变购物车。 +- `returnDestination` 是独立的一次性页面目标,只允许 `ProductDetail(productId)`、`SeckillActivityDetail(activityId)`、`Cart`、`Checkout`、`Cashier(orderId)`、`PaymentResult(orderId)`、`Wallet`、`TopupRecords`、`PaymentRecords`、`PaymentDetail(paymentId)`、`Profile`、`Addresses`、`Orders`、`OrderDetail(orderId)`、`Favorites`、`BrowsingHistory`、`Messages`、`MessageDetail(messageId)`、`AfterSalesList`、`AfterSalesDetail(requestId)`;除括号内标准 UUID 外不得携带查询串、片段、协议、主机、路径、脚本或任意参数。Buyer 登录后消费一次并由目标页面重新鉴权/加载;`Checkout` 必须重新调用 A208,`Cashier/PaymentResult` 必须重新读取 A404/A406,`SeckillActivityDetail` 必须重新读取 A227,均只恢复安全页面而不保存或自动重放提交动作。目标不存在或无权时进入对应安全列表页。非 Buyer 登录、显式退出、字段非法或消费完成立即清除。 +- `Favorite`/`AddToCart` 可在 Buyer 登录后执行一次原动作。本期需求没有独立“立即购买”能力,商品购买统一走“加入购物车 → 选择条目 → 结算确认 → 主动提交订单”,不得为对标外部平台另建临时购买意图、第二套订单入口或自动下单。 - 本期交付入口仅覆盖 PC Web;Electron 和 Android 仅作为后续客户端规划,不属于本流程当前验收范围。 #### 6. 异常与边界场景 @@ -373,8 +379,12 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | 手机号或密码错误 | 返回统一错误,不说明具体错误字段 | | 账号被禁用 | 拒绝登录并提示联系管理员 | | 登录凭证过期、无效、已退出或属于修改手机号/禁用账号前签发 | 清理失效登录态并引导重新登录 | +| 退出请求进入时令牌有效,但数据库裁决时刚好到达 `exp` | 确认退出结果为“已自然失效”,不写撤销记录或安全广播;不得因 `expires_at > revoked_at` 约束失败误报 503 | | 买家访问商家或管理接口 | 返回 403,不泄露目标资源内容 | | 商家从购物端入口登录 | 登录成功后按角色跳转商家端,不误报密码错误 | +| 商家或管理员携带买家白名单意图登录 | 清除意图并按真实角色进入对应端,不执行买家操作 | +| Buyer 登录后目标商品已下架、售罄或数量超限 | A103 或写接口返回确定业务结果后,返回目标详情、展示最新不可执行原因并清除该意图;不收藏、不加购、不进入无效结算 | +| 恢复收藏或加购时网络中断、超时、`503` 或结果未知 | 保留同一意图与 `actionKey`;恢复后重读 A103,并以 A019 天然幂等或 A201 原 Idempotency-Key 安全重试,不重复累加 | | 登录凭证失效能力暂时不可用 | 无法确认登录态的受保护请求提示服务暂不可用;公开且不依赖认证的能力不受影响 | #### 7. 验收标准与证据 @@ -409,7 +419,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | M01-03-FR01 | 资料查看 | 展示用户名、默认头像和掩码手机号等本人资料 | | M01-03-FR02 | 用户名修改 | 本项目期内允许用户自助重置一次;重置时由服务端按 M01-01-FR05 重新生成唯一用户名,不接受客户端指定任意用户名 | | M01-03-FR03 | 手机号修改 | 要求重新验证当前密码,新手机号遵守 M01-01-FR02 格式与唯一性;修改成功后立即使该用户修改前签发的全部令牌失效并要求重新登录 | -| M01-03-FR04 | 地址列表 | 查询本人地址,清楚标记默认地址 | +| M01-03-FR04 | 地址列表 | 查询本人地址,清楚标记默认地址并返回正整数 `version`,供结算确认后防止地址被并发编辑、删除或替换 | | M01-03-FR05 | 新增地址 | 录入收件人、手机号、省市区和详细地址等必填信息;新地址固定为非默认地址,如需设为默认必须另行执行默认地址切换 | | M01-03-FR06 | 编辑地址 | 只能修改本人地址,并重新校验全部字段 | | M01-03-FR07 | 删除地址 | 删除本人地址;删除默认地址时不静默指定其他地址 | @@ -422,15 +432,17 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, 2. 修改用户名或手机号时,页面展示规则和可能影响;敏感修改按要求完成身份校验。 3. 服务端校验当前买家、字段格式和唯一性,成功后保存并返回最新资料。 4. 买家新增、编辑、删除或设置默认地址,服务端始终按当前用户过滤资源。 -5. 手机号修改成功后旧登录态失效;下单时订单模块重新校验所选地址归属。 +5. 手机号修改成功后旧登录态失效;下单时订单模块在同一外层事务通过 Identity 公开能力锁定所选地址,同时校验本人归属与买家确认的 `addressVersion` 后才保存地址快照。 #### 5. 业务规则与权限 - 用户名只用于展示,全站唯一;每位买家在本项目期内最多自助重置一次,成功后页面立即刷新为新用户名。 - 用户名重置机会只在新用户名成功提交后消耗;两个并发重置请求最多一个成功,失败请求不得额外消耗次数。 - 手机号变更属于敏感操作,必须重新验证当前密码;成功后撤销修改前签发的全部令牌并要求重新登录。 +- 当前密码错误是“当前有效会话内的敏感操作校验失败”,不得返回会触发全局登出的 401;固定返回 `400 AUTH.CURRENT_PASSWORD_INCORRECT`,账号资料、令牌版本和当前登录态均不改变。只有 Bearer 本身无效、到期、撤销或版本失效才返回 401。 - 两个基于同一旧手机号状态的并发修改请求最多一个成功;后到请求必须按资料已变化处理,不能覆盖先成功结果。 - 地址必须属于当前买家;收件人、联系电话、省市区和详细地址为必填。 +- 地址创建时 `version=1`;编辑、设置/取消默认状态等任何会改变当前地址事实的成功写入均递增版本。结算页确认的地址 ID 与版本必须一起提交,A301 锁内发现地址已删除、不归属或版本变化时不得使用旧页面内容静默下单。 - 新增地址不接受默认标记;设置默认地址必须通过独立动作完成。本期不设置单买家地址数量上限。 - 每名买家最多一个默认地址,默认地址切换需原子完成或提供等效一致性保障。 - 删除默认地址后不自动选择其他地址,下单时由买家明确确认。 @@ -555,17 +567,17 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | M02-02-FR02 | 图片浏览 | 主图加载失败时使用占位图;多图可切换且提供可理解的替代文本 | | M02-02-FR03 | 可售状态 | 根据服务端上下架状态与库存决定操作可用性;库存不足时禁止进入无效购买流程 | | M02-02-FR04 | 买家操作 | 买家可收藏、加购或购买;数量校验与最终价格、库存仍由对应模块和服务端负责 | -| M02-02-FR05 | 游客衔接 | 游客触发登录后保留目标商品和原操作意图,减少重复查找 | +| M02-02-FR05 | 游客衔接 | 游客触发登录后按 M01-02-FR09 保留一次性白名单意图;Buyer 登录后重新读取商品并恢复原动作,其他角色或无效意图清除,不允许任意 URL 回跳 | | M02-02-FR06 | 评价展示 | 已选 X01 时展示评分汇总和公开评价列表入口;评价提交资格由 M07 判断 | | M02-02-FR07 | 状态反馈 | 提供加载、无数据、图片失败、商品不存在和商品不可售状态,并给出返回列表入口 | | M02-02-FR08 | 数据刷新 | 后台改价、改库存、上下架或修改内容后,详情按 C07 已确认的一致性窗口收敛到 PostgreSQL 最新值;超过窗口不得继续返回旧值,购买动作始终实时重检 | -| M02-02-FR09 | 内容安全 | 商品描述按受控内容展示,不执行脚本或不可信嵌入内容 | +| M02-02-FR09 | 内容安全 | 商品描述固定为纯文本:服务端规范化换行并按 Unicode 字符数校验,客户端只用文本插值并保留换行,禁止按 HTML 解释、执行脚本或加载不可信嵌入内容 | #### 4. 主流程 1. 用户从列表、搜索、收藏或历史记录进入商品详情。 2. 页面加载商品公开信息,并根据身份决定显示购买操作、登录引导或管理入口。 -3. 商品可售时,买家选择数量后进入收藏、加购或购买流程;服务端再次校验价格、库存和状态。 +3. 商品可售时,买家选择数量后可以收藏或加入购物车;购买统一从 M03 已选条目进入结算并由 M04 提交订单,服务端再次校验价格、库存和状态。 4. 用户查看公开评价;只有满足 M07 条件的买家才能从订单入口提交评价。 5. 商品不存在、已下架或加载失败时,页面展示明确状态并允许返回商品列表或重试。 @@ -576,6 +588,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, - 展示价格、库存和状态以服务端结果为准;C07 仅允许商品详情在有限一致性窗口内出现旧公开值,页面缓存和 Redis 均不得作为下单依据。 - 商品所属分类被停用时,商品只要仍为已上架就继续公开;分类停用只影响筛选入口,不改变商品销售状态。 - 买家专属操作必须同时受前端入口和后端角色、资源归属校验保护。 +- “加入购物车”只形成购物车意图;本期不提供独立“立即购买”入口。买家必须在购物车选择目标条目并进入结算页,再确认地址后主动提交 Ordering。 - 商品图片由 S3 Compatible Object Storage 提供;单个商品最多 8 张,单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 400~4096 像素;第一张作为主图并生成方形缩略图。 - 本期不提供关联推荐、商家评价回复、评价点赞或通过详情页直接编辑商品。 @@ -604,7 +617,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, #### 1. 功能目标与范围 -购物车是买家在下单前的临时容器和服务端计价的唯一来源,本期面向已登录买家提供可持久化的购物车,覆盖查看列表、加入商品、修改数量、删除条目、选择结算条目和在下单成功后清理已结算条目。购物车数据由数据库持久化,前端缓存和角标数字仅用于展示,最终一致性以服务端为准。 +购物车是买家在下单前的临时容器,也是“选中集合与结算预览”的服务端事实来源。本期面向已登录买家提供可持久化的购物车,覆盖查看列表、加入商品、修改数量、删除条目、选择结算条目和在下单成功后清理已结算条目。Cart 只按当前商品事实计算预览金额;Ordering 在提交订单事务中重新读取商品状态、价格、库存和数量并形成最终订单金额,任何购物车预览值都不能直接成为订单事实。购物车数据由数据库持久化,前端缓存和角标数字仅用于展示,最终一致性以服务端为准。 本期购物车是“结算前的暂存区”,不是营销、推荐、优惠和凑单的载体;选中和未选中的状态、价格、库存上限全部由服务端实时计算并返回,前端不得绕过校验直接生成订单。买家刷新、重新登录、关闭浏览器后购物车应当完整保留,断网期间未发出的加车请求必须可重试,不应产生重复条目。 @@ -630,7 +643,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | 商品模块 | 提供已上架商品的实时价格和可售库存;下架或库存变更后通知购物车重新标记失效条目 | | 订单模块 | 提交订单成功后通知购物车清理对应条目;订单回滚或取消时按本期规则保留购物车条目 | | 秒杀模块(C01) | 秒杀立即抢购绕过购物车,由 Seckill 与 Ordering 独立完成资格、限购、库存和订单校验,不读写购物车条目 | -| Cart 模块 | 校验买家身份、维护条目唯一性、执行数量上下限、服务端计价、清理失效条目、保证下单前后一致性 | +| Cart 模块 | 校验买家身份、维护条目唯一性、执行数量上下限、计算服务端预览金额、清理失效条目、向 Ordering 提供本人选中集合 | 购物车数据归属只以买家 ID 为准,角色只决定接口是否可见和可写。商家和管理员不接收任何与购物车结构相关的私人通知,业务模块不允许直接读写其他模块内部的购物车实例。 @@ -641,18 +654,18 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, | F07-FR01 | 加入购物车 | 登录买家提交商品 ID 与数量;商品必须已上架且库存足够;同一买家同一商品只保留一条,重复加入将新数量累加到已有条目,总和不超过实时可售库存;返回当前条目最新数量、小计和生效时间。 | | F07-FR02 | 查看购物车 | 买家查看自己的购物车,支持全量或分页;返回条目 ID、商品基础信息、主图、实时单价、当前数量、小计、是否选中、是否可用、失效原因、最近修改时间;不返回他人或他人的历史条目。 | | F07-FR03 | 修改数量 | 买家将自己的条目数量改为新值;新数量必须为正整数且不大于商品当前可售库存;调小或删除不受库存上限影响,但不允许改为 0 或负数;接口返回最新数量、新的小计和最大允许值。 | -| F07-FR04 | 删除条目 | 买家可单条或多条删除自己的购物车条目;条目不存在或属于他人时拒绝并返回资源不存在或无权限;幂等执行,多次删除同一 ID 结果一致。 | -| F07-FR05 | 全选与单选 | 买家对可见且可用的条目进行全选、反选和单条切换;选中状态保存在服务端;刷新后状态保留;失效条目不允许被选中。 | -| F07-FR06 | 服务端计价 | 选中条目总额由服务端按实时单价计算并返回;前端可本地显示,但结算和下单一律以服务端返回的金额为准,金额不接受客户端传入。 | +| F07-FR04 | 删除条目 | 买家可单条或批量删除购物车条目。单条删除必须按 `(cartItemId, currentBuyerId)` 条件执行,合法身份和合法路由参数下无论命中 1 行还是 0 行都返回相同的无内容成功结果,使重复删除天然幂等且不泄露条目是否存在或属于他人。批量删除必须携带稳定请求标识,先去重并校验 1~100 个目标全部属于当前买家,再在一个事务中全部删除;任一目标不存在或不属于本人时整批拒绝且零删除,同标识同请求重放首次确定结果。 | +| F07-FR05 | 全选与单选 | 买家对本人条目进行全选、全不选、反选和显式单条/多条设置;选中状态保存在服务端。已选条目后来失效时 GET 不写库,合法返回“已选但失效”,仍可取消选择;任何选择命令都不得把当前失效条目从未选变为已选,且全选/反选会把失效条目规范为未选。选择命令只返回可幂等重放的稳定变更摘要,客户端另行分页刷新购物车;纯取消选择不依赖 Catalog,刷新失败也不回滚已提交选择。 | +| F07-FR06 | 服务端计价 | Cart 按当前买家最多 100 个已选条目,或调用方明确给出的 1~100 个已选 `cartItemId` 子集,实时计算并返回结算预览金额与 `checkoutRevision`;目标必须全部属于本人、仍被选中且可结算,任一无效时整体不可结算。前端只能展示,不能提交或修改最终金额。提交订单时 Ordering 必须在同一事务中重新读取同一子集的商品状态、展示快照、价格、库存、数量与版本并重新计算;内容相对 Revision 实质变化时返回最新预览并要求再次确认,不得按未确认的新数量或金额静默下单。 | | F07-FR07 | 失效标记 | 商品状态为 `Draft` / `OffSale` 或实时可售库存归零时,购物车中对应条目标记为不可结算,保留可见、可删、可下调,但不允许调大、累加或选中进入结算;分类停用不反向改变仍为 `OnSale` 商品的可结算资格;前端用明确文案展示失效原因。 | | F07-FR08 | 数量上限 | 加车和改数量必须实时校验库存;超过上限时拒绝并返回当前最大可设值;不依赖前端控制,避免被绕过。 | | F07-FR09 | 下单清理 | 提交订单成功后,订单模块在同一事务中删除该订单覆盖的全部购物车条目;若订单事务回滚,则购物车条目保留原状。 | -| F07-FR10 | 清空购物车 | 买家可一键清空自己购物车中的全部条目;幂等且只影响本人;已失效条目一并清理。 | +| F07-FR10 | 清空购物车 | 买家可一键清空自己购物车中的全部条目;服务端按当前买家条件删除,购物车原本为空或重复调用时仍返回相同的无内容成功结果,不要求额外幂等标识;只影响本人,已失效条目一并清理。 | | F07-FR11 | 接口幂等 | 加入购物车接受客户端可选的请求幂等标识;相同标识在接口约定窗口内重复提交只生效一次,重复请求返回同一结果且不重复累加数量。 | | F07-FR12 | 数据隔离 | 所有读写接口都必须同时按买家 ID 过滤;条目 ID 必须与当前买家身份匹配,禁止仅凭条目 ID 查询或修改。 | | F07-FR13 | 实时校验 | 加车、改数量、选择结算和下单前都必须在服务端再次校验上下架、库存和归属;不允许客户端跳过校验直接下单。 | | F07-FR14 | 日志可追踪 | 加入、修改、删除、清空、下单清理、失败回滚均记录买家 ID、商品 ID、操作类型、数量调整、原因码和 traceId;不记录完整 Token、密码或卡号。 | -| F07-FR15 | 界面与可访问性 | 购物车页统一提供加载占位、空状态、错误重试、删除/数量修改立即反馈;选中与失效状态用一致的颜色和文案区分;最大可设库存、失效原因和最低可结算门槛可直接看到。 | +| F07-FR15 | 界面与可访问性 | 购物车页统一提供加载占位、空状态、错误重试、删除/数量修改立即反馈;可用/失效与已选/未选两个维度必须分别表达;最大可设库存、失效原因和结算门槛可直接看到。本期没有额外最低金额门槛,只有目标集合至少 1 项、全部可结算且服务端总额大于 0 才可继续。 | #### 4. 主流程 @@ -660,20 +673,21 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, 买家进入购物车页,服务端返回当前买家全部条目及实时单价;商品为 `Draft` / `OffSale` 或实时可售库存归零时,条目标记为不可结算并附原因,仍可显示、下调数量或删除。分类停用不反向改变仍为 `OnSale` 商品的可结算资格。买家调整数量、删除条目或切换选中状态,每次操作都即时写库;选中与未选中状态以服务端记录为准。 -买家点“去结算”,服务端再次校验选中条目的上下架、库存、数量上限和归属。通过则把选中条目和实时总价交给订单模块并进入提交订单流程;不通过则返回失败原因并把对应条目标记失效。提交订单成功后,订单模块在同一事务中删除选中条目;订单回滚则购物车原状保留。 +买家点“去结算”时,不带显式子集则使用当前全部持久化已选条目,但同样最多 100 个;页面也可以把当前准备结算的 1~100 个已选 `cartItemId` 作为显式子集,以固定本次结算目标而不混入之后新选中的条目。Cart 再次校验目标条目的上下架、库存、数量上限、选中状态和归属,并返回完全相同的目标集合、服务端预览金额与 `checkoutRevision`;已选后失效的条目仍在目标集合中并阻断整次预览,不得被静默丢弃。不通过则返回全部问题条目及原因,不把可用子集包装成可直接下单结果。买家提交订单时只提交同一购物车条目标识子集、Revision、本人地址标识和订单幂等标识,不提交最终金额;Ordering 在下单事务中重新读取全部事实并计算最终金额。Revision 对应内容发生变化时整次拒绝并返回最新预览,买家确认后用新 Key 提交;成功后 Ordering 在同一事务中只删除本次已结算条目,订单回滚则购物车原状保留。 买家手动清空购物车时,服务端幂等删除全部条目;超时或异常场景下买家重新进入购物车应当看到与服务端一致的数据,不依赖前端缓存。 #### 5. 业务规则与权限 -- 购物车主键为 `(买家ID, 商品ID)`,同一组合在同一购物车中只允许一条;重复加入按累加处理,禁止多行同时存在。 +- 每个购物车条目使用独立 `cartItemId` 作为资源标识,`(买家ID, 商品ID)` 是业务唯一键而不是物理主键;同一组合只允许一条,重复加入按累加处理,禁止多行同时存在。 - 数量约束:每次加车和修改都必须满足 `1 ≤ 数量 ≤ 商品当前实时可售库存`;调小或删除不受上限约束,但不允许设为 0 或负数。 -- 价格与计价:单价取商品当前上架价格,不在下单前生成最终快照;选中条目总额 = Σ(实时单价 × 当前数量),由服务端计算并返回。 +- 价格与计价:Cart 以商品当前上架价格计算 `预览总额 = Σ(实时单价 × 当前数量)`,不保存价格快照;Ordering 提交订单时重新读取并计算最终金额,只有订单事务提交后的订单项单价和订单总额才是持久化交易事实。 - 商品状态与库存:商品为 `Draft` / `OffSale` 后,已加入条目仍可显示、可删、可下调,但禁止调大、累加或选中下单;实时可售库存归零同理。分类停用不反向改变仍为 `OnSale` 商品的可结算资格。 -- 结算准入:去结算前必须再次校验选中条目的上下架、库存和归属;任何一个条目不通过都拒绝整单并把对应条目标记失效。 +- 选择与失效是两个维度:持久化 `isSelected` 表示用户最近选择意图,实时 `isAvailable` 表示当前可结算资格。GET 不因商品后来失效写回选择状态;因此允许 `isSelected=true/isAvailable=false`,该条目计入已选数但不计入可结算金额。用户可显式取消选择;A206 的全选/反选和任何设为已选的动作都只允许当前可结算条目,并把当前失效条目规范为未选。若在用户发出新选择命令前商品恢复可售,原选择意图恢复为可结算选择。 +- 结算准入:去结算前必须再次校验目标条目的选中状态、上下架、库存和归属;A208 未提供显式 ID 时使用全部持久化已选条目,提供时只校验并返回该稳定去重子集,两种模式都限制 1~100 项。任何一个目标不通过都拒绝该结算集合并把对应条目标记失效,不能悄悄并入或丢弃其他条目。有效预览签发内容 Revision;下单重算不一致时零建单并要求确认新预览。结算没有额外金额下限,但目标必须至少 1 项、全部有效且服务端计算总额大于 0。 - 下单清理:订单提交与购物车清理在同一事务;订单回滚则购物车条目原状保留。买家主动取消或订单超时取消(C03)后,本期不自动恢复购物车条目,避免与重新加入的状态混淆。 -- 接口幂等:加车接受稳定的请求幂等标识;同一操作窗口内重复提交视为同一请求,结果相同且不重复累加。标识传递方式与有效窗口由 OpenAPI 和接口设计确定。 -- 数据归属:所有读写按 `(买家ID, 条目ID)` 或 `(买家ID, 商品ID)` 双重过滤;条目 ID 不允许跨用户访问,操作他人购物车返回资源不存在或无权限,不泄露条目是否存在。 +- 接口幂等:加车接受可选稳定请求标识;批量删除和包含 `Invert` 的选中变更必须携带稳定请求标识,同键同请求重放首次确定结果,同键换请求拒绝。单条删除和清空采用条件删除的天然幂等语义,不要求为无必要的 DELETE 操作持久化幂等结果。 +- 数据归属:所有读写按 `(买家ID, 条目ID)` 或 `(买家ID, 商品ID)` 双重过滤;条目 ID 不允许跨用户访问。读取、修改、选择和批量删除的无权目标统一按不可见资源处理;单条删除对未命中目标返回与已删除相同的 204,避免通过删除响应探测他人条目。 - 服务端为唯一事实来源:数据库为准,前端缓存只用于展示;刷新、重新登录和断线重连后均按服务端数据重新渲染。 - 时间口径:服务端统一使用 UTC 写入 `createdAt` / `updatedAt`,前端按用户时区展示。 - 库存协作:购物车不预先占用库存;普通订单在提交事务中扣减库存。C01 秒杀路径绕过购物车,由 Seckill 与 Ordering 使用独立的限购、条件扣减和订单创建契约。 @@ -684,25 +698,26 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, - 商品下架或库存归零:加入和调大被拒绝;已加入条目标记失效,下单前再次校验;失效原因在购物车页明确展示。 - 数量非法:传入 0、负数、非整数或超过库存上限时拒绝,返回当前最大允许值和原因;前端据此自动截断到上限。 - 重复加入与幂等:携带同一请求幂等标识的请求在接口约定窗口内重复提交只生效一次,返回同一结果且不重复累加。 -- 并发修改:同一买家对同一商品连续“修改数量 + 删除”或两次“修改数量”,以数据库最终落库为准;条目被删除后再修改返回资源不存在,提示刷新购物车。 +- 并发修改:同一条目并发“修改数量 + 删除”时,以条件更新实际命中结果裁决;删除先提交则修改返回资源不存在,修改先提交后仍可被删除。不得仅以“最后一次 HTTP 到达”为准,也不得在删除后把旧数量写回。 - 越权访问:买家 B 用买家 A 的条目 ID 访问或修改被拒绝,响应不暴露该条目是否存在,不返回条目归属信息。 -- 下单竞态:选中条目在结算瞬间被他人抢光或下架,对应条目标记失效后返回“已更新,请重新选择”,其余条目不连带失败。 +- 下单竞态:任一目标条目在提交瞬间被他人抢光、下架或发生未确认的结算内容变化时,整笔订单创建失败且零写入;响应逐项返回全部问题条目和稳定原因,同时返回仍可结算条目的最新预览。其他条目不会被误删、扣减或标记失败,原购物车保持不变;买家修正选择并确认新 Revision 后再整单提交,本期不自动拆出“可用子单”。 - 下单事务回滚:订单写入或库存扣减回滚,已选中的购物车条目保留,不会被错误清理。 - 订单取消或超时:买家主动取消或 C03 自动取消后,购物车本期不恢复对应条目;用户希望重新购买时需手动再次加车。 - C01 秒杀边界:立即抢购不加入或读取购物车;超出活动库存或个人限购时由 Seckill 明确拒绝,普通购物车条目不受影响。 - 接口异常与重试:网络失败、500 和 401 等场景给出明确错误:401 引导重新登录,400 显示字段问题,5xx 提供重试入口,避免页面整页刷新或丢失购物车状态。 - 失效条目清理:用户手动删除/清空购物车时同步清理;后台不主动物理删除失效条目,便于排查;后续如需归档清理应单独定义保留期。 -- 价格变动:商品改价、上下架或参与促销后,购物车再次展示时使用实时单价,金额随单价变化即时刷新,未支付不锁定价格。 +- 价格变动:商品改价或上下架后,购物车再次展示时使用实时单价,金额随单价变化即时刷新,未支付不锁定价格;本期不引入促销价、优惠分摊或第二套计价来源。 #### 7. 验收标准与证据 - 加车、修改、删除、单选/全选、清空和去结算接口全部覆盖,购物车表数据与接口返回完全一致;前端刷新和重新登录后数据不丢失。 - 同一买家同一商品多次加入只生成一条记录,数量按接口调用顺序正确累加,最终数量不超过实时库存上限。 - 修改数量超过商品实时可售库存时拒绝,返回最大可设值;改为 0、负数或非整数被拒绝并提示原因。 -- 选中条目总额由服务端按实时单价计算;前端篡改金额或数量再提交被服务端拒绝,未出现订单总额与数据库计算结果不一致的情况。 +- Cart 返回的选中条目总额只作为服务端预览;Ordering 下单事务重新读取并计算最终金额。前端篡改金额、数量或复用旧 Revision 不能改变订单;数量、展示快照、单价或资格变化时返回最新预览要求再次确认,订单总额必须等于事务内订单项快照小计之和。 - 商品为 `Draft` / `OffSale` 后,已加入条目在购物车页被标记“不可结算”,不可调大、不可累加、不能被勾选进入结算;实时可售库存为 0 时同样标记;分类停用不反向改变仍为 `OnSale` 商品的可结算资格。 - 失效条目可下调数量、可删除;只有商品重新处于 `OnSale` 且下调后的数量不超过实时可售库存时,条目才恢复正常可结算状态。 - 越权:用买家 B 身份请求买家 A 的条目被拒绝,响应不暴露该条目是否存在及归属信息。 +- 删除幂等:单条条目首次删除、重复删除、不存在或属于他人时在合法鉴权和参数前提下都返回 204 且只可能删除当前买家一行;批量删除中任一目标无效时整批零删除,同请求标识重试返回首次结果;清空空购物车和重复清空均返回 204。 - 下单成功后,对应购物车条目在同一事务内被清除;订单事务回滚时购物车条目原状保留,未出现“订单失败但条目丢失”的情况。 - 买家主动取消订单或 C03 自动取消后,对应购物车条目本期不自动恢复,符合本期范围说明。 - 并发:同一条目同时被改数量和删除,最终只出现删除结果或最新数量,两者不会同时生效导致数据错乱。 @@ -721,7 +736,7 @@ M00 是六人协作的内部公共基建模块,不新增课程功能编号, M04-01 要求买家选择购物车中的商品和收货地址完成订单提交,系统在单个数据库事务内完成库存原子扣减、订单与订单项创建、购物车清理,最终返回唯一订单号。核心目标是让买家下单"**提交即成功、状态无歧义、重复不双扣**":买家点下单后不会因网络抖动、重复点击或回调延迟看到"订单创建了但不知道有没有扣库存"的困惑状态。 -本期不实现多人拼单、优惠卷抵扣、地址新增后返回下单页等横向扩展功能。 +本期不实现多人拼单、优惠券抵扣等横向扩展功能。结算页直接复用 F03 的地址列表与新增地址能力:存在唯一默认地址时预选并突出展示,但买家提交前仍能切换和确认;没有默认地址时不擅自选择地址列表第一项。首次买家无地址或主动新增时,新增成功后保留当前 `cartItemIds + checkoutRevision`,回到同一结算上下文并由买家显式选择新地址;不新增接口、不自动设默认地址,也不自动提交订单。 #### 2. 身份处理与协作边界 @@ -738,30 +753,29 @@ M04-01 要求买家选择购物车中的商品和收货地址完成订单提交 | 编号 | 功能 | 详细要求 | |---|---|---| -| M04-01-FR01 | 购物车商品预校验 | 服务端接收买家提交的选择商品 ID 列表后,先校验:(1) 所有商品属于当前买家购物车;(2) 所有商品当前可售(已上架且有库存);(3) 每个商品选择的数量不超过实时库存且大于 0。预校验失败时整单失败,不进入库存扣减。 | -| M04-01-FR02 | 地址归属校验 | 买家提交的收货地址 ID 必须属于当前买家(`address.user_id = current_user_id`);不属于或地址不存在时整单失败,返回明确错误提示。 | +| M04-01-FR01 | 结算内容校验 | A301 只接收 A208 已确认的购物车条目集合与 `checkoutRevision`。服务端在同一事务和固定锁序内重读条目归属/选择、数量/版本、商品名称/主图/实时价格、销售状态、普通库存和正金额资格;任一变化统一以 `ORDER.CHECKOUT_CHANGED + latestPreview` 整单拒绝,零建单、零扣库存、零购物车清理,不静默提交可用子集。 | +| M04-01-FR02 | 地址确认与归属校验 | 买家提交的 `addressId + addressVersion` 必须对应当前买家(`addresses.buyer_id = currentBuyerId`)。A301 在同一外层事务通过 Identity 公开能力锁定 DB003:不存在或不属于本人时 404;版本已变化时返回 `409 IDENTITY.ADDRESS_VERSION_CONFLICT` 并要求刷新、重新确认;只有 ID、版本和归属同时匹配才保存地址快照。 | | M04-01-FR03 | 库存原子扣减 | 在同一数据库事务内使用带库存充足条件的原子更新扣减每个商品库存;任一商品扣减失败时事务整体回滚,不产生部分订单。库存扣减必须具备订单维度幂等保障,防止重复扣减。 | | M04-01-FR04 | 订单创建 | 事务扣减库存成功后创建唯一订单事实,保存订单号、买家 ID、地址快照、`assignedMerchantUserId`、`PendingPayment` 状态、服务端计算总额、支付截止时间、创建时间和幂等键;客户端不得指定处理商家或订单金额。 | | M04-01-FR05 | 订单项快照 | 为每个订单项创建订单项记录(`order_items`):保存商品 ID、商品名称(快照)、商品主图 URL(快照)、成交单价(快照,下单时服务端的实时价格)、购买数量。订单项单价以创建订单时的服务端实时价格为准,不受后续商品改价影响。 | | M04-01-FR06 | 购物车清理 | 在订单事务内清理本次已下单的购物车条目;清理失败时整笔订单事务回滚,不产生订单、不扣减库存,也不丢失购物车条目。 | -| M04-01-FR07 | 幂等键设计 | 买家客户端生成唯一幂等键(UUID),随下单请求一同发送;服务端以 `(user_id, idempotency_key)` 为唯一范围并绑定地址与购物车条目指纹。同键同内容重放首次确定结果:成功时返回同一订单,库存不足、商品不可售、总额不合法或已确定的默认商家配置不可用时重放同一拒绝;同键换内容拒绝。依赖中断、数据库连接失败或事务结果未知等未形成确定裁决的失败不得固化,可用同一键安全重试。 | +| M04-01-FR07 | 幂等键设计 | 买家客户端生成唯一幂等键(UUID),随下单请求一同发送;服务端以 DB104 的 `operation_code='A301' + scope_key='buyer:{buyerId}' + idempotency_key` 为唯一范围,并绑定 `addressId + addressVersion`、升序规范化购物车条目 ID 与 `checkoutRevision` 的请求指纹。同键同内容重放首次确定结果:成功时返回同一订单,地址/结算内容已变化或已确定的默认商家配置不可用时重放同一拒绝;同键换内容拒绝。依赖中断、数据库连接失败或事务结果未知等未形成确定裁决的失败不得固化,可用同一键安全重试。 | | M04-01-FR08 | 价格服务端计算 | 订单总额和订单项实付单价均由服务端计算,不接受前端传入;商品最新价格从 `products.price` 实时读取;订单项保存快照后,商品后续改价不影响已有订单。 | | M04-01-FR09 | 事件通知 | 订单创建成功后,通过 Outbox 发布 `OrderCreatedIntegrationEvent`,只将当前买家列为接收账号,供 M09 站内消息消费;商家待处理提醒统一由支付成功事件触发,避免重复通知。C03 直接扫描 PostgreSQL 的支付截止时间,不消费该事件保存唯一调度事实。 | #### 4. 主流程 -1. 买家在购物车页面选择要结算的商品,点击"提交订单"并选择收货地址,客户端生成幂等键并发送请求。 +1. 买家在购物车取得 A208 结算预览后进入确认页。页面读取本人地址:存在唯一默认地址时预选并突出展示;无默认地址时保持未选择,不能自动取第一条。买家确认或显式切换地址后,客户端生成幂等键并发送请求。 2. 服务端校验买家身份、幂等键格式和固定请求结构,并以地址与购物车条目形成规范请求指纹。 3. 服务端先查询该买家与幂等键:同键同内容已有确定结果时原样重放成功或拒绝;同键换内容时拒绝复用;没有确定结果时继续读取可变事实。 -4. 校验所有选中商品是否属于当前买家购物车、是否可售、库存是否充足、数量是否合法。 -5. 校验收货地址是否属于当前买家且状态正常,并解析唯一且启用的默认商家。 -6. 开启数据库事务: - a. 按商品维度依次执行条件更新扣减库存(`WHERE stock >= quantity`),任一失败则整体回滚。 - b. 解析并保存处理商家账号,创建状态为 `PendingPayment` 的订单主记录。 - c. 创建每个订单项记录,保存商品信息快照和成交单价。 - d. 删除已下单的购物车条目。 - e. 写入 Outbox `OrderCreatedIntegrationEvent` 事件。 - 提交事务。 +4. 开启数据库事务并保持到确定结果提交:先取得 DB104 幂等范围;再锁定唯一启用默认商家责任门并读取本人地址快照;随后按稳定顺序锁定本人购物车条目和 Catalog 商品/普通库存。 +5. 在锁内按 A208 相同算法重算完整结算内容与 `checkoutRevision`。地址不存在/不归属、默认商家权威结构不可用分别返回自己的全局错误;仍属于本人的目标条目发生选择、数量、版本、展示、价格、销售状态、库存资格或正金额变化时统一返回 `ORDER.CHECKOUT_CHANGED + latestPreview`。 +6. Revision 一致后,在同一事务: + a. 生成订单/订单项标识,保存默认商家、地址及商品历史快照、`PendingPayment`、服务端总额和固定支付截止时间。 + b. 按商品稳定顺序执行 `WHERE stock >= quantity` 条件扣减并写普通库存流水;任一影响行数异常则整单回滚并按最新事实返回 `ORDER.CHECKOUT_CHANGED`。 + c. 写支付过期责任、Outbox `OrderCreatedIntegrationEvent` 与 C07 两阶段失效责任。 + d. 精确删除本次购物车条目并核对行数,写入幂等确定结果。 + e. 一次提交;任一步失败整体不生效。 7. 事务提交成功后,返回订单号给客户端。 8. 客户端跳转到支付页面或收银台,等待买家操作。 @@ -771,7 +785,7 @@ M04-01 要求买家选择购物车中的商品和收货地址完成订单提交 - 订单总额 = Σ(订单项成交单价 × 订单项数量),由服务端计算,前端不可篡改。 - 每个订单项的单价以**下单时刻**的服务端实时价格为准,与购物车中的价格或前端传入价格无关。 - 库存扣减使用条件更新而非先查后改,避免并发超卖。 -- 幂等键 `(user_id, idempotency_key)` 绑定规范请求指纹;确定成功与确定业务拒绝均稳定重放,换内容复用返回冲突。只有形成明确裁决的结果可以固化,依赖中断、连接失败和事务结果未知不得写成可重放结果。 +- DB104 以 `operation_code='A301' + scope_key='buyer:{buyerId}' + idempotency_key` 唯一定位幂等责任,并绑定地址 ID/版本、升序规范化购物车条目 ID 与 `checkoutRevision` 的请求指纹;确定成功与确定业务拒绝均稳定重放,换内容复用返回冲突。只有形成明确裁决的结果可以固化,依赖中断、连接失败和事务结果未知不得写成可重放结果。 - 购物车清理与订单创建处于同一事务;清理失败时整笔事务回滚,购物车条目保持原状,买家可根据提示重新提交。 - 地址快照保存下单时刻的完整地址文本,后续地址修改不影响已有订单。 - 商品名称/图片快照保存下单时刻的完整信息,后续商品信息修改不影响已有订单。 @@ -782,12 +796,11 @@ M04-01 要求买家选择购物车中的商品和收货地址完成订单提交 | 场景 | 处理方式 | 买家体验 | |---|---|---| -| 商品不在购物车中 | 整单失败,返回"部分商品已下架或不在购物车中" | 刷新购物车后重新选择 | -| 商品已下架 | 整单失败,返回"部分商品已下架" | 刷新购物车后重新选择 | -| 商品库存不足 | 整单失败,返回"商品 [名称] 库存不足,当前可购 [X] 件" | 可修改数量后重新提交 | -| 商品数量非法(≤0 或超过库存) | 整单失败,返回参数错误 | 修改数量后重新提交 | +| 商品不在本人购物车中或已被删除 | 返回 404,且不返回可用于推断他人条目的预览 | 刷新购物车后重新选择 | +| 本人条目取消选中、商品下架、库存不足、数量/价格/展示/版本变化或总额不再为正 | 统一返回 `ORDER.CHECKOUT_CHANGED + latestPreview`,整单零写入 | 查看逐项原因,确认新预览后使用新 Revision 与新幂等键提交 | | 收货地址不存在或不归属当前买家 | 整单失败,返回"收货地址无效" | 重新选择地址 | -| 订单总额计算后为 0 或负数 | 整单失败,返回"订单金额异常" | 联系客服或重新下单 | +| 买家确认后地址被另一标签页编辑、切换默认状态或其他受控动作改变版本 | 返回 `409 IDENTITY.ADDRESS_VERSION_CONFLICT`,不建单、不扣库存 | 刷新地址列表并重新确认当前内容,再使用新幂等键提交 | +| 地址列表没有默认地址 | 结算页保持未选择,不自动选择第一项;A301 仍要求显式地址 ID | 买家选择已有地址或新增后显式选择 | | 同一幂等键同内容重复提交 | 原样重放首次确定的成功订单或业务拒绝,不重复扣库存 | 获得稳定结果,不出现先失败后又意外建单 | | 同一幂等键换地址或购物车条目 | 拒绝复用该幂等键 | 生成新幂等键后重新提交 | | 并发扣减库存竞争 | 条件更新失败的事务回滚,返回库存不足错误 | 重试或减少数量 | @@ -830,7 +843,7 @@ M04-01 要求买家选择购物车中的商品和收货地址完成订单提交 - 保存订单、订单项、商品库存和购物车清理前后的数据证据。 - 能解释为何选择条件更新而非先查后改扣减库存,以及并发下的正确性保障。 - 能解释订单项快照的设计原因:为何快照能保护买家利益、为何商家改价不影响已有订单。 -- 能解释幂等键如何防止重复提交, `(user_id, idempotency_key)` 唯一约束如何生效。 +- 能解释幂等键如何防止重复提交,以及 DB104 的 `operation_code + scope_key + idempotency_key` 唯一约束和请求指纹如何共同生效。 - 能解释 Outbox 事件在订单创建成功后的投递关系,以及 M09 如何消费 `OrderCreatedIntegrationEvent`;C03 为什么改为扫描 PostgreSQL 支付截止时间而不消费该事件保存唯一调度事实。 - 能解释购物车清理与订单创建为何必须处于同一事务,以及清理失败时如何回滚并保持购物车原状。 @@ -840,7 +853,7 @@ M04-01 要求买家选择购物车中的商品和收货地址完成订单提交 M04-02 要求买家分页查看自己的订单列表,并按订单状态筛选,同时能够查看单个订单的完整详情。列表页展示订单摘要(订单号、状态、总额、创建时间、商品摘要),详情页展示完整订单信息(地址快照、订单项快照、金额明细、状态时间线)。核心目标是让买家"**找得到订单、看得到详情、状态无歧义**"。 -本期不实现订单导出、订单打印、订单评论跳转、订单修改地址等横向扩展功能。 +本期不实现订单导出、订单打印、整单评论页/独立评论中心、订单修改地址等横向扩展功能;已选择 X01,因此订单详情仍须按具体订单项提供评价入口。 #### 2. 身份处理与协作边界 @@ -859,14 +872,14 @@ M04-02 要求买家分页查看自己的订单列表,并按订单状态筛选 |---|---|---| | M04-02-FR01 | 订单列表查询 | 买家分页查询自己的订单列表,支持按页码和每页条数(默认 10 条,上限 50 条)分页;列表按订单创建时间倒序(最新订单在前)排列。 | | M04-02-FR02 | 状态筛选 | 买家可按订单状态筛选列表,支持筛选条件包括:全部、`PendingPayment`(待支付)、`Paid`(已支付)、`Shipped`(已发货)、`Completed`(已完成)、`Cancelled`(已取消)。不传筛选条件时默认返回全部状态订单。 | -| M04-02-FR03 | 列表摘要信息 | 列表每条记录必须展示:订单号(完整 UUID 或脱敏后 8 位字母数字)、订单状态(中文展示)、订单总额、订单创建时间(相对时间或精确时间)、商品摘要(最多展示 3 个商品名称,多的显示"+X 件")。 | +| M04-02-FR03 | 列表摘要信息 | 列表每条记录必须展示:订单号(完整 UUID 或脱敏后 8 位字母数字)、订单五态、订单总额、订单创建时间、商品摘要(最多 3 个名称,多的显示“+X 件”),以及服务端派生的履约、售后和评价摘要。三个摘要不新增订单状态:必须能区分已支付待发货、售后阻断、部分退款后仍可履约、全量退款无需发货,以及已完成订单待评价/部分已评/全部已评 | | M04-02-FR04 | 订单详情查询 | 买家通过订单号查询单个订单的完整详情;服务端校验订单是否属于当前买家,不属于或不存在时返回 403 或 404。 | | M04-02-FR05 | 详情地址快照 | 详情页展示下单时刻保存的收货地址快照:收件人姓名、联系电话(脱敏展示如 `138****8888`)、收货省份市区、详细地址。 | -| M04-02-FR06 | 详情订单项快照 | 详情页展示每个订单项的完整快照:商品名称、商品主图 URL、成交单价、购买数量、订单项小计(单价 × 数量)。 | +| M04-02-FR06 | 详情订单项快照 | 详情页展示每个订单项的完整快照:商品名称、商品主图 URL、成交单价、购买数量、订单项小计,并按订单项返回售后处理中/已退款/剩余可申请数量、当前资格和评价状态;评价与申请售后动作必须绑定具体 `orderItemId` | | M04-02-FR07 | 详情金额明细 | 详情页展示订单金额计算:订单总额 = Σ(订单项成交单价 × 数量);展示订单项数量合计。 | | M04-02-FR08 | 详情状态时间线 | 详情页展示订单状态时间线:订单创建时间、支付时间(已支付时)、发货时间(已发货时)、完成时间和完成方式(已完成时)、取消时间(已取消时)。时间使用用户本地时区展示。 | -| M04-02-FR09 | 详情支付信息 | 已支付订单详情展示支付信息:支付流水号、支付时间、支付方式(钱包支付)。未支付订单不展示支付信息。 | -| M04-02-FR10 | 订单操作入口 | 详情页根据订单状态展示可用操作入口:待支付订单展示"去支付"入口;已发货订单展示"确认收货"入口;已完成订单展示"去评价"入口(若 X01 已实现);已取消订单不展示操作入口。 | +| M04-02-FR09 | 详情支付信息 | 已支付订单详情展示唯一成功支付事实:支付流水号、支付时间和真实支付来源。来源为 F10 同步小金库时显示 `Wallet/小金库支付`,来源为 C08 受控模拟通道时显示 `SimulatedChannel/模拟通道支付`;不得把两种来源统一伪装成钱包支付。未支付订单不展示成功支付信息。 | +| M04-02-FR10 | 订单操作入口 | 订单级入口仅承载“去支付、取消订单、确认收货”;评价和申请售后属于订单项动作,必须在对应项返回目标与资格。`PendingPayment` 且早于 `paymentDeadline` 显示支付/取消;到期后不再显示支付;`Paid` 按每项 M10 资格提供未发货仅退款;`Shipped` 显示确认收货并按项提供售后;`Completed` 按项展示未评价入口,并仅在完成后 7 天内按项提供售后;`Cancelled` 不展示上述入口。查询摘要未知时必须隐藏相关写入口并提示刷新,不能把未知当作无售后、可发货或未评价;写接口仍重新复核。 | #### 4. 主流程 @@ -876,8 +889,8 @@ M04-02 要求买家分页查看自己的订单列表,并按订单状态筛选 2. 服务端校验买家身份(JWT + `role=buyer`),解析买家 ID。 3. 查询 `orders` 表,筛选条件为 `buyer_id = current_user_id AND (status = @status OR @status IS NULL)`。 4. 按 `created_at DESC` 排序,分页返回订单列表。 -5. 对每个订单,关联查询订单项摘要(商品名称列表,最多 3 条)。 -6. 返回分页结果,包含总条数、总页数、当前页数据。 +5. 一次读取当前页订单项摘要,并通过 AfterSales/Review 批量应用能力取得本页完整售后聚合和已评价事实;不得逐订单、逐订单项循环 HTTP 调用。 +6. 返回分页结果、三个派生摘要和组合状态;扩展摘要暂不可用时保留核心订单结果但把对应摘要置为未知并关闭依赖它的入口。 **订单详情查询:** @@ -885,8 +898,8 @@ M04-02 要求买家分页查看自己的订单列表,并按订单状态筛选 2. 服务端校验买家身份,解析买家 ID。 3. 查询 `orders` 表,校验 `order_id = @orderId AND buyer_id = current_user_id`。 4. 若订单不存在或不属于当前买家,返回 403 或 404。 -5. 关联查询订单项列表、支付记录(如有)。 -6. 组装地址快照、订单项快照、金额明细、状态时间线。 +5. 查询订单项和支付记录,并一次取得全部订单项的售后资格/数量及已评价事实。 +6. 组装地址快照、订单项快照、金额明细、状态时间线、订单级三个摘要和订单项级可执行动作。 7. 返回完整订单详情。 #### 5. 业务规则与权限 @@ -896,8 +909,11 @@ M04-02 要求买家分页查看自己的订单列表,并按订单状态筛选 - 订单状态使用受控枚举,不允许客户端传入任意状态值;状态筛选参数必须校验枚举有效性。 - 订单金额以服务端持久化的订单金额为准,不接受客户端传入值作为最终结果。 - 地址和商品信息以订单创建时的快照为准,后续修改不影响已有订单的展示。 -- 分页参数 `page` 从 1 开始,`pageSize` 默认 10,上限 50;超出范围时使用边界值。 +- 分页参数 `page` 从 1 开始,`pageSize` 默认 10、上限 50;`page < 1` 或 `pageSize` 越界返回可定位字段的 400,不静默钳制。合法 `page` 超过 `totalPages` 时返回 200、空 `items` 和真实分页元数据,不自动跳到最后一页。 - 列表查询不返回完整订单项列表,只返回商品摘要,避免列表接口数据量过大。 +- 订单核心状态仍只有五态。履约、售后、评价摘要只在读取时由订单、售后和评价权威事实派生,不写入订单表、不作为筛选条件,也不允许客户端在命令请求中回传。 +- 剩余可申请数量 = 购买数量 − 非终态售后占用数量 − 已退款数量;剩余可履约数量 = 购买数量 − 已退款数量。处理中售后不从最终剩余可履约量扣除,但会阻断整单当前发货,两个概念不得混用。 +- 评价汇总按订单项行数计算,不按购买件数;当前需求没有“发生退款即失去评价资格”的规则,`Completed` 订单仍按订单项是否存在唯一评价事实派生入口。 - 订单时间均使用 UTC 存储,前端按用户时区转换展示;相对时间(如"3 小时前")与精确时间并存。 #### 6. 异常与边界场景 @@ -906,13 +922,15 @@ M04-02 要求买家分页查看自己的订单列表,并按订单状态筛选 |---|---|---| | 订单列表查询成功但无订单 | 返回空列表,附带"暂无订单"提示文案 | 引导去购物车添加商品 | | 订单状态筛选参数非法 | 拒绝请求并返回可定位到 `status` 的字段错误,不把非法值静默当作“全部” | 保留当前筛选并提示重新选择 | -| 分页参数超出范围(page ≤ 0 或 > 总页数) | 返回空列表或最后一页数据,不报错 | 正常翻页 | +| `page ≤ 0` 或 `pageSize` 越界 | 400 字段校验错误,不静默改值 | 保留输入并提示修正 | +| 合法 `page > totalPages` | 200 + 空 `items` + 真实 `page/pageSize/total/totalPages`,不跳转末页 | 展示无数据并允许返回上一页 | | 查询他人订单详情(猜测订单号) | 返回 403 或 404,不泄露订单是否存在 | 无感知 | | 买家跨身份访问(商家/管理员身份调用) | 返回 403,不返回任何订单数据 | 无感知 | | 游客(未登录)访问订单列表或详情 | 返回 401 并引导登录 | 跳转登录页 | | 订单详情中商品图片已删除或 URL 失效 | 展示默认占位图,不报错 | 看到占位图 | | 订单详情中收货地址不完整 | 展示已有字段,不展示空值 | 看到部分地址 | | 并发修改订单状态(如支付回调同时到达) | 详情查询返回最新状态,不产生错误 | 看到最新状态 | +| 售后或评价批量摘要暂不可用/缺少任一返回项 | 核心订单仍可返回,但对应摘要标为未知、组合状态降级,并隐藏依赖该摘要的售后/评价/发货入口 | 看到“摘要暂不可用,请刷新”,不看到虚假可操作按钮 | #### 7. 验收标准与证据 @@ -928,15 +946,18 @@ M04-02 要求买家分页查看自己的订单列表,并按订单状态筛选 | 6 | 使用"已完成"状态筛选 | 只展示 `Completed` 状态订单。 | | 7 | 使用"已取消"状态筛选 | 只展示 `Cancelled` 状态订单。 | | 8 | 点击订单进入详情页 | 展示完整订单信息:地址快照、订单项快照(商品名称/图片/单价/数量)、金额明细、状态时间线。 | -| 9 | 在详情页查看已支付订单的支付信息 | 展示支付流水号、支付时间、支付方式(钱包支付)。 | -| 10 | 在详情页查看待支付订单的操作入口 | 展示"去支付"按钮。 | -| 11 | 在详情页查看已发货订单的操作入口 | 展示"确认收货"按钮。 | +| 9 | 分别查看由 F10 小金库和 C08 受控模拟通道成功支付的订单 | 两者均展示唯一支付流水号和支付时间,并分别显示 `Wallet/小金库支付`、`SimulatedChannel/模拟通道支付`,不存在双成功来源。 | +| 10 | 查看仍早于固定支付截止时间的待支付订单 | 同时展示“去支付”和“取消订单”;服务端仍在执行动作时复核状态和截止时间。 | +| 10a | 查看已经达到固定支付截止时间但 Worker 尚未完成取消的待支付订单 | 不展示“去支付”;展示过期取消处理中或刷新后的最新取消结果,任何支付通道均不能再成功。 | +| 11 | 查看已发货订单的操作入口 | 展示“确认收货”,并仅按 M10 当前资格展示售后入口。 | | 12 | 查看已取消订单的详情 | 不展示支付信息,状态时间线包含取消时间,操作入口为空。 | | 13 | 尝试访问他人订单(猜测订单号) | 返回 403 或订单不存在,不泄露订单信息。 | | 14 | 用商家/管理员身份访问订单列表或详情 | 返回 403。 | | 15 | 游客(未登录)访问订单列表 | 返回 401 并跳转登录页。 | | 16 | 查看商品图片已下架的订单详情 | 商品图片展示默认占位图,不报错。 | | 17 | 同时打开两个标签页,一个支付订单,一个查看详情 | 两边状态均更新为最新,不产生歧义。 | +| 18 | 查看四笔核心状态均为 `Paid`、但分别为无售后、售后处理中、部分退款、全量退款的订单 | 依次显示待发货、售后阻断、部分退款仍可履约、已全量退款且不可发货;核心状态仍均为 `Paid`。 | +| 19 | 查看包含多个订单项的 `Completed` 订单 | 已评、未评项分别展示,订单摘要能够区分待评价、部分已评和全部已评;动作精确绑定订单项。 | **验收证据与答辩要求:** @@ -992,12 +1013,13 @@ M04-03 要求买家取消自己创建的待支付订单,系统在单个受控 - `status = 'PendingPayment'` 若任一条件不满足,返回明确错误。 5. 开启数据库事务: - a. 将订单状态更新为 `Cancelled`(带状态条件更新 `WHERE status = 'PendingPayment'`)。 - b. 根据 `order_items` 查询该订单涉及的商品和数量。 - c. 按订单项原通道回补 Catalog 普通库存,或回补 Seckill 活动库存并释放限购额度。 - d. 记录 `cancelled_at = NOW()`。 - e. 写入 Outbox `OrderCancelledIntegrationEvent` 事件。 - 提交事务。 + a. 锁定目标订单并在锁内重新校验买家归属和当前状态;锁等待前的查询结果不能作为最终裁决依据。 + b. 取得订单锁后调用一次数据库 `clock_timestamp()` 形成该事务唯一 `decisionTime`,后续状态、截止时间、取消原因和取消时间均复用该值,不使用事务开始时的旧 `now()`。 + c. 若订单已 `Cancelled`,读取并返回首次取消结果,不回补库存、不写第二个取消时间;若已 `Paid` / `Shipped` / `Completed`,拒绝取消。 + d. 若仍为 `PendingPayment`,以 `decisionTime < paymentDeadline` 形成 `BuyerRequested`,以 `decisionTime >= paymentDeadline` 形成 `PaymentExpired`,再使用带 `PendingPayment` 条件的状态更新竞争唯一取消结果。 + e. 根据 `order_items` 的原库存来源,回补 Catalog 普通库存,或回补 Seckill 原活动库存并释放限购额度。 + f. 记录 `cancelled_at = decisionTime`、稳定取消原因,并写入 Outbox `OrderCancelledIntegrationEvent`。 + g. 订单状态、全部订单项回补、秒杀限购释放、取消字段和 Outbox 一次提交。 6. 事务提交成功后,返回取消成功响应。 7. 前端刷新订单列表和详情,显示订单状态已变为"已取消"。 @@ -1007,6 +1029,7 @@ M04-03 要求买家取消自己创建的待支付订单,系统在单个受控 - 只有 `PendingPayment`(待支付)状态的订单可以取消;已支付、已发货、已完成、已取消订单一律拒绝取消。 - 库存回补量等于订单项购买数量;普通订单和秒杀订单必须回到各自原库存通道,不得把秒杀配额误加到普通库存。 - 取消操作使用**条件更新**(`WHERE status = 'PendingPayment'`)防止并发取消或并发支付导致的状态冲突。 +- 扫描或事务开始时读取的时间只能用于发现候选;最终取消资格必须在锁定订单后以同一 `decisionTime` 裁决。支付、主动取消和过期取消都必须遵守这一锁后时间边界。 - 幂等设计以订单号为准:同一订单号只能成功取消一次,重复取消返回幂等成功。 - 取消订单后,订单仍保留在数据库中作为历史记录,不得物理删除。 - 买家取消订单不触发退款(因为尚未支付);退款只在售后流程 X04 中处理已支付订单。 @@ -1144,7 +1167,7 @@ M06-02 提供发货结果,M04 维护订单状态,M09 向买家发送完成 - 使用演示参数验证到期未确认订单可以自动完成,并能说明正式环境为发货后 7 天。 - 同时触发买家确认和自动完成时,订单只有一个确定结果,完成时间和完成方式不冲突。 - 主动完成和自动完成均生成一条面向买家的站内消息;实时通知失败时仍可从消息中心补查。 -- 已完成订单正确显示评价入口;售后入口严格遵守 M10 后续确认的状态与时限规则。 +- 已完成订单正确显示评价入口;售后入口按 M10 已冻结规则派生:`Paid`、`Shipped` 可按剩余可售后数量申请,`Completed` 仅在完成时间起 7 天内开放,超过期限或数量已耗尽时不展示可提交入口,正式提交仍由服务端重检。 - 保存主动确认、自动完成、非法状态、越权、重复操作和并发竞争的页面与状态证据。 ### M05-01 模拟支付(F10)— 张海洋 @@ -1173,8 +1196,8 @@ M04 提供订单金额、状态和支付截止时间,M09 接收支付结果通 | M05-01-FR03 | 充值幂等 | 充值请求携带幂等键,重试不得重复到账 | | M05-01-FR04 | 充值记录 | 买家可查询本人充值时间、金额和结果 | | M05-01-FR05 | 统一收银台 | 下单成功页、订单列表和订单详情的支付入口进入同一收银台 | -| M05-01-FR06 | 支付校验 | 校验订单归属、待支付状态、应付金额和钱包余额 | -| M05-01-FR07 | 原子支付 | 钱包扣款、支付记录、订单状态和 Outbox 在同一事务内完成 | +| M05-01-FR06 | 支付校验 | 校验订单归属、待支付状态、应付金额和钱包余额;新支付在取得订单、钱包和财务提交水位等全部可能阻塞的共享事实后,只读取一次数据库权威 `finalTime`,以 `finalTime < paymentDeadline` 作为最终支付资格 | +| M05-01-FR07 | 原子支付 | 钱包扣款、支付记录、订单状态、财务提交序号和 Outbox 在同一事务内完成;同一 `finalTime` 同时作为截止裁决时间和该笔成功支付的入账/支付时间 | | M05-01-FR08 | 支付幂等 | 重复提交只扣款和记账一次,并返回首次确定结果 | | M05-01-FR09 | 结果反馈 | 展示订单金额、钱包余额、处理状态和下一步;余额不足时提供充值入口 | @@ -1183,15 +1206,17 @@ M04 提供订单金额、状态和支付截止时间,M09 接收支付结果通 1. 买家从待支付订单进入收银台,查看订单号、应付金额和当前余额。 2. 余额不足时买家完成合规模拟充值,再返回当前收银台。 3. 买家确认支付,客户端发送幂等键。 -4. 服务端校验买家、订单状态和余额,在事务内扣款、创建支付记录、更新订单为已支付并写入 Outbox。 +4. 服务端校验买家并取得全部支付事实的并发控制;在最后一个可能阻塞的共享锁之后形成唯一 `finalTime`,重新校验订单状态、截止时间、金额和余额,通过后在事务内扣款、创建支付记录、更新订单为已支付并写入 Outbox。 5. 页面展示确定结果并返回订单详情;后续消息和发货流程消费已提交的支付事实。 #### 5. 业务规则与权限 - 余额、应付金额和支付结果以服务端为准,客户端不得指定最终扣款金额。 +- 支付请求网络超时后,原 `Idempotency-Key` 重试是权威恢复动作;订单支付结果查询必须与同订单支付写事务同步,并区分成功、当前未支付、确认中、已过期/取消和数据不确定。确认中或不确定时不得把空支付记录解释为失败、不得生成新 Key 重复付款。 - 钱包余额不得为负;仅待支付且未取消的本人订单可以支付。 - 充值和支付分别使用稳定幂等键,重复请求不重复到账、扣款或记账。 - 支付与 C03 超时取消通过待支付状态条件竞争,最终只能一个操作成功。 +- 支付请求到达时间、页面读取时间、事务开始时间和取得订单锁的时间都不能授权成功。若等待钱包或财务提交水位期间跨过截止点,最终 `finalTime >= paymentDeadline`,必须拒绝扣款;不得出现截止前裁决、截止后入账的支付。 - 钱包扣款、支付记录、订单状态和可靠事件必须保持一致。 - 所有查询和写操作按当前买家过滤,商家和管理员不得访问买家钱包。 @@ -1205,12 +1230,14 @@ M04 提供订单金额、状态和支付截止时间,M09 接收支付结果通 | 订单已支付 | 返回已有支付结果,不重复扣款 | | 订单或钱包不属于当前用户 | 拒绝访问且不泄露他人信息 | | 支付与取消并发 | 仅一个条件更新成功,另一个返回当前最终状态 | +| 支付等待共享锁时跨过截止点 | 以取得最后一个可能阻塞锁后的 `finalTime` 判定为已过期,不扣款、不创建成功支付,并交给统一过期取消继续收敛 | | 网络中断或结果未知 | 允许按幂等键查询或重试,不诱导重复付款 | #### 7. 验收标准与证据 - 合法模拟充值即时到账,非法金额被拦截,重复充值请求不重复增加余额。 - 买家可支付本人待支付订单,余额、支付记录和订单状态变化一致。 +- 支付等待任一订单、钱包或财务共享锁跨过截止点时,最终结果为过期拒绝;成功支付各事实的支付/入账时间均等于同一 `finalTime` 且严格早于固定截止时间。 - 余额不足、订单取消、重复支付、越权和网络重试均得到正确结果。 - 游客、商家和管理员没有买家钱包操作权限。 - 保存充值、支付、重复请求、余额不足及支付取消竞争证据,并能说明事务、幂等和服务端计价。 @@ -1246,7 +1273,7 @@ M04 提供订单金额、状态和支付截止时间,M09 接收支付结果通 | M06-01-FR06 | 编辑商品 | 可修改允许变更的商品信息;保存成功后展示最新数据 | | M06-01-FR07 | 上下架 | 商家可切换商品销售状态;下架后购物端列表和搜索不再返回该商品 | | M06-01-FR08 | 删除约束 | 无历史关联时可按设计删除;已有订单关联时禁止破坏性删除并建议下架 | -| M06-01-FR09 | 图片管理 | 图片上传到 S3 Compatible Object Storage;单商品最多 8 张,单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 400~4096 像素,第一张作为主图 | +| M06-01-FR09 | 图片管理 | 图片上传到 S3 Compatible Object Storage;单商品当前图库最多 8 张,单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 400~4096 像素,第一张作为主图。商家从当前图库删除图片时,历史订单已经快照引用的原图/缩略图必须继续可读;无任何历史订单引用时才允许异步清理对象 | | M06-01-FR10 | 并发保护 | 编辑依据并发标记或条件更新防止静默覆盖;冲突时保留已填写内容并提示刷新确认 | | M06-01-FR11 | 变更传播 | 商品事务提交后触发对应首页摘要和详情缓存失效;商品创建、编辑、上下架、删除或关联分类名称修改时,检索文本、销售状态和数据库索引随同一 PostgreSQL 事实同步维护,不通过异步处理器复制搜索索引 | | M06-01-FR12 | 操作反馈 | 保存、上下架和删除均显示明确结果;危险操作需要确认,失败时保留可恢复的表单数据 | @@ -1259,18 +1286,20 @@ M04 提供订单金额、状态和支付截止时间,M09 接收支付结果通 4. 事务成功后返回明确结果;搜索索引随商品数据同步更新,缓存失效在事务提交后处理。 5. 商家执行上架后,购物端可浏览该商品;执行下架后,购物端不再提供购买入口。 6. 商家删除存在订单、购物车、收藏、浏览、评价、秒杀等任何历史关联的商品时,系统拒绝物理删除并引导改为下架。 +7. 商家删除当前商品图片时,系统先完成主图替换和当前图库移除;若历史订单已引用该图片,则只归档图片关联并保留对象,历史订单仍显示下单时图片。只有确认没有历史订单引用时才登记对象清理任务。 #### 5. 业务规则与权限 -- 商品名称、合法的分类关联、当前价格、库存和主图为必填;价格不得为负,库存必须为非负整数。新建、切换分类或上架时分类必须在购物端有效(自身及父分类均启用),既有商品在原分类或其父分类后来停用时仍可修改不改变分类归属的字段。 +- 商品名称、合法的分类关联、当前价格、库存和主图为必填;草稿价格不得为负,库存必须为非负整数。价格为 0 只允许作为尚未上架的草稿占位;执行上架时价格必须大于 0,避免产生总额为 0 却没有免支付状态和资金语义的订单。新建、切换分类或上架时分类必须在购物端有效(自身及父分类均启用),既有商品在原分类或其父分类后来停用时仍可修改不改变分类归属的字段。 - 商品销售状态只有草稿、已上架和已下架三种;物理删除是实体不存在的终止结果,不得保存为“已删除”状态。 -- 上架前必须满足完整性校验;新建商品不得绑定购物端无效分类,已有商品切换分类时不得选择无效分类。 +- 上架前必须满足完整性校验,至少包括商品名、主图、有效分类、`price > 0` 与非负库存;新建商品不得绑定购物端无效分类,已有商品切换分类时不得选择无效分类。 - 分类自身或其父分类停用后,已有已上架商品继续公开展示;它们可以继续修改不改变分类归属的字段,但一旦下架,必须先迁移到有效分类,或使原分类及其父分类均启用后才能再次上架。 - 停用顶级分类不批量修改子分类的存储状态,但整个子树立即退出购物端分类筛选;重新启用顶级分类后,只有自身仍为启用的子分类恢复为有效入口。启用一个父分类仍停用的子分类只保存其启用状态,不提前公开。 - 顶级分类已有直属子分类时,不得把该顶级分类直接改绑为另一顶级分类的子分类,否则会产生超过一层的层级;必须先逐个迁移或删除直属子分类,系统不得隐式级联修改它们。 - 已上架商品库存降为零时保持已上架并在购物端标记售罄,不自动下架。 - 本期为单店 B2C 统一经营目录;所有正常商家账号维护同一套分类和商品,不按商家账号隔离商品所有权。 - 下架不删除购物车记录、收藏记录、浏览记录或历史订单快照,由对应模块显示不可售状态。 +- 从当前商品图库删除图片不等于删除历史快照资产。当前商品列表、详情和编辑页只返回仍附着于商品的图片;历史订单和售后详情继续读取订单项中的图片快照。系统不得因商家删图让已成交订单改用新主图或立即失去原图。 - 商品公开可见范围由服务端状态过滤保证,不能只依赖前端隐藏。 - 商品与分类写操作仅允许商家;游客、买家和管理员均不能越权调用。 - 搜索索引由 PostgreSQL 随商品数据同步维护;缓存失效仍在事务提交后处理。 @@ -1287,9 +1316,11 @@ M04 提供订单金额、状态和支付截止时间,M09 接收支付结果通 | 停用父分类但子分类仍为启用 | 父分类及整个子树退出购物端筛选,子分类存储状态保持不变;父分类恢复后按子分类自身状态重新生效 | | 购物端无效分类下商品尝试重新上架 | 拒绝上架,提示迁移到有效分类,或使原分类及其父分类均启用 | | 图片上传失败 | 明确标记失败图片并允许重试,不清空其他表单字段 | +| 删除已被历史订单快照引用的图片 | 从当前商品图库移除并按固定规则提升新主图,但保留历史图片对象;历史订单和售后详情继续可读 | +| 删除未被历史订单引用的图片 | 提交图库关联和主图变化后登记对象清理;清理失败由补偿任务重试,不回滚已提交的图库结果 | | 两名操作人并发编辑 | 后提交者收到冲突提示,不静默覆盖已生效修改 | | 删除存在订单、购物车、收藏、浏览、评价、秒杀等任何历史关联的商品 | 拒绝物理删除,提示改为下架并永久保留商品事实 | -| 上架条件不完整 | 拒绝上架并指出缺失字段 | +| 上架条件不完整或价格为 0 | 拒绝上架并指出缺失字段或“上架价格必须大于 0” | | 事务提交失败 | 数据回滚,页面显示保存失败且允许安全重试 | | 缓存失效失败 | 不回滚已经正确提交的商品事务;由缓存处理器重试并记录可追踪错误 | | 搜索索引异常 | 暂停进阶搜索并回退基础查询,重建数据库索引后恢复 | @@ -1299,6 +1330,7 @@ M04 提供订单金额、状态和支付截止时间,M09 接收支付结果通 - 商家可完成分类维护,以及商品新增、查询、编辑、受约束删除和上下架。 - 商品必填项、价格、库存、分类和图片校验在前后端均生效。 - 下架商品不会出现在购物端列表和搜索结果,旧链接不再允许购买;历史订单快照保持可读。 +- 商家删除当前商品图片后,购物端不再展示该图;已有订单引用的下单时图片仍可展示,未引用对象才进入安全清理。 - 停用分类不会自动隐藏其下已上架商品;库存为零的已上架商品保持可见并显示售罄。 - 已有直属子分类的顶级分类不能直接变成子分类,分类树始终最多一层且不会发生隐式级联改绑。 - 商家账号共同维护单店统一经营目录,不按当前操作人过滤商品归属。 @@ -1327,9 +1359,9 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 | 编号 | 功能 | 详细要求 | |---|---|---| -| M06-02-FR01 | 订单列表 | 只按当前 `assignedMerchantUserId` 过滤,按创建时间倒序分页展示订单号、买家必要摘要、金额、状态和时间 | +| M06-02-FR01 | 订单列表 | 只按当前 `assignedMerchantUserId` 过滤,按创建时间倒序分页展示订单号、买家必要摘要、金额、核心状态、时间、履约摘要和售后摘要;`Paid` 必须区分可发货、售后阻断、部分退款后可发剩余与全量退款 | | M06-02-FR02 | 状态筛选 | 支持按待支付、已支付、已发货、已完成和已取消等受控状态筛选 | -| M06-02-FR03 | 订单详情 | 展示订单项、地址快照、金额、状态时间及售后影响后的剩余可履约数量,敏感字段按最小必要原则提供 | +| M06-02-FR03 | 订单详情 | 展示订单项、地址快照、金额、状态时间、订单级履约/售后摘要,以及每项处理中售后数量、已退款数量、剩余可履约数量和已发货数量;敏感字段按最小必要原则提供 | | M06-02-FR04 | 发货处理 | 仅允许处理已支付且不存在履约阻断的订单;部分退款后只发剩余可履约数量,并记录发货时间和必要说明 | | M06-02-FR05 | 幂等发货 | 重复提交发货不得重复改变状态或生成多条相同通知 | | M06-02-FR06 | 结果通知 | 发货事务成功后通过可靠事件触发 M09 买家通知 | @@ -1337,7 +1369,7 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 #### 4. 主流程 -1. 商家进入后台订单列表并按状态查找已支付订单。 +1. 商家进入后台订单列表并按五种核心状态查找订单;服务端一次批量取得本页售后聚合,展示可履约、售后阻断、部分退款或全量退款摘要。 2. 商家查看订单详情,确认订单项和收货地址快照。 3. 商家提交发货操作,服务端校验商家权限、订单当前状态以及售后履约快照。 4. 无处理中售后且仍有可履约数量时,条件更新成功后订单变为已发货,只记录剩余可履约数量并写入可靠事件。 @@ -1350,6 +1382,7 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 - 发货状态更新使用条件更新或等效并发保护,重复操作保持幂等。 - 待审核、待退货、待收货、退款中或退款失败等仍可能改变履约结果的售后申请阻断发货;已拒绝或已撤销申请不阻断。 - 已退款数量不得再次发货;部分退款后只发剩余数量,全部数量已退款时不允许发货。该规则只影响履约动作,不新增订单核心状态。 +- `canShip=true` 只能由 `Paid + 无非终态售后 + 剩余可履约数量 > 0 + 售后摘要完整` 派生。售后摘要未知或缺失任一订单/订单项结果时必须关闭发货入口,不得按零售后处理。 - 提交售后与商家发货必须在各自事务中先经 Ordering 公开应用契约锁定同一订单记录并复核最新状态,锁保持到业务写入提交,避免同一数量同时进入退款和发货。 - 商家不得修改订单金额、支付记录、地址快照或订单项快照。 - 后台响应只返回履约所需数据,不暴露无关买家隐私。 @@ -1364,6 +1397,7 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 | 两次并发发货 | 仅一次状态更新成功,另一次返回已处理结果 | | 发货与售后申请并发 | 先完成的一方决定后续校验口径,不出现同一数量既退款又发货 | | 存在处理中售后或已全额退款 | 阻断发货并显示原因;部分退款完成后仅发剩余数量 | +| 售后批量摘要暂不可用或缺少返回项 | 列表/详情保留核心订单并标为降级,`canShip=false` 且不显示发货入口;A307 仍在提交时完整复核 | | 发货事务失败 | 状态保持原样,页面允许安全重试 | | 通知投递暂时失败 | 发货事实保持成功,由 Outbox/Worker 重试通知 | @@ -1373,6 +1407,7 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 - 已支付订单能够发货并变为已发货,其他状态发货均被拒绝。 - 重复和并发发货不会重复推进状态或重复产生业务结果。 - 售后申请、部分退款和全部退款场景下,发货入口、错误提示及实际发货数量与履约快照一致。 +- 核心状态同为 `Paid` 的正常待发货、售后处理中、部分退款和全量退款订单具有明确不同摘要;售后摘要故障不会误开放发货。 - 游客、买家和管理员不能越权发货,敏感信息遵循最小展示原则。 - 保存列表筛选、正常发货、非法状态、重复发货和通知衔接证据。 @@ -1482,20 +1517,20 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 | 编号 | 功能 | 详细要求 | |---|---|---| -| M07-FR01 | 评价资格 | 根据当前买家、订单项归属、订单完成状态和是否已评价判断入口 | +| M07-FR01 | 评价资格 | 根据当前买家、订单项归属、订单完成状态和是否已评价判断入口;Review 提供按当前订单页/详情全部 `orderItemId` 一次查询已评价事实的内部批量能力,Ordering 不逐项调用单项接口或直接读取评价表 | | M07-FR02 | 评价表单 | 提供 1~5 分评分、1~500 字文字评价和最多 6 张可选晒图 | -| M07-FR03 | 图片上传 | 单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 200~4096 像素;逐张显示上传状态,失败图片可重试或移除 | +| M07-FR03 | 图片上传 | 单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 200~4096 像素;每张上传必须携带幂等键,服务端先持久化本人、订单项、图片 ID、不可变对象 Key 和内容哈希,再写对象并完成暂存,响应丢失后用原键重试不得重复占用 6 张上限 | | M07-FR04 | 提交校验 | 服务端重新校验身份、订单项归属、订单状态、评分、文字、图片和重复提交 | -| M07-FR05 | 幂等与唯一性 | 同一订单项只能形成一条评价;重复点击或重复请求不得新增多条记录 | +| M07-FR05 | 幂等与唯一性 | 同一订单项只能形成一条评价;评价图片上传与正式提交分别使用稳定幂等键,同键同内容重放首次确定结果、同键换内容拒绝,重复点击、响应丢失或重复请求不得新增图片名额或评价 | | M07-FR06 | 公开列表 | 商品详情分页展示评分、文字、图片、评价时间和评价提交时形成的脱敏买家展示名快照,不返回手机号、邮箱等敏感信息 | | M07-FR07 | 评分汇总 | 根据全部成功提交的评价计算总数和平均分;本期没有审核、隐藏或删除状态,新增评价提交成功后立即成为公开计分事实 | | M07-FR08 | 提交反馈 | 提交期间防止重复点击;成功后明确标记“已评价”,失败时保留文字和已上传状态 | -| M07-FR09 | 内容安全 | 文字按纯文本或受控内容展示,图片经过类型和大小校验,不执行脚本或泄露存储凭据 | +| M07-FR09 | 内容安全 | 评价正文固定为纯文本:服务端规范化换行并按 Unicode 字符数校验,客户端只用文本插值并保留换行、禁止按 HTML 解释;图片经过类型、大小和尺寸校验,`Uploading/Pending` 暂存图只在当前页面使用本地预览且不公开,A142 成功切为 `Attached` 后才返回稳定公开读取 URL | #### 4. 主流程 1. 买家进入订单详情,系统为已完成且未评价的订单项显示“评价商品”入口。 -2. 买家选择 1~5 分、填写文字并按需上传图片;页面显示输入与上传状态。 +2. 买家选择 1~5 分、填写文字并按需逐张上传图片;每张生成独立幂等键,页面使用本地文件 URL 预览并显示持久暂存状态,网络结果未知时以原键重试。 3. 买家提交后,服务端按当前登录用户重新校验订单项归属、完成状态和唯一性。 4. 校验通过后原子保存评价、脱敏展示名快照及图片关联并返回成功;“已评价”由 M07 的唯一评价事实派生,M04 订单和订单项仍保持 Completed,重复操作返回既有已评价结果。 5. 用户进入商品详情,可分页查看公开评价和更新后的评分汇总。 @@ -1505,9 +1540,11 @@ M04 拥有订单状态、快照和处理商家归属,M05 提供已支付事实 - 评分只能为 1~5 的整数,评价必须关联真实商品和订单项。 - 只有订单项所属买家且订单状态为已完成时可以提交评价。 - 同一订单项只能评价一次,数据库唯一约束或等效机制必须作为最终保障。 +- 订单列表/详情的评价摘要按订单项行数统计:非 `Completed` 为不适用;已完成且零项已评为待评价,部分已评为部分评价,全部已评为已评价。批量查询失败或少返回任一订单项时状态为未知,不得把未知显示成未评价。 - 页面最初展示入口只代表当时的提示结果;正式提交时必须重新校验当前买家、订单项归属、订单已完成和尚未评价,不能沿用打开表单时的旧资格。 - 本期评价没有待审核、已隐藏、已删除等状态;提交事务成功的评价全部公开并进入总数与平均分,任何角色都没有隐藏或修改入口。 - 图片为可选;每条评价最多 6 张,单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 200~4096 像素;前端提示与服务端校验必须一致。 +- 暂存图片默认 24 小时有效;未被评价关联的过期图片由可恢复 Worker 登记对象清理。A141 只返回不可猜测的 `imageId`,不返回公共 URL;只有 A142 原子关联成功的图片才可由 A140/A142 返回公开 URL。 - 评价公开展示时不得返回手机号、邮箱、内部用户标识等不必要的敏感信息。 - 公开展示名在提交评价时形成脱敏快照;用户以后修改资料不改变历史评价展示,也不得为评价列表逐条查询用户资料。 - 游客、商家和管理员只能查看公开评价,不能通过伪造订单项提交或修改评价。 @@ -1635,7 +1672,7 @@ M09 为已登录用户提供可持久化的站内通知,覆盖订单创建/取 |---|---| | 订单模块 | 在订单创建、取消、发货和完成成功后提供事件事实、业务 ID 和买家 ID;支付成功事实还提供订单指定处理商家。 | | 支付模块 | 提供已确认且经过幂等处理的支付结果,不以尚未落库或处理中状态生成成功通知。 | -| 售后模块 | 在买家提交申请、提交寄回说明、商家审核以及退款得到确定结果后,按固定接收人矩阵提供事件事实。 | +| 售后模块 | 在买家提交申请、提交寄回说明、商家审核以及退款得到确定成功或最终失败后,按固定接收人矩阵提供事件事实;退款结果未知和单次自动恢复不生成失败事件。 | | Messaging 模块 | 校验事件、接收身份和数据范围,按不同身份选择文案与跳转目标,完成去重、持久化、未读状态和实时推送衔接。 | 消息归属最终以接收用户 ID 和业务数据范围为准,角色只用于选择消息模板、入口和可执行动作。禁止仅按“全部买家”或“全部商家”广播包含订单、支付或售后信息的私人通知。 @@ -1649,7 +1686,9 @@ M09 为已登录用户提供可持久化的站内通知,覆盖订单创建/取 | 订单创建、取消、发货、完成 | 订单买家 | | 支付成功 | 订单买家 + `assignedMerchantUserId` | | 售后申请提交、买家提交寄回说明 | `assignedMerchantUserId` | -| 售后审核结果、退款成功、退款确定失败 | 申请买家 | +| 售后审核结果、退款成功、退款最终失败 | 申请买家 | +| 退款最终失败且 `recoveryDisposition=MerchantRetryRequired` | `assignedMerchantUserId`(另生成可操作的退款重试提醒) | +| 退款结果未知、自动重试中或 `OperatorAttentionRequired` 运维告警 | 不生成额外商家消息;运维告警不进入 M09 | | 支付失败、回调 `Ignored`、回调 `Difference`、对账差异发现或管理员对账处置 | 不生成 M09 消息 | 同一事件要求的全部接收人是一个不可拆分的生成结果:任一必需接收账号缺失、角色不符或与业务归属不一致时,整事件零消息并告警,不允许部分接收人先成功。账号已禁用但身份和业务归属仍有效时仍保存消息历史,只是不允许该账号查询或接收实时推送;重新启用后可查询禁用期间形成的本人消息。 @@ -1687,6 +1726,7 @@ M09 为已登录用户提供可持久化的站内通知,覆盖订单创建/取 - `createdAt` 由服务端生成并统一使用 UTC 存储;前端负责按用户时区展示。 - 未读状态以 PostgreSQL 为准,Redis 或前端角标只可作为缓存,不得成为唯一事实来源。 - 业务事件必须在原业务事务成功后才能生成通知;被回滚的订单、支付或售后操作不得产生成功通知。 +- 退款失败消息只在售后确实进入 `RefundFailed` 后生成。`MerchantRetryRequired` 对买家生成状态通知、对订单指定商家生成 `RefundRetryRequired` 行动通知;`OperatorAttentionRequired` 只给买家状态通知并走系统运维告警,不能向商家展示无法执行的重试入口。 - 禁用账号不能查询消息、建立实时连接或调用已读操作,但其既有消息和禁用期间按有效业务归属生成的新消息均保留;启用后仍只能查看本人消息。 - 消息详情返回历史正文;关联目标不存在、当前无权限或目标模块暂时无法确认权限时,安全操作入口为空并显示“目标暂不可用”,不能隐藏正文或返回未经确认的跳转。 - 默认不提供物理删除消息能力,避免破坏验收追踪;后续如需清理历史数据,应单独定义保留期和归档规则。 @@ -1704,7 +1744,7 @@ M09 为已登录用户提供可持久化的站内通知,覆盖订单创建/取 #### 7. 验收标准与证据 - 分别触发订单创建/取消、支付成功、发货、订单完成和售后审核事件,正确用户能够看到类型、内容和关联对象正确的消息。 -- 支付成功只通知订单买家和指定处理商家;售后申请/寄回只通知指定商家;审核与退款确定结果只通知买家;支付失败、忽略回调、对账差异和管理员对账处置不生成 M09 消息。 +- 支付成功只通知订单买家和指定处理商家;售后申请/寄回只通知指定商家;审核、退款成功和退款最终失败通知买家。只有 `MerchantRetryRequired` 再通知订单指定商家;结果未知、自动重试、系统告警、支付失败、忽略回调、对账差异和管理员对账处置不生成额外 M09 消息。 - 构造一个缺失或角色错误的必需接收人,整事件不生成任何接收人的消息并留下可追踪告警。 - 消息列表分页、类型筛选和未读筛选结果正确,排序稳定且不出现其他用户数据。 - 单条已读、重复已读和全部已读均满足幂等要求,未读数与数据库实际未读记录一致。 @@ -1745,8 +1785,8 @@ M04 提供订单项与实付快照,M05 执行幂等模拟退款,M09 通知 | M10-FR04 | 申请详情 | 展示订单项快照、实付金额、申请内容、审核意见和状态时间线 | | M10-FR05 | 商家审核 | 商家同意或拒绝待审核申请,并填写审核意见 | | M10-FR06 | 状态流转 | 支持待审核、待退货、待收货、退款中、已退款、退款失败、已拒绝和已撤销状态,未定义跳转被拒绝 | -| M10-FR07 | 模拟退款 | 审核通过且满足退款条件后,将金额幂等退回本人小金库,成功后余额立即可用 | -| M10-FR08 | 消息通知 | 申请提交、审核和退款结果通过 M09 通知对应用户 | +| M10-FR07 | 模拟退款 | 审核通过且满足退款条件后,将金额幂等退回本人小金库,成功后余额立即可用;首次进入退款中必须同事务建立唯一退款操作和 `Initial/Executing` 首个尝试,防止无恢复责任的悬空状态。每个申请只建立一个稳定退款操作,结果未知只核实原尝试,确定失败按固定恢复策略自动重试或开放商家重试,任何恢复都不得创建第二笔业务退款 | +| M10-FR08 | 消息通知 | 申请提交、审核和退款确定结果通过 M09 通知对应用户;结果未知和单次自动重试不发送失败消息,只有进入需要商家操作的退款失败时才额外通知订单指定商家,系统关注类失败只产生运维告警而不伪造商家可操作入口 | | M10-FR09 | 操作反馈 | 申请与审核期间防止重复提交,页面明确显示处理中和最终结果 | | M10-FR10 | 撤销申请 | 买家只能撤销本人仍处于待审核状态的申请,审核后不得撤销或回退 | | M10-FR11 | 退货处理 | 退货退款经商家确认收到退货后进入退款;仅退款不要求填写退货物流 | @@ -1754,11 +1794,11 @@ M04 提供订单项与实付快照,M05 执行幂等模拟退款,M09 通知 #### 4. 主流程 1. 买家从订单详情选择符合条件的订单项发起售后。 -2. 服务端校验买家身份、订单项归属、可申请状态、数量和金额。 +2. 服务端先取得 A412 幂等范围,无锁解析订单不可变的 `assignedMerchantUserId` 只用于定位责任门;同一事务随后按“目标商家 DB001 责任门 → DB061 订单 → DB086 售后聚合”锁定并重检买家归属、商家归属、可申请状态、数量和服务端金额。预读结果不能直接授权。 3. 校验通过后创建待审核申请,并通知商家。 4. 商家查看完整申请信息,填写意见并同意或拒绝。 5. 仅退款审核通过后进入退款;退货退款审核通过后等待买家退货和商家确认收货,再执行退款。 -6. M05 幂等退回小金库;成功后余额立即可用,退款失败时保留明确状态并允许安全重试。 +6. 唯一 RefundOrchestrator 先按订单、申请、退款操作和尝试的固定锁序派生服务端结算上下文;首次退款原子建立退款操作与首个执行尝试,Payment 独占退款/钱包事实,AfterSales 独占资格和状态,Catalog/Seckill 独占库存回补。Initial、AutomaticRetry、ManualRetry 的执行租约统一为 60 秒、每 20 秒按原 `attemptId + executionToken + Executing` 续租,且从 `startedAt` 起最多连续持有 5 分钟;到期后恢复 Worker 原子转为 Unknown,旧执行器条件更新失败后必须重读。结果未知时系统只核实原尝试;确认无任何成功副作用后,瞬态失败最多自动重试 3 次,自动额度耗尽或需要商家重新触发时进入退款失败并开放受控重试。 7. 买家可随时查看本人申请进度和结果。 #### 5. 业务规则与权限 @@ -1770,14 +1810,16 @@ M04 提供订单项与实付快照,M05 执行幂等模拟退款,M09 通知 - 售后状态独立于订单履约状态;退款成功后订单保持原核心状态,并展示售后/退款结果,不新增会破坏部分退款表达的统一“已退款”订单状态。 - 已支付但未发货订单退款成功后回补库存;已发货或已完成订单仅在退货且商家确认收货后按退货数量回补,仅退款不回补库存。 - 待审核申请允许买家主动撤销;审核通过后不允许回退。商家超时未处理不自动同意或拒绝,只持续显示待处理并提醒。 -- 退款成功后小金库余额立即可用;退款失败保持“退款失败”并允许幂等重试,不伪装成功。 +- 退款成功后小金库余额立即可用。`Unknown` 始终保持“退款中”并只核实原尝试;确定失败分为自动重试、需要商家重试和需要系统关注三种处置。自动重试期间仍显示“退款中”,只有后两类进入“退款失败”,不得把未知结果伪装成失败或成功。 +- 自动重试固定最多 3 次(初始执行不计入),使用 30 秒、2 分钟、10 分钟退避;人工重试必须等待上一次确定失败后至少 60 秒,并与 Worker 对同一退款操作串行。人工重试不设永久总次数上限,但任何时刻最多一笔执行中或待核实尝试。 +- “退款执行租约”保护一次真实退款执行,“恢复租约”保护 Worker 的核实/调度,两者字段、Token 和生命周期不得混用。三类真实执行统一在数据库时间 `startedAt` 建立 60 秒执行租约,每 20 秒条件续租且不得超过 `startedAt + 5 分钟`;租约到期或达到上限只允许把原尝试转为 Unknown 并核实,不能直接开启第二次退款。 - 买家只能访问本人申请,商家只能访问授权经营范围内申请。 - 禁用账号不能新建或主动操作售后,但已存在申请继续由商家和系统处理;账号恢复后可查询最终结果。 - 退款流水纳入 C08 每日对账,至少核对售后退款成功、钱包入账和退款流水三方一致。 -- 申请提交后通知商家;审核通过、审核拒绝、待退货、退款成功和退款失败均通知买家。 +- 申请提交后通知商家;审核通过、审核拒绝、待退货、退款成功以及最终进入“退款失败”均通知买家。只有 `MerchantRetryRequired` 同时向订单指定商家发送可操作提醒;`Unknown`、自动重试中和运维告警不生成 M09 失败消息。 - 同一订单的不同订单项可以分别申请;同一订单项也可以按未售后数量分次申请,但处理中和已退款数量不得重复占用。 - 未发货的已支付订单只允许仅退款;已发货或在售后期限内的已完成订单才提供退货退款选项。 -- 售后申请与商家发货在同一订单行上串行复核并提交;处理中申请阻断发货,已退款数量从可履约数量中扣除,部分退款后仅发剩余数量,全部退款后不得发货。 +- 售后申请与商家发货在同一订单行上串行复核并提交;A412 为避免与商家禁用责任门形成逆序死锁,固定先无锁解析不可变责任商家、锁 DB001 商家门并重检启用,再锁 DB061 订单并重检归属;A307 可从 DB061 订单开始。处理中申请阻断发货,已退款数量从可履约数量中扣除,部分退款后仅发剩余数量,全部退款后不得发货。 - 售后申请和退款处理不参与 C03 待支付订单超时扫描。 - 售后页面展示状态时间线;本期不做批量审核,不接入真实退货物流平台,只记录必要退货说明。 @@ -1790,6 +1832,10 @@ M04 提供订单项与实付快照,M05 执行幂等模拟退款,M09 通知 | 重复申请 | 返回已有申请或拒绝重复额度 | | 两名商家并发审核 | 仅第一次符合状态的审核成功 | | 重复退款请求 | 返回首次结果,不重复增加钱包余额 | +| 退款执行结果未知 | 保持退款中,按原尝试身份安全核实;不得创建新尝试或发送失败通知 | +| 瞬态确定失败且仍有自动额度 | 保持退款中,按固定退避自动重试;不要求商家点击、不重复通知买家 | +| 自动重试耗尽或需商家触发 | 进入退款失败,通知买家和订单指定商家;商家在冷却结束后可重试同一退款操作 | +| 数据不变量或商家无法解决的确定失败 | 进入退款失败,通知买家并立即运维告警;不向商家展示虚假重试入口 | | 通知暂时失败 | 售后事实保持成功,由可靠消息重试 | #### 7. 验收标准与证据 @@ -1798,6 +1844,8 @@ M04 提供订单项与实付快照,M05 执行幂等模拟退款,M09 通知 - 越权、超额、超时和重复申请被拦截,审核意见与状态时间线可查。 - 已支付、已发货及完成后 7 天内的订单按规则进入售后;超过期限或不符合状态时被明确拦截。 - 同意退款后金额按订单项实付单价和数量正确返回本人小金库且立即可用,重复处理不重复退款。 +- 退款结果未知时只核实原尝试;自动重试最多 3 次,商家重试与 Worker 并发只产生一个新尝试,任何恢复都复用同一退款操作且最多退款一次。 +- 买家只在确定成功或最终退款失败时收到状态消息;指定商家仅在确有可执行重试动作时收到经营提醒,系统关注类失败进入运维告警而不新增管理员消息中心。 - 仅退款、未发货退款和退货退款分别遵守已确认的库存处理规则,订单核心履约状态不被部分退款错误覆盖。 - 四类身份入口和权限正确,申请与审核过程均有清楚反馈和站内通知。 - 保存申请、审核、拒绝、退款、重复处理和越权场景证据,并能说明状态机与幂等边界。 @@ -1833,28 +1881,28 @@ C01 要求在面向买家的高并发秒杀场景下保证库存“只减不超 | 编号 | 功能 | 详细要求 | |---|---|---| | C01-FR01 | 秒杀活动定义 | 商家在后台维护秒杀活动:绑定一个已上架的商品、设定开始时间、结束时间、秒杀价、秒杀库存总量和单用户限购数量。开始时间 ≥ 当前时间、结束时间 > 开始时间、秒杀库存 ≤ 商品当前可售库存且为正整数;保存为受控枚举状态:草稿、已发布、进行中、已结束、已取消。 | -| C01-FR02 | 秒杀活动浏览 | 买家在指定入口查看正在进行和即将开始的秒杀活动:返回活动 ID、关联商品基础信息、主图、秒杀价、原价、活动开始时间、活动结束时间、剩余库存和已售数量;同一城市/同一时区用户看到一致的剩余库存和倒计时。 | -| C01-FR03 | 秒杀下单入口 | 买家点击“立即抢购”直接进入秒杀提交订单:必须登录、必须命中进行中的活动、可购数量必须大于 0;提交前在客户端防抖、防连点和按钮置灰,但所有可购买性校验以服务端为准。 | -| C01-FR04 | 库存条件扣减 | 服务端以数据库条件更新作为秒杀库存扣减的唯一正确性手段,条件至少同时约束活动、时间窗口、状态和剩余库存;未命中更新条件时拒绝请求,不得在应用层先读后写或仅依赖 Redis 扣减。 | -| C01-FR05 | 事务一致性 | 秒杀库存扣减、订单创建和秒杀订单项快照写入必须在同一数据库事务中完成;任何一步失败整事务回滚:库存不被扣减、不产生订单、不产生订单项、不产生支付前置记录。 | +| C01-FR02 | 秒杀活动浏览 | 买家在指定入口查看正在进行和即将开始的秒杀活动:返回活动 ID、关联商品基础信息、主图、秒杀价、原价、活动开始时间、活动结束时间、有效状态、剩余库存、已售数量、服务端权威时间和单调结果版本。GET 只基于同一次数据库权威时间计算 `effectiveStatus`,不得为了追赶生命周期而写库;同一时点的用户看到一致状态和倒计时,前端只接受版本不小于当前结果的快照。 | +| C01-FR03 | 秒杀下单入口 | 买家点击“立即抢购”后,游客先登录并只恢复活动详情,Buyer 进入独立秒杀确认页而不经过购物车。确认页复用 A010/A011:存在唯一默认地址时预选并突出展示,无默认地址时保持未选择且不得自动取第一条;新增地址固定非默认并只刷新本页。买家明确确认正整数数量和 `addressId + addressVersion` 后才生成稳定请求标识并主动提交;任何登录恢复、新增地址或页面返回都不得自动下单。 | +| C01-FR04 | 库存条件扣减 | 服务端以数据库条件更新作为秒杀库存扣减的唯一正确性手段。命令先锁定活动并取得一次锁后数据库 `decisionTime`,按该时间追赶并持久化应发生的生命周期状态,再以同一 `decisionTime` 同时约束活动、`Ongoing`、`startAt <= decisionTime < endAt` 和剩余库存;未命中时拒绝,不得在应用层先读后写、使用锁等待前时间或仅依赖 Redis 扣减。 | +| C01-FR05 | 事务一致性 | 秒杀下单先取得 DB104 幂等范围,再在同一数据库事务依次锁定唯一默认商家门、本人地址并复核提交版本、活动和买家配额;秒杀库存扣减、限购占用、订单创建、地址/订单项快照与可靠事件必须整体完成。地址不存在/越权或版本变化时零扣库存、零占限购、零建单;其他任一步失败也整事务回滚,不产生支付前置记录。 | | C01-FR06 | 单用户限购 | 在同一秒杀活动中,同一买家当前有效秒杀数量不得超过限购;以 PostgreSQL 中 `(activity_id, buyer_id)` 唯一配额事实原子累加,取消成功时按订单幂等释放,不能使用普通聚合查询或 Redis 作为并发正确性边界。 | -| C01-FR07 | 时间窗口校验 | 提交秒杀订单时服务端再次校验当前 UTC 时间落在活动开始和结束之间;活动开始前和结束后均不进入扣减;状态字段也参与校验,避免服务器之间时钟轻微漂移造成提早或延后成功。 | -| C01-FR08 | 接口幂等 | 秒杀下单接口接受稳定的请求幂等标识,同一买家、活动和标识在约定窗口内重复提交只生成一笔订单并返回同一结果;重复请求不得重复扣减库存或创建订单。 | +| C01-FR07 | 时间窗口校验 | 所有写命令在取得目标活动并发控制后只调用一次数据库权威时间形成 `decisionTime`,同一事务复用该值。`decisionTime < startAt` 不得抢购,`startAt <= decisionTime < endAt` 才可抢购,`decisionTime >= endAt` 必须先形成或视为自然 `Ended` 并拒绝;请求到达网关或开始事务的时间不决定资格。Worker 扫描时间只用于找候选,锁后仍须重新取时并复核。 | +| C01-FR08 | 接口幂等 | 秒杀下单接口接受稳定请求标识,请求指纹固定绑定规范化 `activityId + quantity + addressId + addressVersion`;同一买家和标识的同内容重复提交只生成一笔订单或重放首次确定拒绝,换内容复用被拒绝。地址版本冲突属于可重放的确定拒绝;数据库连接中断、事务结果未知等瞬态结果不固化,客户端复用原标识安全重试。 | | C01-FR09 | 失败回退与取消 | 秒杀订单与普通订单取消(买家主动或 C03 超时取消)后的库存回补必须回补到同一活动的秒杀可售库存,不污染普通商品库存;同一笔秒杀订单不得重复回补,无论取消接口被调用多少次都只能回补一次。 | | C01-FR10 | 状态展示与售罄 | 活动剩余库存为 0 时立即展示“已售罄”并禁用抢购入口。前端轮询或服务端推送每次都必须返回读取时点的数据库权威库存快照,旧结果不得覆盖新结果;本期不做库存预占,展示快照与并发提交之间可能发生竞争,若最后库存被其他请求先扣减,失败响应必须带回提交后的权威库存并在同一交互中立即刷新。已确认售罄后不得继续显示可抢,数据库仍有库存时不得错误展示售罄。 | | C01-FR11 | 流量削峰与体验保护 | 在 Nginx、API、应用层或数据库连接池执行有上限的限流,超过上限的请求被快速拒绝并返回 `429/409`;普通商品入口和秒杀入口流量隔离,秒杀高并发不得拖垮普通商品查询。 | | C01-FR12 | 日志与追踪 | 所有秒杀提交请求记录买家 ID、活动 ID、商品 ID、请求数量、抢购买结果(成功/已售罄/超限/重复/未开始/已结束)、受影响行数、订单号(成功时)和 traceId;不记录 Token、密码或支付卡号。 | | C01-FR13 | 与订单、支付、消息衔接 | 秒杀成功订单沿用 M04 提交订单后状态机、支付(M05)、商家发货(M06-02)、消息(M09)和售后(M10)流程;不需要为秒杀订单开辟独立支付通道或独立通知渠道,但需要在订单上保留“秒杀活动 ID / 秒杀价快照”便于追溯。 | -| C01-FR14 | 活动运营动作 | 商家可对草稿、已发布和进行中活动执行“取消”动作:取消后本期保留已分配库存、不回收到普通库存,避免取消后被普通订单夹带走量;已结束或已取消活动拒绝重复状态变更。 | +| C01-FR14 | 活动运营动作 | 商家可取消本人创建的活动。`Draft` 不由 Worker 推进且无库存划拨,无论计划时间是否已过都可取消,避免形成无法处置的过期草稿;`Published` / `Ongoing` 仅在锁后 `decisionTime < endAt` 时可取消,达到结束时间时先按自然结束裁决并拒绝取消。取消后本期保留已分配库存、不回收到普通库存;已结束或已取消活动拒绝新取消请求,同一幂等标识仍重放首次结果。 | | C01-FR15 | 演示数据与脚本 | 提供可重复执行的压测脚本、配套数据和环境变量,并支持一键回滚到秒杀前的库存基线和订单基线。 | #### 4. 主流程 -商家创建秒杀活动并发布,进入“已发布”状态;到达开始时间后自动转为“进行中”,结束时间后转为“已结束”。活动期间数据库中秒杀库存以“剩余可售”数值作为条件更新依据。 +商家创建秒杀活动并发布,进入“已发布”状态;到达开始时间后转为“进行中”,结束时间后转为“已结束”。Worker 负责持久化常规生命周期推进;公开 GET 只按一次权威时间计算有效状态而不写库;发布、取消和抢购等写命令锁定活动后必须用同一锁后 `decisionTime` 追赶到应有状态再裁决,因此 Worker 延迟既不能让活动提前开售,也不能让已到期活动继续成交。活动期间数据库中秒杀库存以“剩余可售”数值作为条件更新依据。 -买家发起秒杀订单请求,服务端按以下顺序校验:登录身份、目标活动存在、活动处于进行中、单用户未超限购、客户端可购买数量大于 0、当前 UTC 时间落在窗口内。任一校验失败立即返回明确原因,不进入库存扣减。 +买家发起秒杀订单请求,服务端先完成身份、固定字段和幂等重放校验;全新请求通过准入保护后,在同一数据库连接与事务中先锁定默认商家责任门、校验本人地址并形成 `trustedCheckoutContext`,保持该行锁,再按固定顺序取得目标活动和买家配额事实并生成锁后 `decisionTime`,追赶活动状态并校验商品、时间窗口、剩余库存和单用户限购。任一校验失败形成明确的确定结果且不进入订单创建;基础设施未知结果不得伪装为售罄或超限。 -校验通过的请求进入数据库事务:使用条件更新一次性扣减秒杀库存并累加已售数量,原子占用买家限购配额,影响行数符合预期才继续;接着通过 Ordering 公开应用契约写入共享 `orders` / `order_items` 订单事实,不建立第二套秒杀订单状态机。事务提交成功后将订单号、库存扣减结果和活动 ID 一并返回。 +校验通过后在当前事务使用条件更新一次性扣减秒杀库存并累加已售数量,原子占用买家限购配额,影响行数符合预期才继续;接着把既有 `trustedCheckoutContext` 交给 Ordering 公开应用契约写入共享 `orders` / `order_items`,Ordering 不重新解析地址/商家、不释放默认商家责任门锁、不另开事务,也不建立第二套秒杀订单状态机。整体提交成功后将订单号、库存扣减结果和活动 ID 一并返回。 任一步骤失败时事务整体回滚:库存未被扣减、订单未被写入、不存在半扣减或不存在的订单项;受影响请求以一致的错误码返回,前端据此刷新抢购状态。 @@ -1865,11 +1913,11 @@ C01 要求在面向买家的高并发秒杀场景下保证库存“只减不超 - 秒杀库存与普通商品库存互不干扰:普通下单只能扣减普通库存,秒杀下单只能扣减秒杀库存;取消或回补必须落到原通道。 - 条件更新为唯一正确性保障:库存扣减一律走 `UPDATE ... WHERE remaining >= :qty AND (时间窗口条件) AND (状态条件)`,命中受影响行数 = 0 即失败;不得在应用层以 SELECT 后 DECREMENT 的方式扣减,避免并发下出现超卖。 - 锁与事务边界:秒杀事务只覆盖秒杀库存行、订单写入和必要快照,事务应保持短小,不在事务内调用外部 HTTP、不在事务内等待用户输入;锁的粒度按活动 ID 单行,不全表扫描。 -- 队列不是唯一正确性保障:消息队列仅用于活动开始消息广播、库存剩余量缓存和压测流量削峰;不允许以“队列收到请求 = 抢购成功”为业务判定,最终仍以数据库条件更新的结果为准,避免因为队列丢失、重复或乱序导致超卖或少卖。 +- 队列不是唯一正确性保障:消息队列仅用于可靠事件传递、活动提示和必要的异步削峰协调,不缓存或裁决活动状态、剩余库存、限购与抢购结果;不允许以“队列收到请求 = 抢购成功”为业务判定,最终仍以数据库条件更新的结果为准,避免因为队列丢失、重复或乱序导致超卖或少卖。 - 幂等与去重:买家请求携带稳定的幂等标识;同一买家、活动和标识在约定窗口内重复提交只生成一笔订单。即使缺少幂等标识,也不能跳过单用户限购与重复校验。 - 单用户限购校验口径:以 `(activity_id, buyer_id)` 唯一配额行记录当前占用数量;待支付、已支付订单占用名额,取消成功后按订单最多释放一次,避免并发聚合误判和“取消即永久占名额”造成少卖。 -- 库存语义:`remaining` 为剩余可售,`sold` 为已售;`remaining + sold + frozen` 在活动期间恒等于初始总量;`frozen` 表示被锁定但未最终成功的请求,本期不启用 `frozen`,所有提交要么直接成功,要么立即失败。 -- 时间口径:所有时间以 UTC 存储;活动状态推进依赖数据库 `now()` 或带时区的 UTC 时间,避免服务器本地时间和数据库时间漂移带来的漏洞。 +- 库存语义:Draft 仅有 `plannedQuantity`,尚未形成活动库存;发布成功后固化 `allocatedQuantity = plannedQuantity`,活动全生命周期始终满足 `remainingStock + soldCount = allocatedQuantity`。本期没有 `frozen`、预占或待确认库存;成功请求在原子事务中直接增加已售并形成订单,失败请求不留下库存占用。 +- 时间口径:所有时间以 UTC 存储。读取请求用一次数据库权威 `serverTime` 计算有效状态;可能等待行锁的状态命令必须在取得目标事实并发控制后调用一次 `clock_timestamp()` 形成 `decisionTime`,同一事务复用,禁止使用应用节点时间、客户端发生时间、事务开始时的 `now()` 快照或锁等待前预取时间越过边界。 - 幂等与拒绝顺序:完成认证和固定字段校验后,先按 PostgreSQL 幂等记录处理相同 Key;同指纹直接重放首次结果,不再受当前限流、时间、库存或限购变化影响,不同指纹返回 409。只有全新 Key 才依次进入限流、时间窗口、售罄、单用户限购和其他业务校验;前端不把 5xx 误判为“已售罄”。 - 数据隔离:秒杀活动接口、订单接口和库存接口在读写上都必须按活动 ID、买家 ID 双重过滤;活动维度数据不暴露他人抢购明细,只返回当前请求可见信息。 - 日志脱敏:秒杀日志记录买家 ID、活动 ID、行为结果和 traceId;不输出完整 Token、密码或支付卡号;截图和答辩材料中订单金额、库存数据按需脱敏。 @@ -1883,9 +1931,9 @@ C01 要求在面向买家的高并发秒杀场景下保证库存“只减不超 - 重复提交与连点:买家在同一秒内多次点击“立即抢购”,按钮立即置灰、幂等键去重、条件更新受影响行数 = 0 三道闸门共同保证只生成一笔订单;移动端断网重发也按幂等键处理。 - 单用户超限:同一买家试图抢多件超过单用户限购时,第 N+1 次请求以“超过单用户限购”拒绝,剩余可购数量随剩余库存动态变化;不允许通过更换账号或绕过前台校验绕过此限制。 - 网络与重试:HTTP 5xx、重定向超时和连接中断都可能让客户端重试,幂等键 + 条件更新保证重试不产生副作用;返回 5xx 时前端友好提示“稍后重试”,且重试仍携带原幂等键。 -- 服务器时钟漂移:活动开始/结束时间以数据库 UTC 时间为权威;应用节点间时钟漂移不影响业务结果;活动开始前 1 秒到达的请求仍可能被延迟到 0.x 秒后处理,文档说明允许秒级误差。 +- 服务器时钟漂移与边界排队:活动开始/结束时间以锁后数据库 `decisionTime` 为权威,应用节点间时钟漂移不影响结果。请求即使在开始前到达,只要锁后裁决时已进入窗口即可参与;请求即使在结束前到达,只要锁后裁决时已达到 `endAt` 就必须失败。本期不接受“秒级误差”作为越界成功理由。 - 取消与回补竞争:买家主动取消或 C03 超时取消时回补秒杀库存并释放限购名额;已支付订单不走取消回补,后续退款/退货按 M10 处理。同笔订单的重复取消请求只回补一次。 -- 活动提前取消:商家在“已发布”或“进行中”状态下取消活动,已存在订单继续按既有流程走完;未提交的请求直接拒绝;本期不回收已分配库存,避免与普通购买混淆。 +- 活动取消:过期 Draft 仍可由创建人取消;已发布或进行中活动只在锁后 `decisionTime < endAt` 时可取消。达到结束时间后先自然结束并拒绝取消。取消先提交时后续抢购条件不命中;抢购原子结果先提交时该订单继续走既有流程;本期不回收已分配库存。 - 缓存不一致:秒杀活动列表、活动状态和库存均不进入缓存;商品详情若命中 C07 旧公开值,只能存在于已约定的一致性窗口,参与资格、活动状态和库存始终重新读取数据库事实。 - 越权访问:买家只能查看自己的秒杀订单和抢购资格,不允许通过订单 ID、买家 ID 或活动 ID 直接访问他人的购买明细;接口不存在或无权限返回统一错误,不暴露是否命中目标记录。 - 后台人工干预:演示时手动将 `remaining` 调整为 0 以模拟售罄、压测后重置库存必须使用唯一、明确的运维脚本;不允许直接在生产库上手动 UPDATE 后未记录原因。 @@ -1900,10 +1948,10 @@ C01 要求在面向买家的高并发秒杀场景下保证库存“只减不超 | 1 | 重置演示环境:通过脚本创建或恢复一个秒杀活动,绑定商品,库存 = 10,单用户限购 = 1,开始时间 ≤ 现在 < 结束时间。 | 数据库中活动处于“进行中”,剩余库存 = 10,已售 = 0;前端秒杀入口可访问,按钮可点击。 | | 2 | 准备压测脚本:以 100 并发、同请求体对秒杀提交订单接口发送不少于 100 个请求,记录每个请求的响应码、响应时间、是否成功、对应订单号。 | 压测脚本输出原始日志;并发工具与目标接口确认无误;各独立请求使用不同幂等标识。 | | 3 | 启动压测并观察结果。 | 成功订单数 = 10,剩余可售库存 = 0,已售 = 10;其它 90 个请求以 409/已售罄或 429/限流明确失败;无 5xx 长期堆积现象。 | -| 4 | 校验数据库一致性:查询秒杀库存行、订单表、订单项表,确认所有成功订单均对应一次正常库存扣减;查询 `remaining + sold + frozen = 初始总量`;查询所有“失败请求”没有遗留的扣减或未提交订单。 | 库存不超卖也不少卖;没有“订单存在但库存未扣”或“库存扣了但订单未生成”的孤立记录。 | +| 4 | 校验数据库一致性:查询活动、库存流水、订单和订单项,确认每个成功订单对应一次扣减;核对 `remainingStock + soldCount = allocatedQuantity`,并确认不存在 `frozen` 字段或预占记录;查询所有失败请求没有遗留扣减、限购占用或未提交订单。 | 库存不超卖也不少卖;没有冻结量、孤立库存、孤立限购或“订单存在但库存未扣”的部分结果。 | | 5 | 单用户限购验证:使用同一买家账号依次提交 2 次秒杀请求,仅有 1 次成功。 | 第二次返回“超过单用户限购”或“已售罄”;数据库中同一 `(activity_id, buyer_id)` 仅有一笔秒杀订单。 | -| 6 | 时间窗口验证:把活动开始时间改为未来 1 分钟、结束时间改为未来 2 分钟,重新发布后立刻抢购。 | 所有请求返回“活动未开始”,库存未变动;到达开始时间后再尝试可正常抢购。 | -| 7 | 已结束验证:把活动结束时间改为过去 1 分钟,立刻抢购。 | 所有请求返回“活动已结束”,库存未变动;接口入口退化到普通商品详情。 | +| 6 | 时间窗口验证:新建开始时间为未来 1 分钟、结束时间为未来 2 分钟的 Draft 并在开始前发布,立刻抢购;不要修改已发布活动。 | 开始前返回“活动未开始”且库存不变;到达开始边界后,即使 Worker 尚未扫描,写命令按锁后权威时间追赶为可抢状态并正常裁决。 | +| 7 | 已结束验证:使用结束时间很近的已发布活动等待到 `endAt`,在 Worker 尚未扫描或刚好并发扫描时发起抢购和取消。 | GET 只返回派生 `Ended` 且不写库;抢购不得扣库存;写命令/Worker 最终只持久化一次 `Ended`;取消返回状态冲突,活动不被误写为 `Cancelled`。 | | 8 | 幂等验证:在约定幂等窗口内使用同一标识连续提交两次相同买家、同一活动请求。 | 仅生成一笔订单;剩余库存只扣减一次;两次响应携带同一订单号。 | | 9 | 取消与回补验证:成功抢购一笔订单后主动取消或触发超时取消(C03)。 | 秒杀可售库存回补 +1,已售数量 −1;同笔订单重复取消请求只回补一次。 | | 10 | 流量隔离验证:秒杀压测同时反复访问普通商品列表和详情接口。 | 普通接口响应时间未因秒杀压测显著恶化;秒杀入口自身 P95/P99 在压测报告中记录。 | @@ -1941,11 +1989,11 @@ M04 提供订单状态和库存回补逻辑,M05 与自动取消竞争待支付 | 编号 | 功能 | 详细要求 | |---|---|---| | C03-FR01 | 超时时间 | 正式环境按订单创建后 30 分钟判断,配置需可追踪 | -| C03-FR02 | 到期任务 | Worker 使用定时扫描、延迟任务或经验证的等效方式发现到期订单 | +| C03-FR02 | 到期任务 | Worker 统一扫描 PostgreSQL 到期责任;配置键 `OrderLifecycle:ScanIntervalSeconds/BatchSize/MaxBatchesPerRun` 默认分别为 5 秒、100 条、10 批,允许范围 1~60 秒、1~500 条、1~100 批,多实例配置必须一致 | | C03-FR03 | 条件取消 | 仅当前仍为待支付且已到期的订单可以自动取消 | | C03-FR04 | 库存回补 | 订单取消与对应普通或秒杀库存回补在一致事务边界内完成 | | C03-FR05 | 幂等处理 | 同一任务可重复执行,但不得重复取消或重复回补 | -| C03-FR06 | 失败重试 | 暂时失败的任务可重试,并保留重试次数和最终结果 | +| C03-FR06 | 失败重试 | 逐笔责任保存尝试次数、最近结果、租约与最终结果;60 秒租约每 20 秒续租,暂时失败按 5 秒、30 秒、2 分钟、10 分钟、30 分钟、之后每小时持续重试,第 5 次 Warning、第 20 次及以后每 24 小时 Critical,不因次数耗尽丢弃责任 | | C03-FR07 | 结果通知 | 取消成功后通过可靠事件通知买家并刷新订单状态 | #### 4. 主流程 @@ -2076,7 +2124,7 @@ C06 在 M09 持久化消息之上为当前 PC Web 提供低延迟实时到达能 | 商家(运营人员) | 是 | 按认证用户及经营数据范围接收待发货、售后待处理等通知,点击后进入商家端已授权页面。 | | 管理员 | 否(本期) | 当前没有管理员站内通知需求,不订阅买家或商家消息通道;未来新增时必须单独定义管理员事件。 | -M09 是消息持久化事实来源,C06 只负责实时到达;C10 提供 Nginx、多实例和 Redis Backplane 运行环境。业务模块不得直接向客户端广播未落库的成功事实。 +M09 是消息持久化事实来源,C06 只负责实时到达;C10 提供 Nginx、多实例和版本化应用级 Redis Pub/Sub 分发环境。业务模块不得直接向客户端广播未落库的成功事实。 #### 3. 功能需求 @@ -2086,23 +2134,23 @@ M09 是消息持久化事实来源,C06 只负责实时到达;C10 提供 Ngin | C06-FR02 | 用户定向推送 | 服务端按认证用户标识向目标用户的全部在线连接推送,不向无关用户或公共广播组泄露业务通知。 | | C06-FR03 | 多标签页 | 同一账号在同一浏览器或不同浏览器打开多个标签页时,每个有效连接都能收到通知;任一标签页标记已读后,共享的数据库未读状态保持一致。 | | C06-FR04 | 自动重连 | 前端在非主动退出导致的连接中断后依次立即、2 秒、5 秒、10 秒重连;仍失败时暂停自动尝试,等待浏览器恢复在线或用户手动重试。重连状态不得阻塞页面其他功能。 | -| C06-FR05 | 事实补查 | 初次连接、重连成功和收到实时提示后,前端重新查询 M09 权威未读数,并按需要补查最近消息或消息详情;不假设断线期间的推送能够重放,也不得用本地角标直接 `+1` 推算未读数。 | -| C06-FR06 | 多实例广播 | 两个 API 实例使用 Redis Backplane 或等效共享通道传播 Hub 消息;无论用户连接落在哪个实例、事件由哪个实例触发,都能收到通知。 | -| C06-FR07 | 最小载荷 | 推送至少包含消息 ID、类型、标题/摘要、关联业务类型与 ID、创建时间;不发送完整订单、支付信息、地址、Token 或其他敏感字段。 | +| C06-FR05 | 事实补查 | 初次连接、重连成功和收到实时提示后立即查询 M09 权威未读数;已认证且页面可见时,WebSocket 正常每 60 秒安全校正一次,四次重连失败后每 15 秒补查,隐藏时暂停,重新可见/恢复在线/进入消息中心时立即补查。相同账号、端点与条件只保留一个在途请求,失败按 5/15/30/60 秒封顶退避;不得假设推送重放或用本地角标直接 `+1`。 | +| C06-FR06 | 多实例广播 | 每条已落库消息形成一条 60 秒有效的实时提示。任一 API 入口消费者从共享提示队列取得后,把封闭分发命令发布到环境隔离、版本化的应用级 Redis Pub/Sub 频道;每个 API 实例各接收一次,只复核并发送本实例登记且凭证仍有效的 `connectionId`。不得从入口直接按 `Clients.User(userId)` 广播而绕过远端实例逐连接复核;Redis 发布失败且提示未过期时重投,实例离线或提示过期由 M09 HTTP 补查。 | +| C06-FR07 | 最小载荷 | `MessagingRealtimeHintRequestedV1` 和客户端 `MessageCreated` 使用同一封闭快照,只允许 `eventId`、`schemaVersion="v1"`、`messageId`、`recipientUserId`、`recipientRole`、`messageType`、`title`、`summary`、同空同非空的 `relatedResourceType/relatedResourceId`、同空同非空的 `actionTarget/actionResourceId`、`createdAt`、`expiresAt`。枚举与长度复用已落库 DB101 契约,`expiresAt=createdAt+60 秒`;禁止出现正文、完整订单/支付信息、地址、Token、内部路由或未知属性。 | | C06-FR08 | 失败隔离 | SignalR 或 Redis 推送失败不得回滚已提交的订单、支付、售后事务和 M09 消息;失败需留下可关联的日志和指标。 | -| C06-FR09 | 连接生命周期 | 用户主动退出、JWT 到期、手机号修改或账号禁用后,相关既有连接必须关闭,旧凭证不得重新连接;账号启用也不恢复旧凭证。服务端清理断开的连接状态,不依赖单个 API 实例内存保存跨实例唯一在线状态。 | +| C06-FR09 | 连接生命周期 | 用户主动退出、JWT 到期、手机号修改、账号禁用或页面卸载后,相关既有连接与 HTTP 补查必须停止,旧凭证不得重新连接或在后台继续请求;账号启用也不恢复旧凭证。服务端清理断开的连接状态,不依赖单个 API 实例内存保存跨实例唯一在线状态。 | | C06-FR10 | 非打扰式反馈 | 实时消息采用轻量提示和未读角标,不使用必须立即关闭的连续模态弹窗;相同消息 ID 只展示一次。短暂断线静默重连,持续断线才显示连接状态,重连成功后自动恢复提示并刷新未读数。 | | C06-FR11 | 身份化路由 | Hub 连接和用户通道必须来自服务端认证结果;买家与商家可以复用技术通道,但接收组、消息模板和跳转目标按身份隔离,客户端不得通过修改参数订阅其他身份或其他用户。 | | C06-FR12 | 固定传输 | 当前 PC Web 只使用 WebSockets 并跳过 SignalR 协商;Nginx 必须支持连接升级。WebSocket 不可用时不自动切换 SSE 或长轮询,只保留 M09 HTTP 查询或定期补查能力。 | #### 4. 主流程 -1. 业务模块提交事务并形成事件。 -2. M09 消费事件、幂等生成站内消息并提交数据库事务。 -3. 消息提交成功后调用实时推送能力,按接收用户 ID 发送最小载荷。 -4. Redis Backplane 将通知传播到持有该用户有效连接的 API 实例;推送前若不能确认连接身份仍有效,则关闭连接而不是继续发送。 -5. 前端收到通知后仅按消息 ID 去重展示轻提示,并立即通过 M09 查询权威未读数,按需要补查最近消息或详情;不得本地执行“角标 +1”,也不得仅凭推送载荷修改业务最终状态。 -6. 若步骤 3~5 任一步失败,用户在重连、网络恢复、手动重试或打开消息中心时通过 M09 HTTP 查询补偿。 +1. 业务模块提交事务并形成可靠来源事件。 +2. M09 Worker 消费来源事件,在同一 PostgreSQL 事务内幂等生成全部接收人的站内消息、成功消费事实,以及每条消息各自的 `MessagingRealtimeHintRequestedV1` Outbox;任一步失败均不留下部分接收人或孤立提示。 +3. Outbox Publisher 发布实时提示,任一 API 入口消费者从 RabbitMQ 共享队列取得;提示已过 `message.createdAt + 60 秒` 时确认丢弃,消息事实仍保留。 +4. 未过期提示由入口消费者发布到版本化应用级 Redis Pub/Sub 分发频道;每个存活 API 实例的轻量订阅者各接收一次,只从本地连接登记选出目标账号连接,并即时复核 `jti`、`tokenVersion`、账号状态和到期时间。 +5. 每个实例只对复核通过的本地 `connectionId` 调用 Hub 发送最小载荷;不能安全确认的连接先关闭。前端仅按消息 ID 去重展示轻提示,并立即通过 M09 查询权威未读数,按需补查最近消息或详情;不得本地执行“角标 +1”,也不得仅凭推送载荷修改业务最终状态。 +6. RabbitMQ、Redis、Hub、实例订阅或客户端任一步失败都不回滚 M09;用户在重连、网络恢复、手动重试或打开消息中心时通过 M09 HTTP 查询补偿。 #### 5. 业务规则与权限 @@ -2110,12 +2158,13 @@ M09 是消息持久化事实来源,C06 只负责实时到达;C10 提供 Ngin - WebSocket 握手、普通 API 和 Nginx 转发使用一致的 JWT 验签配置;Token 出现在连接参数时不得被日志完整记录。 - 当前 PC Web 的 SignalR 客户端固定为 WebSockets 且跳过协商;部署侧不得再依赖协商请求与升级请求之间的会话亲和。 - 实时载荷只是“有新事实可查”的提示。未读数、已读状态和消息正文始终以 M09/PostgreSQL 查询结果为准。 +- Redis 分发只承载短期命令,不保存消息历史或唯一在线状态。入口消费者不得直接按用户广播;持有连接的实例必须从本地登记选出并即时复核具体连接,再按 `connectionId` 发送。 - 主动退出、凭证到期、手机号修改和账号禁用均会使旧凭证及其连接失效;服务端不能安全确认撤销或账号状态时,受保护的 Hub 连接按失败关闭处理。 #### 6. 异常与边界场景 - 网络抖动造成重复连接或重复推送时,前端以消息 ID 去重展示;数据库未读数不得因重复推送增加。 -- Redis Backplane 短暂不可用时实时能力降级;只有 Identity 仍能安全确认账号与凭证状态时,M09 受保护 HTTP 查询才可继续,否则同样失败关闭。恢复后不要求重放所有推送,因为持久化列表负责补偿。 +- 应用级 Redis Pub/Sub 暂时不可用时实时能力降级;只有 Identity 仍能安全确认账号与凭证状态时,M09 受保护 HTTP 查询才可继续,否则同样失败关闭。恢复后不要求重放所有推送,因为持久化列表负责补偿。 - 单个 API 实例停止后,连接到该实例的客户端应进入重连流程并切换到可用实例;系统不承诺连接完全无中断,但必须保证消息事实不丢失。 - 前端页面不可见或浏览器节流时,不以客户端收到时间作为业务发生时间,统一展示服务端消息创建时间。 - WebSocket 被代理或网络阻断时,客户端按固定节奏完成有限重连后暂停;页面显示实时能力不可用,但仍可在鉴权可安全完成时使用 M09 HTTP 查询或定期补查,不启用 SSE 或长轮询。 @@ -2131,7 +2180,7 @@ M09 是消息持久化事实来源,C06 只负责实时到达;C10 提供 Ngin | 多标签页 | 同一账号打开至少两个标签页并触发一条消息 | 两个标签页均收到通知;任一标签页标记已读后,刷新另一标签页可看到一致已读状态。 | | 用户隔离 | 用户 A、B 同时在线,只触发 A 的订单消息 | 只有 A 的连接收到通知,B 的消息列表和未读数不变化。 | | 身份隔离 | 买家、被指定的商家运营账号、未被指定的商家运营账号和管理员同时在线,触发一笔订单状态变化 | 只向事件明确指定的买家或商家运营账号推送;其他在线账号不收到该私人订单通知。 | -| 多实例 | 两个 API 实例运行,连接落到实例 1,事件由实例 2 触发 | 通过 Redis Backplane 成功送达,并能用实例标识、Trace 或日志证明跨实例路径。 | +| 多实例 | 两个 API 实例运行,连接落到实例 1,实时提示由实例 2 的入口消费者取得 | 实例 2 发布 Redis 分发命令,实例 1 的订阅者只复核并发送本地连接;成功送达且可用实例标识、Trace 或日志证明“共享队列 → Redis 分发 → 本地逐连接复核”路径。 | | 单实例故障 | 保持客户端在线并停止其当前连接所在 API 实例 | 客户端重连到存活实例;重新查询后消息和未读状态完整。 | | 推送依赖故障 | 暂停 Redis/实时推送后触发消息 | 原业务和消息落库成功;恢复后用户通过列表补查,不出现消息丢失。 | | 非打扰体验 | 连续触发多条消息并制造一次短暂断线 | 当前表单或操作不被中断;相同消息不重复弹出;短暂断线自动恢复,用户仍能从消息中心查看全部消息。 | @@ -2144,7 +2193,7 @@ M09 是消息持久化事实来源,C06 只负责实时到达;C10 提供 Ngin - 保留多标签页、断线重连、用户隔离、多 API 实例和单实例停止的可重复操作脚本或步骤。 - 保存每次演示的 API 实例标识、连接/重连时间、消息 ID、`traceId`、关键日志和最终数据库查询结果。 - 能说明 WebSocket 与 HTTP 的差异、SignalR 的作用、JWT 如何认证连接、为什么不能相信客户端传入的用户 ID。 -- 能说明 Redis Backplane 解决的是跨实例连接路由而非消息持久化,以及 M09 如何补偿断线和推送失败。 +- 能说明应用级 Redis Pub/Sub 解决的是跨实例分发命令传播,各 API 仍只路由并复核本地连接;它不保存消息事实,并能说明 M09 如何补偿断线和推送失败。 - 能证明客户端使用 WebSockets 并跳过协商,且未读角标来自 M09 权威查询而不是累计推送次数。 ### C07 缓存与性能优化 — 罗皓晨 @@ -2174,13 +2223,13 @@ C07 使用 Redis 优化首页商品摘要和商品详情两个高频只读场景 | 编号 | 功能 | 详细要求 | |---|---|---| -| C07-FR01 | 缓存对象 | 本期只缓存固定首页商品摘要和 A103 商品详情中的商品自身公开字段。固定首页没有用户筛选参数,只取 `OnSale` 商品,按 `createdAt DESC, productId DESC` 稳定排序并固定返回前 12 条;普通库存为零的商品仍展示并标记售罄。分类、A102 普通商品列表、关键词搜索、组合筛选、秒杀活动、M07 评价汇总和评价列表均不缓存。缓存内容不含管理员字段、连接信息或用户敏感数据。 | -| C07-FR02 | Cache-Aside 读取 | 查询先读取 Redis;命中时返回缓存值,未命中时查询 PostgreSQL、生成与原接口一致的响应并写入缓存。正常值 TTL 固定为 60 秒;请求取得回填资格后,数据库查询到缓存写入决定的有效回填窗口最多 2 秒,超时仍可按接口规则返回数据库结果,但不得再回填该次可能过旧的值。 | +| C07-FR01 | 缓存对象 | 本期只缓存 A102 的唯一固定首页形态和 A103 商品详情中的商品自身公开字段。固定首页请求必须无分类、关键词、价格、库存和用户筛选,固定 `page=1`、`pageSize=12`、`sortBy=createdAt`、`sortOrder=desc`,只返回 `OnSale` 商品并以 `productId DESC` 作为同时间稳定次序;普通库存为零的商品仍展示并标记售罄。除该固定首页形态外,A102 的普通列表、分类、关键词、组合筛选和自选排序,以及秒杀活动、M07 评价汇总和评价列表均不缓存。缓存内容不含管理员字段、连接信息或用户敏感数据。 | +| C07-FR02 | Cache-Aside 读取 | 查询先读取 Redis;命中时返回缓存值,未命中时取得跨实例回填资格后才开启短生命周期 PostgreSQL `READ COMMITTED` 读取,生成与原接口一致的响应并尝试回填。不得复用取得资格前已开始的 Repeatable Read 或长事务旧快照。正常值 TTL 固定为 60 秒;从取得回填资格到原子校验锁所有权并写缓存的有效窗口最多 2 秒,超时或失去锁仍可返回数据库结果,但不得回填。A102/A103 JSON 固定 `Cache-Control: no-store`,Nginx/CDN/Service Worker 不得建立第二层响应缓存。 | | C07-FR03 | Key 隔离 | Key 必须包含环境、模块、资源类型、资源 ID 或稳定查询标识及必要版本信息,避免不同环境、不同查询条件和不同数据结构互相污染。 | -| C07-FR04 | 写后失效 | 商品、分类展示或库存事实的数据库事务提交后立即执行首次失效,3 秒后执行一次二次失效。名称、价格、普通库存、描述、分类展示、图片新增/删除/排序/主图、销售状态和删除等已提交变化必须失效受影响的 A103 详情;影响首页展示字段、成员资格或稳定顺序时同时失效固定首页。事务回滚时不得发布成功失效。 | -| C07-FR05 | 最终一致窗口 | 正常值 TTL 为 60 秒,空值 TTL 为 10 秒;首次失效在事务提交后立即触发,二次失效在提交后 3 秒触发。提交前已经开始的旧查询最多在提交后 2 秒内回填,因此两次删除都失败时,正常旧值最坏在提交后 62 秒到期,旧空值最坏在提交后 12 秒到期;二次失效用于通常更早清除旧回填,不能替代 TTL 上限。 | +| C07-FR04 | 写后失效 | 商品、分类展示或库存事实的数据库事务一次原子写入 Immediate 与 Delayed 两条独立 Outbox 责任。Immediate 使用固定立即调度并在事务提交可见后尽快投递;Delayed 以 `after_commit_delay` 模式写入且初始未武装,不预先计算绝对投递时间。Outbox 调度器只能扫描已提交可见的 Delayed 行,并以数据库 `clock_timestamp()` 原子写入 `armed_at`、`available_at=armed_at+3 秒` 后再等待到期,因此最早不早于来源事务提交可见后 3 秒;调度延迟只会让二删更晚,不能让其提前。Immediate 的创建、成功或失败均不得创建、武装、取消或推迟 Delayed。两个消费者分别执行幂等 DEL,Key 不存在视为成功;删除失败或结果未知时各自按同 `eventId` 重投,只有确认成功后才写 Inbox 并 ACK。名称、价格、普通库存、描述、分类展示、图片新增/删除/排序/主图、销售状态和删除等已提交变化必须失效受影响的 A103 详情;影响首页展示字段、成员资格或稳定顺序时同时失效固定首页。事务回滚时两条责任均不存在。 | +| C07-FR05 | 最终一致窗口 | 正常值 TTL 为 60 秒,空值 TTL 为 10 秒;Immediate 与 Delayed 分别缩短正常收敛窗口,后者不依赖前者成功。取得回填资格后才开启的新数据库快照最多在 2 秒窗口内写入,因此即使 Immediate 删除、Delayed 武装或 Delayed 删除持续失败,正常旧值最坏在来源事务提交可见后 62 秒到期,旧空值最坏在 12 秒到期;任何 HTTP/Nginx/客户端响应缓存不得绕过该上限。 | | C07-FR06 | 空值保护 | A103 对不存在或不可公开商品写入 10 秒空值;上架、恢复公开或创建同标识资源后立即失效对应空值。固定首页空结果也只保存 10 秒,不得用空值掩盖新上架商品。 | -| C07-FR07 | 热点保护 | 同一热点 Key 未命中时,全系统只允许一个跨实例填充者查询并回填;其他请求最多等待 500 毫秒,仍未得到缓存结果时直接查询 PostgreSQL 并返回,不继续争抢填充资格,也不得无限阻塞。 | +| C07-FR07 | 热点保护 | 同一热点 Key 未命中时,填充者使用 Redis `SET lockKey randomToken NX PX 3000` 取得 3 秒租约;`randomToken` 至少 128 bit,不续租。取得锁后才开启数据库读取,2 秒内通过 Lua 原子执行“锁值仍等于 Token → 写缓存及 TTL”,锁丢失或超时禁止回填;释放使用 compare-and-delete Lua,不能误删新持有者。其他请求最多等待 500 毫秒并重读,仍未命中则直查 PostgreSQL 返回,不争抢填充、不回填、不中断。 | | C07-FR08 | 故障降级 | Redis 不可用时,允许首页和商品详情回退到 PostgreSQL 并记录降级指标;不得返回无法判断新旧的缓存副本冒充数据库结果。 | | C07-FR09 | 多实例一致使用 | 两个 API 实例共享同一 Redis 和 Key 约定;任一实例完成商品变更后,其他实例后续读取应遵守同一失效结果。 | | C07-FR10 | 可观测性 | 记录命中、未命中、写入、失效、错误和降级次数,以及缓存读取耗时;日志携带资源标识和 `traceId`,但不记录完整缓存值中的敏感信息。 | @@ -2201,15 +2250,17 @@ C07 使用 Redis 优化首页商品摘要和商品详情两个高频只读场景 **商品变更:** 1. 商品、订单、秒杀或售后模块通过所属公开能力在 PostgreSQL 事务内完成商品展示事实或普通库存变化。 -2. 事务提交后立即删除受影响的商品详情 Key;若首页展示字段、成员资格或稳定顺序受影响,同时删除唯一固定首页 Key。 -3. 提交后 3 秒再次删除同一组 Key,清理可能由提交前旧查询在首次失效后回填的旧值;失效失败记录告警和重试证据。 -4. 下一次有效读取未命中后从 PostgreSQL 回填新值,所有 API 实例共享更新结果;即使两次删除都失败,旧值也不得超过提交后 62 秒。 +2. 来源事务同时写入 Immediate 固定调度责任和初始未武装的 Delayed 责任;提交可见后,Immediate 尽快删除受影响的商品详情 Key,必要时同时删除唯一固定首页 Key。 +3. Outbox 调度器扫描到已提交可见的 Delayed 行后,以数据库时间原子武装并令其在武装后 3 秒才可投递;到期后再次删除同一组 Key,清理可能由提交前旧查询在首次失效后回填的旧值。武装或删除失败均保留原 Delayed 责任并重试。 +4. 下一次有效读取未命中后从 PostgreSQL 回填新值,所有 API 实例共享更新结果;即使 Immediate、Delayed 调度或两次删除持续失败,正常旧值也不得超过提交可见后 62 秒,旧空值不得超过 12 秒。 #### 5. 业务规则与权限 - 价格、库存和上下架状态以 PostgreSQL 当前值为准;下单流程必须重新校验数据库,不能相信首页或详情缓存中的库存和价格。 - 主动失效与有限 TTL 必须同时存在:主动失效缩短正常更新窗口,TTL 负责约束删除失败或漏删后的最长旧值时间。 -- 延迟 3 秒的二次失效只用于缩短“提交前旧查询回填旧值”的窗口,不能替代事务提交后的首次失效和 60 秒 TTL。 +- Immediate 与 Delayed 是来源事务内同时形成的两条独立可靠责任;Delayed 只用于缩短旧查询回填窗口,首删失败时仍会独立武装和执行,不能替代 Immediate 或 60/10 秒 TTL。调度器只武装已存在的 Delayed,不得创建新责任;任一 DEL 失败/未知不得写成功 Inbox 或 ACK。 +- Redis 填充锁不是业务事实。回填必须在锁 Token 仍归当前请求、2 秒窗口未过且数据库读取是在取得锁后新开的短 `READ COMMITTED` 快照时完成;任何条件不满足只返回数据库结果,不写缓存。 +- A102/A103 JSON 响应使用 `Cache-Control: no-store`,Nginx `proxy_cache` 对这些路径关闭,Service Worker 不缓存;带内容寻址或版本化 Key 的不可变媒体 URL 可按媒体契约长期缓存,不受商品 JSON 规则误伤。 - 固定首页没有分类、关键词、分页或用户筛选参数;只缓存最新 `OnSale` 商品前 12 条的唯一摘要 Key,不为任意查询参数生成缓存 Key。 - A103 缓存只包含商品自身公开字段,不包含 M07 评价、评分汇总、收藏、购物车或任何身份化字段。 - 秒杀库存与普通库存是两个通道。只有改变普通库存或公开商品字段的已提交动作触发 C07;活动内部秒杀库存变化不触发。 @@ -2220,9 +2271,9 @@ C07 使用 Redis 优化首页商品摘要和商品详情两个高频只读场景 - Redis 完全不可用:接口回退数据库并保持结果正确,健康状态和日志能够反映缓存降级。 - 缓存中存在损坏或旧版本数据:视为未命中并删除异常 Key,不向客户端返回反序列化异常或错误结构。 -- 商品刚下架时发生并发读取:提交后立即失效并在 3 秒后再次失效;即使两次删除失败,旧详情最迟在提交后 62 秒失效,下单始终通过 PostgreSQL 拒绝不可售商品。 +- 商品刚下架时发生并发读取:提交可见后 Immediate 尽快失效,Delayed 在其已提交可见并被数据库时间武装后等待 3 秒再失效;即使武装或两次删除持续失败,旧详情最迟在提交可见后 62 秒失效,下单始终通过 PostgreSQL 拒绝不可售商品。 - 热点 Key 同时过期:仅一个跨实例填充者回填,其余请求等待最多 500 毫秒后直查 PostgreSQL,不无限等待或继续争抢填充。 -- 多实例环境中由实例 1 修改商品、实例 2 查询:实例 2 共享同一 Redis、填充资格和失效结果,最坏在提交后 62 秒内读取到新值。 +- 多实例环境中由实例 1 修改商品、实例 2 查询:实例 2 共享同一 Redis、填充资格和失效结果,最坏在提交可见后 62 秒内读取到新值。 - C01 发布改变普通库存时触发失效;秒杀活动内部扣减、取消回补和售后回补只返回原活动库存,不清理与该通道无关的 C07 Key。 - 游客和会员读取同一公开商品时可共享公开缓存,但会员个人字段由独立接口返回;商家修改商品后,游客和会员均在一致性窗口内看到新值,商家管理页始终显示其有权查看的完整字段。 @@ -2245,9 +2296,10 @@ C07 使用 Redis 优化首页商品摘要和商品详情两个高频只读场景 - 保存压测脚本、数据初始化方式、软硬件环境、Commit SHA、配置、测试时间、原始输出和汇总表。 - 保存商品改价、库存变化、上下架、多实例读取、Redis 故障和热点 Key 过期的操作步骤与结果。 -- 保存固定首页排序和 12 条边界、售罄展示、A102 不缓存、A103 不含评价、普通/秒杀库存失效矩阵的验证结果。 -- 能解释 Cache-Aside 的读写流程、数据库为何仍是事实来源、60/10 秒 TTL、提交后立即及 3 秒二次失效的分工,以及 62 秒最坏旧值窗口的来源。 -- 能证明同一热点只有一个跨实例填充者,其他请求最多等待 500 毫秒并可回退 PostgreSQL。 +- 保存 A102 固定首页唯一形态的排序、12 条边界和售罄展示,以及其他 A102 查询不缓存、A103 不含评价、普通/秒杀库存失效矩阵的验证结果。 +- 能解释 Cache-Aside 的读写流程、数据库为何仍是事实来源、60/10 秒 TTL、Immediate 固定立即调度与 Delayed 提交可见后武装并等待 3 秒的分工,以及 62 秒最坏旧值窗口的来源。 +- 能证明同一热点只有一个跨实例填充者,锁误删、锁超时、旧快照和迟到回填均被拦截;其他请求最多等待 500 毫秒并可回退 PostgreSQL。 +- 能证明 A102/A103 经浏览器、Nginx 和 Service Worker 不出现第二层旧响应;不可变媒体 URL 仍可单独长期缓存。 - 能区分缓存穿透、击穿和雪崩,并说明本项目实际处理了哪些场景、没有实现哪些高级方案及原因。 ### C08 支付回调幂等与对账 — 张海洋 @@ -2274,13 +2326,13 @@ M05 处理支付事实,M04 维护订单状态,Worker Service 生成对账批 | 编号 | 功能 | 详细要求 | |---|---|---| | C08-FR01 | 回调接收 | 接收唯一回调标识、支付流水、订单标识、结果和发生时间 | -| C08-FR02 | 回调鉴别 | 按模拟渠道约定验证必要字段和来源,不接受任意客户端伪造结果 | -| C08-FR03 | 幂等处理 | 回调标识和支付流水建立唯一约束,重复回调返回首次处理结果 | -| C08-FR04 | 乱序处理 | 根据订单当前状态和事件时间决定接受、忽略或登记差异 | -| C08-FR05 | 事务一致性 | 支付记录、订单状态、Inbox/处理记录和 Outbox 在一致事务边界内处理 | -| C08-FR06 | 对账批次 | Worker 按日生成对账批次,记录范围、总数、匹配数和差异数 | -| C08-FR07 | 差异识别 | 至少识别支付成功但订单未更新、订单已支付但缺支付流水等差异 | -| C08-FR08 | 差异闭环 | 差异具有待处理、处理中、已解决等受控状态和处理说明 | +| C08-FR02 | 回调鉴别 | 按模拟渠道约定验证必要字段和来源,不接受任意客户端伪造结果。原始 UTF-8 请求实体固定不超过 16 KiB,网关、服务端和接口读取使用同一上限;超限时立即停止读取并返回 413,不得先无界缓冲或进入 HMAC、JSON、数据库处理 | +| C08-FR03 | 幂等处理 | `callbackId` 唯一标识一次回调投递并绑定规范请求指纹:同标识同内容重放首次确定结果,同标识换内容按安全冲突拒绝。`channelTransactionNo` 标识一次模拟通道支付尝试,不是第二个回调幂等键;同一支付流水允许聚合多个不同 `callbackId`,以处理先失败后成功、先成功后失败等合法乱序信号,但首次绑定的订单、金额和币种不可改变。 | +| C08-FR04 | 乱序处理 | 先串行化回调标识和支付流水绑定,再取得订单、支付尝试、既有成功来源与财务提交水位等全部可能阻塞的共享事实;最后只读取一次数据库权威 `finalTime`,根据该时刻的订单状态、既有成功来源、金额/币种和 `paymentDeadline` 决定 `ProcessedSuccess`、`ProcessedFailure`、`Ignored` 或 `Difference`。调用方提交的回调发生时间只用于追踪和对账,不能授权已经过期的支付或覆盖订单终态。 | +| C08-FR05 | 事务一致性 | 支付记录、订单状态、Inbox/处理记录、财务提交序号和 Outbox 在一致事务边界内处理;同一 `finalTime` 同时作为截止裁决时间和该回调确定结果的业务提交时间 | +| C08-FR06 | 对账批次 | Worker 固定每日 00:05 UTC 触发,并每分钟检查是否有已经到触发点的缺失日;启动时立即执行同一补偿检查。每个完整 UTC 日独立生成批次,记录范围、水位、支付/退款锚点数、匹配锚点数和存在至少一条差异的锚点数 | +| C08-FR07 | 差异识别 | 固定识别 12 类支付/回调/退款差异。一个唯一财务 `postingSequence` 是一个比较锚点;同一锚点可命中多条不同规则,但 `differenceCount` 只把该锚点计一次。每类必须冻结锚点、主体、规则码/版本、期望/实际 JSON 键和证据来源,不能由实现人员在 Order、Payment、Callback、AfterSalesRequest、RefundOperation 或 WalletTransaction 中任意选主体 | +| C08-FR08 | 差异闭环 | 差异具有待处理、处理中、已解决等受控状态、并发版本、处理说明和可验证证据。C08 不直接修改订单、支付、退款、钱包或库存;只有所属模块受控动作完成后由管理员引用系统签发结果并重跑原比较规则,或固定规则证明无业务影响,才可关闭 | | C08-FR09 | 稳定反馈 | 买家端不展示回调内部过程,重复和乱序不得造成状态反复跳变 | | C08-FR10 | 退款对账 | 每日核对售后退款成功记录、退款流水和小金库入账,识别缺失、重复或金额不一致 | @@ -2288,20 +2340,26 @@ M05 处理支付事实,M04 维护订单状态,Worker Service 生成对账批 1. 模拟支付渠道发送包含唯一标识的回调。 2. 服务端验证字段并查询是否已处理;重复回调直接返回已有结果。 -3. 首次回调根据订单当前状态和允许流转规则处理支付记录与订单状态。 +3. 首次回调取得全部可能阻塞的权威事实后形成唯一 `finalTime`,再根据该时刻订单状态、截止时间和允许流转规则处理支付记录与订单状态。 4. 事务成功后写入可靠事件,买家和商家只看到确认后的业务状态。 -5. Worker 每日汇总支付记录、订单状态、售后退款和小金库流水,生成对账批次及差异清单。 +5. Worker 每日 00:05 UTC 触发,每分钟检查漏跑,进程启动时立即补查;对所有已结束但尚未生成批次的 UTC 自然日按日期升序逐日汇总支付记录、订单状态、售后退款和小金库流水。每个日期使用 `runKey=YYYY-MM-DD` 生成独立批次及差异清单,空日也生成零笔匹配批次。 6. 管理员查看差异,按受控流程记录原因和处理结果。 #### 5. 业务规则与权限 -- 回调 ID 和支付流水号必须唯一,不能只依赖内存去重。 +- 回调 ID 必须全局唯一并绑定请求指纹,不能只依赖内存去重;支付流水号在模拟通道内唯一标识一次支付尝试,可关联多个回调投递,不得对每个回调重复创建支付尝试。 +- 回调金额固定为 `0.01~9999999999999999.99` 的非指数 JSON 十进制数、最多两位小数,币种固定 CNY;签名头必须使用接口设计冻结的唯一 ASCII 格式和长度,重复头、首尾空白、非规范 Base64 或超限原始 Body 在业务落库前拒绝。 - 重复成功回调不得重复记账、改变订单或发送重复业务通知。 - 已取消订单收到迟到成功回调时不得直接改为已支付,应登记为对账差异。 - 支付记录、订单状态和可靠消息必须在一致事务边界内处理。 - 对账数据仅向管理员开放;买家和商家只看到与自身业务相关的稳定结果。 - 差异修复必须可追踪,不能通过直接改库隐藏原因。 +- 对账差异不得由 Worker 自动修改业务事实。管理员不能只凭文字说明关闭;引用所属模块动作时必须校验动作真实成功、主体和金额匹配,并重新执行发现该差异的原比较规则。若本期没有合法纠正动作且又不能证明无业务影响,差异保持处理中。 +- 多日停机恢复不得只补“昨天”、跳过失败日期或把多日合并成一个批次;每个完整 UTC 日独立冻结范围、水位和批次,先前日期失败时本轮停止,恢复后从首个缺失日期继续。 - 售后显示退款成功但钱包未入账、钱包重复入账或退款金额不一致均必须进入对账差异。 +- 对账中的“订单已支付”是状态谱系而不是只等于瞬时 `Paid`:`Paid`、`Shipped`、`Completed` 且 `paidAt/paymentPostingSequence` 完整都属于已支付谱系,并必须关联唯一成功支付事实;成功支付只允许对应这三种状态,若订单仍为 `PendingPayment` 或已为 `Cancelled` 才属于状态不一致。 +- 历史日批次只由本业务日内、且未超过冻结水位的新财务结果作为计数锚点;锚点稳定引用的更早支付、订单、回调、退款或钱包事实可以作为佐证但不重复计数,`rangeTo` 之后提交的事实不得污染旧日结论。模拟通道历史状态必须由不可变回调记录重建,不能用可变的当前流水聚合倒推旧日。 +- 比较锚点按唯一 `postingSequence` 去重:存在成功退款操作时归入 RefundOperation;否则存在成功支付时归入 Payment;仅有回调终态时归入 Callback。同步钱包支付的支付/扣款流水、成功模拟回调的回调/支付、退款成功的申请/退款/入账/累计退款共享一个锚点而不重复计数。`matchedCount` 是零差异锚点数,`differenceCount` 是至少命中一条规则的锚点数,必须满足 `totalCount=matchedCount+differenceCount`;`differenceCountsByType` 统计差异行,允许总和大于 `differenceCount`。 #### 6. 异常与边界场景 @@ -2312,6 +2370,8 @@ M05 处理支付事实,M04 维护订单状态,Worker Service 生成对账批 | 已取消订单收到迟到成功 | 不改为已支付,进入对账差异 | | 事务处理中断 | 整体回滚,安全重试后仍只处理一次 | | Worker 重复执行对账 | 同一日期和范围不重复生成矛盾批次 | +| Worker 停机多日后恢复 | 从首个缺失的完整 UTC 日起升序逐日补批;每个日期独立事务,失败日之后不跳跃,已成功日期不回滚 | +| 管理员无合法纠正动作但希望关闭差异 | 保持处理中;不得以说明文字、截图或直接改库代替权威事实复核 | | 差异处理并发 | 使用状态条件保证仅一次有效处理 | #### 7. 验收标准与证据 @@ -2319,6 +2379,8 @@ M05 处理支付事实,M04 维护订单状态,Worker Service 生成对账批 - 重复、乱序和延迟回调不会造成重复支付、错误订单状态或重复通知。 - 迟到成功回调不能把已取消订单改成已支付,并能在对账中被发现。 - 每日对账可列出匹配与差异数据,差异状态和处理记录可追踪。 +- 多日停机后能够按完整 UTC 日逐日补齐独立批次,包括无交易的空批次;失败后从首个缺失日期安全恢复。 +- 关闭差异时能证明所属模块动作引用有效或固定规则确认无影响,并能展示原比较规则复核结果;仍有资金或订单不一致时不得关闭。 - 退款对账能够发现售后、退款流水和钱包入账之间的缺失、重复及金额不一致。 - 买家和商家只看到稳定业务状态,管理员能够查看并闭环差异。 - 保留回调重放、乱序脚本、环境参数、原始结果和对账清单,并能解释唯一约束、事务、Inbox/Outbox 和修复流程。 @@ -2344,7 +2406,7 @@ C10 要求使用 Docker Compose 从同一版本一次性启动可演示的完整 | 商家(运营人员) | 是 | JWT 和 `MerchantOnly` 策略在两个实例结果一致;实例切换后仍只访问商家角色及当前账号有权处理的商品、订单和售后数据。 | | 管理员 | 是 | JWT 和 `AdminOnly` 策略在两个实例结果一致;实例切换不扩大管理权限,也不允许查看未授权的用户私人业务数据。 | -M00 提供服务装配和公共健康检查,C06依赖 WebSocket 转发与 Redis Backplane,各业务模块负责自身多实例幂等和数据一致性。Aspire 用于本地开发编排,C10 验收以 Docker Compose 和 Nginx 为准。 +M00 提供服务装配和公共健康检查,C06 依赖 WebSocket 转发与应用级 Redis Pub/Sub 分发,各业务模块负责自身多实例幂等和数据一致性。Aspire 用于本地开发编排,C10 验收以 Docker Compose 和 Nginx 为准。 #### 3. 功能需求 @@ -2357,7 +2419,7 @@ M00 提供服务装配和公共健康检查,C06依赖 WebSocket 转发与 Redi | C10-FR05 | 负载均衡 | Nginx 将 API 请求分发到两个健康实例,并正确转发客户端 IP、协议、Host、请求 ID 及 WebSocket Upgrade 所需请求头。 | | C10-FR06 | 健康检查 | API 提供存活和就绪检查。全局就绪门槛固定为安全配置完整、运行版本兼容、目标 Migration 版本匹配和 PostgreSQL 可用;Redis、RabbitMQ、SeaweedFS 以能力级状态反映,不因单项故障错误阻断全部公开读取。Nginx/Compose 不向停止或全局未就绪实例持续分发新请求。 | | C10-FR07 | 登录态共享 | 身份使用由两个实例共同验证的 JWT;必要的令牌失效记录和共享状态不依赖单实例内存 Session。Redis 不可用或无法确认撤销、账号禁用、手机号修改及全部旧凭证失效事实时,相关受保护 HTTP 和 Hub 连接失败关闭;Redis 恢复后必须先恢复有效期内的撤销事实并通过安全健康检查,才能恢复这些受保护能力。 | -| C10-FR08 | 实时连接 | Nginx 支持 SignalR WebSocket Upgrade;当前 PC Web 只使用 WebSockets 并跳过协商,两个 API 通过 Redis Backplane 共享实时消息通道。单实例停止后客户端可重连到存活实例;WebSocket 持续不可用时回退 M09 HTTP 查询或定期补查,不启用 SSE 或长轮询。 | +| C10-FR08 | 实时连接 | Nginx 支持 SignalR WebSocket Upgrade;当前 PC Web 只使用 WebSockets 并跳过协商。任一 API 入口消费者把提示发布到版本化应用级 Redis Pub/Sub 频道,每个 API 实例各接收一次并只向本地复核通过的连接发送。单实例停止后客户端可重连到存活实例;WebSocket 持续不可用时回退 M09 HTTP 查询或定期补查,不启用 SSE 或长轮询。 | | C10-FR09 | Worker 单独运行 | Worker Service 使用独立容器运行 Outbox、超时取消或对账任务,不随某个 API 实例停止;同一任务的并发与幂等规则由对应业务模块保证。 | | C10-FR10 | 数据持久化 | PostgreSQL、Redis(需要保留的运行数据)、RabbitMQ 和 SeaweedFS 使用明确持久卷;重建应用容器不得删除数据库和对象文件。 | | C10-FR11 | 配置与 Secret | 环境差异通过环境变量或受控文件注入;仓库提供不含真实秘密的示例配置,不提交真实密码、Token、证书私钥或生产连接信息。 | @@ -2501,7 +2563,7 @@ stateDiagram-v2 | 端 | 页面 | |---|---| -| 购物端 | 登录、注册、首页/商品列表、商品详情、购物车、提交订单、订单列表、订单详情、个人信息、地址管理、买家消息中心 | +| 购物端 | 登录、注册、首页/商品列表、商品详情、购物车、提交订单、统一收银台、支付结果、小金库余额与充值、充值/支付记录、订单列表、订单详情、个人信息、地址管理、买家消息中心 | | 商家端 | 商家登录/入口、商品列表与编辑、分类管理、订单列表与详情、发货、售后审核、商家消息中心 | | 管理端 | 管理员登录/入口、买家账号管理、商家账号管理 | | 已选选做 | 商品评价与晒图、收藏、浏览历史、买家/商家站内消息、售后申请、商家售后审核 | @@ -2543,32 +2605,32 @@ Electron 后续原则上复用 PC Web 原型,只补充窗口、桌面导航和 | 教师编号 | 模块与负责人 | 页面或操作入口 | 接口契约(Axxx) | 测试用例 | 当前状态 | |---|---|---|---|---|---| -| F01 | M01-01 用户注册—唐宇昊 | PC Web 注册页 | A001 | 待测试计划登记 | 注册后显式登录、角色拒绝和原子账号创建已统一;接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| F02 | M01-02 登录与退出—唐宇昊 | PC Web 统一登录页、退出入口 | A002~A004;A005 为历史取消编号 | 待测试计划登记 | 单一 JWT、退出和全部旧凭证失效已统一;接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| F03 | M01-03 个人信息与地址—唐宇昊 | PC Web 买家个人中心、地址管理 | A006~A008、A010~A014;A009 为历史取消编号 | 待测试计划登记 | 资料字段、并发修改和独立默认地址切换已统一;接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| F04 | M02-01 商品列表与分类—顾欣月 | 购物端商品列表、分类入口 | A101、A102 | 待测试计划登记 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| F05 | M02-01 商品搜索—顾欣月 | 购物端搜索框、商品列表 | A102 | 待测试计划登记 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| F06 | M02-02 商品详情—顾欣月 | 商品详情页 | A103 | 待测试计划登记 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| F07 | M03-01 购物车—朱惠惠 | 商品加购入口、购物车页 | A201~A208 | 待测试计划登记 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| F08 | M04-01 提交订单—韦乾强 | 购物车结算、提交订单页 | A301 | 待测试计划登记 | 幂等、超时和商家归属已统一,待数据库、OpenAPI 与交叉评审 | -| F09 | M04-02/M04-03/M04-04 订单查询、取消与完成—韦乾强 | 买家订单列表、订单详情、确认收货 | A302~A304、A308 | 待测试计划登记 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| F10 | M05-01 模拟支付—张海洋 | 小金库、统一收银台、支付记录 | A401~A408 | 待测试计划登记 | 重复支付与幂等语义已统一,待数据库、OpenAPI 与交叉评审 | -| F11 | M06-01 后台商品管理—顾欣月 | 商家分类管理、商品管理 | A110~A115、A120~A128 | 待测试计划登记 | 创建、受约束分类删除、图片顺序与同步索引边界已统一,待数据库、OpenAPI 与交叉评审 | -| F12 | M06-02 后台订单管理—韦乾强 | 商家订单列表、详情和发货入口 | A305~A307 | 待测试计划登记 | Ordering 命名、状态与商家归属已统一,待数据库、OpenAPI 与交叉评审 | -| F13 | M06-03 后台用户管理—唐宇昊 | 管理端用户管理 | A015~A017 | 待测试计划登记 | 买家/商家分流、固定责任清单、禁用竞争和全部旧凭证失效已统一;接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| X01 | M07 商品评价与晒图—顾欣月 | 订单评价入口、商品评价区 | A140~A143;A144 为历史取消编号 | 待测试计划登记 | 订单入口、提交重检、唯一评价、公开计分和图片边界已统一,待数据库、OpenAPI 与交叉评审 | -| X02 | M08 收藏与浏览历史—唐宇昊 | 商品收藏、收藏页、浏览历史页 | A018~A022、A024、A025;A023 为历史取消编号 | 待测试计划登记 | 收藏幂等、默认开启、最近 200 条及关闭后旧历史可见已统一;接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| X03 | M09 站内消息—罗皓晨 | 买家/商家消息中心 | A501~A505 | 待测试计划登记 | 固定接收人、整事件原子性和稳定已读水位已冻结;接口已按流程重建,待数据库、实现与交叉评审 | -| X04 | M10 售后流程—张海洋 | 买家售后申请、商家售后审核 | A411~A417、A419、A434;退款入账使用 Payment 应用契约 | 待测试计划登记 | A418/A431/A432/A433 为取消历史号,待数据库、OpenAPI 与交叉评审 | -| C01 | 秒杀与防超卖—朱惠惠 | 秒杀活动页、商家活动管理、压测脚本 | A220~A228;A229/A230 为历史取消编号;订单查询复用 A302/A303;生命周期为 Worker 内部契约 | 待挑战测试登记 | 原子限购、库存通道与状态推进已定义,待数据库、OpenAPI 与跨模块联调 | -| C03 | 订单超时取消—韦乾强 | 订单倒计时、Worker 验收脚本 | 复用 A304;超时扫描为 Worker 内部契约 | 待挑战测试登记 | Worker 扫描、取消复用和原库存通道回补已定义,待数据库与测试评审 | -| C04 | 商品搜索进阶—顾欣月 | 商品搜索页、性能对比报告 | A102 | 待挑战测试登记 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| F01 | M01-01 用户注册—唐宇昊 | PC Web 注册页 | A001 | 待测试计划登记 | 注册后显式登录、角色拒绝和原子账号创建已统一;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| F02 | M01-02 登录与退出—唐宇昊 | PC Web 统一登录页、退出入口 | A002~A004;A005 为历史取消编号 | 待测试计划登记 | 单一 JWT、退出和全部旧凭证失效已统一;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| F03 | M01-03 个人信息与地址—唐宇昊 | PC Web 买家个人中心、地址管理 | A006~A008、A010~A014;A009 为历史取消编号 | 待测试计划登记 | 资料字段、并发修改和独立默认地址切换已统一;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| F04 | M02-01 商品列表与分类—顾欣月 | 购物端商品列表、分类入口 | A101、A102 | 待测试计划登记 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| F05 | M02-01 商品搜索—顾欣月 | 购物端搜索框、商品列表 | A102 | 待测试计划登记 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| F06 | M02-02 商品详情—顾欣月 | 商品详情页 | A103 | 待测试计划登记 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| F07 | M03-01 购物车—朱惠惠 | 商品加购入口、购物车页 | A201~A208 | 待测试计划登记 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| F08 | M04-01 提交订单—韦乾强 | 购物车结算、提交订单页 | A301 | 待测试计划登记 | 幂等、超时和商家归属已统一;接口设计已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| F09 | M04-02/M04-03/M04-04 订单查询、取消与完成—韦乾强 | 买家订单列表、订单详情、确认收货 | A302~A304、A308 | 待测试计划登记 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| F10 | M05-01 模拟支付—张海洋 | 小金库、统一收银台、支付记录 | A401~A408 | 待测试计划登记 | 重复支付、截止裁决与幂等语义已统一;接口设计已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| F11 | M06-01 后台商品管理—顾欣月 | 商家分类管理、商品管理 | A110~A115、A120~A128 | 待测试计划登记 | 创建、受约束分类删除、图片顺序、历史图片保留与同步索引边界已统一;接口设计已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| F12 | M06-02 后台订单管理—韦乾强 | 商家订单列表、详情和发货入口 | A305~A307 | 待测试计划登记 | Ordering 命名、状态、商家归属和组合摘要已统一;接口设计已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| F13 | M06-03 后台用户管理—唐宇昊 | 管理端用户管理 | A015~A017 | 待测试计划登记 | 买家/商家分流、固定责任清单、禁用竞争和全部旧凭证失效已统一;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| X01 | M07 商品评价与晒图—顾欣月 | 订单评价入口、商品评价区 | A140~A143;A144 为历史取消编号 | 待测试计划登记 | 订单入口、提交重检、唯一评价、公开计分和图片边界已统一;接口设计已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| X02 | M08 收藏与浏览历史—唐宇昊 | 商品收藏、收藏页、浏览历史页 | A018~A022、A024、A025;A023 为历史取消编号 | 待测试计划登记 | 收藏幂等、默认开启、最近 200 条及关闭后旧历史可见已统一;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| X03 | M09 站内消息—罗皓晨 | 买家/商家消息中心 | A501~A505 | 待测试计划登记 | 固定接收人、整事件原子性和稳定已读水位已冻结;接口设计已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| X04 | M10 售后流程—张海洋 | 买家售后申请、商家售后审核 | A411~A417、A419、A434;退款入账使用 Payment 应用契约 | 待测试计划登记 | A418/A431/A432/A433 为取消历史号;退款恢复与失败分流已补齐,接口设计已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| C01 | 秒杀与防超卖—朱惠惠 | 秒杀活动页、商家活动管理、压测脚本 | A220~A228;A229/A230 为历史取消编号;订单查询复用 A302/A303;生命周期为 Worker 内部契约 | 待挑战测试登记 | 原子限购、库存通道与状态推进已定义;接口设计已完整定义、数据库设计已确认,待 OpenAPI、实现与跨模块联调 | +| C03 | 订单超时取消—韦乾强 | 订单倒计时、Worker 验收脚本 | 复用 A304;超时扫描为 Worker 内部契约 | 待挑战测试登记 | Worker 扫描、取消复用和原库存通道回补已定义;数据库设计已确认,待实现与测试评审 | +| C04 | 商品搜索进阶—顾欣月 | 商品搜索页、性能对比报告 | A102 | 待挑战测试登记 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | | C06 | 实时消息推送—罗皓晨 | 实时通知、连接状态、消息中心 | SignalR 契约(无 Axxx),补查复用 A501~A505 | 待挑战测试登记 | WebSockets、固定重连、失败关闭与权威补查契约已按流程重建,待部署、实现与测试承接 | | C07 | 缓存与性能优化—罗皓晨,顾欣月协作 | 首页、商品详情、压测报告 | 复用 A102 的固定首页摘要调用、A103 商品详情;不缓存普通列表与搜索 | 待挑战测试登记 | 缓存范围、60/10 秒 TTL、单填充和双删契约已按流程重建,待实现与压测承接 | -| C08 | 支付回调幂等与对账—张海洋 | 回调重放脚本、管理端对账 | A421~A426;退款入账使用 Payment 应用契约 | 待挑战测试登记 | A431/A432/A433 为取消历史号,待数据库、OpenAPI 与交叉评审 | +| C08 | 支付回调幂等与对账—张海洋 | 回调重放脚本、管理端对账 | A421~A426;退款入账使用 Payment 应用契约 | 待挑战测试登记 | A431/A432/A433 为取消历史号;跨日补偿、财务水位和差异闭环的接口设计已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | | C10 | 容器化与负载均衡—罗皓晨 | 统一访问入口、健康与实例验证 | A506、A507 | 待部署验收登记 | Migrator、全局就绪/能力降级、Redis 安全恢复、WebSocket 与优雅停止契约已按流程重建,待部署和验收承接 | -99 个活动 HTTP 及必要的非 HTTP 协作契约均已按流程重建。负责人完成数据库反查、真实 OpenAPI 和交叉评审后,才可把对应状态更新为“已确认”;测试计划完成后,应替换为真实测试用例编号,并随需求变更同步维护状态。 +99 个活动 HTTP 及必要的非 HTTP 协作契约均已按流程重建,并已反查统一数据库设计。负责人完成真实 OpenAPI、实现和交叉评审后,才可把对应交付状态更新为“已实现”;测试计划完成后,应替换为真实测试用例编号,并随需求变更同步维护状态。 ### 9.2 已确认范围 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" index ded89c8..501a8c8 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/C04-\344\270\255\346\226\207\346\220\234\347\264\242\346\265\201\347\250\213.md" @@ -21,7 +21,7 @@ C04 在 M02-01 的基础搜索入口上增加中文分词模糊搜索、多条 | C04 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 完整定义、待交叉评审 | 已确认角色、分支、降级和模块出入口 | | 搜索适配器接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | -| DBxxx 索引维护 | 部分定义 | 本文不发明表字段、索引名和分词参数 | +| DB027 搜索投影与索引 | 完整定义,数据库设计已确认 | 三字段同步投影、1~2 字参数化 ILIKE、3~50 字 GIN 候选、固定 trigram N=3 和重建校验已经承接;待 Migration、实现与压测验证 | | 性能对比原始结果 | 缺失 | 本文登记对比场景与口径,不预填压测结论 | ## 二、模块直接出入口 @@ -227,15 +227,15 @@ flowchart TD - C07:A102 的普通列表、关键词搜索和组合筛选不进入缓存;C04 只读取 PostgreSQL 商品事实。 - M04:下单时由 M04 重读商品事实进行条件扣减,不信任搜索结果中的价格或库存。 -## 十一、接口承接结果与数据库、验证待评审项 +## 十一、接口与数据库承接结果及验证待办 -1. 字符 N-gram 的最小长度和最大长度参数需要在数据库设计中明确,避免过短导致误命中或过长导致索引过大。 -2. 倒排索引的具体列(商品名称、分类名称、描述)需要在数据库设计中确认是否全列建立或部分建立;具体索引类型(pg_trgm/GIN 等)由数据库设计统一约定,本流程图与文字不重复枚举。 +1. 字符分流已经固定:去空白后的 1~2 字关键词对商品名、分类名和描述三字段执行参数化 `ILIKE`;3~50 字使用 PostgreSQL `pg_trgm` 固定 trigram(N=3)和 GIN 候选,不存在另配最大 N 或按请求改变 N 的入口。 +2. DB027 已固定保存规范化的 `product_name_text/category_name_text/description_text` 与三字段拼接的 `search_text`,并建立 `GIN(search_text gin_trgm_ops)`。商品创建、改名、改描述、改分类及分类改名在同一 Catalog 事务同步 UPSERT;投影只负责候选和排名,销售状态、价格、库存和分类筛选继续读取权威表。 3. A102 已冻结稳定次级排序:相关度相同时依次按 Catalog 持有的上架时间倒序、`productId` 倒序;价格或创建时间相同时追加 `productId`。 4. A102 不返回索引缺失、查询计划等内部原因;进阶能力不可用时先执行保持完整字段范围、强制过滤和稳定排序的安全降级,无法保证时返回 `503 CATALOG.SEARCH_UNAVAILABLE`,具体原因只留在日志和指标。 5. 性能对比环境的固定参数(CPU、内存、PostgreSQL 配置、连接池大小)需要在执行前统一记录,避免环境差异影响结论。 6. 进阶搜索失败时的告警和可观测性要求,需要与 M00 公共基建的可观测性约束一致。 -7. DBxxx 索引维护语句与分词参数需要在数据库设计任务中给出可执行定义,不在本流程中预填。 +7. Migration 必须启用 `pg_trgm`、创建 DB027 及其 GIN 索引,并提供从 `products + categories` 全量重建及 `sourceHash` 核对脚本;具体 SQL、查询计划和压测结果属于实现/验证证据,不能再以“数据库待定”改变上述已确认语义。 8. A102 必须沿用 M02 的分类有效性及筛选范围:顶级分类包含直接归属自身及有效直属子分类,子分类只匹配自身;进阶和降级查询不得各自解释分类层级。 ## 十二、验收证据清单 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" index bf10373..6629c99 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M02-\345\210\206\347\261\273\344\270\216\345\225\206\345\223\201\346\265\201\347\250\213.md" @@ -20,7 +20,7 @@ | M02-02/F06 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 完整定义、待交叉评审 | 已确认角色、状态、分支和模块出入口 | | 分类与商品接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | -| DBxxx 分类/商品表 | 部分定义 | 本文不发明表字段、状态码和索引 | +| DB021~DB023、DB026、DB027 | 完整定义,数据库设计已确认 | 分类、商品、图片、库存流水和同步搜索投影均已承接;本文不反向发明表字段或索引 | | C07 缓存协作 | 完整定义 | 只接入固定首页摘要和 A103 商品自身详情;60/10 秒 TTL、3 秒二次失效和 62 秒兜底已冻结 | | C04 中文搜索进阶 | 完整定义,已完成统稿校准 | 在 M02-01 同一查询入口增强,强制过滤、同步索引、降级和性能口径与本文一致 | @@ -65,7 +65,7 @@ flowchart LR - 商品模块不直接访问用户私有数据;买家专属操作(收藏、加购、购买)由 M08、M03、M04 提供,商品模块只提供事实输入和入口。 - 评价(X01)汇总来自 M07,商品详情只做公开读取,不修改评价事实。 - C07 本期只缓存无用户筛选、只取 `OnSale`、按 `createdAt DESC, productId DESC` 稳定排序的前 12 条固定首页摘要,以及 A103 商品自身公开详情;库存为零仍显示售罄。除该固定首页调用外,M02 分类、普通列表、关键词搜索和组合筛选不得接入 C07,每次都按 PostgreSQL 已提交事实执行。A103 不含 M07 评价或评分;正常旧值最坏不超过事务提交后 62 秒,旧空值不超过 12 秒,缓存不可用时回退 PostgreSQL。 -- C04 在 M02-01 列表入口上替换底层搜索实现,对外参数白名单、已上架过滤和返回口径与本文保持一致;接口与数据待评审项在第十章集中登记。 +- C04 在 M02-01 列表入口上替换底层搜索实现,对外参数白名单、已上架过滤和返回口径与本文保持一致;接口与数据库承接结果及实现待办在第十章集中登记。 - 扩展完成后回到的核心结果:F04 公开列表仍只返回已上架商品、F05 关键词查询仍按统一搜索契约返回、F06 详情仍遵守已上架、售罄和有限一致性窗口口径。扩展失败不能改变上述核心结果。 ## 三、购物端商品列表与组合筛选 @@ -107,11 +107,11 @@ flowchart TD S -- "草稿或已下架" --> Y["展示暂不可售,禁用购买入口
提供返回列表入口"] S -- "已上架" --> D["展示名称、主图/图片、描述、当前价格、库存状态和分类
分类停用不改变商品可见性"] D --> E{"当前角色?"} - E -- "游客" --> F["显示登录引导并保留目标商品与原操作意图"] + E -- "游客" --> F["显示登录引导
仅保留 action + productId + quantity + actionKey 白名单意图"] E -- "商家或管理员" --> G["仅展示公开效果,不显示买家专属操作"] E -- "买家" --> H{"当前库存是否充足?"} H -- "否" --> I["保留详情展示并标记售罄,禁用购买入口"] - H -- "是" --> J["可收藏、加购或购买;数量与最终价格仍由 M03/M04 服务端校验"] + H -- "是" --> J["可收藏或加入购物车;购买统一从 M03 已选条目进入结算
数量与最终价格仍由 M03/M04 服务端校验"] D -. "已选 X01" .-> K["展示评分汇总和公开评价列表入口
评价提交资格由 M07 判断"] ``` @@ -121,9 +121,11 @@ flowchart TD - 已上架且库存为零时保留详情并明确标记售罄,不提供加购或购买入口。 - 所属分类自身或其父分类停用时,只要商品仍为已上架就继续公开;详情可以展示分类信息,但购物端分类筛选入口不再提供受影响分类。 - 商品主图加载失败时使用占位图,不阻断其他信息浏览。 +- 商品描述固定为纯文本。服务端把 `CRLF/CR` 统一为 `LF`,去除首尾 Unicode 空白后将空串视为未填写,按 Unicode 标量值计数且最多 2000 个;购物端使用文本插值配合 `white-space: pre-wrap` 展示,禁止 `v-html` 或任何 HTML/脚本解释。 - 后台改价、改库存、上下架或修改内容后,详情在 C07 已约定的一致性窗口内收敛到 PostgreSQL 最新值;超过窗口不得继续返回旧值。 - 图片合规:单个商品最多 8 张,单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 400~4096 像素,第一张作为主图。 - 已下架商品的旧链接只能显示不可售状态,不提供购买入口;历史订单中的商品快照仍可读,但不受当前上下架状态影响。 +- 游客意图不保存任意回跳 URL、身份、价格或金额;Buyer 登录后必须重读商品事实,收藏和加购按对应接口至多恢复一次。本期没有独立“立即购买”,购买统一从购物车选择、A208 结算预览和 A301 主动提交进入,同一流程不得直接从 M02 跳到 M04。 ## 五、核心状态与并发边界 @@ -153,9 +155,9 @@ flowchart TD B -- "搜索/筛选" --> C["展示加载、无结果或失败反馈
保留原条件与清空入口"] B -- "查看详情" --> D["展示名称、价格、库存、分类、图片和描述"] B -- "评价入口(X01)" --> E["展示评分汇总与公开评价列表"] - B -- "收藏/加购/购买" --> F{"当前身份"} + B -- "收藏/加购" --> F{"当前身份"} F -- "游客" --> G["引导登录并保留原商品与意图"] - F -- "买家" --> H["进入 M08 收藏 / M03 加购 / M04 下单主链"] + F -- "买家" --> H["进入 M08 收藏或 M03 加购;购买只能加购后去购物车结算"] F -- "商家或管理员" --> I["不显示买家专属操作"] C --> J["页面恢复或重试,不展示空白页"] D --> J @@ -163,6 +165,8 @@ flowchart TD H --> J ``` +本期没有商品详情“立即购买”动作、游客购物车或 M02 直连 M04 的下单入口。买家购买统一走“加入 M03 购物车 → 选择条目 → A208 结算预览 → A301 主动提交”,详情页不得用视觉捷径绕过该流程。 + 角色化操作边界: - 游客进入购买相关操作时引导登录,登录后保留目标商品和原意图。 @@ -195,21 +199,21 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| | 查询购物端有效分类 | A101 Catalog 分类 | 只返回自身和父级均启用的分类,不返回孤立子分类;顶级分类的 `productCount` 统计自身及有效直属子分类范围,子分类只统计自身范围;分类失效不改变商品销售状态 | 待交叉评审 | -| 查询固定首页摘要、普通列表与组合筛选/关键词搜索 | A102 Catalog 列表/搜索 | 只有无筛选、`OnSale`、`createdAt DESC, productId DESC`、前 12 条的固定首页摘要可由 C07 缓存;所有用户控制的普通列表、关键词和组合筛选每次按 PostgreSQL 强制已上架过滤、分页、白名单排序并正常返回空结果;顶级分类包含自身及有效直属子分类,子分类只匹配自身;库存为零时标记售罄,分类失效时仍可通过全部商品和关键词命中;F05 与 C04 共用同一契约 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 查询商品详情 | A103 Catalog 详情 | 仅公开已上架商品;允许库存为零并标记售罄,允许所属分类停用;缓存只含商品自身公开字段,不含 M07 评价/评分或个人字段;旧值最坏不超过提交后 62 秒 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 查询固定首页摘要、普通列表与组合筛选/关键词搜索 | A102 Catalog 列表/搜索 | 只有无筛选、`OnSale`、`createdAt DESC, productId DESC`、前 12 条的固定首页摘要可由 C07 缓存;所有用户控制的普通列表、关键词和组合筛选每次按 PostgreSQL 强制已上架过滤、分页、白名单排序并正常返回空结果;顶级分类包含自身及有效直属子分类,子分类只匹配自身;库存为零时标记售罄,分类失效时仍可通过全部商品和关键词命中;F05 与 C04 共用同一契约 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 查询商品详情 | A103 Catalog 详情 | 仅公开已上架商品;允许库存为零并标记售罄,允许所属分类停用;缓存只含商品自身公开字段,不含 M07 评价/评分或个人字段;旧值最坏不超过提交后 62 秒 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | | 公开评价汇总与列表(X01 衔接) | A140(由 M07 流程派生) | 商品详情只读取公开评价和评分汇总,不混入上传、提交或资格判断能力 | 待交叉评审 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 ## 九、扩展接入边界 -- C07 缓存:A102 中只有无筛选、`OnSale`、稳定倒序、前 12 条的固定首页摘要调用可以接入缓存,A103 只缓存商品自身公开字段;A101 分类及 A102 普通列表、关键词搜索和组合筛选不缓存。正常值 60 秒、空值 10 秒,事务提交后立即及 3 秒执行两次失效,提交前旧查询的有效回填窗口最多 2 秒,双删失败时正常旧值最坏 62 秒、旧空值最坏 12 秒。 +- C07 缓存:A102 中只有无筛选、`OnSale`、稳定倒序、前 12 条的固定首页摘要调用可以接入缓存,A103 只缓存商品自身公开字段;A101 分类及 A102 普通列表、关键词搜索和组合筛选不缓存。正常值 60 秒、空值 10 秒;商品事务同时建立 Immediate 与初始未武装 Delayed,提交可见后前者尽快失效,后者由数据库时间独立武装并等待 3 秒二次失效。提交前旧查询的有效回填窗口最多 2 秒,即使武装或双删持续失败,正常旧值最坏不超过提交可见后 62 秒、旧空值最坏不超过 12 秒。 - C04 中文搜索:在 M02-01 列表查询入口上替换底层搜索实现,返回口径与基础模糊查询一致;公开浏览口径、参数白名单和已上架过滤不变。 - M03 购物车:只接收本模块输出的已上架商品与实时价格库存;下架或库存归零由 M03 标记失效,不反向修改商品状态。 - M04 订单:下单时由 M04 重读本模块的最新事实进行条件扣减,不信任购物端传入的金额和库存。 - M07 评价:商品详情只读取 M07 公开评价与评分汇总;评价提交入口必须从已完成订单走 M07,商品详情不开放绕过入口。 -## 十、接口承接结果与剩余数据待评审项 +## 十、接口与数据库承接结果及实现待办 1. A101~A103 已明确为公开接口,不要求 JWT;客户端即使携带失效令牌也按游客公开范围处理,不误判为受保护接口。 2. A102/A103 已由服务端强制只公开 `OnSale` 商品;A103 对不存在、不可公开与 `OnSale` 但库存为零分别返回确定结果。 @@ -218,7 +222,7 @@ flowchart TD 5. A103 已同时返回实时 `stock` 与 `stockStatus`;库存为零的 `OnSale` 商品返回 `SoldOut` 并保留详情,不作为下单事实。 6. 评价公开汇总字段由 M07/A140 单独派生;本期不混入 A103 或 C07 商品详情缓存,避免评价变更扩大商品缓存失效范围。 7. C04 进阶搜索继续由 A102 承载,保留商品列表、筛选、分页和返回口径;实现不得建立第二套前端字段契约。 -8. DBxxx 分类/商品表字段尚未形成可实施的完整定义,数据库字段、约束、索引和 GIN/pg_trgm 维护策略仍由后续数据库设计按本流程与接口派生。 +8. DB021~DB023/DB027 已形成完整字段、约束、索引和 GIN/pg_trgm 同步投影设计;当前尚未实现实体、Migration 或搜索测试,不能把设计完成描述为已落地。 9. 接口和测试必须承接 C07 已冻结的固定首页语义、A103 字段隔离、60/10 秒 TTL、跨实例单填充、3 秒二次失效及 62 秒最坏窗口,不得继续保留“缓存参数待确认”的旧口径。 10. A101/A102 必须承接分类有效状态、父子层级、顶级分类筛选范围和同口径 `productCount`;后台存储状态不得直接当作购物端有效状态。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" index 2324cc2..d8e470e 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M06-01-\345\220\216\345\217\260\345\225\206\345\223\201\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -21,8 +21,8 @@ | M06-01/F11 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 完整定义、待交叉评审 | 已确认角色、状态、分支和模块出入口 | | 商家端写操作接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | -| DBxxx 分类/商品表 | 部分定义 | 本文不发明表字段、状态码和索引 | -| C07 缓存失效协作 | 完整定义 | 商品事实提交后按冻结矩阵立即失效并在 3 秒后二次失效,不混入商家写操作核心结果 | +| DB021~DB023、DB026、DB027、DB102、DB105 | 完整定义,数据库设计已确认 | 已承接目录、图片、库存、搜索、可靠失效与对象清理;本文不反向发明数据语义 | +| C07 缓存失效协作 | 完整定义 | 商品事实提交可见后由 Immediate 尽快失效,Delayed 独立武装并等待 3 秒二次失效,不混入商家写操作核心结果 | | C04 搜索索引更新 | 完整定义,已完成统稿校准 | PostgreSQL 同步维护索引,失败安全回退基础搜索,不建设独立索引任务 | ## 二、模块直接出入口 @@ -49,7 +49,7 @@ flowchart LR - M06-01 仅允许已认证且账号正常的商家进入;游客、买家、管理员和已禁用商家都不能读取后台经营数据或执行写操作。前端隐藏入口不能替代服务端身份校验。 - 本项目是单店 B2C,所有正常商家账号共同维护一套分类和商品目录,不按创建人或当前操作人过滤商品所有权。 - 商品模块只暴露分类与商品事实;商家不得修改买家账号、支付事实或订单金额。 -- 商品名称、合法的分类关联、当前价格、库存和主图为必填;价格不得为负,库存必须为非负整数。 +- 商品名称、合法的分类关联、当前价格、库存和主图为必填;草稿价格不得为负,库存必须为非负整数。价格为 0 只允许作为草稿占位,商品上架时价格必须大于 0。 - 商品事务提交后才通知 C07 失效固定首页摘要和目标商品详情;分类、后台列表及用户控制的购物端普通列表和搜索本期不缓存。事务失败时不发布成功结果。 - 删除约束:存在订单、购物车、收藏、浏览、评价、秒杀等任何历史关联时禁止物理删除,由系统建议改为下架。 - 并发保护:编辑与上下架必须使用条件更新防止静默覆盖;业务要求由本流程确定,具体并发字段和失败响应再由接口与数据库设计承接。 @@ -114,10 +114,12 @@ flowchart TD 商品字段与图片校验: -- 商品名称、合法的分类关联、当前价格、库存和主图为必填;价格不得为负,库存必须为非负整数。新建、改绑分类和上架时分类必须在购物端有效(自身及父分类均启用);既有商品的原分类或其父分类后来停用时仍可修改不改变分类归属的字段。 +- 商品名称、合法的分类关联、当前价格、库存和主图为必填;草稿价格不得为负,库存必须为非负整数,执行上架时价格必须大于 0。新建、改绑分类和上架时分类必须在购物端有效(自身及父分类均启用);既有商品的原分类或其父分类后来停用时仍可修改不改变分类归属的字段。 - 新建商品只能绑定购物端有效分类;既有商品在无效分类下可以修改名称、价格、库存、图片和描述,但不能改绑另一个无效分类。 +- 商品描述只接受纯文本:服务端统一换行为 `LF`、去除首尾 Unicode 空白,规范化后为空则存空值,非空最多 2000 个 Unicode 标量值;编辑器不得生成或提交 HTML,重新打开表单时按普通字符串回填并保留内部换行。 - 图片上传到 S3 Compatible Object Storage;单商品最多 8 张,单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 400~4096 像素,第一张作为主图。 - 图片上传失败时明确标记失败图片并允许重试,不清空其他表单字段。 +- 当前图库只统计仍附着于商品的图片。商家删除图片时先完成当前图库移除和主图替换;已被历史订单项图片快照引用的图片进入归档并保留原图/缩略图对象,未被历史订单引用的图片才进入对象清理。删除当前图片不得反向改写历史订单或售后展示。 - 编辑商品时使用并发标记或条件更新防止静默覆盖;冲突时保留已填写内容并提示刷新确认。 ## 五、商品上下架与删除约束 @@ -128,7 +130,7 @@ flowchart TD B -- "上架" --> US{"当前销售状态?"} US -- "已上架" --> U0["返回既有已上架结果
不重复产生状态变化"] US -- "草稿或已下架" --> C{"完整性校验通过且分类在购物端有效?"} - C -- "否" --> X["拒绝上架并指出缺失字段或停用分类"] + C -- "否" --> X["拒绝上架并指出缺失字段、零价或停用分类"] C -- "是" --> D["事务内将商品状态置为已上架"] B -- "下架" --> DS{"当前销售状态?"} DS -- "已上架" --> E["原子推进为已下架
购物端列表与搜索不再返回该商品"] @@ -136,7 +138,7 @@ flowchart TD DS -- "草稿" --> T["拒绝无意义的下架请求
保持草稿"] B -- "删除" --> F{"当前是否为草稿或已下架
且不存在任何历史关联?"} F -- "否" --> Y["拒绝物理删除
已上架则先下架,有历史关联则永久保留"] - F -- "是" --> I["确认后物理删除
商品实体不再存在"] + F -- "是" --> I["同一事务登记全部图片对象清理
删除图库关联、检索投影与内部库存流水后物理删除商品"] D --> J["提交后通知 C07 失效目标详情和受影响的固定首页摘要"] E --> J I --> J @@ -148,7 +150,7 @@ flowchart TD - 下架商品的旧链接只能显示不可售状态,不提供购买入口;历史订单快照不受影响。 - 已上架但库存为 0 的商品继续出现在公开列表、搜索和详情中,明确标记售罄并禁用购买,不自动下架。 - 所属分类自身或其父分类停用不改变已有商品状态;已有已上架商品继续公开。只有后续重新上架时才要求分类在购物端有效。 -- 物理删除只允许草稿或已下架且没有订单、购物车、收藏、浏览、评价、秒杀等任何历史关联的商品;已上架商品必须先完成下架。 +- 物理删除只允许草稿或已下架且没有订单、购物车、收藏、浏览、评价、秒杀、售后等任何外部业务关联的商品;已上架商品必须先完成下架。Catalog 自有图片、搜索投影和普通库存维护流水属于删除成功时必须同步收敛的内部数据,不应被误判为外部历史引用:事务先为全部原图和缩略图登记对象清理责任,再删除 DB023 关联,由商品删除级联收敛 DB026/DB027,最后写 C07 可靠失效事实并删除商品;任一步失败整体回滚。 ## 六、商品销售状态机与并发边界 @@ -208,9 +210,11 @@ flowchart TD | 无效分类下商品尝试重新上架 | 拒绝上架 | 迁移到有效分类,或使原分类及其父分类均启用 | | 已上架商品库存降为零 | 保持已上架并标记售罄 | 不提供加购、结算和购买 | | 图片上传失败 | 标记失败图片并允许重试 | 不清空其他表单字段 | +| 删除已被历史订单图片快照引用的图片 | 从当前图库移除并按固定规则选择新主图,保留归档图片对象 | 历史订单与售后继续显示下单时图片 | +| 删除未被历史订单引用的图片 | 提交图库变化后登记原图与缩略图清理 | 清理失败由补偿任务重试,不把图库结果回滚 | | 两名操作人并发编辑 | 后提交者收到冲突提示 | 不静默覆盖已生效修改 | | 删除存在订单、购物车、收藏、浏览、评价、秒杀等任何历史引用的商品 | 拒绝物理删除 | 提示改为下架并永久保留商品事实 | -| 上架条件不完整 | 拒绝上架 | 指出缺失字段 | +| 上架条件不完整或价格为 0 | 拒绝上架 | 指出缺失字段或“上架价格必须大于 0” | | 事务提交失败 | 数据回滚 | 页面显示保存失败,允许安全重试 | | 缓存失效失败 | 不回滚商品事务 | 由缓存处理器重试并记录可追踪错误 | | 搜索索引异常 | 暂停进阶搜索并回退基础查询 | 重建数据库索引后恢复 | @@ -226,7 +230,7 @@ flowchart TD | 后台商品详情 | A121 后台商品详情 | 返回含 version 字段的全状态商品事实,供编辑并发校验 | 待交叉评审 | | 新建商品 | A122 商品创建 | 校验字段、购物端有效分类和图片,保存为草稿并返回商品事实 | 待交叉评审 | | 编辑商品 | A123 商品编辑 | 允许无效分类下既有商品修改非分类字段;改绑分类只能选择购物端有效分类;并发保护后保存并返回最新事实 | 待交叉评审 | -| 商品上架 | A125 商品上架 | 校验完整性和购物端有效分类;从草稿或已下架变为已上架 | 待交叉评审 | +| 商品上架 | A125 商品上架 | 校验完整性、`price > 0` 和购物端有效分类;从草稿或已下架变为已上架 | 待交叉评审 | | 商品下架 | A126 商品下架 | 事务内将状态置为已下架;购物端列表与搜索立即不再返回 | 待交叉评审 | | 商品后台删除 | A124 商品删除 | 仅允许草稿或已下架且无任何历史关联时物理删除;删除后实体不存在,不返回“已删除”状态 | 待交叉评审 | | 后台分类列表 | A110 后台分类列表 | 返回全部分类的存储状态和派生的购物端有效状态;不得把父分类停用但自身启用的子分类标成有效 | 待交叉评审 | @@ -236,29 +240,29 @@ flowchart TD | 停用分类 | A114 停用分类 | 停用顶级分类时整棵子树移出 A101,但不改写子分类状态或商品状态;A102 仍可在全部商品和关键词搜索中返回其下已上架商品 | 待交叉评审 | | 删除分类 | A115 删除分类 | 仅允许无子分类、无商品和无其他历史引用的分类物理删除;任一引用存在则整体拒绝,并发新增引用与删除只允许一个结果,删除后不保留伪状态 | 待交叉评审 | | 上传商品图片 | A127 商品图片上传 | 按合规要求上传到对象存储,返回主图与附加图标识 | 待交叉评审 | -| 删除商品图片 | A128 商品图片删除 | 删除商品图片关联与对象存储对象,保持引用一致 | 待交叉评审 | +| 删除商品图片 | A128 商品图片删除 | 原子完成当前图库移除和主图替换;有历史订单引用时归档并保留对象,无历史引用时才登记安全清理 | 待交叉评审 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段和错误码不得反向写入业务图。 ## 十、扩展接入边界 -- C07 缓存:商品事务提交后立即失效目标详情;名称、价格、库存、分类展示、图片/主图、销售状态或首页成员资格变化时同步失效唯一固定首页,并在提交后 3 秒对同一 Key 二次失效。分类、后台商品列表及购物端普通列表和搜索不缓存。缓存不可用不影响商品事务;正常值 60 秒、空值 10 秒,双删失败时正常旧值最坏不超过提交后 62 秒、旧空值不超过 12 秒。 +- C07 缓存:商品事务同时建立 Immediate 与初始未武装的 Delayed 两条独立失效责任;提交可见后 Immediate 尽快失效目标详情,名称、价格、库存、分类展示、图片/主图、销售状态或首页成员资格变化时同步失效唯一固定首页。Delayed 由 Outbox 调度器在提交可见后以数据库时间武装,等待 3 秒后对同一 Key 二次失效;Immediate 不得创建、武装、取消或推迟 Delayed。分类、后台商品列表及购物端普通列表和搜索不缓存。缓存不可用不影响商品事务;正常值 60 秒、空值 10 秒,即使武装或双删持续失败,正常旧值最坏不超过提交可见后 62 秒、旧空值不超过 12 秒。 - C04 搜索:商品创建、编辑、上下架、删除或关联分类名称修改时,检索文本、销售状态与 PostgreSQL 数据库索引同步维护;本模块不建设独立索引同步任务,事务回滚时检索事实同样回滚;进阶搜索不可用时由 C04 回退基础模糊查询。 - M03 购物车:商品变为 `Draft` / `OffSale` 或实时可售库存归零后,由购物车模块按 M03 规则标记失效;分类停用不反向改变仍为 `OnSale` 商品的可结算资格,也不反向写入商品状态。 -- M04 订单:商品事务不修改历史订单的地址或商品快照;价格或上下架变更不影响已有订单。 +- M04 订单:商品事务不修改历史订单的地址或商品快照;价格、上下架或当前图库删除不影响已有订单。下单事务只快照当时仍附着的主图,并与 A128 按同一商品事实串行,确保删图与下单只有一个合法先后结果。 - M01 Identity:本模块不修改账号、角色或令牌状态;账号禁用由 M06-03 独立流程处理。 -## 十一、接口承接结果与剩余数据待评审项 +## 十一、接口与数据库承接结果及实现待办 1. A121/A123/A125/A126 已统一使用整数 `version` 与条件更新承接商品编辑、上架和下架并发;冲突返回稳定错误且不静默覆盖。 -2. 商品物理删除必须按全量历史关联判断,不按订单状态排除已取消订单;订单、购物车、收藏、浏览、评价和秒杀等任何历史引用均阻止删除,后续由数据库设计落实约束。 +2. 商品物理删除必须按全量外部历史关联判断,不按订单状态排除已取消订单;订单、购物车、收藏、浏览、评价、秒杀和售后等任何外部引用均阻止删除。DB022~DB023/DB026/DB027/DB102/DB105 已承接“先登记对象清理、再移除内部关联、最后删除商品并可靠失效”的原子顺序,外键与并发测试承担最终竞争保护。 3. 无效分类下已有已上架商品继续公开已冻结;接口需要确保 A101 按“自身及父级均启用”移除受影响入口时,A102/A103 不按分类有效状态额外隐藏商品。 4. A110~A114 必须同时返回分类存储状态与派生的购物端有效状态,并承接顶级分类停用整棵子树退出、子分类状态不改写和父分类恢复后的重新生效规则。 5. A127/A128 已冻结 `isPrimary`、`sortOrder`、首图主图、显式主图替换和删除主图后的固定提升规则;所有主图切换与图片关联写入形成一个一致事务结果。 -6. 商品写接口必须在事务提交后向 C07 提供受影响商品及首页范围,支持立即和 3 秒二次失效;失效失败记录可追踪错误,但不能把缓存失败伪装成商品保存失败。 +6. 商品写接口必须在来源事务内向 C07 提供受影响商品及首页范围并原子建立两条责任;提交可见后由 Immediate 尽快失效,Delayed 由数据库时间独立武装并等待 3 秒二次失效。武装或失效失败记录可追踪错误,但不能把缓存失败伪装成商品保存失败。 7. 搜索索引异常时的降级语义需要在接口契约和 C04 搜索实现之间达成一致;搜索不接入 C07,商家端不感知底层使用哪种索引实现。 -8. DBxxx 分类/商品表字段尚未形成可实施的完整定义,数据库字段、约束、索引和 GIN/pg_trgm 维护策略仍由后续数据库设计按本流程与接口派生。 -9. A122/A127 上传先写受控对象,再在数据库事务内提交商品/图片关联、排序与主图事实;数据库提交失败时清理本次对象,清理失败进入补偿。A128 删除先提交图片关联与新主图事实,再删除对象;对象删除失败进入补偿。所有商家端入口复用同一顺序。 +8. DB021~DB023/DB026/DB027 已形成完整字段、约束、索引、历史图片保留和 GIN/pg_trgm 同步投影设计;当前尚未实现实体、Migration 或数据库测试。 +9. A122 新建商品先持久化幂等工作清单和全部对象 Key,再写原图/缩略图,最后凭围栏 Token 原子提交草稿商品、图片、搜索、库存流水、缓存失效与确定结果;中断后只能续传同一清单或进入清理。A127 后续加图固定为“数据库预留图片名额与 Key → 写原图/缩略图 → 新事务切换为当前图库图片”,15 分钟预留到期后由 Worker 登记对象清理;不能先写对象再首次保存 Key。A128 锁定商品和目标图片后先提交当前图库移除与新主图事实:存在历史订单图片快照引用时保留归档关联和对象,不登记删除任务;不存在历史引用时才登记原图、缩略图清理并移除关联。清理 Worker 删除前必须再次证明对象没有当前图库或历史订单引用;并发下单与删图以同一商品锁和数据库引用约束保证只有一个合法结果。 ## 十二、验收证据清单 @@ -266,6 +270,7 @@ flowchart TD - [ ] F11:商家可完成分类维护,以及商品新增、查询、编辑、受约束删除和上下架;商品必填项、价格、库存、分类和图片校验在前后端均生效。 - [ ] F11:下架商品不会出现在购物端列表和搜索结果,旧链接不再允许购买;历史订单快照保持可读。 +- [ ] F11:删除当前商品图片不会破坏历史订单或售后图片快照;有历史引用时对象保留,无引用时才进入可重试清理。 - [ ] F11:所有正常商家账号维护同一经营目录;分类元数据编辑不受引用阻断,物理删除才检查全部历史引用。 - [ ] F11:停用分类只移出筛选入口,已有已上架商品继续公开;库存为零的已上架商品保持可见并显示售罄。 - [ ] N04:游客、买家和管理员无法进入或调用商家写操作,后端返回正确的认证或授权结果。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" index c8441b0..10c9357 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/gxy/M07-\345\225\206\345\223\201\350\257\204\344\273\267\346\265\201\347\250\213.md" @@ -21,7 +21,7 @@ | M07/X01 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 完整定义、待交叉评审 | 已确认角色、状态、分支和模块出入口 | | 评价接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | -| DBxxx 评价表 | 模板/占位 | 本文不发明表字段、暂存结构、状态码和索引 | +| DB024 `reviews`、DB025 `review_images` | 完整定义、已确认 | 唯一评价事实、图片暂存与绑定已由统一数据库设计承接 | | F06 评价公开读取 | 完整定义 | 商品详情只读取,不在本模块内重复实现 | ## 二、模块直接出入口 @@ -32,7 +32,7 @@ flowchart LR ORD["M04 Ordering
Completed 订单项与归属事实"] -->|"本人订单项归属与完成状态"| RV DET["F06 商品详情"] -->|"只请求公开评价与评分汇总"| RV - RV -->|"是否已评价的派生结果"| ORD + RV -->|"批量返回每个订单项是否已有唯一评价事实"| ORD RV -->|"公开评价与评分汇总"| DET RV -->|"买家脱敏展示名快照"| DET @@ -48,6 +48,9 @@ flowchart LR - 评价公开展示时不得返回手机号、邮箱、内部用户标识等不必要的敏感信息。 - 公开展示名在提交评价时形成脱敏快照;用户以后修改资料不改变历史评价展示,也不得为评价列表逐条查询用户资料。 - **M07 不修改 M04 订单项状态**:"订单项是否已评价"由 M07 的唯一评价事实派生(按订单项 ID 关联查询得到是否已有评价记录);订单项本身的状态机只由 M04 维护,不允许 Review 越界修改 Ordering 的内部订单项状态。 +- M07 为 Ordering 提供批量评价事实公开应用能力:输入当前买家与一组订单项标识,一次返回每个订单项是否已有 DB024 唯一评价事实;它不是新 HTTP 接口,也不返回评价正文或其他买家的信息。 +- 批量结果必须覆盖每个请求订单项;成功结果中不存在评价事实才表示“未评价”。缺少请求 Key、调用失败或归属上下文不一致时,Ordering 必须把评价摘要降级,不能把全部订单项显示为可评价。 +- Ordering 负责结合自身权威 `Completed` 状态派生 `NotApplicable`、`Reviewable`、`PartiallyReviewed`、`Reviewed`;M07 不复制订单状态,也不替 Ordering 判断核心状态。 - 商品详情只读取公开评价,不在本模块内实现评价提交。 - 本期没有评价审核、隐藏、删除、商家回复或管理员治理状态;成功提交的评价立即成为公开事实,并全部进入总数和平均分。 - M07 公开评价、评分汇总和图片不进入 C07 商品详情缓存;每次公开读取都以 PostgreSQL 已提交评价事实为准。 @@ -76,7 +79,9 @@ flowchart TD 关键规则: - 评价表单:1~5 分评分、1~500 字文字评价和最多 6 张可选晒图。 -- 图片合规:单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 200~4096 像素;逐张显示上传状态,失败图片可重试或移除。 +- 评价正文只接受纯文本:服务端把 `CRLF/CR` 统一为 `LF`、去除首尾 Unicode 空白,按 Unicode 标量值计数,规范化后必须为 1~500 个字符;公开页面只用文本插值配合 `white-space: pre-wrap` 展示,禁止 `v-html` 或任何 HTML/脚本解释。 +- 图片合规:单图不超过 5 MB,仅接受 JPEG、PNG、WebP,宽高均为 200~4096 像素;每张生成独立 `Idempotency-Key`,服务端先持久化图片 ID、不可变对象 Key 和文件哈希再写对象。同键同文件重试返回同一 `imageId`,同键换文件拒绝,响应丢失不得重复占用 6 张上限。 +- A141 成功只返回 `imageId`;表单使用浏览器本地 object URL 预览原文件,刷新后不承诺恢复暂存图预览。`Uploading/Pending` 对象不得通过公共媒体源访问,只有 A142 成功关联为 `Attached` 后才返回稳定公开 URL。 - 评分只能为 1~5 的整数;评价必须关联真实商品和订单项。 - 只有订单项所属买家且订单状态为已完成时可以提交评价。 - 打开表单时的资格只用于展示入口;正式提交时重新校验身份、归属、Completed 状态和唯一性。 @@ -114,6 +119,22 @@ stateDiagram-v2 `未评价`、`已评价` 是按订单项是否存在唯一评价事实派生的展示结果,不是 M04 订单项的新状态,也不存在待审核、隐藏或已删除等评价状态。 +订单级评价摘要由 Ordering 使用本模块批量事实派生,固定按订单项行数而不是购买件数计数: + +```text +reviewedItemCount = COUNT(存在 DB024 唯一评价事实的订单项) +reviewableItemCount = + Completed 时的订单项行数 - reviewedItemCount + 非 Completed 时为 0 +``` + +- 非 `Completed` 订单为 `NotApplicable`。 +- `Completed` 且已评价数为零时为 `Reviewable`。 +- `Completed` 且已评价数大于零但小于订单项行数时为 `PartiallyReviewed`。 +- `Completed` 且每个订单项均已有评价时为 `Reviewed`。 +- 同一订单项购买多件仍只对应一条评价资格,不按数量生成多次评价。 +- 当前需求没有“发生售后后失去评价资格”的规则,M07 不得自行增加;A142 仍在提交时校验本人、`Completed` 与唯一性。 + 图片上传状态机: ```mermaid @@ -185,10 +206,11 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 上传评价图片 | A141 评价图片上传 | 先校验当前买家与目标订单项当时具备评价资格,再校验数量、类型、大小和尺寸;上传结果仅归当前买家和目标订单项使用,A142 仍须重检最终资格;不在流程中预设对象键或暂存表结构 | 待交叉评审 | +| 上传评价图片 | A141 评价图片上传 | 先校验当前买家与目标订单项当时具备评价资格,再以必需幂等键持久预留同一图片 ID、不可变对象 Key 和内容哈希,校验数量、类型、大小和尺寸后写对象并完成暂存;同键重放、不公开暂存图,A142 仍须重检最终资格 | 待交叉评审 | | 提交商品评价 | A142 评价提交 | 正式提交时重新校验身份、订单项归属、Completed 状态、唯一性、字段与图片,并原子形成评价、脱敏展示名快照及图片关联 | 待交叉评审 | | 查询订单项评价资格/结果 | A143 评价资格 | 仅为当前买家的订单详情返回可评价或已评价提示;该结果不替代 A142 提交时重检 | 待交叉评审 | | 商品公开评价分页 + 评分汇总 | A140 公开评价 | 无需登录即可分页返回全部成功评价的评分、文字、图片、时间和脱敏展示名,并返回总数与平均分 | 待交叉评审 | +| 订单页批量评价事实 | Review 内部批量应用契约 | 当前买家 + 订单项集合一次返回每个订单项是否已有唯一评价事实;结果全量覆盖、缺 Key 失败,不按订单项循环调用 A143 | 已由流程完整定义,待公开应用签名、实现与测试承接 | 接口详细定义与实现必须承接上述流程结果。现有 A144“单条评价详情/举报链路”没有需求与流程入口,已取消并保留为历史编号,不得反向新增评价详情页或举报流程。HTTP 状态码、请求字段和错误码不得反向写入业务图。 @@ -196,6 +218,7 @@ flowchart TD - F06 商品详情:只读取 M07 公开评价与评分汇总,不在商品详情页内提交评价;评价提交入口由订单详情提供。 - F09 订单完成:评价入口只能从本人 Completed 订单项接入,不能由商品详情绕过。 +- M04 订单列表/详情:通过 Review 批量公开应用能力组合订单级与订单项级评价摘要;Ordering 不直接读取 DB024,也不循环调用 A143。 - M09 站内消息:本期评价提交不产生站内消息,不新增买家、商家或管理员接收人。 - C07 缓存:M07 评价汇总、公开列表和图片不进入本期缓存,商品详情组合展示时分别读取商品缓存结果和 PostgreSQL 评价事实。 - 商家回复、隐藏或点赞:本期不实现;后续如需扩展,必须先修订主需求和本文档的边界约束。 @@ -208,8 +231,10 @@ flowchart TD 4. 脱敏展示名在 A142 成功提交时形成快照,A140 直接返回该快照;具体脱敏格式由接口与前端统一。 5. A140 是公开读取,游客、买家、商家和管理员使用同一公开字段;A142、A143 才要求当前买家身份。 6. 评价、脱敏展示名快照和成功图片关联必须形成一个完整业务结果,任一必要写入失败时不得留下可公开的部分评价。 -7. DBxxx 评价表及图片关联字段尚未形成可实施的完整定义,由后续数据库设计统一派生;流程不预设对象键、暂存表或清理调度。 +7. DB024、DB025 已形成可实施的完整定义;DB024 的订单项唯一约束承担并发防重,DB025 承担评价图片暂存和原子绑定。Ordering 只能经 Review 公开应用能力读取评价存在性,不因共用数据库而直接访问评价表。 8. A144 缺少独立业务入口,已在接口整合中转为历史取消号;不得为保留旧编号而补造举报或评价详情需求。 +9. 批量评价事实契约只返回 `orderItemId + reviewed`,不返回评价正文、图片、评分或评价标识;列表与详情所需的 `NotApplicable/Reviewable/PartiallyReviewed/Reviewed` 由 Ordering 结合核心状态计算。 +10. 当前页或详情的全部订单项应在同一个短生命周期只读 PostgreSQL 快照中批量读取;即使展示快照随后过时,A142 仍以写入时资格重检和 DB024 唯一约束为最终保障。 ## 十一、验收证据清单 @@ -224,6 +249,8 @@ flowchart TD - [ ] N04:评价、脱敏展示名快照和图片关联形成完整原子结果,任一步失败不公开部分评价;评分与文字字段级错误不写入数据库。 - [ ] N02:评价列表为空或加载失败时展示友好空状态或重试入口,不显示空白页。 - [ ] 多订单项并发:同一订单的多个订单项独立评价互不影响;两个浏览器同时提交同一订单项时仅一个成功。 +- [ ] 订单列表与详情使用一次 Review 批量公开应用能力覆盖当前页或详情的全部订单项,不按订单项循环调用 A143;缺 Key 或调用失败时不伪造“未评价”。 +- [ ] 订单级评价摘要按订单项行数派生,正确区分不可评价、待评价、部分已评价和全部已评价;同一订单项购买多件仍只有一次评价资格。 - [ ] 缓存边界:评价汇总、公开列表和图片不进入 C07;商品详情读取评价时以 PostgreSQL 已提交事实为准。 - [ ] 答辩能够说明唯一约束或等效机制如何阻止重复评价、提交时资格重检、脱敏快照生成时机和评价与图片关联的原子结果。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C06-\345\256\236\346\227\266\346\216\250\351\200\201\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C06-\345\256\236\346\227\266\346\216\250\351\200\201\346\265\201\347\250\213.md" index c790a64..25ff146 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C06-\345\256\236\346\227\266\346\216\250\351\200\201\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C06-\345\256\236\346\227\266\346\216\250\351\200\201\346\265\201\347\250\213.md" @@ -18,16 +18,21 @@ C06 在 M09 消息已经成功持久化之后,为当前 PC Web 的已登录买 | C06 需求与教师验收 | 完整定义 | 作为传输、断线重连、多标签页、多实例和持久化补查边界 | | 本文业务流程 | 完整定义 | 冻结连接生命周期、定向推送、权威补查、固定重连和失败隔离 | | 接口设计 4.3.7、4.3.8 | 已按本文重建、未冻结 | 由流程派生 Hub 与服务端事件映射;待部署、实现与交叉评审 | -| A501~A505 | 完整定义,待交叉评审 | 用于补查、详情和已读校正,不为 SignalR 新增 Axxx;待数据库、OpenAPI 与交叉评审 | -| Redis Backplane / C10 | 技术与部署能力待验证 | 只承接跨实例通道,不保存唯一消息事实 | +| A501~A505 | 完整定义,待交叉评审 | 用于补查、详情和已读校正,不为 SignalR 新增 Axxx;数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| Worker、RabbitMQ、API Hosted Service | 完整责任定义,待实现验证 | Worker 可靠形成实时提示责任,API 轻量消费并通过 Hub 推送;不让 Worker 持有 WebSocket 连接 | +| 应用级 Redis Pub/Sub / C10 | 技术与部署能力待验证 | 只承接跨实例分发命令传播;各 API 仍只路由本地连接,不保存唯一消息事实、认证事实或在线事实 | ## 二、直接出入口与不可变结果 ```mermaid flowchart LR ID["M01 Identity
有效 JWT、用户 ID、角色和账号状态"] -->|"允许买家或商家建立本人连接"| RT["C06 实时推送"] - MSG["M09 Messaging
已提交的本人消息"] -->|"消息标识、最小展示信息和接收用户"| RT - C10["C10 多实例环境
Nginx 与共享实时通道"] -->|"连接转发与跨实例传播"| RT + WORKER["M09 Worker
同事务提交 DB101 消息、DB103 Inbox、DB102 实时提示 Outbox"] -->|"MessagingRealtimeHintRequestedV1"| MQ["RabbitMQ 共享实时提示队列"] + WORKER -->|"已持久化消息事实"| MSG["M09 HTTP 查询
DB101 消息与未读事实"] + MQ -->|"任一存活 API 实例消费"| RT + C10["C10 多实例环境
Nginx、API Hosted Service、应用级 Redis Pub/Sub"] -->|"连接转发与跨实例传播"| RT + ID -->|"凭证失效广播"| SECURITY["每个 API 实例的独立临时队列
本地连接登记与服务端 Abort"] + SECURITY --> RT RT -->|"本人全部在线连接"| TABS["PC Web 一个或多个标签页"] TABS -->|"查看详情、校正未读或标记已读"| MSG @@ -37,11 +42,12 @@ flowchart LR 不可变结果: -- C06 只能从“M09 消息事务已提交”接入,来源模块不得绕过 M09 直接广播未落库的成功事实。 +- C06 只能从“M09 消息、DB103 成功消费事实和 DB102 实时提示责任在同一事务提交”接入。来源模块不得绕过 M09 直接广播未落库的成功事实,M09 也不得在数据库提交前直接调用 Hub。 - 推送失败、重复或延迟都不能改变消息未读状态,也不能回滚订单、支付、发货或售后结果。 - 推送只表示“有新事实可查”,客户端收到提示后必须重新查询 M09 权威未读数;不得按推送次数对角标执行本地 `+1`。 - 客户端不得仅凭推送载荷修改订单、支付或售后最终状态;需要业务详情时重新调用所属模块接口。 -- Redis 只解决跨实例 Hub 消息传播,不保存永久消息、唯一未读数或唯一在线状态。 +- `MessagingRealtimeHintRequestedV1` 是 60 秒内有效的可丢弃到达提示,不是业务事件或消息事实;过期、重复、乱序和未送达均由 A501/A503 补查收敛。 +- Redis 只解决跨实例 Hub 消息传播,不保存永久消息、认证撤销事实、唯一未读数或唯一在线状态。 - 当前 PC Web 固定只使用 WebSockets 并跳过 SignalR 协商;Nginx 负责连接升级,部署不依赖协商请求与升级请求之间的会话亲和。 ## 三、连接鉴权与生命周期 @@ -65,45 +71,72 @@ stateDiagram-v2 连接规则: - 客户端携带有效 JWT 建立连接;服务端从认证上下文取得用户 ID 和角色,不接受客户端声明任意接收用户、角色或组。 +- `IUserIdProvider` 只接受认证主体中**恰好一个** `sub` Claim,且该值必须能解析为 UUID;返回值固定为小写 `D` 格式。`sub` 缺失、重复、格式非法,或角色不是 Buyer/Merchant 时,握手直接拒绝。Query、Header 和 Hub 参数中的任何客户端 `userId` 都不能覆盖该结果。 - 买家与商家可以复用技术通道,但消息接收范围、文案和安全操作入口仍按身份隔离。 - 同一账号的每个有效标签页分别建立连接,服务端向该用户全部在线连接发送消息。 -- 用户主动退出、JWT 到期、手机号修改、账号禁用或其他全部旧凭证失效动作发生后,相关既有连接必须关闭,旧凭证不得重新连接;账号启用也不恢复旧凭证。 -- 建连、重连和连接存续期间无法确认账号状态、撤销事实或凭证有效性时失败关闭,不能为可用性继续保留受保护连接。 -- 主动退出、被动断网、连接超时或 API 实例中断后,服务端必须清理该实例持有的断开连接状态;跨实例唯一在线状态不得只保存在单个 API 内存中。 +- Hub 固定启用 `CloseOnAuthenticationExpiration = true`,JWT 自然到期立即由服务端关闭;鉴权 `ClockSkew = 0`,不能用宽限时间继续保留过期连接。 +- 每个 API 实例只登记本实例连接,最少保存 `connectionId → userId、jti、tokenVersion、expiresAt、role、服务端 Abort 句柄`,并建立 `userId`、`jti` 的本地反向索引。登记只服务于定向校验和断连,断开时立即清理,不作为跨实例唯一在线事实。 +- 每次准备推送前必须复核目标连接的签名有效期、`jti` 撤销、账号状态和 `tokenVersion`;连接空闲时也必须至少每 30 秒复核一次。任一事实不匹配或撤销/账号状态无法安全确认时,先执行服务端 `Abort`,再排除该连接,不能“未知但继续推送”。 +- A003 当前令牌退出仅在数据库 `decisionTime < JWT exp` 且首次提交 DB004 撤销事实时,于同一事务写入 `IdentityRealtimeCredentialInvalidatedV1` Outbox,变体固定为 `TokenRevoked(userId,jti,invalidatedAt)`;裁决时已经自然到期则由连接有效期机制关闭,不写伪撤销广播。A006 手机号修改、A016 账号禁用及其他全部旧凭证失效动作在提交 DB001 `tokenVersion` 的同一事务写入 `AccountCredentialsInvalidated(userId,currentTokenVersion,accountStatus,invalidatedAt)` 变体。Outbox Publisher 将安全事件发布到广播 Exchange;不得在 Identity 事务内直接调用 Hub。 +- 每个运行中的 API 实例都声明一个绑定安全失效广播 Exchange 的独立、排他、自动删除队列,收到后按本地登记立即 Abort 所有命中连接:`TokenRevoked` 只匹配同一 `jti`,账号级变体关闭 `tokenVersion < currentTokenVersion` 的连接,`accountStatus=Disabled` 关闭该账号全部连接;一个实例消费不能替代其他实例消费。 +- 安全失效广播只缩短断连延迟,不是认证事实源。广播丢失或实例刚恢复时,`CloseOnAuthenticationExpiration`、每次推送复核和最长 30 秒周期复核仍必须失败关闭;账号启用也不恢复旧凭证。 +- 主动退出、被动断网、连接超时或 API 实例中断后,服务端必须清理该实例持有的断开连接状态;离线实例没有存量连接,不需要补发安全失效广播。 - 非主动断线依次立即、2 秒、5 秒、10 秒重连;四次均失败后暂停自动尝试并显示简短状态,等待浏览器恢复在线或用户手动重试。 ## 四、消息提交后的定向推送 ```mermaid flowchart TD - A["M09:本人消息事务提交成功"] --> B["取得接收用户和最小展示载荷"] - B --> C{"目标身份是否为本期支持的买家或商家?"} - C -- "否" --> X["不建立实时推送
消息事实仍保留"] - C -- "是" --> D{"目标连接的凭证和账号状态仍可安全确认?"} - D -- "否" --> Q["关闭失效或不可确认的连接
不继续推送"] - D -- "是" --> E["按服务端认证用户标识发送"] - E --> F["共享实时通道把消息传播到持有连接的 API 实例"] - F --> R{"目标用户是否有在线连接?"} - R -- "否" --> Y["结束实时尝试
等待 M09 补查"] - R -- "是" --> G["向该用户全部有效连接推送"] - G --> H{"标签页是否已展示同一消息标识?"} + A["Mall.Worker:同一事务提交
DB101 + DB103 + DB102 实时提示 Outbox"] --> B["Outbox Publisher 发布
MessagingRealtimeHintRequestedV1"] + B --> C["RabbitMQ 共享实时提示队列
至少一次投递"] + C --> D["任一 Mall.Api 轻量入口消费者取得提示"] + D --> E{"当前时间是否早于 expiresAt?"} + E -- "否,已满 60 秒" --> X["确认并丢弃过期提示
等待 M09 HTTP 补查"] + E -- "是" --> F{"接收账号仍为 Buyer/Merchant 且状态可确认?"} + F -- "否" --> Q["确认本次不推送;若有连接则服务端 Abort"] + F -- "是" --> G["发布版本化应用级 Redis Pub/Sub 分发命令
所有存活 API 实例各接收一次"] + G --> R["各实例 RealtimeFanoutHostedService
只按本地登记逐连接即时复核"] + R --> S{"连接凭证仍有效且可安全确认?"} + S -- "否" --> T["Abort 并排除该连接"] + S -- "是" --> U["本实例通过 IHubContext.Clients.Client
只向通过复核的 connectionId 推送 MessageCreated"] + U --> V{"目标用户是否仍有有效在线连接?"} + V -- "否" --> Y["确认实时尝试结束
消息事实不变"] + V -- "是" --> H{"标签页是否已展示同一 messageId?"} H -- "是" --> I["忽略重复轻提示
不改变未读数"] H -- "否" --> J["显示非阻塞轻提示
不直接累加角标"] J --> K["查询 A503 权威未读数
按需补查 A501/A502"] K --> L["校正角标;用户按需进入消息中心或业务详情"] - E -. "发送失败" .-> Z["记录消息标识、实例和 traceId
不回滚 M09"] - F -. "共享通道失败" .-> Z - G -. "连接中断" .-> Z + D -. "API 消费瞬态失败" .-> Z["未过期则重投;记录 messageId、实例和 traceId
不回滚 M09"] + G -. "Redis 发布或本地发送失败" .-> Z + U -. "连接中断" .-> Z Z --> Y ``` 最小载荷边界: -- 只包含消息标识、类型、标题/摘要、关联业务类型与标识、安全操作描述和服务端创建时间。 +- M09 为每条已持久化消息形成一个 `MessagingRealtimeHintRequestedV1`;Outbox 去重责任固定绑定 `messageId`,不得把来源业务 `eventId` 复用为提示事件 ID。Envelope 与客户端 `MessageCreated` 只允许以下封闭字段,缺失必填字段、重复属性或出现未知属性都拒绝发布: + +| 字段 | 精确规则 | +|---|---| +| `eventId` | 本条提示独立 UUID;客户端去重仍使用 `messageId` | +| `schemaVersion` | 精确字符串 `v1` | +| `messageId`、`recipientUserId` | 已提交 DB101 的消息与接收人 UUID | +| `recipientRole` | `Buyer` / `Merchant`,必须与 DB101 和当前连接角色一致 | +| `messageType` | 只允许 M09/DB101 已冻结的 12 个 PascalCase 消息类型 | +| `title`、`summary` | 已提交 DB101 的同值纯文本快照;先做 Unicode NFC、去首尾 Unicode 空白,分别保留 1~100、1~200 个 Unicode 标量值,禁止任何 Unicode `Cc` / `Cf` 类字符 | +| `relatedResourceType`、`relatedResourceId` | 同空同非空;类型只允许 `Order` / `Payment` / `AfterSales`,值为 DB101 同一 UUID | +| `actionTarget`、`actionResourceId` | 同空同非空;目标只允许 `OrderDetail` / `PaymentDetail` / `AfterSalesDetail`,值为 DB101 同一 UUID,不是 URL 或路由字符串 | +| `createdAt`、`expiresAt` | UTC;前者等于消息创建时间,后者严格等于 `createdAt + 60 秒` | + +- `expiresAt = message.createdAt + 60 秒`。API 消费者在推送前检查;已过期提示直接确认且不推送,未过期的瞬态失败只可重投至该截止时间,不能把实时提示变成长期重试业务。 - 不发送完整订单、支付信息、收货地址、密码、完整 Token、连接配置或内部前端路由。 -- 消息标识是客户端轻提示去重键;服务端创建时间是展示事实,不使用浏览器实际收到时间替代。 +- `messageId` 是客户端轻提示去重键;RabbitMQ、Outbox Publisher、API 消费者和应用级 Redis Pub/Sub 均允许至少一次重复,客户端对同一 `messageId` 只展示一次,但每次连接恢复仍以 A503/A501 校正事实。 +- 服务端创建时间是展示事实,不使用浏览器实际收到时间替代。 - 载荷中的关联信息不能代替详情授权;目标不存在或权限暂不可确认时沿用 M09 的“目标暂不可用”结果。 +- 应用级 Redis Pub/Sub 分发命令使用环境隔离且带版本的频道 `eshop:{environment}:signalr:message-hints:v1`;命令只含本节封闭轻提示字段。入口消费者发布成功后即可结束当前 RabbitMQ 投递;Redis 发布失败且尚未到 `expiresAt` 时重投,过期后确认丢弃。Redis Pub/Sub 不保存历史,实例离线期间遗漏命令由 A501/A503 补查,不增加重放仓库。 +- 每个 API 实例由 `RealtimeFanoutHostedService` 订阅同一频道并各接收一次,只查询本实例连接登记、即时复核后调用 `IHubContext.Clients.Client(connectionId)`。禁止入口消费者直接调用 `Clients.User(userId)` 把未经远端实例逐连接复核的消息交给标准 Backplane,也禁止任一实例发送不属于本实例登记的连接;这一落点不要求自定义 `HubLifetimeManager`。 +- 应用级 Redis Pub/Sub 只传播分发命令,不能绕过持有连接实例的本地校验门。重复命令仍按 `messageId` 在客户端去重,频道断线只影响实时性,不改变 M09 消息事实。 ## 五、断线重连、多标签页与补查 @@ -138,17 +171,24 @@ flowchart TD - 页面不可见或被浏览器节流时,不把客户端收到时间当作业务发生时间。 - 持续断线只影响实时性,不阻塞公开商品浏览;受保护的订单、消息 HTTP 查询只有在 Identity 能安全确认凭证状态时才继续,否则失败关闭。 - WebSocket 不可用时使用 M09 HTTP 查询或定期补查作为功能补偿,不切换到 SSE 或长轮询。 +- 已认证且页面可见时,WebSocket 正常连接仍每 60 秒调用一次 A503 校正跨标签页已读变化;固定四次重连均失败后改为每 15 秒补查 A503,消息中心当前可见时再按用户当前分页条件补查 A501。WebSocket 恢复后立即补查一次并回到 60 秒安全校正。 +- 页面进入隐藏状态后完成当前在途请求即暂停定时补查;`visibilitychange` 回到可见、浏览器 `online`、WebSocket 重连成功或用户主动进入消息中心时立即触发一次补查。页面隐藏期间不累计“补跑次数”,也不在恢复时并发补发多次。 +- 同一账号、同一端点和同一查询条件最多一个在途补查;在途期间收到多个实时提示、定时器或可见性事件,只合并为一次尾随刷新。HTTP 瞬态失败按 5 秒、15 秒、30 秒、60 秒封顶退避,任一次成功即恢复正常周期。 +- 主动退出、401/403、令牌自然到期、手机号修改导致旧凭证失效、账号禁用或页面卸载时,立即停止轮询、取消可取消的在途请求并清理本地实时状态;不得继续用旧凭证后台补查。 ## 六、多实例、故障与责任 | 场景 | C06 处理 | 最终状态与责任 | |---|---|---| -| 用户连接在实例 1、事件由实例 2 触发 | 通过共享实时通道传播到实例 1 | M09 消息保持唯一事实 | +| 用户连接在实例 1、实时提示由实例 2 消费 | 实例 2 的入口消费者发布 Redis 分发命令;实例 1 的 `RealtimeFanoutHostedService` 收到后只复核并发送本地登记连接 | M09 消息保持唯一事实 | | 同一账号打开多个标签页 | 向全部有效连接推送,客户端按消息标识去重 | 数据库未读数只增加一次 | | 网络抖动造成重复连接或重复推送 | 允许连接恢复,轻提示按消息标识去重 | 不重复生成消息或改变业务状态 | -| Redis Backplane 暂时不可用 | 实时能力降级并记录指标 | M09 数据事实仍保留;只有 Identity 仍能安全完成鉴权时,受保护的 M09 HTTP 查询才能继续,否则按失败关闭策略处理 | +| 实时提示队列或 API 消费者暂时不可用 | 未过期提示按至少一次语义重投;达到 60 秒后确认丢弃 | M09 数据事实仍保留,由 A501/A503 补偿 | +| 应用级 Redis Pub/Sub 暂时不可用 | 未过期时重试实时分发并记录指标,过期后停止 | M09 数据事实仍保留;只有 Identity 仍能安全完成鉴权时,受保护的 M09 HTTP 查询才能继续,否则按失败关闭策略处理 | | 当前连接所在 API 停止 | 客户端进入重连并切换到存活实例 | 不承诺连接无中断,但消息不丢失 | -| 用户主动退出、令牌到期、手机号修改或账号禁用 | 关闭相关既有连接并拒绝旧凭证重连;无法确认失效事实时失败关闭 | 启用账号不恢复旧凭证,不允许用客户端参数绕过认证 | +| 当前令牌退出 | 安全失效广播投递到每个 API 实例,各实例按 `jti` Abort;广播失败时由推送前/30 秒复核补偿 | 只关闭该令牌连接,同账号其他有效令牌不受影响 | +| 令牌到期、手机号修改、账号禁用或全部旧凭证失效 | 到期由 `CloseOnAuthenticationExpiration` 关闭;账号级事件由每实例队列按 `userId/tokenVersion` Abort | 启用账号不恢复旧凭证,不允许用客户端参数绕过认证 | +| 安全失效广播通道不可用 | 不维持未知连接;每次推送及最长 30 秒复核失败即 Abort | PostgreSQL/Identity 认证事实保持权威,广播不承担唯一安全责任 | | WebSocket 升级不可用 | 完成固定四次有限重连后暂停,回退 M09 HTTP 查询或定期补查 | 不启用 SSE 或长轮询,不改变消息事实 | | 推送载荷处理失败 | 不显示或转为普通消息入口补查 | 不使用错误载荷改变订单状态 | | 用户主动退出 | 关闭当前连接并清理本地实时状态 | 不删除 M09 历史消息 | @@ -160,28 +200,32 @@ SignalR 连接和服务端事件不是 HTTP 接口,不占用 Axxx,也不得 | 流程能力 | 当前派生契约 | 事实来源 | 当前状态 | |---|---|---|---| -| 买家或商家以 WebSockets 跳过协商建立本人实时连接 | 接口设计 4.3.7“Hub 连接” | M01 认证上下文 | 固定传输和失败关闭契约已补齐,待部署与测试 | -| M09 消息提交后向全部有效在线连接发送提示 | 接口设计 4.3.8“MessageCreated” | M09 已持久化消息 | 提示语义与最小载荷已补齐,待实现与测试 | -| 初次连接、重连和收到提示后校正未读数 | A503 | M09/PostgreSQL | 待补齐权威补查时机 | -| 补查断线期间消息 | A501;查看详情时使用 A502 | M09/PostgreSQL | 待交叉评审 | -| 任一标签页标记已读并校正 | A504、A505,随后复用 A503 | M09/PostgreSQL | 待交叉评审 | -| 两个 API 实例共享实时通道 | 接口设计 1.16、4.3.7 | Redis Backplane,不登记 DBxxx | 待 C10 部署验证 | +| 买家或商家以 WebSockets 跳过协商建立本人实时连接 | 接口设计 4.3.7“Hub 连接” | M01 认证上下文 | 严格 `sub`、本地连接登记、到期关闭和失败关闭契约已补齐,待部署与测试 | +| M09 消息提交后可靠形成短期实时提示责任 | `MessagingRealtimeHintRequestedV1` 内部事件 | DB101 + DB103 + DB102 同事务 | 每消息一提示、60 秒过期和至少一次边界已冻结,待实现与测试 | +| 任一 API 消费提示并向全部有效在线连接发送 | API Hosted Service、接口设计 4.3.8“MessageCreated” | 已持久化 M09 消息的轻提示快照 | API/Worker 边界、逐连接复核与客户端 `messageId` 去重已冻结,待实现与测试 | +| 主动退出及账号级旧凭证失效即时断开 | Identity 安全失效广播 + 每 API 实例独立临时队列 | DB004 / DB001 `tokenVersion` 权威事实 | 广播加速、每实例 Abort 与 30 秒失败关闭补偿已冻结,待实现与测试 | +| 初次连接、重连和收到提示后校正未读数 | A503 | M09/PostgreSQL | 时机已固定:初次建连成功、每次重连成功、每次去重后的 MessageCreated 提示后均调用;待实现与测试 | +| 补查断线期间消息 | A501;查看详情时使用 A502 | M09/PostgreSQL | 按稳定分页补查、详情再授权契约已确认,待 OpenAPI、实现与测试 | +| 任一标签页标记已读并校正 | A504、A505,随后复用 A503 | M09/PostgreSQL | 共享已读事实与高水位校正已确认,待 OpenAPI、实现与多标签页测试 | +| 两个 API 实例共享实时通道 | 接口设计 1.16、4.3.7 | 版本化应用级 Redis Pub/Sub,不登记 DBxxx | 待 C10 部署验证 | 架构承接章节: -- 系统架构 7.10“C06 实时消息推送”:SignalR、断线补查和 Redis Backplane; +- 系统架构 7.10“C06 实时消息推送”:SignalR、断线补查和应用级 Redis Pub/Sub; - 系统架构 7.13“C10 容器化部署与负载均衡”:Nginx WebSocket Upgrade、双实例与故障切换; - 系统架构 8“安全设计”:JWT、账号状态、令牌撤销与资源隔离。 ## 八、下游设计与验证约束 -1. Hub 契约必须固定 WebSockets、跳过协商、服务端认证用户通道和失败关闭;不得引入客户端任意加组或其他传输回退。 -2. Identity 必须向现有连接传播主动退出、到期、手机号修改、账号禁用和全部旧凭证失效结果;无法确认状态时连接关闭。 -3. C10 必须提供 Nginx WebSocket Upgrade、双 API、Redis Backplane、连接实例停止和恢复就绪的可重复部署证据。 -4. 客户端重连数组固定为立即、2 秒、5 秒、10 秒;四次失败后只有网络恢复或用户手动操作重新启动一轮。 -5. 客户端收到 `MessageCreated` 只去重提示并调用 A503,按需调用 A501/A502;不得通过推送次数累计角标。 -6. Redis Backplane 故障和恢复要记录指标、日志和告警;不承诺恢复后重放实时事件,由 M09 持久化查询补偿。 -7. 真实 Hub 集成测试、多标签页端到端测试和部署验证尚未建立,因此“流程完整”不等于“已实现或已验证”。 +1. Hub 契约必须固定 WebSockets、跳过协商、`CloseOnAuthenticationExpiration = true`、严格 `sub` UUID 用户标识、服务端认证用户通道和失败关闭;不得引入客户端任意加组或其他传输回退。 +2. Messaging Worker 必须在同一事务提交 DB103、整事件全部 DB101 消息和每消息一条 DB102 实时提示 Outbox;不得在事务内调用 RabbitMQ、Redis 或 Hub,也不得由 Worker 持有 WebSocket 连接。 +3. API 轻量消费者必须共享消费实时提示队列,由任一实例经应用级 Redis Pub/Sub 发起分发;提示 60 秒过期,重复由客户端 `messageId` 去重,失败不得修改 M09 消息。 +4. Identity 必须用每 API 实例独立队列传播按 `jti` 或 `userId/tokenVersion` 的安全失效;各实例维护并清理本地连接登记,收到事件立即 Abort。每次推送和最长 30 秒周期复核必须作为安全兜底。 +5. C10 必须提供 Nginx WebSocket Upgrade、双 API、版本化应用级 Redis Pub/Sub、实时提示共享队列、安全广播每实例队列、连接实例停止和恢复就绪的可重复部署证据。 +6. 客户端重连数组固定为立即、2 秒、5 秒、10 秒;四次失败后只有网络恢复或用户手动操作重新启动一轮。 +7. 客户端收到 `MessageCreated` 只按 `messageId` 去重提示并调用 A503,按需调用 A501/A502;不得通过推送次数累计角标。 +8. RabbitMQ、API Hosted Service 和应用级 Redis Pub/Sub 故障及恢复要记录指标、日志和告警;不承诺恢复后重放已过期实时提示,由 M09 持久化查询补偿。 +9. 真实 Hub 集成测试、多标签页端到端测试和部署验证尚未建立,因此“流程完整”不等于“已实现或已验证”。 ## 九、验收证据清单 @@ -192,10 +236,14 @@ SignalR 连接和服务端事件不是 HTTP 接口,不占用 Axxx,也不得 - [ ] 用户 A、用户 B 同时在线时,只有明确接收人获得私人消息。 - [ ] 买家、指定商家、无关商家和管理员同时在线时,推送范围符合身份和接收账号约束。 - [ ] 连接落在实例 1、事件由实例 2 触发时能够通过共享实时通道送达,并保存实例与 Trace 证据。 +- [ ] M09 事件消费只在 DB101、DB103 和实时提示 DB102 同事务提交后确认;模拟提交前崩溃不会推送,提交后发布失败可在 60 秒内重试。 +- [ ] 同一 `MessagingRealtimeHintRequestedV1` 重投至少两次时,客户端按 `messageId` 只展示一次;延迟超过 60 秒时不再推送且 A501/A503 可查到消息。 - [ ] 验证当前 PC Web 只使用 WebSockets 并跳过协商;WebSocket 失败后只回退 M09 HTTP 查询或定期补查,不启用 SSE 或长轮询。 - [ ] 停止当前连接所在 API 后,客户端可重连到存活实例,消息和未读状态完整。 - [ ] 主动退出、被动断网、连接超时和实例中断后,服务端均能清理断开连接状态,且无需依赖单实例内存保存唯一在线状态。 -- [ ] 主动退出、JWT 到期、手机号修改和账号禁用都会关闭相关既有连接,旧凭证不能重连;失效事实无法确认时失败关闭。 +- [ ] 缺失、重复或非 UUID 的 `sub` 均拒绝连接;客户端传入其他 `userId` 不能改变服务端用户标识。 +- [ ] 当前令牌退出只关闭相同 `jti` 的连接;JWT 到期、手机号修改、账号禁用和全部旧凭证失效关闭所有命中旧版本连接,旧凭证不能重连。 +- [ ] 至少两个 API 实例同时持有同账号连接时,每个实例的安全广播队列都收到失效事件并执行本地 Abort;停掉广播后,推送前或最长 30 秒复核仍会失败关闭。 - [ ] 暂停 Redis/实时推送后,来源业务和 M09 消息事实仍成功;Identity 可安全鉴权时用户可通过列表补查,否则受保护请求按失败关闭策略处理。 - [ ] 连续消息和短暂断线不使用阻塞弹窗打断当前表单或支付操作。 - [ ] 重复或乱序推送、跨标签页已读和断线新增消息均通过 A503 校正角标,不出现客户端 `+1` 漂移。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" index 916d003..e2479b3 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C07-\347\274\223\345\255\230\346\265\201\347\250\213.md" @@ -11,15 +11,15 @@ C07 使用 Redis 优化本期固定首页商品摘要和购物端商品详情两个高频公开只读场景。游客和买家可以共享只含公开字段的缓存;个人字段、商家管理字段、管理员字段及购物车、订单、支付、售后写操作不进入本期缓存。 -PostgreSQL 始终是价格、库存、上下架状态和商品内容的事实来源。缓存命中与未命中的响应必须保持同一业务口径;Redis 故障可以使查询变慢,但不能改变公开范围、权限或结果正确性。A102 只有无筛选、固定排序、第一页 12 条的首页摘要请求进入缓存,其他普通列表、分类、关键词和组合筛选全部直读 PostgreSQL;A103 只缓存商品自身公开字段,不含 M07 评价或评分。 +PostgreSQL 始终是价格、库存、上下架状态和商品内容的事实来源。缓存命中与未命中的响应必须保持同一业务口径;Redis 故障可以使查询变慢,但不能改变公开范围、权限或结果正确性。A102 只有无筛选、固定排序、第一页 12 条的首页摘要请求进入服务端 Redis,其他普通列表、分类、关键词和组合筛选全部直读 PostgreSQL;A103 只缓存商品自身公开字段,不含 M07 评价或评分。A102/A103 的 JSON 响应固定 `Cache-Control: no-store`,Nginx、CDN 与 Service Worker 不得再缓存;不可变版本化媒体 URL 可独立使用长期公共缓存,该媒体缓存不属于 C07 JSON Cache-Aside。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | C07 需求与教师验收 | 完整定义 | 作为 Cache-Aside、写后失效、降级和压测边界 | | 本文业务流程 | 完整定义 | 冻结缓存对象、读取参数、事务后双删、跨实例填充、库存通道和错误出口 | -| A102、A103 与 Catalog 写接口 | 已按本文重建、未冻结 | 由流程映射,不改变原接口响应;待数据库、OpenAPI 与交叉评审 | -| DB022、DB023 | 仅为接口文档引用,数据库主文档未确认 | 不把接口引用写成已冻结表设计 | -| Redis Key、TTL 与热点保护 | 流程参数已冻结 | 正常值 60 秒、空值 10 秒、回填窗口 2 秒、二次失效 3 秒、等待 500 毫秒 | +| A102、A103 与 Catalog 写接口 | 已按本文重建、未冻结 | 由流程映射,不改变原接口响应;数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| DB021~DB023、DB026、DB102、DB103 | 完整定义,数据库设计已确认 | PostgreSQL 保存商品/普通库存与来源事务同时建立的 Immediate/Delayed 两条独立失效责任;Redis 缓存值、Key 和填充锁不登记 DBxxx | +| Redis Key、TTL 与热点保护 | 流程参数已冻结 | 正常值 60 秒、空值 10 秒、回填窗口 2 秒、Delayed 提交可见后武装并等待 3 秒、等待 500 毫秒 | 职责边界: @@ -39,11 +39,11 @@ flowchart LR CACHE -->|"命中且内容有效"| WEB["游客/买家公开响应"] CACHE -->|"未命中、损坏或 Redis 降级"| CAT CAT -->|"已上架商品的公开事实直接返回"| WEB - CAT -. "60 秒正常值/10 秒空值尽力回填;写入失败不阻塞响应" .-> CACHE + CAT -. "取得填充锁后开启新的短 READ COMMITTED 查询
60 秒正常值/10 秒空值尽力回填" .-> CACHE - WRITE["F11:商品/分类/图片/销售状态
普通订单扣减或回补、普通库存售后回补
C01 发布划拨普通库存"] -->|"所属事实事务提交成功"| INVALIDATE["C07 失效入口"] - INVALIDATE -->|"删除详情和受影响的固定首页缓存"| CACHE - INVALIDATE -->|"失败记录、3 秒二次失效与告警"| RETRY["60/10 秒 TTL 约束最长旧值窗口"] + WRITE["F11:商品/分类/图片/销售状态
普通订单扣减或回补、普通库存售后回补
C01 发布划拨普通库存"] -->|"来源事务同时写 Immediate 与 Delayed Outbox"| INVALIDATE["两条独立 C07 失效责任"] + INVALIDATE -->|"Immediate 固定立即调度,可用即 DEL"| CACHE + INVALIDATE -->|"Delayed 提交可见后由数据库时间武装
等待 3 秒再独立 DEL,不依赖首删"| RETRY["武装/投递/删除失败均可恢复;60/10 秒 TTL 兜底"] ORDER["F08 提交订单"] -->|"重新读取销售状态、价格和库存"| CAT CACHE -. "不得作为下单事实" .-> ORDER @@ -57,6 +57,7 @@ flowchart LR - F08 下单始终从 PostgreSQL 事实重新校验销售状态、价格和库存,不接受页面或 Redis 中的旧值作为交易依据。 - Redis 写入、删除或重试失败只影响性能和约定的一致性窗口,不得让数据库事务回滚或返回无法判断新旧的副本。 - 两个 API 实例共享同一 Redis 和 Key 规范,不能使用单实例内存保存跨实例唯一缓存事实。 +- A102/A103 的 HTTP 响应不进入浏览器、代理、CDN 或 Service Worker 缓存;服务端 Redis 命中仍按本节正常工作。媒体对象只有在 URL/Key 不可变且可长期读取时才允许 `public, max-age=31536000, immutable`。 ## 三、Cache-Aside 读取流程 @@ -67,28 +68,34 @@ flowchart TD B -- "是,属于固定首页或 A103" --> C["构造包含环境、模块、资源、固定查询标识和版本的稳定 Key"] B -- "是,但属于其他 A102 查询" --> R["直接查询 PostgreSQL
不读取或写入 C07"] C --> D{"Redis 是否可用?"} - D -- "否" --> DB["记录降级并查询 PostgreSQL"] + D -- "否" --> DIRECT["记录降级并以短查询读取 PostgreSQL
仅返回,不回填"] D -- "是" --> E{"Key 是否命中且可正常反序列化?"} E -- "是" --> F["返回与原接口一致的公开响应并记录命中"] - E -- "否,未命中" --> LOCK{"是否取得该 Key 的跨实例填充资格?"} - LOCK -- "是" --> DB + E -- "否,未命中" --> LOCK{"SET lockKey token NX PX 3000
是否取得填充锁?"} + LOCK -- "是" --> START["记录 fillStartedAt
此后才开启新的短 READ COMMITTED 查询"] LOCK -- "否" --> WAIT["最多等待 500 毫秒后重读缓存"] WAIT --> HIT{"已出现有效缓存?"} HIT -- "是" --> F - HIT -- "否" --> DIRECT["直查 PostgreSQL 并返回
不争抢填充、不回填"] + HIT -- "否" --> DIRECT E -- "否,损坏或旧版本" --> G["删除异常 Key 并按未命中处理"] G --> LOCK - DB --> H{"数据库查询结果?"} + START --> H{"数据库查询结果?"} H -- "已上架商品/摘要" --> I["生成原接口公开响应"] H -- "不存在或不可公开" --> J["按原公开查询规则返回空结果或稳定不可用结果"] H -- "查询失败" --> P["保留页面结构并显示统一错误反馈
提供就地重试入口"] - I --> K{"从取得填充资格起是否未超过 2 秒且 Redis 可写?"} - K -- "是" --> L["以 60 秒 TTL 回填"] - K -- "否" --> M["仅记录写入失败"] + I --> K{"未超过 2 秒且 Lua 比较 token 仍持锁?"} + K -- "是" --> L["同一 Lua 原子 SET payload PX 60000"] + K -- "否" --> M["只返回数据库结果,禁止回填"] L --> N["返回本次数据库结果"] M --> N - J --> O["在 2 秒回填窗口内以 10 秒 TTL 写入空值"] - O --> Q["返回原查询结果并结束加载状态"] + J --> O{"未超过 2 秒且 Lua 比较 token 仍持锁?"} + O -- "是" --> O1["同一 Lua 原子 SET 空值 PX 10000"] + O -- "否" --> Q + O1 --> Q["返回原查询结果"] + DIRECT --> DR["按原接口返回本次数据库结果
不持锁、不回填"] + N --> REL["以 compare-and-delete Lua 释放本人 token 对应锁
锁已失效或不属于本人则不删除"] + Q --> REL + P --> REL ``` 读取规则: @@ -97,34 +104,57 @@ flowchart TD - 固定首页强制 `OnSale`、无用户筛选、`createdAt DESC, productId DESC`,固定前 12 条;库存为零仍返回并标记售罄。 - 正常值 TTL 为 60 秒;A103 不存在/不可公开结果和固定首页空结果的空值 TTL 为 10 秒。上架、恢复公开或创建同标识资源时主动失效对应空值。 - 同一 Key 只允许一个跨实例填充者。其他请求等待最多 500 毫秒;超时后直查 PostgreSQL 并返回,但不继续争抢填充资格或回填。 -- 取得填充资格后,数据库查询到缓存写入决定的有效回填窗口最多 2 秒;超时结果仍按接口规则处理,但不再写缓存,防止迟到旧查询延长旧值窗口。 +- 填充锁固定使用 `SET NX PX 3000`,Token 来自密码学安全随机源且熵不少于 128 bit;锁不续期。只有取得锁后才开启一个新的短 `READ COMMITTED` 只读事务查询 PostgreSQL,不能复用锁前查询或长事务快照。 +- 取得填充资格后,数据库查询到缓存写入决定的有效回填窗口最多 2 秒;回填使用 Lua 先比较当前锁值与 Token,匹配时才在同一脚本原子写值与 TTL。超过 2 秒、锁已过期、Token 不匹配或脚本结果未知时仍返回数据库结果,但不得写缓存,防止迟到旧查询延长旧值窗口。 +- 释放使用 compare-and-delete Lua,只删除值仍等于本人 Token 的锁;不得普通 `DEL lockKey` 误删后继填充者的锁。锁丢失、Redis 故障或释放失败都不改变本次 PostgreSQL 响应。 - 缓存写入失败时,本次 PostgreSQL 查询结果仍正常返回;不得把缓存错误暴露为商品业务错误。 - 商家管理查询和个人字段默认直接走原授权接口,不复用公共商品缓存。 +- A102/A103 的 JSON 响应一律带 `Cache-Control: no-store`;Nginx/CDN 对对应 API 关闭响应缓存,Service Worker 不得保存这些 JSON。商品/评价图片等不可变媒体可按对象 URL 单独长期缓存,但不得把其缓存策略扩展到 JSON。 - 每次缓存读取记录命中/未命中、读取耗时和错误;回填、主动失效与降级分别记录写入、失效、错误和降级次数,并携带资源标识与 `traceId`,不得记录完整缓存值。 ## 四、商品与库存变更后的失效 ```mermaid flowchart TD - A["所属模块接收商品、分类或普通库存变更"] --> B["通过公开能力在 PostgreSQL 事务内校验并写入最新事实"] - B --> C{"事务是否提交成功?"} - C -- "否" --> X["保持原数据库事实
不发布成功失效结果"] - C -- "是" --> D["按冻结矩阵确定详情 Key
及是否影响唯一固定首页 Key"] - D --> E["提交后立即执行首次删除"] - E --> F{"首次删除是否成功?"} - F -- "否" --> G["记录失败范围、traceId 和告警
保留受控重试证据"] - F -- "是" --> H["等待提交后第 3 秒"] - G --> H - H --> I["对同一组 Key 执行二次删除"] - I --> J{"二次删除是否成功?"} - J -- "否" --> K["记录失败和告警
由 60/10 秒 TTL 兜底"] - J -- "是" --> L["清除可能的并发旧回填"] - K --> M["理论最坏窗口:正常旧值 62 秒
旧空值 12 秒"] - L --> N["后续查询从 PostgreSQL 回填新值"] - M --> N - N --> Q["所有 API 实例共享同一 Redis、填充资格和失效结果"] + A["所属模块接收商品、分类或普通库存变更"] --> B["同一 PostgreSQL 事务写业务事实
并计算 operationId/invalidationBaseTime/精确商品集合"] + B --> C["同事务插入 Immediate 与 Delayed 两条独立 DB102
Immediate 固定立即调度;Delayed 初始未武装"] + C --> D{"业务事实和两条责任是否整体提交?"} + D -- "否" --> X["整体回滚,不报告业务成功
不留下只有一阶段的责任"] + D -- "是" --> E["Immediate 事件可用即消费"] + D -- "是" --> ARM["调度器扫描已提交可见的 Delayed
以 clock_timestamp() 原子武装"] + ARM --> H["available_at=armed_at+3 秒
到期后独立消费,不等待 Immediate"] + E --> F{"对派生 Key 的 Redis DEL 结果可确认成功?"} + H --> I{"对同一组 Key 的 Redis DEL 结果可确认成功?"} + F -- "是,包括 Key 不存在" --> F1["写本消费者 DB103 Processed 后 ACK"] + I -- "是,包括 Key 不存在" --> I1["写本消费者 DB103 Processed 后 ACK"] + F -- "失败或结果未知" --> G["不写 Inbox、不 ACK
重投同一 Immediate eventId"] + I -- "失败或结果未知" --> J["不写 Inbox、不 ACK
重投同一 Delayed eventId"] + F1 --> L["通常立即清除旧值"] + I1 --> M["清除提交前查询可能在 2 秒内产生的迟到回填"] + G --> T["重试期间由有限 TTL 兜底"] + J --> T + T --> N["最坏窗口:正常旧值 62 秒
旧空值 12 秒"] + L --> Q["所有实例共享同一 Redis、事件责任与失效结果"] + M --> Q + N --> Q ``` +两条事件使用同一封闭 Schema `CatalogCacheInvalidationRequestedV1`: + +| 字段 | 规则 | +|---|---| +| `operationId` | 来源业务动作稳定 UUID;同一次业务提交的两阶段相同 | +| `phase` | 仅 `Immediate` / `Delayed` | +| `invalidationBaseTime` | 来源事务一次取得的 UTC 数据库时间;两阶段相同,只用于审计业务发生时间,不作为 Delayed 调度锚点 | +| `delayAfterCommitSeconds` | Immediate 固定 `0`,Delayed 固定 `3`;表达相对提交可见性的语义,不携带预提交绝对投递时间字段 | +| `productIds` | 受影响商品 UUID 去重后按 UUID 字节升序;1~100 个 | +| `invalidateHomepage` | 是否失效唯一固定首页 Key,由下方矩阵确定 | +| `schemaVersion` | 固定 `v1`;未知字段和未知版本拒绝并告警 | + +两条 DB102 使用不同 `eventId`,去重键固定 `cache:{operationId}:immediate` 与 `cache:{operationId}:delayed`,payload 除 `phase/delayAfterCommitSeconds` 外必须一致。Immediate 对应 `schedule_mode=fixed`、`delay_seconds=null`,创建时即有 `available_at/next_attempt_at`;Delayed 对应 `schedule_mode=after_commit_delay`、`delay_seconds=3`,创建时 `armed_at/available_at/next_attempt_at` 全空。来源事务不能只写 Immediate,也不能由 Immediate 消费者、调度器或首删成功结果事后创建 Delayed;调度器只能武装来源事务已经提交的 Delayed。 + +Delayed 武装固定由 Outbox 调度器在短事务中完成:按 `created_at,eventId` 扫描已提交可见且仍未武装的行,使用 `FOR UPDATE SKIP LOCKED` 互斥领取,以同一数据库 `clock_timestamp()` 写 `armed_at`,并原子写 `available_at=next_attempt_at=armed_at+3 秒`。武装事务回滚或进程崩溃时行仍保持未武装,后续轮次继续扫描;武装已提交但尚未投递时由普通 Outbox 到期扫描恢复。Immediate 的投递、成功、失败和重试均不得创建、武装、取消或修改 Delayed 的调度字段。 + 冻结失效矩阵: | 已提交变更 | 详情缓存 | 固定首页摘要 | 说明 | @@ -144,7 +174,7 @@ flowchart TD | 秒杀订单取消或售后回补原活动库存 | 不失效 | 不失效 | 库存严格回到原活动通道 | | M07 评价、评价图或评分变化 | 不失效 | 不失效 | 评价链路不在 A103 商品自身缓存内 | -所有“失效”均表示提交后立即首次删除、提交后 3 秒二次删除;影响多个商品时按确定的商品集合执行,不把任意筛选列表扩大为缓存对象。 +所有“失效”均表示来源事务同时建立“固定立即调度”和“提交可见后武装并等待 3 秒”两条独立责任;影响多个商品时按确定的商品集合执行,不把任意筛选列表扩大为缓存对象。每个阶段的 `DEL` 对 Key 不存在也视为幂等成功;只有 Redis 明确完成删除后才能写本阶段 DB103 并 ACK,失败或结果未知必须重投同一 eventId。 ## 五、缓存运行状态与一致性窗口 @@ -164,9 +194,9 @@ stateDiagram-v2 一致性边界: - 正常缓存 TTL 固定为 60 秒,空值 TTL 固定为 10 秒;主动失效缩短正常更新窗口,TTL 约束漏删或删除失败时的最长旧值时间。 -- 事务提交后立即首次失效,提交后 3 秒二次失效。二次失效只用于清除提交前旧查询可能在首次删除后回填的旧值,不能替代首次删除和 TTL。 -- 取得填充资格后的有效回填窗口最多 2 秒。最不利情况下,提交前开始的旧查询在提交后第 2 秒写回,因此两次删除均失败时,正常旧值最迟在提交后第 62 秒到期,旧空值最迟在提交后第 12 秒到期。 -- 3 秒二次失效正常成功时会更早清除上述回填;“62 秒”是失败兜底上限,不是系统故意等待时间。下单、取消和售后事务始终重读 PostgreSQL,不等待缓存一致。 +- 来源事务同时提交 Immediate 与初始未武装的 Delayed 两条独立失效责任。Delayed 只用于清除提交前旧查询可能在首次删除后回填的旧值;它在提交可见后由数据库时间武装,并且最早在 `armed_at+3 秒` 可投递。其存在、武装和执行不依赖 Immediate 成功;任一阶段单独失败都恢复本阶段,不能取消或推迟另一阶段。 +- 取得填充资格后的有效回填窗口最多 2 秒。最不利情况下,提交前开始的旧查询在提交可见后第 2 秒写回,因此武装或两次删除持续失败时,正常旧值最迟在提交可见后第 62 秒到期,旧空值最迟在提交可见后第 12 秒到期。 +- Delayed 正常武装并成功删除时会清除上述迟到回填;调度器观察晚只会使删除晚于提交可见后 3 秒,绝不能提前。“62 秒/12 秒”分别由 2 秒回填窗加 60/10 秒 TTL 得出,是武装或删除持续失败时仍成立的兜底上限,不是系统故意等待时间。下单、取消和售后事务始终重读 PostgreSQL,不等待缓存一致。 ## 六、故障、并发与身份隔离 @@ -176,10 +206,14 @@ stateDiagram-v2 | Redis 回退后 PostgreSQL 也失败 | 保留页面结构,返回统一错误反馈和就地重试 | 不暴露 Redis、连接串或数据库技术细节 | | 缓存值损坏或结构版本旧 | 视为未命中并删除异常 Key | 不向客户端返回错误结构 | | 缓存写入失败 | 返回本次数据库结果 | 不改变接口业务结果 | -| 商品事务回滚 | 不产生成功失效动作 | 原缓存仍对应原数据库事实 | -| 事务提交后首次删除失败 | 记录范围、告警并在提交后第 3 秒再次删除 | 二次失效成功则提前收敛;否则由 60/10 秒 TTL 兜底 | -| 事务前旧查询在首次失效后回填旧值 | 提交后第 3 秒删除同一组 Key | 二次删除也失败时,正常旧值上限为 2+60=62 秒,旧空值上限为 2+10=12 秒 | +| 来源事务无法同时写业务事实和两条 Outbox | 整体回滚,不报告业务成功 | 不产生“业务已变但失效责任缺失”或只有一阶段的结果 | +| Immediate 删除失败/结果未知 | 不写该阶段 Inbox、不 ACK,重投同一 eventId | Delayed 仍由调度器独立武装并在 `armed_at+3 秒` 到期;Immediate 不得改其字段 | +| Delayed 武装事务失败或调度器崩溃 | 武装更新整体回滚,保留同一未武装 eventId,后续轮次继续扫描 | 不复制新事件;Immediate 结果不受影响,有限 TTL 持续兜底 | +| Delayed 已武装、到期投递前实例崩溃 | 已提交的 `armed_at/available_at/next_attempt_at` 保留 | 普通 Outbox 到期扫描接管,同一 eventId 不提前投递 | +| Delayed 删除失败/结果未知 | 不写该阶段 Inbox、不 ACK,重投同一 eventId | Immediate 结果不被回滚;由重试和 60/10 秒 TTL 兜底 | +| 事务前旧查询在 Immediate 后回填旧值 | 独立 Delayed 对同一组 Key 删除 | Delayed 也持续失败时,正常旧值上限为 2+60=62 秒,旧空值上限为 2+10=12 秒 | | 热点 Key 同时过期 | 仅一个跨实例填充者回填,其余等待最多 500 毫秒 | 未等到有效值则直查 PostgreSQL,不争抢填充、不无限阻塞 | +| 填充锁过期、Token 改变或回填 Lua 结果未知 | 返回已取得的数据库结果但禁止缓存写入 | 后继持锁者不会被旧执行器覆盖 | | 商品下架时并发旧读 | 失效并最终不再公开;F08 始终重读数据库 | 旧展示不能绕过下单校验 | | 实例 1 完成变更、实例 2 查询 | 共享 Redis 与 Key 规范 | 在约定窗口内读取新值 | | 游客与买家共享公开缓存 | 只保存双方共同可见字段 | 收藏、购物车等个人字段独立查询 | @@ -191,12 +225,12 @@ stateDiagram-v2 | 流程能力 | 当前派生契约 | 数据设计 | 当前状态 | |---|---|---|---| -| 固定首页公开查询 | A102 的无筛选固定首页语义;其他 A102 请求不缓存 | 数据库设计待汇总 | 待补齐固定排序、12 条和售罄字段 | -| 购物端商品自身公开详情 | A103 | 数据库设计待汇总 | 待明确排除 M07 评价、评分和个人字段 | -| 商品、分类展示和图片写入后失效 | A112、A122~A128 提交后的内部协作 | 目标表设计待数据库汇总 | 待为创建短空值、名称、价格、描述、分类展示、图片、主图、状态和删除逐项补齐后置条件 | -| 普通订单与普通库存售后回补后失效 | Catalog 公开库存应用能力,不新增 Axxx | 库存事实待数据库汇总 | M04/M10 下游应用契约已承接原库存通道,待数据库和公开签名 | -| C01 发布划拨后失效;活动内部库存不失效 | C01 与 Catalog 的内部协作 | 普通库存与活动库存设计待汇总 | 接口/事件契约已承接冻结矩阵,待数据库和公开签名 | -| Redis 故障回退 PostgreSQL | 继续复用固定首页/A103 响应口径 | Redis 不登记 DBxxx | 待架构和测试承接 | +| 固定首页公开查询 | A102 的无筛选固定首页语义;其他 A102 请求不缓存 | DB021~DB023;Redis 不登记 DBxxx | 固定排序、12 条、售罄字段和 Cache-Aside 口径已确认,待实现与压测 | +| 购物端商品自身公开详情 | A103 | DB021~DB023;Redis 不登记 DBxxx | 已确认只缓存商品自身公开字段并排除 M07 评价、评分和个人字段,待实现与压测 | +| 商品、分类展示和图片写入后失效 | A112、A122~A128 来源事务内的双阶段 Outbox | DB021~DB023、DB102、DB103 | 创建短空值、名称、价格、描述、分类展示、图片、主图、状态和删除的两条独立失效责任已确认,待实现与测试 | +| 普通订单与普通库存售后回补后失效 | Catalog 公开库存应用能力,不新增 Axxx | DB026、DB102、DB103 | M04/M10 原库存通道和事务后失效责任已确认,待公开签名、实现与测试 | +| C01 发布划拨后失效;活动内部库存不失效 | C01 与 Catalog 的内部协作 | DB026、DB042~DB044、DB102、DB103 | 普通库存划拨触发失效、活动内部库存不失效的矩阵已确认,待公开签名、实现与测试 | +| Redis 故障回退 PostgreSQL | 继续复用固定首页/A103 响应口径 | Redis 不登记 DBxxx | 架构降级边界已确认,待实现和测试承接 | 架构承接章节: @@ -209,10 +243,10 @@ stateDiagram-v2 1. A102 必须可表达唯一固定首页语义:无用户筛选、只取 `OnSale`、`createdAt DESC, productId DESC`、前 12 条、库存为零仍返回并标记售罄;其他 A102 查询不得写入 C07。 2. A103 只承载商品自身公开字段,M07 评价与评分、收藏、购物车和任何身份化字段走独立查询。 -3. 接口、事件或应用协作必须为失效矩阵中的每个已提交动作提供明确后置失效入口;不得在数据库事务提交前发布“成功失效”。 -4. Redis 实现必须提供跨实例单填充者、500 毫秒等待、2 秒有效回填窗口、60/10 秒 TTL、提交后立即及 3 秒二次失效,并记录可验证指标。 +3. 接口、事件或应用协作必须在每个来源业务事务中原子写入 Immediate/Delayed 两条 DB102;不得在数据库事务提交前发布事件,也不得让 Immediate 消费者或调度器事后派生 Delayed。Delayed 只能在提交可见后由 Outbox 调度器以数据库时间武装。 +4. Redis 实现必须提供 `SET NX PX 3000`、不少于 128 bit Token、无续租、取得锁后新开短 READ COMMITTED、500 毫秒等待、2 秒有效回填窗口、Lua 比较 Token 后回填/释放、60/10 秒 TTL,以及两条独立阶段失效,并记录可验证指标。 5. 普通库存与秒杀活动库存必须携带原通道语义:普通扣减/回补触发 C07,活动内部扣减和原活动回补不触发。 -6. 数据库表、索引及 OpenAPI 仍待后续汇总;流程完整不代表实现或压测已经完成。 +6. DB021~DB023、DB026、DB102、DB103 已承接商品事实、普通库存流水和两阶段可靠失效责任;Redis Key 不登记 DBxxx。真实 OpenAPI、缓存实现、部署与压测仍未完成,流程和数据库设计完整不能被误报为已落地。 7. 压测固定请求集合、并发参数、预热/冷缓存轮次和资源必须在测试计划中登记,确保开关缓存公平比较。 ## 九、压测与验收证据清单 @@ -223,11 +257,12 @@ stateDiagram-v2 - [ ] 记录请求数、成功率、吞吐量、平均耗时、P50、P95、P99、缓存命中/未命中次数、读取耗时、写入/失效次数、错误/降级次数、Redis 错误和 PostgreSQL 查询次数。 - [ ] 缓存开关前后 A102/A103 业务字段、公开范围和错误结果一致。 - [ ] 固定首页只返回最新 `OnSale` 前 12 条且排序稳定,库存为零仍显示售罄;其他 A102 查询、M07 评价与评分均不进入缓存。 -- [ ] 改价、普通库存、描述、分类展示、图片/主图、上架、下架、删除及多实例读取在提交后立即/3 秒失效链路,以及正常旧值 62 秒、旧空值 12 秒失败兜底内得到正确结果。 +- [ ] 改价、普通库存、描述、分类展示、图片/主图、上架、下架、删除的来源事务同时写两条独立失效责任;Immediate 失败时 Delayed 仍在提交可见后独立武装并于 `armed_at+3 秒` 到期,任一阶段失败都恢复本 eventId,并由正常旧值 62 秒、旧空值 12 秒兜底。 - [ ] Redis 故障时可回退数据库;恢复后能够重新回填并产生正常命中。 - [ ] Redis 与 PostgreSQL 同时失败时保留页面结构,展示统一错误反馈和就地重试,不暴露技术异常。 -- [ ] 构造提交前旧查询在首次失效后第 2 秒内回填旧值的场景,证明 3 秒二次失效通常清除,双删失败时正常值 60 秒 TTL 保证提交后 62 秒、空值 10 秒 TTL 保证提交后 12 秒上限。 -- [ ] 热点 Key 过期时只有一个跨实例填充者;其他请求最多等待 500 毫秒后直查 PostgreSQL,不出现无限阻塞。 +- [ ] 构造来源事务持续超过 3 秒且提交前旧查询在首次失效后第 2 秒内回填旧值的场景,证明 Delayed 在事务未提交时不可见、提交可见后才武装、`available_at=armed_at+3 秒` 且绝不提前;同时证明武装或双删持续失败时正常值 60 秒 TTL 仍保证提交可见后 62 秒、空值 10 秒 TTL 保证 12 秒上限。 +- [ ] 热点 Key 过期时只有一个跨实例填充者;锁为 `SET NX PX 3000` 且不续期,Token 熵不少于 128 bit。取得锁后才开启新 READ COMMITTED 查询,其他请求最多等待 500 毫秒后直查;锁丢失或超过 2 秒的旧查询不能回填,释放不会误删后继锁。 +- [ ] A102/A103 JSON 均为 `Cache-Control: no-store`,Nginx/CDN/Service Worker 不产生第二份 JSON 缓存;不可变媒体 URL 的长期缓存不会泄露私有字段或改变交易事实。 - [ ] C01 发布划拨普通库存会失效详情与首页;秒杀活动内部扣减、秒杀订单取消和售后原活动回补均不触发 C07。 - [ ] 普通订单扣减、主动/超时取消回补以及 M10 普通库存售后回补均触发正确失效。 - [ ] F08 在旧页面或旧缓存条件下仍按 PostgreSQL 最新状态、价格和库存决定下单结果。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C10-\351\253\230\345\217\257\347\224\250\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C10-\351\253\230\345\217\257\347\224\250\346\265\201\347\250\213.md" index 389cc8d..0aff413 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C10-\351\253\230\345\217\257\347\224\250\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/C10-\351\253\230\345\217\257\347\224\250\346\265\201\347\250\213.md" @@ -32,7 +32,7 @@ flowchart LR API1 --> PG["PostgreSQL
业务事实来源"] API2 --> PG - API1 --> REDIS["Redis
共享令牌状态、缓存和实时通道"] + API1 --> REDIS["Redis
可重建安全镜像、缓存与版本化提示频道"] API2 --> REDIS API1 --> MQ["RabbitMQ
集成事件传输"] API2 --> MQ @@ -40,6 +40,10 @@ flowchart LR API2 --> STORE WORKER["Mall.Worker 独立容器"] --> PG WORKER --> MQ + MQ -->|"共享实时提示队列
任一 API 入口竞争消费"| API1 + MQ -->|"共享实时提示队列
任一 API 入口竞争消费"| API2 + REDIS -->|"message-hints:v1
各 API 本地 Fanout 各收一次"| API1 + REDIS -->|"message-hints:v1
各 API 本地 Fanout 各收一次"| API2 MIGRATOR["一次性 Migrator
同版本、成功后退出"] -->|"目标 Migration 版本"| PG API1 -->|"存活/全局就绪/能力状态"| HEALTH["存活与就绪检查"] @@ -52,11 +56,15 @@ flowchart LR - 后端容器端口默认只在内部网络可见,不直接暴露给公网;现场验收不得绕过 Nginx 访问业务接口。 - Nginx 必须正确转发客户端 IP、协议、Host、请求 ID 和 WebSocket Upgrade 所需请求头;受控实例标识只用于脱敏验收证据。 - Hub 握手路径中的 `access_token` Query 必须在 Nginx、ASP.NET Core、Serilog 和 Trace 中完整脱敏,集成、演示和发布环境只使用 HTTPS/WSS。 -- Redis Backplane 只传播跨实例 Hub 消息;当前 PC Web 固定只使用 WebSockets 并跳过 SignalR 协商,Nginx 负责 Upgrade,不依赖协商请求与升级请求之间的会话亲和。 +- 当前 PC Web 固定只使用 WebSockets 并跳过 SignalR 协商,Nginx 负责 Upgrade,不依赖协商请求与升级请求之间的会话亲和。跨实例提示采用应用级 Redis Pub/Sub,不要求自定义 `HubLifetimeManager`,也不允许把入口 `Clients.User` 广播当成远端实例安全复核的替代品。 - 两个 API 使用同一 Commit SHA/版本 Tag 构建的同一镜像和等价业务配置;只允许实例标识等运行信息不同。 -- JWT Issuer、Audience、签名、Policy、账号状态和令牌失效语义在两个实例上必须一致。 +- 每个 API 分别从 HTTP Bearer 与 SignalR Hub 的实际生效选项生成认证配置摘要,固定包含 `Issuer`、`Audience`、验签材料 `keyFingerprint`、`accessTokenLifetimeSeconds`、`clockSkewSeconds=0`、`tokenVersionValidationRule`;按固定属性名的规范 JSON 执行 SHA-256。`keyFingerprint` 只来自非秘密 KeyId 或验签公钥指纹,摘要不含 Secret、私钥、对称密钥哈希或原始配置值。 +- Compose 注入同版本期望摘要。A507 必须确认本实例 HTTP 摘要、Hub 摘要和期望摘要三者相等,部署门再确认两个 API 返回相同摘要;任一不一致时 C10 全局 `NotReady`,Nginx 不开放双实例业务流量。 - PostgreSQL 保存业务事实;Redis、RabbitMQ、容器内存和前端状态不得成为无法恢复的唯一业务事实。 - Worker 独立于任一 API 实例运行;后台任务的领取、幂等和状态规则仍由对应业务负责人定义。 +- Mall.Worker 的 Messaging 消费事务原子写 DB103、整事件全部 DB101 消息和每消息一条 DB102 `MessagingRealtimeHintRequestedV1`;Worker 不调用 Hub。Outbox Publisher 把 60 秒有效提示发布到 RabbitMQ 共享队列,由任一 Mall.Api 入口消费者竞争消费一次,再发布到 `eshop:{environment}:signalr:message-hints:v1`;每个 API 的 `RealtimeFanoutHostedService` 各收一次,只向本地安全复核通过的 `connectionId` 调用 `Clients.Client(connectionId)`。 +- 入口消费者只有在 Redis 发布成功或提示已经到期时才 ACK RabbitMQ 消息;发布失败且提示未过期时保留同一消息重投。Redis Pub/Sub 不补发实例离线期间的提示,至少一次重复由客户端按 `messageId` 去重,所有遗漏由 A501/A503 HTTP 补查。 +- A003 在令牌未自然到期且首次撤销时,以及 A006/A016 使全部旧凭证失效时,原子建立的安全失效 Outbox 发布到广播 Exchange;每个 API 实例必须声明自己独立、排他、自动删除的队列,并维护本地 `connectionId → userId/jti/tokenVersion/expiresAt/Abort` 登记。安全事件按 `jti` 或 `userId/tokenVersion` Abort 本地连接,一个实例消费不能替代其他实例。 - API 与 Worker 不执行启动时自动 Migration;同版本一次性 `Migrator` 是唯一迁移执行者。 ## 三、Compose 启动与受控迁移 @@ -72,7 +80,7 @@ flowchart TD F --> G{"迁移是否成功退出且目标版本匹配?"} G -- "否" --> Y["阻止 API/Worker 启动或就绪
保留日志并修复迁移问题"] G -- "是" --> H["启动两个 API、Worker、PC Web 与 Nginx"] - H --> I["检查配置、版本、Migration、PostgreSQL
并分别报告能力级依赖状态"] + H --> I["每个实例分别检查 expected/HTTP/Hub 三摘要、版本、Migration、PostgreSQL
部署门另行比较双 API 响应,并分别报告能力级依赖状态"] I --> J{"两个 API 是否都满足全局就绪?"} J -- "否" --> Z["仅允许符合就绪准入规则的实例用于诊断
C10 双实例启动与现场验收不通过"] J -- "是" --> K["通过统一入口检查首页、健康与一条核心查询"] @@ -83,7 +91,7 @@ flowchart TD - Compose 提供明确的一条启动命令和一条日常停止命令;日常停止不得删除数据卷。 - 数据库迁移由 Compose 中与应用同版本的一次性 `Migrator` 执行;PostgreSQL 就绪后运行,成功退出后两个 API 与 Worker 才启动或进入就绪。失败时阻止业务流量,不允许 API 自行并发补跑。 -- A506 只说明进程能够响应;A507 的全局就绪固定检查安全配置、运行版本、目标 Migration 版本和 PostgreSQL。 +- A506 只说明进程能够响应;每个 A507 只检查本实例 `expectedDigest=httpDigest=hubDigest`、安全配置、运行版本、目标 Migration 版本和 PostgreSQL,不反向调用另一实例。部署门分别读取两个实例响应并另行比较三摘要,决定双实例整体是否准入。 - Redis、RabbitMQ、SeaweedFS 的异常通过 A507 能力状态与指标反映,并按第七章降级;单项异常不错误阻断所有公开读取。 - Aspire 只用于本地开发编排,C10 现场验收统一使用 Docker Compose 和 Nginx。 @@ -95,7 +103,7 @@ flowchart TD stateDiagram-v2 [*] --> Stopped: 尚未启动 Stopped --> Starting: 容器启动 - Starting --> NotReady: 配置/版本/Migration/PostgreSQL 未满足 + Starting --> NotReady: 认证摘要/配置/版本/Migration/PostgreSQL 未满足 Starting --> Ready: 全局就绪检查通过 NotReady --> Ready: 全局门槛恢复并重新检查通过 Ready --> NotReady: 任一全局门槛失败 @@ -110,8 +118,8 @@ stateDiagram-v2 流量规则: - 只有达到 `Ready` 的实例才允许接收新业务流量;`Stopped`、`Starting` 和 `NotReady` 实例不得持续接收新请求。 -- `Ready` 的固定门槛是安全配置完整、运行版本兼容、Migration 版本匹配和 PostgreSQL 可用;Redis、RabbitMQ、SeaweedFS 的单项异常改变能力状态,不直接把整个 API 置为 `NotReady`。 -- 恢复实例必须同时通过版本、配置、Migration 版本、PostgreSQL 和 A507 检查,再重新参与负载均衡,不能仅凭容器 `running` 状态加入。 +- 单实例 `Ready` 的固定门槛是本实例 `expectedDigest=httpDigest=hubDigest`、安全配置完整、运行版本兼容、Migration 版本匹配和 PostgreSQL 可用;双实例开放还要求部署门比较两个 A507 响应的三摘要完全一致。Redis、RabbitMQ、SeaweedFS 的单项异常改变能力状态,不直接把整个 API 置为 `NotReady`。 +- 恢复实例必须同时通过认证摘要、版本、配置、Migration 版本、PostgreSQL 和 A507 检查,再重新参与负载均衡,不能仅凭容器 `running` 状态加入。 - 存活与就绪响应不得包含连接字符串、主机、端口、异常堆栈、凭据或其他敏感配置。 - Nginx/Compose 必须以 A507 和有界探针阈值摘除、恢复实例;具体秒数属于部署配置,但不得绕过上述准入条件。 @@ -137,13 +145,14 @@ flowchart TD L --> O["用户保持登录,已提交业务事实不丢失、不重复"] N --> O O --> P["恢复实例 1"] - P --> Q["实例 1 通过版本、配置、Migration、PostgreSQL
存活与全局就绪检查"] + P --> Q["实例 1 通过认证摘要、版本、配置、Migration、PostgreSQL
存活与全局就绪检查"] Q --> S["重新加入流量并继续共享 PostgreSQL、Redis、RabbitMQ 和对象数据"] ``` 故障切换的不变项: - 同一 Token、角色、资源和请求在两个实例上得到一致的认证、授权和数据范围结果。 +- 每个 API 的 A507 都必须满足 `expectedDigest = httpDigest = hubDigest`,部署门还必须确认两个实例返回的三份摘要全部相同;任一摘要漂移时全局 `NotReady`,不能用单个看似正常的实例冒充双实例登录态连续。 - 安全查询可以有限重试;会产生库存、金额或状态副作用的提交不得因实例切换被客户端盲目重放。 - 结果未知时回到所属模块:业务流程已定义幂等标识时复用原标识,否则使用已确认的结果查询入口;C10 不自行假设所有提交都具备幂等标识。 - 单实例停止不能删除 PostgreSQL、RabbitMQ、Redis 或对象存储的持久化数据,也不能停止独立 Worker 容器。 @@ -156,7 +165,7 @@ flowchart TD ```mermaid flowchart TD A["用户携带 JWT 通过统一入口访问"] --> B["请求落到任一就绪 API"] - B --> C["校验签名、Issuer、Audience、有效期、jti、账号状态和令牌版本"] + B --> C["按 ClockSkew=0 校验签名、Issuer、Audience、有效期、jti、账号状态和令牌版本"] C --> D{"认证和当前账号状态是否可确定?"} D -- "有效" --> E["继续执行 Policy、资源归属和业务状态校验"] D -- "无效" --> X["拒绝访问并按登录失效处理"] @@ -170,6 +179,9 @@ flowchart TD 安全边界: - Nginx 不保存业务 Session;登录连续性来自任一实例可验证的 JWT 和可安全确认的共享令牌失效事实。 +- JWT 到达 `exp` 即失效,Hub 固定 `CloseOnAuthenticationExpiration=true`。`IUserIdProvider` 只接受认证主体中恰好一个可解析为 UUID 的 `sub`,返回小写 D 格式;缺失、重复、非法或客户端试图覆盖 `userId` 时拒绝握手。 +- 每个 API 只登记本实例连接及服务端 Abort 句柄,断开时立即清理,不把内存登记当跨实例唯一在线事实。`RealtimeFanoutHostedService` 收到版本化频道提示后只枚举本实例目标用户连接,每次发送前逐连接复核有效期、DB004/Redis 撤销、DB001 状态与 `tokenVersion`,并明确调用 `Clients.Client(connectionId)`;连接空闲时最长每 30 秒复核一次,依赖失败或事实未知即 Abort。 +- A003 当前令牌退出广播按 `jti` 关闭命中连接;A006 手机号修改、A016 账号禁用和全部旧凭证失效按 `userId/currentTokenVersion/accountStatus` 关闭旧版本或全部连接。每 API 实例独立队列只加速断连,事件丢失仍由推送前和 30 秒周期复核失败关闭;离线实例没有连接,不需要补发。 - 任何需要确认主动退出、JWT 撤销、手机号修改、账号禁用或全部旧凭证失效事实的受保护 HTTP 与 Hub 连接,在 Redis 不可用或事实无法确认时都失败关闭;不得只限制 M09,也不得为可用性静默放行。 - Redis 恢复后,先从受控持久化或可重建事实源恢复有效期内的撤销事实,并通过安全健康检查;完成前相关受保护能力继续失败关闭,防止旧 Token 复活。 - 前端隐藏菜单、缓存身份或记录上一次成功实例都不能作为服务端授权依据。 @@ -179,17 +191,17 @@ flowchart TD | 故障对象 | 允许继续的能力 | 必须停止或降级的能力 | 恢复责任与边界 | |---|---|---|---| -| 单个 API | 存活实例继续处理查询和受控业务请求 | 当前连接短暂中断 | 恢复实例就绪后重新加入 | +| 单个 API | 存活实例继续处理查询;任一存活入口可继续消费共享提示,所有存活实例继续订阅版本化 Redis 频道 | 目标实例本地连接短暂中断;其离线期间不补收 Pub/Sub 提示 | 目标实例停收共享提示/频道消息/安全广播、Abort 本地连接;恢复且 A507 就绪后重新加入,客户端重连后以 HTTP 补查 | | Worker | 普通同步 API 可继续 | Outbox 投递和后台任务暂缓 | 恢复后按各模块幂等规则继续,不重复业务结果 | -| Redis | C07 固定首页与 A103 回退 PostgreSQL;公开列表等原本直读能力继续;M09 已持久化事实保留 | C06 实时推送关闭;任何需要确认撤销、账号禁用或全部旧凭证失效事实的受保护 HTTP/Hub 失败关闭 | Redis 基础健康后可恢复公开缓存;先恢复有效期内撤销事实并通过安全健康检查,才恢复受保护能力和实时推送 | -| RabbitMQ | 同步事务与 PostgreSQL Outbox 事实继续按所属契约提交 | 集成事件实时投递暂停 | Outbox 保留待发布事实,恢复后重投并由消费者防重 | +| Redis | C07 固定首页与 A103 回退 PostgreSQL;公开列表等原本直读能力继续;M09 已持久化事实保留 | C06 版本化提示发布/订阅关闭;任何需要确认撤销、账号禁用或全部旧凭证失效事实的受保护 HTTP/Hub 失败关闭 | Redis 基础健康后可恢复公开缓存;先恢复有效期内撤销事实并通过安全健康检查,才恢复受保护能力和实时提示;断连期间消息由 HTTP 补查,不补播历史 Pub/Sub | +| RabbitMQ | 同步事务与 PostgreSQL Outbox 事实继续按所属契约提交 | 集成事件和共享实时提示入口投递暂停 | Outbox 保留业务事件;恢复后重投并由消费者防重,超过 60 秒的实时提示确认丢弃并由 M09 HTTP 补查 | | PostgreSQL | 存活端点仍可反映进程 | 全部数据库业务请求不得伪装成功;实例全局未就绪 | 恢复并确认 Migration 版本后重新检查,不能用缓存冒充完整事实 | | SeaweedFS | 与对象写入无关的业务可按契约继续;已有对象读取失败时使用所属页面占位/重试 | 新上传和依赖对象写入的操作明确失败 | 对象恢复后继续,不写入无效对象引用 | | Nginx | 内部容器可用于诊断 | 用户统一入口不可用 | 恢复入口不应要求重建业务数据 | 本表定义正确失败边界,不表示这些共享依赖已经具备冗余高可用。演示时不得把“能看到容器状态”写成依赖故障已经自动切换。 -全局就绪与能力状态是两层结论:配置、版本、Migration 或 PostgreSQL 失败会把实例置为 `NotReady`;Redis、RabbitMQ、SeaweedFS 失败时实例仍可为全局 `Ready`,但必须按本表关闭或降级对应能力,健康响应和指标不得伪装为全部正常。 +全局就绪与能力状态是两层结论:任一实例本地 expected/HTTP/Hub 三摘要、配置、版本、Migration 或 PostgreSQL 检查失败,或部署门发现两个实例摘要不一致,都会使 C10 双实例整体 `NotReady`;Redis、RabbitMQ、SeaweedFS 失败时实例仍可为全局 `Ready`,但必须按本表关闭或降级对应能力,健康响应和指标不得伪装为全部正常。 ## 八、优雅停止、数据卷与运行责任 @@ -198,15 +210,17 @@ flowchart TD ```mermaid flowchart TD A["发起停止"] --> B{"停止范围?"} - B -- "单个 API 实例" --> C1["Nginx 停止向目标实例分发新请求"] - C1 --> D1["目标 API 有界等待在途请求"] - D1 --> E1["关闭目标实例 Hub 连接
刷新目标实例日志与遥测"] - E1 --> F1["只停止目标 API
Worker 和共享依赖继续"] + B -- "单个 API 实例" --> C1["目标实例先变为 NotReady
Nginx 停止分发新 HTTP/WebSocket"] + C1 --> D1["停止竞争消费共享提示
停止本实例 Redis Fanout 订阅并排空 HTTP"] + D1 --> E1["未 ACK 提示交回共享队列
按本地登记逐一 Abort Hub 连接"] + E1 --> E1A["停止本实例安全广播队列
刷新日志、Trace 与 Metric"] + E1A --> F1["只停止目标 API
另一 API、Worker 和共享依赖继续"] B -- "整套环境" --> C2["Nginx 停止接收全部新业务请求"] - C2 --> D2["全部 API 有界等待在途请求"] - D2 --> E2["关闭 Hub 连接
客户端转为不可用提示/后续补查"] - E2 --> F2["Worker 停止领取新任务"] + C2 --> D2["全部 API 停止竞争消费共享提示
停止各自 Redis Fanout 并有界排空请求"] + D2 --> E2["处理或交回未 ACK 提示
各 API 按本地登记 Abort 全部 Hub"] + E2 --> E2A["停止各实例安全广播队列"] + E2A --> F2["Worker 停止领取新任务"] F2 --> G2{"当前任务能否在期限内安全完成?"} G2 -- "是" --> H2["提交确定结果和检查点"] G2 -- "否" --> I2["安全释放领取权
保留可重试事实"] @@ -220,8 +234,10 @@ flowchart TD - 单个 API 维护只摘除并停止目标实例,不停止 Worker 或共享依赖;存活实例继续服务。 - API 在有界排空期间不得接受新提交;在途提交必须得到确定结果,或由所属流程保留原幂等标识/可查询事实,不能静默丢失后让客户端盲目重放。 +- 目标 API 先停止竞争领取新的 `MessagingRealtimeHintRequestedV1`,再停止本实例 `RealtimeFanoutHostedService` 订阅;尚未确认的 RabbitMQ 提示交给其他存活入口,Redis 已发布但目标实例未收到的提示不补播,客户端重连后由 A501/A503 补查。已超过 60 秒的提示确认丢弃,实时提示不是业务事实,不因排空延长有效期。 +- 安全广播队列在本地连接全部 Abort 后才允许随实例自动删除;停机前仍收到的失效事件必须先处理。关闭本实例不会删除其他 API 的独立队列或连接登记。 - Worker 收到停止信号后不再领取新任务;当前任务要么完成提交,要么安全释放并保留可重试事实,不留下永久“处理中”假状态。 -- Hub 连接关闭只影响实时性;客户端按 C06 固定重连,持续不可用时回到 M09 HTTP 查询或定期补查。 +- Hub 连接由服务器通过本地登记主动 Abort;关闭只影响实时性,客户端按 C06 固定重连,持续不可用时回到 M09 HTTP 查询或定期补查。 - 整套环境日常停止先完成应用排空和遥测刷新,再停止 PostgreSQL、Redis、RabbitMQ、SeaweedFS;不删除数据卷。 ### 8.2 数据卷、版本与配置 @@ -237,13 +253,14 @@ flowchart TD | 流程能力 | 当前派生契约 | 数据设计 | 当前状态 | |---|---|---|---| -| 判断 API 进程能否响应 | A506 `/health/live` | 无 DBxxx | 待补齐最小脱敏响应 | -| 判断全局就绪与能力状态 | A507 `/health/ready` | 无 DBxxx | 待补齐配置/版本/Migration/PostgreSQL 门槛及 Redis/RabbitMQ/SeaweedFS 能力状态 | +| 判断 API 进程能否响应 | A506 `/health/live` | 无 DBxxx | 最小脱敏响应已完整定义,待 OpenAPI、实现与探针验证 | +| 判断全局就绪与能力状态 | A507 `/health/ready` | 无 DBxxx;只读取本实例认证摘要、Migration 元数据和依赖能力 | 每个 A507 的 expected/HTTP/Hub 三摘要一致及配置/版本/Migration/PostgreSQL 门槛已定义;双 API 一致由部署门比较两个响应,待 OpenAPI、实现与故障演练 | | 证明两个实例分别响应 | A506/A507 的受控 `instanceId` 或结构化日志 | 无 DBxxx | 待部署资产承接 | -| Nginx 转发 WebSockets 跳过协商的 SignalR 连接 | 接口设计 4.3.7“Hub 连接” | Redis Backplane 不登记 DBxxx | 待 Nginx/双实例验证 | -| 实例切换后保持认证授权并失败关闭 | 接口设计 1.6、4.6;系统架构 8 | 撤销事实的持久化/可重建来源待数据库设计承接 | Redis 故障与安全恢复契约已补齐,待数据库、部署与测试 | +| Nginx 转发 WebSockets 跳过协商的 SignalR 连接 | 接口设计 4.3.7“Hub 连接” | 版本化 Redis 提示频道;本地连接登记不登记 DBxxx | `CloseOnAuthenticationExpiration`、30 秒复核、每实例安全队列和 Abort 待 Nginx/双实例验证 | +| Worker 持久化消息并由 API 推送短期提示 | M09、C06 内部事件契约 | DB103 + 全部 DB101 + 每消息一条 DB102 同事务 | 共享 RabbitMQ 入口队列 → `message-hints:v1` → 每 API 本地 Fanout;60 秒过期、Redis 发布重投和 `messageId` 去重待实现与故障演练 | +| 实例切换后保持认证授权并失败关闭 | 接口设计 1.6、4.6;系统架构 8 | DB001 `token_version`、DB004 当前 JTI 撤销事实;Redis 仅作可重建镜像 | Redis 故障与安全恢复契约及数据库来源已确认,待部署、实现与测试 | | 唯一 Migrator 门禁 | Compose 服务依赖与镜像版本约定,不新增 Axxx | Migration 历史由数据库设计承接 | 待部署资产与失败演练 | -| Worker、Outbox 与依赖恢复 | 系统架构 7.4 及对应业务 Worker 契约 | 相关 DBxxx 尚未冻结 | 各模块分别负责 | +| Worker、Outbox 与依赖恢复 | 系统架构 7.4 及对应业务 Worker 契约 | DB063、DB093、DB102、DB103、DB106、DB107 等统一数据库设计已确认 | 各模块责任、租约和恢复协议已确认,待实现、集成测试与部署演练 | 架构承接章节: @@ -256,34 +273,37 @@ flowchart TD ## 十、下游设计与验证约束 1. Compose 必须提供同版本一次性 `Migrator`,并以其成功退出作为两个 API 和 Worker 的启动/就绪门禁;API 与 Worker 不得自行执行 Migration。 -2. A507 必须区分全局就绪和能力状态:配置、版本、Migration、PostgreSQL 决定流量准入;Redis、RabbitMQ、SeaweedFS 分别按第七章降级。 +2. A507 必须区分全局就绪和能力状态:每个 A507 只用本实例 expected/HTTP/Hub 三摘要、配置、版本、Migration、PostgreSQL 决定该实例准入,不得调用另一实例或增加伪跨实例字段;部署门另行比较两个响应决定双实例整体准入。任一摘要漂移即整体 `NotReady`,Redis、RabbitMQ、SeaweedFS 才分别按第七章做能力降级。 3. Nginx/Compose 要使用有界探针阈值完成摘除和重新加入;恢复实例必须重新验证版本、配置、Migration 和 PostgreSQL,不能只看 `running`。 -4. Hub 与 Nginx 只承接 WebSockets 跳过协商;不设计 SSE、长轮询或依赖会话亲和的第二条连接路径。 -5. Identity 与数据设计必须给出有效期内撤销事实的受控持久化或可重建来源;Redis 故障时相关受保护 HTTP/Hub 失败关闭,恢复事实前不能重新开放。 +4. Hub 与 Nginx 只承接 WebSockets 跳过协商;固定 `CloseOnAuthenticationExpiration=true`,不设计 SSE、长轮询或依赖会话亲和的第二条连接路径。 +5. Identity 与数据设计必须给出有效期内撤销事实的 PostgreSQL 权威来源和安全失效 Outbox;每 API 实例独立消费广播、维护本地连接登记并 Abort,推送前和最长 30 秒复核兜底。Redis 故障时相关受保护 HTTP/Hub 失败关闭,恢复事实前不能重新开放。 6. 各业务提交继续使用所属流程的幂等标识或结果查询;C10 只允许安全查询有限重试,不为所有写请求发明统一重放。 -7. 部署资产需提供优雅停止钩子、统一维护页面、实例标识、请求分布、WebSocket 落点、能力降级和重新入池的脱敏证据。 +7. 部署资产需提供优雅停止钩子、实时提示共享队列、版本化 Redis 频道、每 API `RealtimeFanoutHostedService`、安全广播每实例队列、统一维护页面、实例标识、请求分布、WebSocket 落点、能力降级和重新入池的脱敏证据。 8. 当前尚无真实 Compose、Nginx、镜像、环境配置或运行结果,因此“流程完整”不等于“已部署或已验证”。 ## 十一、现场验收证据清单 - [ ] 从停止状态执行一条 Compose 启动命令,必需容器、内部网络和持久卷状态清晰。 - [ ] 两个 API 使用同一版本镜像和等价业务配置,迁移仅由受控步骤执行一次。 +- [ ] 每个 A507 的认证配置摘要精确覆盖 Issuer、Audience、keyFingerprint、AccessToken 有效期、ClockSkew=0 和 tokenVersion 规则,并只比较本实例 expected/HTTP/Hub 三摘要;故意改变任一实例的 HTTP 或 Hub 配置时,该实例返回 `NotReady`,部署门比较两个实例响应后拒绝开放双实例受保护流量。 - [ ] PostgreSQL 就绪后只有同版本一次性 Migrator 运行;迁移失败时两个 API 和 Worker 不启动或不就绪,API 不并发补跑。 - [ ] 浏览器只通过 Nginx 完成登录和业务访问,后端容器端口默认不直接暴露公网。 - [ ] Nginx 正确转发客户端 IP、协议、Host、请求 ID 和 WebSocket Upgrade 请求头,并保留脱敏追踪证据。 - [ ] Hub 连接只使用 WebSockets 并跳过协商,完成 Upgrade 和重连;持续失败回退 M09 HTTP 查询,不启用 SSE/长轮询,日志不出现完整 `access_token` Query。 +- [ ] Hub 到达 JWT `exp` 时由 `CloseOnAuthenticationExpiration` 关闭;缺失/重复/非法 UUID `sub` 被拒绝。每次推送前及最长 30 秒复核失败会 Abort,不维持未知连接。 - [ ] 连续请求通过实例标识或日志证明至少到达两个就绪 API。 - [ ] 任一 API 未能就绪时明确判定 C10 双实例启动与现场验收不通过,不以单实例运行冒充达标。 - [ ] 停止 API 实例 1 后,商品查询、本人消息查询和已登录访问由实例 2 继续处理。 - [ ] 实例切换前后 JWT、Policy、账号状态、资源归属和数据范围一致。 -- [ ] 触发一条实时消息,证明 Nginx WebSocket 转发和跨实例实时通道;停止连接实例后可重连补查。 -- [ ] 恢复实例 1 后,只有版本、配置、Migration、PostgreSQL 和 A507 全部通过才重新接收请求。 +- [ ] 触发一条实时消息,证明 Worker 同事务提交 DB103、全部 DB101 和 DB102 提示;共享 RabbitMQ 队列只由任一入口消费一次,随后发布 `eshop:{environment}:signalr:message-hints:v1`,两个 API 的 `RealtimeFanoutHostedService` 各收一次并只向本地复核通过的 `connectionId` 推送。故意让 Redis 发布失败时,未过期提示不 ACK 并重投;重复提示按 `messageId` 去重,离线或超过 60 秒只由 HTTP 补查。 +- [ ] 两个 API 分别持有连接时,A003 按 `jti`、A006/A016 按 `userId/tokenVersion` 的安全广播由每实例独立队列接收并 Abort;停掉广播后 30 秒复核仍能失败关闭。 +- [ ] 恢复实例 1 后,只有认证摘要、版本、配置、Migration、PostgreSQL 和 A507 全部通过才重新接收请求。 - [ ] 重启应用容器但保留数据卷后,用户、商品、消息和对象文件仍存在。 - [ ] 分别演示 Worker、Redis、RabbitMQ、PostgreSQL 或 Nginx 故障时的正确停止、降级或恢复边界,不夸大为共享依赖高可用。 - [ ] Redis 故障时公开商品查询回退 PostgreSQL、实时关闭且受保护请求失败关闭;恢复有效期内撤销事实并通过安全健康检查后才恢复受保护能力。 - [ ] RabbitMQ 故障时 Outbox 保留且投递暂停;SeaweedFS 故障时上传/对象写入失败,其他能力不被错误全部阻断。 - [ ] 短暂故障仅对安全查询有限重试;持续故障展示统一维护/服务不可用页面、可理解提示和手动重试,不出现 Nginx 默认错误页、白屏或无限加载。 - [ ] 提交类请求在结果未知时,仅在所属流程已定义时复用原幂等标识,否则使用已确认的业务查询确认,不因自动重放产生重复写。 -- [ ] 单实例和整套环境按“停止新流量—API 有界排空—Worker 停领并完成/释放—刷新遥测—停止应用/依赖”执行,日常停止保留全部数据卷。 +- [ ] 单实例和整套环境按“NotReady/停止新流量—API 停止共享提示入口与本地 Redis Fanout、处理或交回未 ACK 提示并排空—本地 Abort Hub/停安全队列—Worker 停领并完成或释放—刷新遥测—停止应用/依赖”执行,日常停止保留全部数据卷。 - [ ] 游客、会员、商家和管理员分别在两个实例上验证菜单入口、接口授权和数据范围一致;跨身份请求均被拒绝,合法登录态不因实例切换丢失。 - [ ] 保存 Compose 配置、示例环境、Commit SHA/镜像 Tag、容器清单、网络/卷说明、健康结果、请求分布、故障恢复和脱敏日志。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/M09-\347\253\231\345\206\205\346\266\210\346\201\257\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/M09-\347\253\231\345\206\205\346\266\210\346\201\257\346\265\201\347\250\213.md" index bd2c528..fdec61d 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/M09-\347\253\231\345\206\205\346\266\210\346\201\257\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/lhc/M09-\347\253\231\345\206\205\346\266\210\346\201\257\346\265\201\347\250\213.md" @@ -4,7 +4,7 @@ > 覆盖:M09、X03 > 基础核心流程:F08、F09、F10、F12;鉴权依赖 F02、F13;接入 X04 售后来源 > 直接协作:韦乾强(M04 Ordering)、张海洋(M05 Payment、M10 AfterSales)、唐宇昊(M01 Identity) -> 文档状态:完整定义;业务语义已按需求冻结,接口已按本文重建,待数据库、实现和测试承接 +> 文档状态:完整定义;业务语义已按需求冻结,接口已按本文重建,数据库设计已确认,待实现和测试承接 > 需求事实源:[需求规格说明书 M09](../../../01-需求文档/需求规格说明书.md) 的“M09 站内消息通知(X03)”完整七节 ## 一、范围与事实来源 @@ -16,39 +16,38 @@ M09 负责把订单、支付和售后模块已经提交的业务事实转换为 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M09/X03 需求 | 完整定义 | 作为角色、规则、异常和验收事实源 | -| 本文业务流程 | 完整定义 | 冻结事件入口、固定接收人、整事件原子性、消息状态、异常和模块出口 | -| A501~A505、接口设计 4.3 | 已按本文重建、未冻结 | 由流程派生并做契约映射,不作为流程输入;待数据库、OpenAPI 与交叉评审 | -| DB101~DB120 | 模板/占位,未冻结 | 不发明消息、Inbox 或 Outbox 的具体 DBxxx、字段、约束和索引 | -| C06 实时推送 | 独立挑战流程 | 只在消息提交成功后接入,不承担消息持久化 | +| 本文业务流程 | 完整定义 | 冻结封闭事件入口、固定接收人、整事件原子性、消息状态、异常和模块出口 | +| A501~A505、接口设计 4.3 | 已按本文重建、未冻结 | 由流程派生并做契约映射,不作为流程输入;数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| DB101~DB103 消息、Outbox/Inbox | 完整定义,已确认设计 | 已承接整事件原子消息、稳定接收人序列、已读水位、来源 Inbox 与实时提示 Outbox;待实现与测试 | +| C06 实时推送 | 独立挑战流程 | 只消费消息事务形成的 60 秒实时提示,不承担消息持久化 | ## 二、模块直接出入口 ```mermaid flowchart LR ID["M01 Identity
已认证用户、角色、账号状态"] -->|"允许买家或商家访问本人消息"| MSG["M09 Messaging
消息生成、查询与已读"] - ORD["M04 Ordering
已提交的创建、取消、发货、完成事实"] -->|"事件标识、业务标识和订单归属"| MSG - PAY["M05 Payment
已提交且幂等确定的支付成功事实"] -->|"支付事实和订单归属"| MSG - AFTER["M10 AfterSales
已提交的申请、审核、寄回或退款确定事实"] -->|"售后事实和申请归属"| MSG + ORD["M04 Ordering
来源事务 + DB102 Outbox"] -->|"封闭 MessagingSourceEventV1"| WORKER["Mall.Worker
Messaging 消费者"] + PAY["M05 Payment
来源事务 + DB102 Outbox"] -->|"封闭 MessagingSourceEventV1"| WORKER + AFTER["M10 AfterSales
来源事务 + DB102 Outbox"] -->|"封闭 MessagingSourceEventV1"| WORKER + WORKER -->|"同事务提交 DB103 + 全部 DB101 + 实时提示 DB102"| MSG MSG -->|"本人消息列表、详情、未读数与已读结果"| BUYER["买家消息中心"] MSG -->|"本人经营消息与安全操作入口"| MERCHANT["指定商家消息中心"] - MSG -. "消息提交成功后的旁路出口" .-> REALTIME["C06 实时推送"] + MSG -. "MessagingRealtimeHintRequestedV1
60 秒有效" .-> REALTIME["C06:API 消费者 + 应用级 Redis Pub/Sub"] BUYER -->|"用户按需打开安全操作入口"| TARGET["M04/M05/M10 业务详情"] MERCHANT -->|"用户按需打开安全操作入口"| TARGET ID -->|"游客、管理员、账号禁用或令牌失效"| X["拒绝访问,不返回私人消息"] - ORD -->|"事务回滚或事实未确定"| Y["不生成成功消息"] - PAY -->|"事务回滚或结果未确定"| Y - AFTER -->|"事务回滚或结果未确定"| Y - MSG -->|"事件非法"| Z["拒绝并记录安全原因
不自动重试无效事件"] - MSG -->|"临时处理或事务失败"| RETRY["记录失败并等待可靠重试
不改变来源业务结果"] + WORKER -->|"事件非法或同 eventId 异内容"| Z["整事件零消息
记录安全原因并死信当前投递"] + WORKER -->|"临时依赖或事务失败"| RETRY["整体回滚并等待可靠重试
不改变来源业务结果"] ``` 边界约束: -- 直接入口必须是来源模块已经提交的业务事实,并包含稳定事件标识、发生时间、业务标识,以及固定接收人矩阵所需的买家和 `assignedMerchantUserId` 归属。 +- 直接入口必须是来源模块已经提交的业务事实,并严格属于第三章定义的 `MessagingSourceEventV1` 封闭联合;支付失败、回调差异、对账处置、退款 Unknown 等“无 M09 消息事实”不得发布到 Messaging 队列。 - 接收人由第六章固定矩阵从业务归属派生,不能由调用方自由指定;角色只决定文案、入口和允许的操作,不允许按“全部买家”或“全部商家”广播私人业务事实。 - 同一事件的全部必需接收人先整体校验、后整批提交;任一必需账号缺失、角色不符或业务归属不一致时,整事件零消息并告警。 +- `Mall.Worker` 是来源事件消费者和消息持久化责任方;API 不消费来源业务事件,也不在 HTTP/Hub 请求中补写消息。Worker 对一个来源事件必须在同一 PostgreSQL 事务提交一条 DB103 成功消费事实、全部 DB101 接收人消息以及每条消息对应的一条 DB102 实时提示 Outbox。 - 确定出口是 PostgreSQL 中可查询的消息、未读数和首次已读结果;实时提示、前端角标和 Redis 均不是消息事实来源。 - 消息中的安全操作入口只描述目标业务对象。进入目标页面时仍由 M04、M05 或 M10 重新校验身份、归属和当前状态。 @@ -56,24 +55,26 @@ flowchart LR ```mermaid flowchart TD - UP["M04/M05/M10:业务事务提交成功"] --> A["提交事件标识、事实类型、发生时间、业务标识和业务归属"] - A --> B{"事实类型是否属于固定消息矩阵?"} - B -- "否,明确为无消息事实" --> W["记录消费结果
不生成 M09 消息"] - B -- "否,未知或非法" --> X["记录失败和 traceId
告警且不生成消息"] - B -- "是" --> C["按固定矩阵解析全部必需接收人"] - C --> D{"全部账号、角色和业务归属都有效?"} - D -- "否" --> Y["整事件零消息并告警
不允许部分成功"] - D -- "是" --> E{"该事件是否已有确定处理结果?"} - E -- "是" --> F["返回整事件既有结果
不重复新增或增加未读数"] - E -- "否" --> G["按各接收身份生成标题、摘要、正文和安全操作入口"] - G --> H["在一个提交边界内保存全部消息和幂等结果"] - H --> I{"整批事务是否提交成功?"} - I -- "否" --> Z["本次整体不生效
等待来源可靠事实重试"] - I -- "是" --> K["M09 确定出口:全部消息可查询且各自未读一次"] - K -. "提交后旁路" .-> L["逐接收用户交给 C06 尝试实时提示"] - L --> M{"实时提示是否成功?"} - M -- "是" --> N["在线用户补查权威未读数"] - M -- "否" --> O["保留消息事实
用户稍后通过消息中心补查"] + UP["M04/M05/M10:业务事务与来源 DB102 Outbox 提交"] --> A["Outbox Publisher 至少一次发布"] + A --> B["Mall.Worker 接收 MessagingSourceEventV1
校验 Routing Key、封闭 Schema 并计算规范哈希"] + B --> C{"同 consumerName + eventId 是否已有 DB103?"} + C -- "有且 hash 相同" --> D["返回既有整事件结果并确认
不新增消息或实时提示"] + C -- "有但 hash 不同" --> X["保持既有 DB103 不变
当前投递零消息、告警并死信"] + C -- "无" --> E{"类型、字段、聚合一致性是否合法?"} + E -- "否,确定契约错误" --> Y["写 Rejected DB103/0 并告警
当前投递死信,不自动重试"] + E -- "是" --> F["按封闭变体解析全部必需接收人"] + F --> G{"全部账号、角色和业务归属都确定有效?"} + G -- "否,确定业务错误" --> Y + G -- "暂时无法确认" --> Z["整体回滚并让 Broker 重投
不留下 Rejected 或部分消息"] + G -- "是" --> H["按 recipientUserId 稳定排序取锁
生成身份专属消息快照"] + H --> I["同一事务写 Processed DB103、全部 DB101、
每消息一条 MessagingRealtimeHintRequestedV1 DB102"] + I --> J{"事务提交成功?"} + J -- "否或结果未知" --> Z + J -- "是" --> K["确认来源 Broker 消息
全部消息可查询且各自未读一次"] + K -. "Outbox 后续发布" .-> L["实时提示进入 C06 共享 API 消费队列"] + L --> M{"提示是否在 message.createdAt + 60 秒前送达?"} + M -- "是" --> N["API/Redis Pub/Sub 尝试轻提示
客户端按 messageId 去重并补查"] + M -- "否" --> O["丢弃过期提示
消息仍由 A501/A503 补查"] ``` 关键规则: @@ -81,10 +82,68 @@ flowchart TD - 被回滚、仍在处理或结果不确定的业务操作不得生成“成功”消息。 - 同一业务事实可按不同接收身份生成不同文案,但同一事件、接收账号和消息类型只能得到一份对应消息;整事件幂等结果包含全部必需接收人。 - 消息正文、摘要和创建时间是生成时的历史快照,不因商品名称、订单展示文本或用户昵称后来变化而重写。 -- 事件重复投递只能返回既有处理结果,不能重复生成消息、重复增加未读数或重复触发相同业务通知。 -- 消息事务失败时不得留下只有消费记录、部分接收人的消息或只有消息正文的部分结果。 +- 同一 `eventId` 且规范哈希相同的重复投递只能返回既有处理结果,不能重复生成消息、重复增加未读数或再次创建实时提示 Outbox;同一 `eventId` 但规范哈希不同属于契约/安全冲突,不能当作幂等重放。 +- 消息事务失败时不得留下只有消费记录、部分接收人的消息、只有消息正文或只有实时提示责任的部分结果。 - 已禁用但身份和业务归属仍有效的账号仍属于有效接收人,消息照常保存;禁用只阻断查询、已读操作和实时推送,不删除历史。 +### 3.1 `MessagingSourceEventV1` 封闭联合 + +`MessagingSourceEventV1` 不是“任意 type + 任意 data”的开放信封。V1 Envelope 只允许以下八个顶层属性,缺失、重复或出现未知属性都属于确定契约错误: + +| 顶层属性 | 精确类型与规则 | +|---|---| +| `eventId` | UUID;来源事务生成的稳定事件标识 | +| `type` | 只允许下表 10 个精确值 | +| `schemaVersion` | 精确字符串 `v1` | +| `occurredAt` | 已提交业务事实发生时间,UTC | +| `aggregateId` | UUID;必须与下表指定的 `data` 标识相等 | +| `correlationId` | 1~64 字符,固定匹配 `^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$`;不得放入 Token、手机号、地址或换行 | +| `ownership`、`data` | 对应 `type` 的精确对象;不得出现 `recipients`、自由路由或下表之外的属性 | + +变体矩阵如下。`ownership` 和 `data` 中列出的属性必须全部出现;“禁止”表示属性不能以 `null` 形式占位: + +| `type` / Routing Key | `aggregateId` | `ownership` 精确属性 | `data` 精确属性与约束 | 消息结果 | +|---|---|---|---|---| +| `OrderCreatedIntegrationEvent` / `ordering.order.created.v1` | `orderId` | `buyerId` | `orderId`、`totalAmount`、`currency="CNY"` | 买家 `OrderCreated` | +| `OrderCancelledIntegrationEvent` / `ordering.order.cancelled.v1` | `orderId` | `buyerId` | `orderId`、`cancelReason=BuyerRequested/PaymentExpired` | 买家 `OrderCancelled` | +| `OrderPaidIntegrationEvent` / `payment.order.paid.v1` | `paymentId` | `buyerId`、`assignedMerchantUserId` | `orderId`、`paymentId`、`amount`、`currency="CNY"` | 买家与指定商家各一条 `PaymentSucceeded` | +| `OrderShippedIntegrationEvent` / `ordering.order.shipped.v1` | `orderId` | `buyerId` | `orderId`、`shippedAt` | 买家 `OrderShipped` | +| `OrderCompletedIntegrationEvent` / `ordering.order.completed.v1` | `orderId` | `buyerId` | `orderId`、`completedAt`、`completedBy=BuyerConfirmed/AutoCompleted` | 买家 `OrderCompleted` | +| `AfterSalesApplicationSubmittedIntegrationEvent` / `after-sales.request.submitted.v1` | `requestId` | `buyerId`、`assignedMerchantUserId` | `requestId`、`orderId`、`requestType=RefundOnly/ReturnAndRefund` | 指定商家 `AfterSalesSubmitted` | +| `AfterSalesApplicationAuditedIntegrationEvent` / `after-sales.request.audited.v1` | `requestId` | `buyerId`、`assignedMerchantUserId` | `requestId`、`orderId`、`decision`、`status` | 见下方审核组合 | +| `AfterSalesReturnInfoSubmittedIntegrationEvent` / `after-sales.return-info.submitted.v1` | `requestId` | `buyerId`、`assignedMerchantUserId` | `requestId`、`orderId`、`status=PendingReceipt` | 指定商家 `AfterSalesReturnSubmitted` | +| `RefundCompletedIntegrationEvent` / `payment.refund.completed.v1` | `refundOperationId` | `buyerId`、`assignedMerchantUserId` | `requestId`、`refundOperationId`、`amount`、`currency="CNY"` | 买家 `RefundSucceeded` | +| `RefundFailedIntegrationEvent` / `after-sales.refund.failed.v1` | `refundOperationId` | `buyerId`、`assignedMerchantUserId` | `requestId`、`refundOperationId`、`failureCode`、`recoveryDisposition`;按下方处分变体 | 买家始终 `RefundFailed`;商家按处分决定 | + +审核与退款失败的封闭子变体: + +- 审核只允许三种组合:`Reject + Rejected → AfterSalesReviewed`、`Approve + PendingReturn → AfterSalesPendingReturn`、`Approve + Refunding → AfterSalesReviewed`;其他 `decision/status` 组合整事件拒绝。 +- `RefundFailed + MerchantRetryRequired`:`recoveryDisposition` 精确为 `MerchantRetryRequired`,`manualRetryAvailableAt` 必填且为 UTC;同一事务生成买家 `RefundFailed` 和指定商家 `RefundRetryRequired`。 +- `RefundFailed + OperatorAttentionRequired`:`recoveryDisposition` 精确为 `OperatorAttentionRequired`,`manualRetryAvailableAt` **禁止出现**;只生成买家 `RefundFailed`。运维告警进入监控系统,不生成商家行动消息,也不创建管理员消息中心能力。 + +通用字段校验: + +- 所有 UUID 必须语义一致:Ordering 的 `aggregateId=data.orderId`,OrderPaid 的 `aggregateId=data.paymentId`,普通售后事件的 `aggregateId=data.requestId`,退款事件的 `aggregateId=data.refundOperationId`;事件中的买家、指定商家、订单、售后和退款归属还必须用各模块公开读取能力与权威事实重检。 +- 金额是 JSON 十进制数,范围 `0.01~9999999999999999.99`、最多两位小数、禁止指数形式;币种仅 `CNY`。`failureCode` 固定匹配 `^[A-Z][A-Z0-9_.]{0,63}$`,只表达稳定安全代码,不携带外部异常正文、控制字符或敏感数据。 +- 时间必须带 UTC 偏移并规范化为 UTC;枚举区分大小写且只接受表内值。V1 不接受数组、任意扩展对象、自由文案模板或前端路由。 +- Routing Key 必须与 `type` 的同一行严格匹配;不匹配视为确定契约错误,不能只相信其中一个字段。 + +### 3.2 规范 JSON 哈希与冲突处理 + +Worker 在查询或写入 DB103 前,对**完整 Envelope**(包括 `eventId`,不包括 RabbitMQ Header、Routing Key 等传输元数据)生成规范 JSON: + +1. 对象属性按 UTF-8 字节序升序排列;V1 没有数组,收到数组直接拒绝。 +2. UUID 统一为小写 `D` 格式;UTC 时间统一为 `.NET "O"` 的 UTC 形式;枚举使用上表精确大小写。 +3. 金额统一为两位小数且无指数;普通字符串先做 Unicode NFC,不擅自 trim 或改写业务值。 +4. 不输出空白、未知属性或未定义的 `null`,再对 UTF-8 字节执行 SHA-256,保存 64 位小写十六进制 `payloadHash`(DB103 `payload_hash`)。 + +DB103 幂等与并发处理固定为: + +- `consumerName + eventId` 不存在:继续完整校验并按一个事务提交确定结果。 +- 已存在且 `payloadHash` 相同:返回原 Processed/Rejected 结果并确认当前投递,不新增 DB101、DB102 或未读数。 +- 已存在但 `payloadHash` 不同:既有 DB103 和消息保持不变;当前投递零消息、零实时提示,记录脱敏的两个哈希、`eventId`、Routing Key 与 `traceId`,产生 Critical 告警并把当前冲突投递送入死信,禁止自动重试或用新 `eventId` 掩盖冲突。 +- 两个首投并发竞争唯一键时,未获胜事务必须回读已提交 DB103 并执行同一哈希判断,不能把唯一冲突直接当成功或 500。 + ## 四、消息查询、详情与已读 ```mermaid @@ -108,12 +167,16 @@ flowchart TD M -- "是" --> O{"当前消息仍为未读?"} O -- "是" --> P["记录首次已读时间"] O -- "否" --> Q["返回原首次已读结果"] - N --> R["服务端捕获本人已提交消息的稳定高水位"] - R --> S["只更新本人且不高于该高水位的未读消息"] - S --> T["高水位之后提交的新消息保持未读"] + N --> R["取得本人消息写入锁
捕获 MAX(serverSequence),无历史则 0"] + R --> S["只更新本人、readAt IS NULL
且 serverSequence ≤ highWatermark 的消息"] + S --> T{"本次实际更新行数是否大于 0?"} + T -- "是" --> T1["所有命中行写同一数据库 readAt
返回 markedCount、highWatermark、readAt"] + T -- "否" --> T2["返回 markedCount=0、highWatermark、readAt=null"] + T1 --> T3["高水位之后提交的新消息保持未读"] + T2 --> T3 P --> U["返回最新已读结果并校正角标"] Q --> U - T --> U + T3 --> U ``` 查询与权限规则: @@ -125,6 +188,9 @@ flowchart TD - 列表按页码分页;新消息导致后续页位移属于当前已知边界,本期不提前引入游标分页。 - 全站提供容易发现但不过度突出的消息入口和未读角标;列表加载时显示与页面结构一致的占位,空数据、请求失败、失败重试和分页加载都给出明确反馈。 - 标记已读成功后立即更新当前列表项与角标;请求失败时恢复操作前状态并提供就地重试,不能把前端乐观状态当作数据库已读事实。 +- A505 只以数据库 `read_at IS NULL` 判断未读,API 的 `isRead` 只是 `read_at IS NOT NULL` 的响应派生值,不得在 SQL 中引用不存在的 `is_read` 列。 +- A505 从未有过任何历史消息时返回 `highWatermark=0`;有历史但当前全部已读时返回现有最大 `serverSequence`。两种情况均为 `markedCount=0、readAt=null`,不能伪造批量已读时间。 +- A505 只有实际更新一条及以上消息时才取得并写入一个共同的数据库 `readAt`;`markedCount` 等于本次由空变为非空的真实行数,重复调用不能返回上次的批量时间。 ## 五、消息状态与批量边界 @@ -140,7 +206,8 @@ stateDiagram-v2 状态约束: - 首次已读时间由服务端生成并持久化;重复或并发标记不得覆盖首次时间。 -- “全部已读”先捕获本人已提交消息的稳定高水位,只覆盖不高于该水位的当前未读集合;不得使用客户端时间或墙上时钟作为截止依据。 +- “全部已读”在本人消息写入锁内捕获已提交消息的稳定高水位,只覆盖不高于该水位且 `read_at IS NULL` 的集合;不得使用客户端时间、墙上时钟或 `createdAt` 作为截止依据。 +- 高水位为 0 或没有命中未读行时不写任何 `read_at`,响应 `readAt=null`;命中时全部行使用同一个数据库时间,避免一个批次出现多个首次已读时间。 - 本期不提供物理删除消息流程。后续确需清理时,必须先定义保留期、归档和验收追踪规则。 - Redis 或前端角标可以加速展示,但未读状态始终以 PostgreSQL 查询结果为准。 @@ -158,7 +225,10 @@ stateDiagram-v2 | 售后申请提交 | M10 AfterSales | 订单指定处理商家 | 商家审核入口 | 买家已在当前操作中得到申请结果,不重复给本人发站内消息 | | 售后审核或待寄回 | M10 AfterSales | 申请买家 | 买家售后详情 | 文案必须反映已提交审核结果 | | 买家提交寄回信息 | M10 AfterSales | 订单指定处理商家 | 商家售后详情 | 不按全部商家广播 | -| 退款成功或确定失败 | M05 Payment / M10 AfterSales | 申请买家 | 售后详情 | 不通知商家;消息不反向修改退款状态 | +| 退款成功 | M05 Payment / M10 AfterSales | 申请买家 | 售后详情 / 小金库记录 | 消息不反向修改退款状态 | +| 退款进入 `RefundFailed + MerchantRetryRequired` | M05 Payment / M10 AfterSales | 申请买家、订单指定处理商家 | 买家售后详情;商家售后详情 / 退款重试入口 | 买家收到 `RefundFailed`;商家收到 `RefundRetryRequired`,进入页面后重检归属、冷却和最新恢复状态 | +| 退款进入 `RefundFailed + OperatorAttentionRequired` | M05 Payment / M10 AfterSales | 申请买家 | 买家售后详情 | 买家仍收到最终失败消息;运维告警走监控系统,不给商家发送行动消息 | +| 退款 `Unknown` 或自动重试中 | M10 AfterSales | 无 | 无 | 尚未形成最终失败,不提前发送失败消息 | | 支付失败、回调 `Ignored`、回调 `Difference`、对账差异发现或管理员对账处置 | M05 Payment / C08 | 无 | 无 | 保留支付业务追踪、回调聚合与对账证据,不生成 M09 消息 | ## 七、异常、补偿与责任 @@ -166,15 +236,18 @@ stateDiagram-v2 | 场景 | M09 处理 | 最终状态与责任 | |---|---|---| | 来源事务回滚或事实未确定 | 不生成成功消息 | 来源模块继续拥有业务状态 | -| 事件字段、类型或任一必需接收人非法 | 整事件零消息,记录失败、`traceId` 和安全原因并告警 | 不自行猜接收人,不允许部分接收人先成功 | -| 同一事件重复到达 | 返回既有处理结果 | 不新增消息、不增加未读数 | +| 事件字段、类型、Routing Key 或任一必需接收人非法 | 整事件零消息,写确定 Rejected DB103,记录 `traceId` 和安全原因并死信 | 不自行猜接收人,不允许部分接收人先成功 | +| 同一 `eventId`、同一规范哈希重复到达 | 返回既有整事件结果 | 不新增消息、实时提示或未读数 | +| 同一 `eventId`、不同规范哈希 | 保留既有结果,当前投递零消息,Critical 告警并死信 | 不覆盖 DB103,不自动换 ID 重试 | | 可靠消息通道暂时不可用 | 由来源模块保留待发布事实并重试 | 已提交业务结果不回滚 | -| 消息事务失败 | 整体不生成,允许可靠重试 | 不留下消息/消费记录的部分结果 | -| SignalR 或 Redis 不可用 | M09 数据事实仍保留并记录实时推送失败;Identity 仍能安全鉴权时可继续查询 | 若令牌撤销状态无法确认,受保护请求按 Identity 失败关闭策略处理 | +| Identity/业务归属暂时无法确认或消息事务失败 | 整体回滚并让 Broker 重投 | 不留下消息、Inbox 或实时提示 Outbox 的部分结果 | +| 实时提示 RabbitMQ、API 消费者、SignalR 或 Redis 不可用 | M09 数据事实仍保留;未过期提示在 60 秒内重试,过期后停止 | 用户通过 A501/A503 补查;若认证事实无法确认,受保护请求和连接失败关闭 | | 查询他人消息 | 与不存在统一处理 | 不泄露消息是否存在、接收人或正文 | | 关联资源不存在、目标模块不可用或权限无法确认 | 返回历史消息正文,操作入口为空并显示“目标暂不可用” | 目标模块恢复后重新查询详情,不用消息覆盖目标状态 | | 账号禁用 | 仍按有效业务归属保存消息,拒绝查询、已读和推送 | 启用后可查历史;旧凭证不恢复 | -| 全部已读期间新消息到达 | 高水位后提交的消息保持未读 | 批量结果只覆盖稳定高水位内集合 | +| `RefundRetryRequired` 送达后退款状态已变化 | 保留历史正文,重新校验后不返回无效重试入口 | 消息不能覆盖 M10 当前恢复状态 | +| 从未有消息或当前没有未读消息 | 返回 `markedCount=0`、相应 `highWatermark`、`readAt=null` | 不写伪造已读时间;无历史时高水位为 0 | +| 全部已读期间新消息到达 | 高水位后提交的消息保持未读 | 批量结果只覆盖稳定高水位内 `read_at IS NULL` 集合 | | 列表首次加载、空数据或分页请求失败 | 保留消息中心页面结构,分别显示加载占位、空状态或就地重试 | 不把加载失败显示成“没有消息” | | 单条或全部已读请求失败 | 恢复操作前的列表项和角标,提示用户重试 | 数据库未提交时不得保留虚假已读状态 | @@ -184,13 +257,14 @@ stateDiagram-v2 | 流程能力 | 当前派生契约 | 数据设计 | 当前状态 | |---|---|---|---| -| 接收已提交事实并按固定矩阵整事件幂等生成消息 | 接口设计 4.3.6“业务模块到 Messaging 的集成事件” | DB101~DB120 尚未分配具体表 | 非 HTTP 契约已按固定矩阵、整事件原子性和无消息事实补齐,待数据库与交叉评审 | -| 查询本人消息列表 | A501 | 待 `database-lhc.md` 和数据库主文档确认 | 待交叉评审 | -| 查询本人消息详情与安全操作入口 | A502 | 待确认 | 待交叉评审 | -| 查询本人未读数 | A503 | 待确认 | 待交叉评审 | -| 首次标记单条消息已读 | A504 | 待确认 | 待交叉评审 | -| 将稳定高水位内的本人当前消息全部已读 | A505 | 待确认 | 待补齐服务端高水位返回与并发约束 | -| 消息提交后实时推送 | 接口设计 4.3.7“Hub 连接”、4.3.8“MessageCreated” | 复用已持久化消息事实 | 转入 C06,待部署与测试评审 | +| 接收已提交事实并按封闭联合整事件幂等生成消息 | 接口设计 4.3.6 `MessagingSourceEventV1` | DB102 来源 Outbox、DB103 Inbox、DB101 消息 | 精确变体、规范哈希、同 ID 异内容冲突和整事件原子性已冻结,待实现与交叉评审 | +| 消息提交后形成短期实时提示责任 | `MessagingRealtimeHintRequestedV1` 内部事件 | DB103 + 全部 DB101 + 每消息一条 DB102 同事务 | 60 秒有效、至少一次、客户端 `messageId` 去重;转入 C06 | +| 查询本人消息列表 | A501 | DB101 | 接口已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 查询本人消息详情与安全操作入口 | A502 | DB101 + 关联模块公开授权能力 | 接口已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 查询本人未读数 | A503 | DB101 | 接口已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 首次标记单条消息已读 | A504 | DB101 | 接口已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 将稳定高水位内的本人当前消息全部已读 | A505 | DB101 `server_sequence/read_at` | 无历史高水位 0、空更新 `readAt=null`、真实更新共用数据库时间已冻结,待 OpenAPI、实现与交叉评审 | +| API 消费提示后实时推送 | 接口设计 4.3.7“Hub 连接”、4.3.8“MessageCreated” | 复用已持久化消息事实 | API Hosted Service + 版本化应用级 Redis Pub/Sub,转入 C06,待部署与测试评审 | 架构承接章节: @@ -200,24 +274,30 @@ stateDiagram-v2 ## 九、下游设计与验证约束 -1. A501~A505 必须承接禁用账号拒绝访问、本人范围、目标暂不可用和稳定高水位,不得用现有草案改变流程。 -2. 接口设计 4.3 的来源事件必须承接固定接收人矩阵、整事件零或全、稳定幂等键和明确无消息事实;非 HTTP 契约不进入 OpenAPI,但必须形成可测试定义。 -3. 数据库设计需明确消息、可靠消费及必要 Outbox/Inbox 事实,保证整事件原子提交、每个接收人的唯一性、首次已读时间和高水位范围。 +1. A501~A505 必须承接禁用账号拒绝访问、本人范围、目标暂不可用和稳定高水位;A505 无历史固定 `highWatermark=0`,零更新固定 `readAt=null`,不得用现有草案改变流程。 +2. 接口设计 4.3 的来源事件必须逐变体承接本文封闭联合、精确聚合映射、字段白名单、Routing Key、规范哈希和同 `eventId` 异内容冲突,不得保留开放 `ownership/data`。非 HTTP 契约不进入 OpenAPI,但必须形成可测试定义。 +3. 数据库设计需明确 DB103、整事件全部 DB101 和每消息一条实时提示 DB102 的同事务边界,保证每个接收人的唯一性、首次已读时间和高水位范围。 4. 来源模块只发布已提交事实及业务归属,不自由指定广播范围;Messaging 不反向修改订单、支付或售后状态。 5. 目标模块负责在用户打开操作入口时重新校验当前身份、资源归属和状态;无法确认时返回目标暂不可用。 +6. `MerchantRetryRequired` 生成买家 `RefundFailed` 与商家 `RefundRetryRequired`;`OperatorAttentionRequired` 仍生成买家 `RefundFailed`,但不生成商家行动消息。Unknown 和自动恢复阶段不生成失败消息。 +7. 实现责任固定为“来源 Outbox Publisher → Mall.Worker Messaging 消费事务 → 实时提示 Outbox Publisher → Mall.Api 轻量消费者 → C06 应用级 Redis Pub/Sub → 各 API 本地连接复核与发送”;不得让来源模块或 Worker 直接调用 Hub。 ## 十、验收证据清单 - [ ] 订单创建、取消、支付、发货、完成和售后事实只在来源事务提交后生成消息。 - [ ] 买家与指定商家收到符合身份的内容,游客、管理员和无关账号不收到私人消息。 -- [ ] 支付成功仅通知买家和指定商家;售后申请/寄回仅通知指定商家;审核和退款确定结果仅通知买家;明确无消息事实不生成消息。 +- [ ] 支付成功仅通知买家和指定商家;售后申请/寄回仅通知指定商家;审核和退款最终结果通知买家;`MerchantRetryRequired` 额外通知指定商家,`OperatorAttentionRequired` 仍通知买家但不给商家行动消息,Unknown/自动恢复不提前通知。 - [ ] 任一必需接收人无效时整事件零消息并告警,不出现部分接收人成功。 -- [ ] 同一事件重复投递至少两次,只形成一份对应接收人的消息。 +- [ ] 10 个 `MessagingSourceEventV1` 变体逐一验证字段白名单、聚合 ID、Routing Key、枚举组合、金额和接收人;未知属性、数组和非法组合均整事件拒绝。 +- [ ] 同一 `eventId`、同一规范哈希重复投递至少两次,只形成一份对应接收人的消息和一组实时提示 Outbox。 +- [ ] 同一 `eventId`、不同规范哈希并发/串行投递均保持首个结果,冲突投递零消息、Critical 告警并进入死信。 +- [ ] 模拟 Worker 在事务前、事务中、提交后但 Broker 确认前崩溃,均不会出现 DB103、部分 DB101 或实时提示 DB102 的拆分结果。 - [ ] 本人列表、详情、筛选、分页和未读数正确,越权请求不泄露消息内容。 -- [ ] 单条已读、重复已读、并发已读和全部已读满足首次时间及稳定高水位边界,高水位后消息保持未读。 +- [ ] 单条已读、重复已读、并发已读和全部已读满足首次时间及稳定高水位边界;无历史返回 `0/0/null`,已有历史但零更新返回 `0/现有水位/null`,高水位后消息保持未读。 - [ ] 消息入口和列表分别验证加载占位、空状态、请求失败重试与分页加载反馈。 - [ ] 标记已读成功时列表与角标立即更新;模拟失败时恢复原状态并提供重试。 - [ ] 可靠消息或实时推送故障时,来源业务结果不回滚,消息能够按既定责任恢复或补查。 +- [ ] 实时提示重复时客户端按 `messageId` 去重;提示延迟超过 60 秒后不再推送,A501/A503 仍可恢复完整消息和未读数。 - [ ] 关联资源不存在、不可访问或权限无法确认时,历史正文仍可查看,操作入口为空并显示“目标暂不可用”。 - [ ] 禁用账号仍按有效归属保存消息但不能查询、已读或接收推送;启用后可查历史且旧凭证不恢复。 - [ ] 保留事件标识、消息标识、接收用户、`traceId`、数据库结果和重试结果的脱敏证据。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" index b93d1c3..2ee0efd 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-01-\347\224\250\346\210\267\346\263\250\345\206\214\346\265\201\347\250\213.md" @@ -20,7 +20,7 @@ A001 由本流程派生,仅在流程评审通过后用于契约映射;现有 | M01-01/F01 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 完整定义 | 已确认角色、判断、状态、事务边界和模块出入口 | | A001 接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | -| DBxxx 用户表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | +| DB001 用户表 | 完整定义,已确认设计 | 已承接账号、手机号/用户名唯一、密码哈希、角色、状态和原子创建约束;待实现与测试 | | M00 公共认证能力 | 内部 P0 | 只登记接入点,规则由 M00 维护 | ## 二、模块直接出入口 @@ -174,7 +174,7 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 公开注册创建买家账号 | A001 | 拒绝角色字段,依次完成基础校验、手机号唯一性、用户名生成、最终密码校验,并原子创建账号与默认资料;成功后不签发登录凭证 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 公开注册创建买家账号 | A001 | 拒绝角色字段,依次完成基础校验、手机号唯一性、用户名生成、最终密码校验,并原子创建账号与默认资料;成功后不签发登录凭证 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | 接口仅承载“创建买家账号”这一业务结果;字段格式、错误码和 `traceId` 由接口设计在评审前统一,不得反向写入业务流程。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" index 8510547..c24c0d3 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-02-\347\224\250\346\210\267\347\231\273\345\275\225\344\270\216\351\200\200\345\207\272\346\265\201\347\250\213.md" @@ -21,7 +21,7 @@ A002~A004 由本流程派生;历史清单中的 A005 刷新凭证没有业 | 本文业务流程 | 完整定义 | 已确认角色、判断、状态、失效边界和模块出入口 | | A002~A004 接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | | A005 刷新凭证 | 无需求来源 | 取消,不进入实现 | -| DBxxx 账号/令牌表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | +| DB001/DB004 账号与撤销表 | 完整定义,已确认设计 | 已承接账号状态、凭证版本、当前 JTI 撤销和多实例可重建事实;待实现与测试 | | C10 多实例认证 | 完整定义 | 任一实例一致验证;共享失效事实不可确认时失败关闭,恢复安全事实后才重新开放 | ## 二、模块直接出入口 @@ -65,10 +65,20 @@ flowchart TD G -- "否" --> X2 G -- "是" --> H{"账号状态正常?"} H -- "否" --> X3["拒绝登录并提示账号停用"] - H -- "是" --> I["签发一个有明确有效期的 JWT 登录凭证"] + H -- "是" --> I["按 ClockSkew=0 签发一个有明确有效期的 JWT 登录凭证"] I --> J["返回账号摘要、服务端确认的角色和当前状态"] - J --> K["前端保存登录态,按角色进入对应端"] - K --> L{"按角色进入?"} + J --> K{"角色是否为 Buyer?"} + K -- "否" --> RC["清除 actionIntent 与 returnDestination"] + K -- "是" --> V{"两类状态各自合法且组合相容?"} + V -- "否" --> RC + V -- "是" --> RT{"有合法一次性 returnDestination?"} + RT -- "有" --> RP["按页面白名单恢复并重新鉴权/加载
不恢复提交动作"] + RT -- "无" --> AI{"有合法 actionIntent?"} + RP --> AI + AI -- "有" --> R["重读 A103
以原 actionKey 恢复 Favorite / AddToCart"] + AI -- "无" --> L{"按角色进入?"} + R --> L + RC --> L L -- "买家" --> M["进入购物端"] L -- "商家" --> M1["进入商家端"] L -- "管理员" --> M2["进入管理端"] @@ -78,9 +88,16 @@ flowchart TD - 账号不存在和密码错误必须返回统一的“账号或密码错误”提示,禁止区分错误字段。 - 账号禁用需明确告知用户联系管理员,但不暴露内部状态码或异常。 -- 登录凭证包含明确有效期;有效期到达后必须重新登录,本期没有刷新凭证或静默续期。 -- 登录凭证的签名与验证配置由公共认证能力统一,接口不能额外派生第二种凭证。 -- 多实例环境下,登录态验签配置必须一致;任一实例可独立验证同一有效登录态。 +- 登录凭证包含明确有效期;JWT 验证固定 `ClockSkew=0`,达到 `exp` 即失效并要求重新登录,本期没有刷新凭证或静默续期。 +- 登录凭证的签名与验证配置由公共认证能力统一,接口不能额外派生第二种凭证。两个 API 与 SignalR Hub 的 Issuer、Audience、签名材料标识、有效期规则、`ClockSkew` 和令牌版本规则必须形成同一脱敏配置摘要;摘要不一致的实例不得承接受保护流量。 +- 多实例环境下,登录态验签配置必须一致;任一就绪实例可独立验证同一有效登录态。 +- PC Web 当前会话把动作意图和安全页面目标分开保存,各最多一份,禁止把业务动作编码进 URL。 + - `actionIntent` 固定为 `Favorite/AddToCart + productId + quantity? + actionKey`,不接受任意 URL、角色、金额、价格、库存或用户 ID。Buyer 登录后必须重读 A103;收藏调用 A019,加购调用 A201 并原样使用 `actionKey` 作为 `Idempotency-Key`。 + - 动作收到确定 `2xx` 或商品下架、售罄、数量超限等确定业务 `4xx` 后才清除;网络中断、超时、`503` 或提交结果未知时保留同一意图和 Key,不能另造 Key 重复执行。 + - 动作意图只可由同一商品详情的登录门创建;它存在时,`returnDestination` 必须为空或精确为同一 `ProductDetail(productId)`。商品 ID 不同,或动作意图与 Checkout、Cashier、PaymentResult、Orders、AfterSales 等其他目标并存时,整组状态非法:登录后清除两者,不导航、不重放,避免在结算、支付或订单页面后台改变购物车。 + - `returnDestination` 只允许需求已冻结的页面枚举及该页面至多一个必要 UUID,不能保存协议、主机、任意路径、查询串、片段或脚本。Buyer 登录后只恢复一次安全页面并重新鉴权、重新读取:Checkout 重调 A208,Cashier/PaymentResult 重读 A404/A406,SeckillActivityDetail 重读 A227;不得保存旧价格、库存、数量或自动调用 A228/A301/A405。 + - 注册后未登录时可继续保留合法状态;商家/管理员登录、字段非法、显式退出或各自确定消费完成时立即清除相应状态。恢复目标不存在或无权时进入对应安全列表页并清除目标。 +- 本期没有独立 `BuyNow` 能力;购买统一从购物车选择、结算确认和 A301 主动提交进入,不得在登录恢复中补造自动下单或第二套购买意图。 ## 四、登录态恢复与角色路由 @@ -94,8 +111,8 @@ flowchart TD E -- "否" --> X E -- "是" --> F["返回当前账号摘要与角色"] F --> G{"路由是否匹配当前角色?"} - G -- "否" --> H["按服务端确认角色跳转对应端"] - G -- "是" --> I["继续展示当前页面"] + G -- "否" --> H["按服务端确认角色跳转对应端
非 Buyer 清除两类买家会话状态"] + G -- "是" --> I["继续展示当前页面
或恢复一次白名单页面/Buyer 动作"] H --> I ``` @@ -104,6 +121,7 @@ flowchart TD - 刷新或重连后必须重新执行服务端身份校验,禁止仅凭前端缓存决定路由。 - 角色路由以服务端确认结果为准;前端隐藏菜单不替代服务端授权。 - 登录态恢复失败、登录态过期或账号被禁用时,立即清理失效登录态并提示用户重新登录。 +- 前端因 401 清理登录态只适用于 Bearer 缺失、失效、过期或撤销;A006 当前密码校验失败是 `400 AUTH.CURRENT_PASSWORD_INCORRECT`,必须保留当前登录态和页面输入。 ## 五、退出与登录态失效 @@ -111,8 +129,15 @@ flowchart TD flowchart TD A["用户在 PC Web 点击退出"] --> B["前端立即进入退出中状态"] B --> C["停止新的受保护请求并清除持久化凭证"] - C --> D["仅使用退出时持有的当前 JWT 调用服务端撤销"] - D --> E{"服务端能否确认当前 JWT 已失效?"} + C --> D["仅使用退出时持有的当前 JWT
完成退出专用校验并查询既有 DB004"] + D --> D0{"已有当前 jti 撤销事实?"} + D0 -- "是" --> E + D0 -- "否" --> D1["在数据库事务内读取一次 decisionTime"] + D1 --> D2{"decisionTime < JWT exp?"} + D2 -- "是" --> D3["原子提交 DB004 当前 jti 撤销
与安全失效 Outbox"] + D2 -- "否" --> D4["确认令牌已自然失效
不写 DB004 或安全 Outbox"] + D3 --> E{"服务端能否确认当前 JWT 已失效?"} + D4 --> E E -- "是" --> F["显示退出成功并进入登录页"] E -- "否" --> G["说明本地已退出、服务端撤销结果无法确认"] G --> H["不得显示退出成功;后续受保护请求失败关闭"] @@ -128,8 +153,10 @@ flowchart TD - 点击退出后,前端必须立即停止新的受保护请求并清理持久化登录凭证;为完成一次撤销调用而暂存的当前 JWT 不得重新写回本地状态。 - 只有服务端明确确认当前 JWT 已失效时才显示“退出成功”;无法确认时说明“本地已退出,服务端撤销结果无法确认”,不允许恢复本地登录态或给出虚假成功。 +- A003 先按 `ClockSkew=0` 完成格式、签名、Issuer、Audience 与进入处理时的自然有效期校验。令牌在进入处理前已经过期仍返回未认证;令牌通过校验后,只以数据库事务内读取一次的 `decisionTime` 裁决后续动作。 +- `decisionTime < exp` 时才允许用同一时间写 DB004 `revoked_at` 和安全失效 Outbox;`decisionTime >= exp` 时令牌已经自然失效,固定形成“已自然失效”的确定退出结果,不写 DB004、Outbox 或 Redis 镜像,也不得伪造较早的撤销时间来绕过数据库约束。 - 主动退出只影响当前登录态;本期不自动撤销同账号的其他设备登录态。 -- M06-03 禁用账号、M01-03 修改手机号必须让业务变化与“此前全部登录凭证失效”形成确定结果;无法确认全部失效时不得把业务变化报告为成功。 +- M06-03 禁用账号、M01-03 修改手机号必须在同一 PostgreSQL 事务更新 DB001 账号状态/令牌版本,使业务变化与“此前全部登录凭证失效”形成权威确定结果;数据库提交失败或未知时不得把业务变化报告为成功。 - 重新启用账号不会恢复禁用前签发的凭证;用户必须重新提交手机号和密码。 - 登录态失效结果需在多实例之间保持一致;失效状态无法确认时受保护请求失败关闭。 - 退出后用户的所有个人页和受保护页必须退出到登录态,禁止出现“看似已退出但仍能访问”的状态。 @@ -158,7 +185,7 @@ flowchart TD 异常约束: - 未认证或登录已失效与身份有效但无权执行当前操作是两类不同的拒绝结果;具体错误码和 HTTP 状态由接口设计统一。 -- 撤销状态共享不可用时不得返回虚假成功,也不得静默放行受保护请求。 +- 当 `decisionTime < exp` 时,DB004 撤销权威事实无法提交或提交结果未知不得返回虚假成功;当 `decisionTime >= exp` 时,不存在待提交的撤销事实,可按自然失效确定成功。DB004 已提交但 Redis 镜像同步失败时退出仍是确定成功,相关受保护请求在安全水位恢复前失败关闭,既不能静默放行,也不能声称 PostgreSQL 已回滚。 - 同一 JWT 已明确撤销后重复提交退出,服务端返回同一已失效结果;撤销结果未知时不得伪造为幂等成功。 ## 七、与核心模块的衔接 @@ -175,7 +202,7 @@ flowchart TD 衔接约束: - 下游模块只接收登录态解析后的身份和角色,不接受客户端自行声明的接收人。 -- 退出、手机号修改或账号禁用只有在所有 API 实例都能一致拒绝相应旧凭证后才能返回成功;无法确认一致失效时不得返回成功,依赖该失效事实的受保护请求必须失败关闭。 +- 退出在数据库裁决时仍未到 `exp`,以 DB004 撤销提交为权威;裁决时已经到达 `exp`,以签名有效期和同一数据库 `decisionTime` 确认自然失效且无需持久化。手机号修改或账号禁用以 DB001 状态/令牌版本事务为权威;存在待提交权威事务时必须确定提交,且所有 API 实例能通过“已同步镜像”或“安全水位未知时失败关闭”一致拒绝旧凭证后才可返回成功。权威提交未知时不得返回成功。 - Redis 等共享失效能力恢复后,C10 必须先恢复有效期内的撤销事实并通过安全健康检查,再恢复相关受保护 HTTP 与 Hub;不得让恢复过程使旧凭证短暂复活。 - 本期仅验收 PC Web 登录入口与角色路由;Electron 和 Android 作为后续客户端规划,不进入当前流程或验收证据。 @@ -183,9 +210,9 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 提交手机号和密码登录 | A002 | 校验凭据与账号状态,只签发一个有明确有效期的 JWT,返回角色与账号摘要 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 主动退出当前登录态 | A003 | 撤销当前 JWT;已明确撤销时可重复返回相同结果,未知时不得返回成功 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 登录态恢复与当前账号查询 | A004 | 校验 JWT、账号状态和失效事实,返回当前账号摘要与角色 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 提交手机号和密码登录 | A002 | 校验凭据与账号状态,只签发一个有明确有效期的 JWT,返回角色与账号摘要 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 主动退出当前登录态 | A003 | 数据库裁决时未到期则撤销当前 JWT,刚好到期则按自然失效收敛;已明确撤销时可重复返回相同结果,仍需撤销但结果未知时不得返回成功 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 登录态恢复与当前账号查询 | A004 | 校验 JWT、账号状态和失效事实,返回当前账号摘要与角色 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | | 刷新登录凭证 | A005 | 无需求来源;本期凭证到期后重新登录 | 取消,保留历史编号 | 接口必须承载“当前账号可登录”这一业务结果;HTTP 状态码、错误码和 `traceId` 由接口设计在评审前统一,不得反向写入业务流程。 @@ -193,7 +220,7 @@ flowchart TD ## 九、下游契约与数据约束 1. A002 响应只返回一个 JWT、有效期、账号摘要和服务端角色,不得出现刷新令牌或刷新入口。 -2. A003 只撤销请求携带的当前 JWT;多设备退出没有需求来源,不得由接口自行扩大。 +2. A003 只处理请求携带的当前 JWT:未到期时撤销,数据库裁决时已到期则确认自然失效;多设备退出没有需求来源,不得由接口自行扩大。 3. A004 必须同时校验 JWT 有效期、签名、当前账号状态和失效事实;任何必需事实无法确认时按服务暂不可用失败关闭。 4. 登录失败次数限制、密码修改和账号锁定均不属于本期业务范围;不得因实现便利写入契约。 5. 多实例必须共享一致的签名配置、账号安全变化和凭证失效判断,但具体存储机制不进入业务接口。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" index c663c35..867f4b9 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M01-03-\344\270\252\344\272\272\344\277\241\346\201\257\344\270\216\346\224\266\350\264\247\345\234\260\345\235\200\346\265\201\347\250\213.md" @@ -21,7 +21,7 @@ A006~A008、A010~A014 由本流程派生;历史清单中的 A009 展示资 | 本文业务流程 | 完整定义 | 已确认角色、判断、状态、事务边界和模块出入口 | | A006~A008、A010~A014 接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | | A009 展示资料修改 | 无需求来源 | 取消,不进入实现 | -| DBxxx 用户资料/地址表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | +| DB001/DB003/DB004 用户、地址与撤销表 | 完整定义,已确认设计 | 已承接资料字段、唯一默认地址、资源归属和敏感变更后凭证失效;待实现与测试 | | M04 地址快照契约 | 完整定义,待交叉评审 | 已冻结完整收件人、联系电话、省市区和详细地址;M04 在订单事务内保存快照 | ## 二、模块直接出入口 @@ -30,9 +30,9 @@ A006~A008、A010~A014 由本流程派生;历史清单中的 A009 展示资 flowchart LR B["已登录买家"] -->|"个人中心入口"| PROF["M01-03 Identity
资料与地址"] PROF -->|"用户名、默认头像、掩码手机号"| UI["个人中心页"] - PROF -->|"地址列表 + 默认地址标记"| ADDR["地址管理页"] + PROF -->|"地址列表 + 默认地址标记 + version"| ADDR["地址管理页"] PROF -->|"手机号修改后全部旧凭证失效"| TOK["M01-02 登录与退出"] - PROF -->|"地址归属校验 + 当前地址数据"| ORD["M04 Ordering
F08 提交订单"] + PROF -->|"锁内复核 addressId + version
返回可信地址快照"| ORD["M04 Ordering / C01 Seckill
提交订单"] ORD -->|"地址快照随订单持久化"| SNAP["历史订单地址快照"] GUEST["游客"] -. "访问个人中心" .-> REJ["引导登录并保留安全返回目标"] MERCH["商家或管理员"] -. "访问买家资料" .-> FORB["拒绝访问买家私人资源"] @@ -40,8 +40,8 @@ flowchart LR 边界约束: -- 资料与地址只能由当前买家修改;M04 只通过公开应用契约读取本次校验的当前地址数据,不能修改地址。 -- 手机号修改与“修改前全部登录凭证失效”必须形成确定结果;无法确认凭证失效时不得报告手机号修改成功。 +- 资料与地址只能由当前买家修改;M04/C01 只通过 Identity 公开应用契约在共享事务中锁定并复核本次提交的 `addressId + addressVersion`,取得可信地址快照,不能修改地址或绕过 Identity 直读 DB003。 +- 手机号修改与 DB001 `token_version + 1` 必须在同一 PostgreSQL 事务形成“修改前全部登录凭证失效”的权威确定结果;事务失败或提交未知时不得报告手机号修改成功。Redis 仅传播可重建镜像,传播失败后由 C10 安全水位失败关闭,不能反向回滚已提交手机号。 - 地址列表与默认地址切换必须按当前买家 ID 隔离,禁止跨用户访问。 ## 三、资料维护主流程 @@ -74,8 +74,8 @@ flowchart TD - 用户名只能由服务端重新生成,不接受客户端指定任意字符串。 - 用户名自助重置机会仅一次,只有新用户名成功提交后才消耗;生成冲突、保存失败或并发落败不得消耗机会。 -- 手机号修改必须验证当前密码,并基于开始修改时读取的旧账号状态做条件提交;同一旧状态上的并发请求最多一个成功。 -- 手机号修改成功必须同时形成“修改前全部登录凭证失效”的确定结果;无法确认时手机号保持不变。 +- 手机号修改必须验证当前密码,并基于开始修改时读取的旧账号状态做条件提交;同一旧状态上的并发请求最多一个成功。当前密码错误固定返回 `400 AUTH.CURRENT_PASSWORD_INCORRECT`,不是 Bearer 认证失败,前端不得清理现有登录态。 +- 手机号修改成功必须同时提交 DB001 `token_version + 1`;该事务失败或结果未知时手机号保持不变。事务已确定提交后,即使 Redis 镜像暂未同步也保持成功,并在安全恢复前拒绝相关受保护请求。 - 资料修改不得在响应或日志中泄露完整手机号、密码哈希或内部异常。 - 本期不存在展示名、简介或其他任意资料编辑;查看资料和用户名重置不改变登录态,修改手机号后必须重新登录。 @@ -115,9 +115,11 @@ flowchart TD - 删除默认地址后不自动选择其他地址,下单时由买家明确选择。 - 新增地址固定创建为非默认地址,不接收默认标记;如需设为默认,必须在创建成功后另行执行设默认动作。 - 编辑地址不允许直接修改默认标记;默认地址切换必须通过设默认动作完成。 +- 地址创建时 `version=1`;编辑、设置为默认以及原默认地址被取消默认等每条地址事实的成功变化都递增对应行版本。A010~A014 返回当前版本,前端提交订单时必须把用户实际确认的 `addressId + addressVersion` 一并提交。 - 本期不设置单买家地址数量上限,接口和数据设计不得自行加入固定上限。 - 同一买家对地址的读写始终按当前用户过滤,禁止仅凭资源 ID 跨用户访问。 - 地址字段变更不影响历史订单的地址快照,下单时间点确定的快照始终保留。 +- 订单提交在 Identity 公开能力中以 DB003 行锁重新校验归属、存在性与版本:地址已删除或不属于本人按统一不存在处理,版本变化返回 `IDENTITY.ADDRESS_VERSION_CONFLICT` 并要求刷新地址后重新确认;不得静默使用编辑后的新内容,也不得沿用页面旧内容。 ## 五、地址归属校验与下单衔接 @@ -158,11 +160,13 @@ flowchart TD B6 --> N6["不自动选择其他地址"] A7["手机号或地址字段格式非法"] --> B7["字段级错误,保留可恢复输入"] A8["新增地址夹带默认标记"] --> B8["拒绝无来源字段,要求另行设置默认地址"] + A9["确认地址后另一标签页编辑/切换默认"] --> B9["订单事务锁内版本不符"] + B9 --> N9["零建单并刷新地址,要求重新确认"] ``` 异常约束: -- 任何敏感修改的失败都必须保持现有账号状态、登录态和地址不变。 +- 任何敏感修改的失败都必须保持现有账号状态、登录态和地址不变;当前密码不正确使用 400 业务校验错误,不能触发全局 401 清理。 - 资料和地址接口不允许通过仅凭资源 ID 跨用户访问,越权请求统一返回“资源不存在或无权限”。 - 并发修改场景下,最终结果必须保持一致;不出现“两个默认地址”或“两个不同手机号同时生效”的状态。 - A006 手机号修改需要携带可验证的旧账号状态条件;不能只依赖新手机号唯一约束解决并发覆盖。 @@ -188,26 +192,26 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 修改手机号 | A006 | 验证当前密码和旧账号状态,校验新手机号格式与唯一性,原子形成新手机号和全部旧凭证失效结果 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 重置用户名 | A007 | 由服务端生成唯一用户名,仅在提交成功后消耗唯一重置机会,并发最多一个成功 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 获取本人资料 | A008 | 只返回用户名、默认头像、掩码手机号和用户名重置机会状态 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 修改手机号 | A006 | 验证当前密码和旧账号状态,校验新手机号格式与唯一性,原子形成新手机号和全部旧凭证失效结果 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 重置用户名 | A007 | 由服务端生成唯一用户名,仅在提交成功后消耗唯一重置机会,并发最多一个成功 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 获取本人资料 | A008 | 只返回用户名、默认头像、掩码手机号和用户名重置机会状态 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | | 修改本人展示资料 | A009 | 展示名、简介等字段没有需求来源 | 取消,保留历史编号 | -| 查询本人地址列表 | A010 | 按当前买家返回本人地址列表与默认标记 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 新增地址 | A011 | 校验必填字段并固定创建非默认地址,不接受默认标记或固定数量上限 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 编辑地址 | A012 | 仅修改本人地址并重新校验字段,不允许修改默认标记 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 删除地址 | A013 | 仅删除本人地址,删除默认地址时不自动指定其他地址 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 设置默认地址 | A014 | 独立动作原子切换默认地址,保证最终最多一个 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 查询本人地址列表 | A010 | 按当前买家返回本人地址列表、默认标记与并发版本;默认地址置顶但无默认时不推断第一条 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 新增地址 | A011 | 校验必填字段并固定创建非默认地址,不接受默认标记或固定数量上限 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 编辑地址 | A012 | 仅修改本人地址并重新校验字段,不允许修改默认标记 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 删除地址 | A013 | 仅删除本人地址,删除默认地址时不自动指定其他地址 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 设置默认地址 | A014 | 独立动作原子切换默认地址,保证最终最多一个 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | 接口仅承载本流程结果;错误码、HTTP 状态码和 `traceId` 由接口设计统一,不得反向写入业务流程。 ## 九、下游契约与数据约束 1. 用户名重置机会必须与新用户名在一个原子提交边界内更新,并由唯一用户名约束兜底;接口不得先消耗机会再尝试保存。 -2. A006 必须携带并校验服务端可识别的旧账号状态条件;手机号变化和全部旧凭证失效必须作为一个确定业务结果对外返回。 +2. A006 必须携带并校验服务端可识别的旧账号状态条件;手机号变化和 DB001 `token_version + 1` 必须在同一事务提交并作为一个确定业务结果对外返回,Redis 只承接提交后的镜像传播。 3. A008 不得出现展示名、简介、自定义头像等字段;A009 取消后不得用通用资料接口绕开专用变更规则。 4. A011 请求不得包含默认标记,也不得校验固定地址数量上限;A012 与 A014 使用不同业务动作。 -5. 默认地址切换的数据设计必须保证同一买家最终最多一个默认地址;删除默认地址允许留下零个默认地址。 -6. M01-03 的公开应用契约只返回地址归属与当前地址数据;订单地址快照由 M04 自己的数据设计和事务负责。 +5. 默认地址切换的数据设计必须保证同一买家最终最多一个默认地址;删除默认地址允许留下零个默认地址。所有成功地址事实变化都递增受影响行的 `version`。 +6. M01-03 的公开应用契约必须在 M04/C01 的共享 PostgreSQL 事务中锁定并复核 `addressId + addressVersion + buyerId`,只返回可信地址快照;订单地址快照仍由 Ordering 在同一事务持久化。版本不符必须零建单并要求重新确认。 ## 十、验收证据清单 @@ -216,6 +220,7 @@ flowchart TD - [ ] 用户名只有成功提交后才消耗重置机会;并发重置最多一个成功。 - [ ] 两个基于同一旧状态的手机号修改最多一个成功,失败请求不覆盖新资料。 - [ ] 默认地址始终最多一个,删除默认地址后下单流程不会擅自选择其他地址。 +- [ ] 结算确认后并发编辑、删除或默认切换地址时,A301/A228 不使用未经本次确认的新内容:不存在/越权安全拒绝,版本变化提示刷新并零建单。 - [ ] 新增地址固定为非默认且不受固定数量上限约束;设置默认必须另行调用独立动作。 - [ ] 编辑地址不能直接修改默认地址标记;只能通过设默认动作完成切换。 - [ ] 本期不存在展示名、简介或通用展示资料修改能力,A009 保持取消。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" index 94fa257..b24adb6 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M06-03-\345\220\216\345\217\260\347\224\250\346\210\267\347\256\241\347\220\206\346\265\201\347\250\213.md" @@ -20,7 +20,7 @@ A015~A017 由本流程派生,仅在流程评审通过后用于契约映射 | M06-03/F13 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 完整定义 | 已确认角色分流、责任阻断、竞争结果、状态与模块出入口 | | A015~A017 接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | -| DBxxx 账号/操作记录表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | +| DB001/DB002/DB004/DB104 账号治理数据 | 完整定义,已确认设计 | 已承接账号状态、治理时间线、全部旧凭证失效和稳定幂等结果;待实现与测试 | | C10 多实例凭证校验 | 完整定义 | 所有实例一致遵守账号状态与失效事实;不可确认及安全恢复完成前失败关闭 | ## 二、模块直接出入口 @@ -204,9 +204,9 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 分页查询账号 | A015 | 只返回买家和商家账号摘要、受控筛选结果与稳定分页 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 禁用账号 | A016 | 使用稳定请求标识;先按买家/商家分流,商家保护默认账号并复核固定责任清单;成功时账号禁用、全部旧凭证失效和最小追踪形成确定结果 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 启用账号 | A017 | 使用稳定请求标识恢复账号状态但不恢复任何旧凭证;重放首次结果或返回当前状态 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 分页查询账号 | A015 | 只返回买家和商家账号摘要、受控筛选结果与稳定分页 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 禁用账号 | A016 | 使用稳定请求标识;先按买家/商家分流,商家保护默认账号并复核固定责任清单;成功时账号禁用、全部旧凭证失效和最小追踪形成确定结果 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 启用账号 | A017 | 使用稳定请求标识恢复账号状态但不恢复任何旧凭证;重放首次结果或返回当前状态 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | 接口仅承载本流程结果;错误码、HTTP 状态码和 `traceId` 由接口设计统一,不得反向写入业务流程。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" index df6b3a6..eba955d 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/tyh/M08-\345\225\206\345\223\201\346\224\266\350\227\217\344\270\216\346\265\217\350\247\210\345\216\206\345\217\262\346\265\201\347\250\213.md" @@ -21,7 +21,7 @@ A018~A022、A024、A025 由本流程派生;历史清单中的 A023 清空历 | 本文业务流程 | 完整定义 | 已确认角色、幂等、开关、记录上限、并发和模块出入口 | | A018~A022、A024、A025 接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | | A023 清空浏览历史 | 无需求来源 | 取消,不进入实现 | -| DBxxx 收藏/浏览表 | 模板/占位 | 本文不发明字段、状态码、索引或迁移 | +| DB005~DB007 收藏/浏览数据 | 完整定义,已确认设计 | 已承接收藏唯一性、浏览开关和每买家最近 200 条约束;待实现与测试 | | M02 商品事实 | 完整定义,已完成统稿校准 | 只登记接入点;价格、库存与销售状态由 Catalog 权威流程提供 | ## 二、模块直接出入口 @@ -170,14 +170,14 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 收藏列表 | A018 | 按收藏时间和记录 ID 稳定分页,返回当前商品摘要与可用状态;下架或不存在商品保留占位 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 收藏商品 | A019 | 先返回本人既有收藏;没有记录时校验商品已上架再创建,并用唯一性处理并发 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 取消收藏 | A020 | 按当前买家和商品删除本人收藏;记录不存在时仍返回未收藏成功结果 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 浏览历史列表 | A021 | 不受开关影响,始终按浏览时间和记录 ID 稳定分页返回已有历史 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 修改浏览记录开关 | A022 | 设置后续浏览写入开关;关闭不删除、不隐藏旧历史,并与浏览写入形成确定顺序 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 收藏列表 | A018 | 按收藏时间和记录 ID 稳定分页,返回当前商品摘要与可用状态;下架或不存在商品保留占位 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 收藏商品 | A019 | 先返回本人既有收藏;没有记录时校验商品已上架再创建,并用唯一性处理并发 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 取消收藏 | A020 | 按当前买家和商品删除本人收藏;记录不存在时仍返回未收藏成功结果 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 浏览历史列表 | A021 | 不受开关影响,始终按浏览时间和记录 ID 稳定分页返回已有历史 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 修改浏览记录开关 | A022 | 设置后续浏览写入开关;关闭不删除、不隐藏旧历史,并与浏览写入形成确定顺序 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | | 清空浏览历史 | A023 | 无需求来源;本期保留旧历史且不提供清空能力 | 取消,保留历史编号 | -| 记录浏览历史 | A024 | 服务端重检商品存在且 OnSale,仅在开关开启时写入或更新时间,并同步保留最近 200 条 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 查询浏览记录开关 | A025 | 无设置事实时返回默认开启,查询不产生写操作 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 记录浏览历史 | A024 | 服务端重检商品存在且 OnSale,仅在开关开启时写入或更新时间,并同步保留最近 200 条 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 查询浏览记录开关 | A025 | 无设置事实时返回默认开启,查询不产生写操作 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | 接口仅承载本流程结果;错误码、HTTP 状态码和 `traceId` 由接口设计统一,不得反向写入业务流程。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" index eca23b9..0093455 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/C03-\350\256\242\345\215\225\350\266\205\346\227\266\346\265\201\347\250\213.md" @@ -68,11 +68,11 @@ flowchart LR ```mermaid flowchart TD - A["Worker 周期触发"] --> B["读取权威时间并查找 paymentDeadline 已到、状态仍为 PendingPayment 的订单"] + A["Worker 周期触发"] --> B["以数据库时间查找 paymentDeadline 已到、状态仍为 PendingPayment 的候选订单"] B --> C{"本轮是否有候选?"} C -- "否" --> Z["结束本轮,等待下次调度"] C -- "是" --> D["按稳定次序领取有界批次,多实例只允许一次有效处理资格"] - D --> E["逐笔调用 M04 统一过期取消,系统原因固定为 PaymentExpired"] + D --> E["逐笔调用 M04 统一过期取消;M04 锁定订单后重新取 decisionTime 并最终裁决"] E --> F{"M04 返回什么结果?"} F -- "首次取消成功" --> G["记录本轮成功;订单、回补和买家通知事实已完整提交"] F -- "已取消" --> H["记录幂等完成,不重复回补或通知"] @@ -89,7 +89,10 @@ flowchart TD 扫描规则: - 只选择 `paymentDeadline <= 权威时间` 且仍为 `PendingPayment` 的订单。 -- 扫描批次必须有界并使用稳定顺序,避免一次任务长期占用资源;具体间隔、批量大小和单轮次数在 Worker 配置阶段确定,不能写死在业务流程。 +- 扫描时间只用于发现候选,不能直接写入取消时间或替代最终资格判断。M04 对每笔候选锁定订单后重新取得一次独立的 `cancelDecisionTime`;只有锁后仍为 `PendingPayment` 且 `cancelDecisionTime >= paymentDeadline` 才执行 `PaymentExpired` 取消。M05/A421 的支付 `finalTime` 只决定支付路径结果,不得替代本取消事务的重新取时和复核。 +- 统一配置键固定为 `OrderLifecycle:ScanIntervalSeconds`、`OrderLifecycle:BatchSize`、`OrderLifecycle:MaxBatchesPerRun`:默认分别为 5 秒、100 条、10 批;允许范围分别为 1~60 秒、1~500 条、1~100 批。启动时任一值越界或多实例配置摘要不一致则 Worker 对该能力 NotReady,不得静默采用不同吞吐参数。 +- 每轮按 DB063 的 `nextAttemptAt asc, dueAt asc, taskId asc` 稳定排序,使用 `FOR UPDATE SKIP LOCKED` 分批领取;单批短事务提交租约后逐笔执行,每笔业务事务独立提交。单笔失败只写该责任的退避结果并继续同批下一条,不回滚已完成订单;数据库连接级失败停止本轮,未领取或租约到期责任由后续轮次恢复。 +- 单轮达到 10 批默认上限后主动让出执行权;若仍有到期积压,下一调度窗口继续处理并记录 backlog 指标,不能在一个运行行中无限循环。 - 多个 Worker 实例并发时可以同时发现同一候选,但最终只有一个 M04 取消动作提交;进程内集合或单机锁不能作为唯一正确性保障。 - 同一订单在一个处理尝试中只调用一次统一取消。暂时失败后按退避策略重新进入后续尝试,不能无间隔反复占用任务。 - Worker 重启后只需重新扫描共享的订单事实;不能依赖未持久化队列或内存标记保存唯一到期责任。 @@ -178,7 +181,8 @@ C03 不派生新的公开 HTTP 接口。它依赖 M04 的 Ordering 内部应用 3. 取消操作必须复用 M04 的状态、原库存通道、限购释放和可靠事实完整结果。 4. 多实例正确性来自共享订单状态与唯一业务推进,不来自单机内存。 5. 任务执行记录与订单业务状态分离;失败次数、退避和告警不能变成新订单状态。 -6. 批量大小、扫描间隔、单轮重试次数属于可观测配置,应按环境验证后确定,不由流程预设固定数字。 +6. 扫描间隔、批量大小和单轮批数采用本流程冻结的配置键、默认值与上下限;租约、续租、逐笔退避和告警阈值属于正确性契约,不允许以环境吞吐调优为由改写。 +7. 逐笔领取租约固定 60 秒、每 20 秒续租;瞬态失败或过期租约按 5 秒、30 秒、2 分钟、10 分钟、30 分钟、之后每小时持续重试。第 5 次失败 Warning,第 20 次及以后每 24 小时聚合 Critical;不设置重试耗尽或 DeadLetter 终态。 ## 九、跨模块边界 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" index 8e46387..3a5d84b 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M04-\350\256\242\345\215\225\346\265\201\347\250\213.md" @@ -21,7 +21,7 @@ M04 拥有订单创建、买家订单查询、待支付订单取消、订单核 | M05 / C08 支付 | 已校准 | 只在截止时间前竞争待支付状态 | | M06-02 商家履约 | 已补齐、待交叉评审 | 使用指定商家、售后履约快照与订单状态公开能力 | | M10 售后 | 已重构 | 提供发货阻断和已退款数量 | -| 本文业务流程 | 已重构、待交叉评审 | 作为接口与后续数据设计输入 | +| 本文业务流程 | 已重构、待交叉评审 | 已派生订单接口、DB061~DB063 及跨模块组合摘要;后续实现不得反向改写业务语义 | ## 二、参与者、事实归属与模块出入口 @@ -91,24 +91,38 @@ stateDiagram-v2 买家提交的业务输入只有: -- 本次选中的本人购物车条目标识; -- 本人收货地址标识; +- 本次选中的 1~100 个本人购物车条目标识及 A208 对同一内容签发的 `checkoutRevision`; +- 本人收货地址标识及买家在结算页实际确认的地址版本; - 当前提交动作的必填唯一幂等标识。 客户端不得提交最终订单金额、订单项成交价、处理商家、支付截止时间、库存来源或可直接创建订单的商品清单。 +结算页先通过 A010 读取本人地址和每条 `version`。存在唯一默认地址时预选并突出展示,但提交前仍允许切换并要求确认;没有默认地址时保持未选择,不得自动取列表第一条。无地址或买家主动新增时,在同一结算上下文调用 A011;成功后保留当前条目与 Revision,刷新地址列表并让买家显式选择。新地址仍按 F03 固定为非默认,A301 提交时必须同时携带并锁内复核 `addressId + addressVersion`;地址创建成功绝不自动下单。 + ### 4.2 主流程 ```mermaid flowchart TD - A["状态正常的买家从购物车选择条目和收货地址"] --> B["生成本次提交的唯一幂等标识"] + A["状态正常的买家从购物车取得有效 checkoutRevision"] --> A1["A010 加载本人地址"] + A1 --> A2{"是否存在唯一默认地址?"} + A2 -- "是" --> A21["预选并突出默认地址
买家可切换且提交前确认"] + A21 --> A4 + A2 -- "否但有地址" --> A22["保持未选择
买家显式选择"] + A22 --> A4 + A2 -- "无地址或主动新增" --> A3["同一结算页复用 A011 新增非默认地址"] + A3 --> A4["保留 cartItemIds + checkoutRevision,刷新列表并显式选择"] + A4 --> B["生成本次提交的唯一幂等标识"] B --> C{"同一买家、同一幂等标识是否已有确定结果?"} C -- "同内容已成功" --> R["重放首次订单号、金额、状态和支付截止时间"] C -- "同内容已有确定拒绝" --> R2["重放首次业务拒绝
不因库存或配置后来变化改写本次结果"] C -- "换内容复用" --> X["拒绝标识复用,不创建新订单"] - C -- "没有结果" --> D["读取本人选中条目、当前数量、商品状态、实时价格和库存来源"] - D --> E["读取并校验本人有效地址,解析唯一启用的默认商家"] - E --> F{"权威事实是否成功读取并形成明确裁决?"} + C -- "没有结果" --> D["按统一顺序锁定默认商家门槛与本人地址
复核 addressVersion 并取得可信快照"] + D --> E{"地址是否仍存在、归属本人且版本一致?"} + E -- "否" --> AV["零建单;不存在/越权安全拒绝
版本变化要求刷新并重新确认"] + E -- "是" --> E1["锁定本人选中条目与 Catalog 商品/普通库存
重读数量、展示、实时价格和库存"] + E1 --> E2{"锁内内容是否仍匹配 checkoutRevision?"} + E2 -- "否" --> V["零建单并返回最新结构化预览
等待买家再次确认"] + E2 -- "是" --> F{"权威事实是否成功读取并形成明确裁决?"} F -- "依赖中断或结果不明" --> W["不固化幂等结果
使用同一标识查询或安全重试"] F -- "明确不可用" --> Y["固化确定业务拒绝
购物车、库存和订单均不变化"] F -- "是" --> G["按权威商品顺序重新校验可售、数量、库存与金额"] @@ -125,13 +139,16 @@ flowchart TD - 订单总额等于服务端实时成交单价乘数量后求和;结果必须为正,客户端金额只可用于显示,不能参与裁决。 - 订单项保存商品名称、主图、成交单价、数量和原库存来源快照;商品后续改名、改价、上下架不改变历史订单。 -- 地址快照在订单成功时形成;地址后续编辑或删除不改变历史订单。 +- 地址快照在订单成功时由本次锁定且版本匹配的 DB003 事实形成;地址后续编辑或删除不改变历史订单。确认后、提交前地址发生变化时不得静默采用新内容。 +- 无地址或主动新增时复用 A010/A011,不新增“结算地址”接口。存在唯一默认地址时仅预选并突出展示;没有默认地址时不自动选择第一条。A011 成功只刷新同一结算页地址集合并保留 `cartItemIds + checkoutRevision`,新地址固定非默认且必须显式选择;任何地址创建都不自动提交订单。 - 处理商家固定为提交时唯一启用的默认商家,保存为 `assignedMerchantUserId`。不存在、重复或不可用时整单失败,不能创建无人处理订单。 - 默认商家禁用与下单必须形成确定顺序:下单先被接受时禁用复核应发现新责任;禁用先生效时下单不能再分配给该账号。 - 普通购物车订单由 M04 协调 Catalog 库存扣减;秒杀入口由 C01 协调独立活动库存与限购,并在同一原子边界调用 M04 的统一订单创建能力。两条入口都由 M04 生成共享订单、指定商家、快照和固定支付截止时间,订单项库存来源不得混用。 - 普通库存扣减完整结果提交后触发 C07 失效目标详情和固定首页;秒杀抢购只改变活动独立库存,不触发 C07。缓存失效失败不回滚订单。 - 任一条目不可售、数量非法、库存不足、地址无效、默认商家不可用、购物车清理失败或可靠创建事实失败时,整单不成立。 -- 同一买家、同一幂等标识绑定地址与购物车条目指纹。同内容的确定成功或确定业务拒绝均稳定重放,换内容复用被拒绝;库存不足、商品不可售、总额不合法,以及成功读取配置后确认默认商家缺失、重复或禁用属于确定拒绝。 +- A208 Revision 绑定条目、购物车版本、数量、商品名称、主图 Key 与实时单价;A301 按相同规范锁内重算。条目取消选中,或数量、版本、展示快照、单价、销售状态、库存可结算性和正金额资格任一变化,都统一返回 `ORDER.CHECKOUT_CHANGED + latestPreview`,零建单、零扣库存;不再并列“库存不足/商品不可售/总额无效”三套重叠错误。买家确认可结算的新预览后使用新 Revision 和新幂等 Key;不可结算预览保持完整目标和逐项原因,不能静默下单可用子集。该机制不锁价、不预占库存,Revision 一致后仍执行最终库存条件扣减。 +- 同一买家、同一幂等标识的规范指纹固定绑定 `addressId + addressVersion + cartItemIds + checkoutRevision`。同内容的确定成功或确定业务拒绝均稳定重放,换内容复用被拒绝;地址版本冲突、库存/商品/总额变化,以及成功读取配置后确认默认商家缺失、重复或禁用属于确定拒绝。 +- 全局锁顺序固定为:DB104 订单幂等范围 → Identity 唯一默认商家 DB001 门行 → 目标 DB003 地址行 → 按 ID 排序的 DB041 购物车行 → 按商品 ID 排序的 DB022 Catalog 行 → DB061/DB062/DB063、库存流水、Outbox 与幂等结果。Identity 公开能力必须加入 Ordering 已开启的同一 `DbConnection + DbTransaction`,锁保持到订单事务提交;任何模块不得通过“先查后改”或跨模块直读绕开该顺序。 - 依赖中断、数据库连接失败、事务回滚结果未知或提交结果未知不固化为幂等结果;第一次结果未知时先查询原结果,再用同一标识安全重试,不能用相同动作再扣一次库存。 - 支付截止时间在订单创建时按当时可追踪配置固定;正式口径为创建后 30 分钟,演示参数只能缩短演示等待,不改变正式规则。 - 订单创建成功只通知当前买家;商家待处理提醒在支付成功后产生,避免未付款订单干扰履约。 @@ -145,9 +162,13 @@ flowchart TD A["已认证买家进入订单页面"] --> B{"查询列表或详情?"} B -- "列表" --> C["只按当前买家分页查询;可按五种核心状态筛选;创建时间倒序"] B -- "详情" --> D["按订单号与当前买家共同校验归属"] - C --> E["展示订单号、状态、总额、创建时间和最多三个商品摘要"] - D --> F["展示地址快照、全部订单项快照、金额、时间线、支付与售后摘要"] - E --> G["按最新状态派生操作入口"] + C --> H["取得本页订单及订单项权威快照"] + D --> H + H --> I["通过公开应用能力批量读取售后数量与评价事实"] + I --> J["组合核心状态、履约摘要、售后摘要和评价摘要"] + J --> E["列表展示订单号、总额、创建时间、最多三个商品摘要及三个派生摘要"] + J --> F["详情展示地址与订单项快照、金额、时间线、支付结果、三个摘要及订单项入口"] + E --> G["只按最新完整事实派生整单操作入口"] F --> G D -. "订单不存在或不属于本人" .-> X["统一拒绝,不泄露他人订单是否存在"] ``` @@ -168,17 +189,98 @@ flowchart TD - 详情使用订单创建时的地址、商品、成交价和数量快照;图片失效时展示占位,不修改快照事实。 - 地址联系电话按展示场景脱敏;订单归属、金额、状态和时间不能由前端覆盖。 - 已支付订单展示真实成功支付来源:默认小金库支付或 C08 受控模拟通道;未支付订单不伪造支付记录。 +- `PendingPayment`、`Paid`、`Shipped`、`Completed`、`Cancelled` 仍是唯一订单核心状态。履约、售后和评价均为查询时派生的正交摘要,不写回订单状态,不新增订单状态,也不接受客户端回传后作为写入依据。 -### 5.2 操作入口矩阵 +#### 5.1.1 履约摘要 + +订单级履约摘要固定包含 `state`、`remainingFulfillableQuantity` 和 `canShip`。`state` 只允许: -| 当前事实 | 买家入口 | +| 派生状态 | 固定含义 | |---|---| -| `PendingPayment` 且权威时间早于支付截止时间 | 去支付、取消订单 | -| `PendingPayment` 且已到支付截止时间 | 不展示可支付入口;展示过期取消处理中或最新取消结果 | -| `Paid` | 查看支付结果;按 M10 规则展示未发货仅退款入口 | -| `Shipped` | 确认收货;按 M10 规则展示售后入口 | -| `Completed` | 未评价订单项展示评价入口;完成后 7 天内按 M10 展示售后入口 | -| `Cancelled` | 无支付、发货、完成或售后入口 | +| `AwaitingPayment` | 订单仍待支付 | +| `ReadyToShip` | 已支付、无处理中售后、未退款且存在可履约数量 | +| `BlockedByAfterSales` | 已支付但存在任一非终态售后,当前不得发货 | +| `PartiallyRefundedReadyToShip` | 已支付、无处理中售后,部分数量已退款且仍有剩余可履约数量 | +| `FullyRefunded` | 已支付但全部购买数量已经退款,不得发货 | +| `Shipped` | 订单已经形成唯一发货事实 | +| `Completed` | 订单已经形成唯一完成事实 | +| `Cancelled` | 订单已经取消 | + +派生顺序固定如下,禁止前端自行调整优先级: + +1. 核心状态为 `PendingPayment`、`Cancelled`、`Shipped`、`Completed` 时,分别直接派生 `AwaitingPayment`、`Cancelled`、`Shipped`、`Completed`。 +2. 只有核心状态为 `Paid` 时继续读取 M10 摘要: + - `processingAfterSalesQuantity > 0` 时为 `BlockedByAfterSales`; + - 否则 `remainingFulfillableQuantity = 0` 时为 `FullyRefunded`; + - 否则 `refundedQuantity > 0` 时为 `PartiallyRefundedReadyToShip`; + - 否则为 `ReadyToShip`。 + +订单级 `remainingFulfillableQuantity` 表示“当前仍允许商家在本期一次性发出的数量”,不是“历史上尚未退款的数量”。因此只在核心状态为 `Paid` 时按全部订单项求和: + +```text +if order.status == Paid: + remainingFulfillableQuantity + = Σ(orderItem.quantity) + - Σ(已进入 Refunded 的售后数量) +else: + remainingFulfillableQuantity = 0 +``` + +`PendingPayment/Cancelled` 尚无可发货数量,`Shipped/Completed` 已经完成唯一一次发货,四态一律返回 0;已发出的真实数量只读订单项 `shippedQuantity`,不能用“购买量减退款量”倒推。`canShip=true` 当且仅当核心状态为 `Paid`、处理中售后数量为零、剩余可履约数量大于零,并且本次售后摘要完整可用。它只是页面展示快照;A307 提交发货时仍按最新事实重新锁定和校验。 + +#### 5.1.2 售后摘要 + +订单级售后摘要固定包含 `state`、`processingQuantity` 和 `refundedQuantity`。`state` 只允许 `NotApplicable`、`None`、`Processing`、`PartiallyRefunded`、`FullyRefunded`,并按以下优先级派生: + +1. `PendingPayment` 或 `Cancelled` 为 `NotApplicable`。 +2. 任一订单项存在非终态售后数量时为 `Processing`;即使此前已经部分退款,也仍优先显示处理中,同时保留真实 `refundedQuantity`。 +3. 无处理中售后且已退款数量等于订单购买总数量时为 `FullyRefunded`。 +4. 无处理中售后且已退款数量大于零时为 `PartiallyRefunded`。 +5. 其余为 `None`。 + +买家详情还必须按订单项返回 M10 计算的处理中数量、已退款数量、剩余可申请数量、当前是否可申请、可申请类型、截止时间和不可申请原因。订单项剩余可申请数量固定为: + +```text +remainingEligibleQuantity += orderItem.quantity +- processingAfterSalesQuantity +- refundedQuantity +``` + +不可申请原因至少区分订单状态不允许、售后窗口已过和无剩余数量;页面不得把“摘要不可用”解释为“没有售后”。 + +#### 5.1.3 评价摘要 + +订单级评价摘要固定包含 `state`、`reviewedItemCount` 和 `reviewableItemCount`。`state` 只允许 `NotApplicable`、`Reviewable`、`PartiallyReviewed`、`Reviewed`: + +- 非 `Completed` 订单为 `NotApplicable`,`reviewableItemCount=0`。 +- `Completed` 订单按订单项行数计算,不按购买件数计算。`reviewedItemCount` 是已存在唯一评价事实的订单项行数,`reviewableItemCount = orderItemCount - reviewedItemCount`。 +- 已评价数为零时为 `Reviewable`;介于零和订单项行数之间时为 `PartiallyReviewed`;等于订单项行数时为 `Reviewed`。 + +订单详情的每个订单项只派生 `NotApplicable`、`Reviewable` 或 `Reviewed`。当前需求没有“发生售后后自动失去评价资格”的规则,因此不得自行添加;是否允许提交仍由 X01 的 `Completed`、本人归属和唯一评价规则在 A142 写入时复核。 + +#### 5.1.4 批量组合、完整性与降级 + +- Ordering 先取得当前页最多 50 张订单及其订单项权威快照,再分别通过 AfterSales、Review 的公开批量应用能力组合摘要;不得直接读取 DB086、DB024 等其他模块内部表,也不得按订单或订单项循环调用 HTTP。 +- AfterSales 批量能力接收订单及订单项权威快照和 `Aggregate` / `ItemDetail` 投影视图,一次返回所有请求订单、订单项的数量、资格、类型、截止时间与统一 `evaluatedAt`;Review 批量能力接收当前买家和订单项集合,一次返回每个订单项是否已有唯一评价事实。 +- 每个被请求的订单和订单项都必须有对应结果;少返回一个 Key、处理中数量与已退款数量之和超过购买数量、`Completed` 订单没有订单项等均属于契约或数据不变量破坏,不能截断、归零或假装正常。 +- 模块化单体共用 PostgreSQL 时,Ordering 必须在读取任何组合事实前开启一个短生命周期 `REPEATABLE READ READ ONLY` 事务,并把同一 `DbConnection + DbTransaction` 交给 AfterSales、Review 的公开批量应用能力;三方读取因此属于同一个 PostgreSQL 快照。默认 `READ COMMITTED` 的逐语句快照不能声称满足该规则。共享事务不授权 Ordering 直接取得其他模块的 DbSet 或仓储;页面快照之后仍可能变化,A307、A412、A142 必须在写入时重检。 +- 订单、订单项、归属或已支付订单的必要支付事实读取失败时,整个查询失败,不返回半张订单。 +- AfterSales 或 Review 扩展摘要失败时,可以返回订单核心事实,但必须标记组合结果为 `Degraded`、对应摘要为未知并明确列出不可用维度;禁止用零、`None`、`Reviewable`、`ReadyToShip` 代替未知。 +- AfterSales 摘要不可用时,所有售后入口隐藏,`Paid` 订单的 `canShip` 不得为真;Review 摘要不可用时,不显示“未评价”或评价入口。降级只收紧页面动作,绝不放宽写入权限。 + +### 5.2 操作入口矩阵 + +整单入口与订单项入口必须分开,不能把缺少目标 `orderItemId` 的售后、评价动作放在订单级 `availableActions` 中: + +| 当前事实 | 整单入口 | 订单项入口 | +|---|---|---| +| `PendingPayment` 且权威时间早于支付截止时间 | 去支付、取消订单 | 无 | +| `PendingPayment` 且已到支付截止时间 | 不展示可支付入口;展示过期取消处理中或最新取消结果 | 无 | +| `Paid` | 查看支付结果 | 仅对 M10 判定可申请的订单项展示未发货仅退款 | +| `Shipped` | 确认收货 | 仅对 M10 判定可申请的订单项展示售后 | +| `Completed` | 无整单评价动作 | 未评价订单项展示评价入口;完成后 7 天内按 M10 展示售后入口 | +| `Cancelled` | 无支付、发货、完成或售后入口 | 无 | 页面入口只是提示。实际动作仍必须重新校验最新状态、截止时间、归属和模块规则。 @@ -203,8 +305,9 @@ flowchart TD C -- "合法买家或系统内部触发" --> D{"当前订单状态?"} D -- "Cancelled" --> R["重放首次取消时间、原因和结果"] D -- "Paid / Shipped / Completed" --> Y["拒绝取消并返回当前最终状态"] - D -- "PendingPayment" --> E["按权威时间确定 BuyerRequested 或 PaymentExpired"] - E --> F["与支付竞争唯一状态结果"] + D -- "PendingPayment" --> E["锁定订单;锁后取得唯一 decisionTime 并重查状态、归属与 paymentDeadline"] + E --> E1["decisionTime < paymentDeadline 为 BuyerRequested;否则为 PaymentExpired"] + E1 --> F["以 PendingPayment 条件与支付竞争唯一状态结果"] F --> G{"取消是否唯一胜出?"} G -- "否" --> H["读取并返回最新 Paid 或 Cancelled 结果"] G -- "是" --> I["按每个订单项原通道恢复库存;秒杀同时释放对应限购数量"] @@ -217,6 +320,7 @@ flowchart TD ### 6.3 取消与回补规则 - 只有 `PendingPayment` 可以首次取消。`Cancelled` 重放幂等成功;`Paid`、`Shipped`、`Completed` 明确拒绝。 +- 读取页面、候选扫描和事务开始时取得的时间都不具有最终裁决力。取消事务锁定订单后只调用一次数据库权威时间形成 `decisionTime`,并将它同时用于截止判断、取消原因、`cancelledAt` 和同事务可靠事实;禁止使用锁等待前时间或 PostgreSQL 事务开始时的旧时间。 - 状态、取消时间与原因、普通 / 秒杀库存、秒杀限购数量和可靠通知事实必须同时成功或同时失败。 - 普通订单回补 Catalog;秒杀订单回补原活动独立库存并释放该买家本次订单占用的限购数量,不得增加普通库存。 - 普通库存回补完整结果提交后触发 C07 失效目标详情和固定首页;秒杀原活动回补不触发 C07。 @@ -231,7 +335,7 @@ flowchart TD ```mermaid flowchart TD A["M05 或 C08 请求确认支付"] --> B["M04 提供应付金额、当前状态和固定支付截止时间"] - B --> C{"仍为 PendingPayment 且权威时间早于截止时间?"} + B --> C{"Payment 取得全部可能阻塞事实后的 finalTime
仍早于截止时间且订单仍为 PendingPayment?"} C -- "否且已到期" --> X["拒绝支付并触发统一过期取消"] C -- "否且已有终态" --> Y["返回当前 Paid 或 Cancelled 结果"] C -- "是" --> D["支付与主动取消竞争待支付状态"] @@ -245,8 +349,9 @@ flowchart TD - M04 不检查钱包余额、不扣款、不创建支付流水,也不决定回调签名;这些属于 M05 / C08。 - M04 只提供订单号、买家、应付金额、当前状态和支付截止时间,并接受一个已由 Payment 确认的成功来源。 -- 权威时间 `< paymentDeadline` 时支付可竞争;`>= paymentDeadline` 时任何支付通道都必须拒绝。 +- Payment 必须在取得该支付路径全部可能阻塞的共享事实后才形成唯一 `finalTime`;`finalTime < paymentDeadline` 时支付可竞争,`>= paymentDeadline` 时任何支付通道都必须拒绝。页面预览时间、请求到达时间、订单锁时间和回调发生时间均不能授权支付。 - 支付成功、订单 `Paid`、支付时间、支付来源和可靠支付事实必须形成一致结果。失败时不得留下订单已支付但没有成功支付来源。 +- 支付路径的 `finalTime` 与 M04 过期取消锁定订单后取得的 `cancelDecisionTime` 属于两个独立事务,各自只裁决本事务;支付到期拒绝不得用旧时间替代后续取消重检,取消也不得回写支付结果时间。 - 同步钱包与受控模拟回调最多一个成为成功来源;另一方读取 `Paid` 后不得重复扣款或入账。 ## 八、商家履约交接 @@ -339,10 +444,10 @@ flowchart TD - **M02 / M03**:M04 读取选中购物车条目和实时商品事实;订单成功才清理选中条目,失败保持购物车。普通库存只由 Catalog 扣减和回补。 - **C07**:普通订单扣减和取消回补提交后失效商品详情与固定首页;秒杀活动库存扣减和原活动回补不触发。缓存失败不改变订单原子结果。 - **C01**:秒杀绕过购物车,但不建立第二套订单创建器。C01 负责活动、独立库存和限购,并在同一原子边界调用 M04 生成共享订单、指定商家、快照和固定支付截止时间;取消只回补活动独立库存。 -- **M05 / C08 / C03**:三者共同遵守支付截止时间。到期后支付无条件拒绝,统一过期取消可由支付请求或 Worker 触发。 -- **M06-02 / M10**:订单指定商家是唯一履约范围;发货前读取售后阻断与已退款数量并串行复核。 +- **M05 / C08 / C03**:三者共同遵守支付截止时间。M05/C08 只以各自取得最后一个可能阻塞共享事实后的 `finalTime` 判定支付;到期后支付无条件拒绝。统一过期取消由支付请求或 Worker 触发,并在独立取消事务中重新形成 `cancelDecisionTime`。 +- **M06-02 / M10**:订单指定商家是唯一履约范围;列表和详情通过 AfterSales 批量公开能力组合售后与履约摘要,发货前再读取售后阻断与已退款数量并串行复核。 - **M09**:订单创建、取消、支付、发货、完成只通知流程明确的买家或指定商家;消息不作为订单状态来源。 -- **X01**:评价入口只在完成订单项出现;Review 提交时重新校验订单项属于当前买家、已完成且未评价。 +- **X01**:订单列表和详情通过 Review 批量公开能力读取订单项是否已有评价事实;评价入口只在完成且未评价的订单项出现,Review 提交时重新校验订单项属于当前买家、已完成且未评价。 ## 十二、由流程派生的接口契约映射 @@ -350,26 +455,30 @@ flowchart TD | 已确认业务能力 | 派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 购物车提交订单 | A301 | 必填幂等标识、选中购物车条目标识、本人地址;服务端金额 / 商家 / 截止时间;确定成功与确定拒绝重放,瞬态失败不固化;整单原子结果 | 接口完整定义,待数据库、公开应用签名、OpenAPI 与交叉评审 | -| 买家订单列表 | A302 | 本人隔离、五状态筛选、稳定分页、最新摘要 | 接口完整定义,待数据库、公开应用签名、OpenAPI 与交叉评审 | -| 买家订单详情 | A303 | 全部快照、截止时间、状态时间线、真实支付来源、售后与操作入口事实 | 接口完整定义,待数据库、公开应用签名、OpenAPI 与交叉评审 | -| 买家取消订单 | A304 | 本人授权、首次取消或幂等重放、按时间确定取消原因、原库存通道完整回补 | 接口完整定义,待数据库、公开应用签名、OpenAPI 与交叉评审 | -| 买家确认收货 | A308 | 本人 `Shipped → Completed`、二次确认、主动 / 自动完成竞争的当前结果 | 接口完整定义,待数据库、公开应用签名、OpenAPI 与交叉评审 | +| 购物车提交订单 | A301 | 必填幂等标识、选中购物车条目标识、本人 `addressId + addressVersion`;服务端金额 / 商家 / 截止时间;地址或结算内容变化时零建单并重新确认;确定结果重放,瞬态失败不固化;整单原子结果 | 接口完整定义,数据库设计已确认,待公开应用签名、OpenAPI、实现与交叉评审 | +| 买家订单列表 | A302 | 本人隔离、五状态筛选、稳定分页;履约、售后、评价三个派生摘要;整单动作与降级状态 | 接口完整定义,待公开应用签名、OpenAPI、实现与交叉评审 | +| 买家订单详情 | A303 | 全部快照、截止时间、状态时间线、真实支付来源、三个订单级摘要、订单项售后/评价事实与分粒度入口 | 接口完整定义,待公开应用签名、OpenAPI、实现与交叉评审 | +| 买家取消订单 | A304 | 本人授权、首次取消或幂等重放、按时间确定取消原因、原库存通道完整回补 | 接口完整定义,数据库设计已确认,待公开应用签名、OpenAPI、实现与交叉评审 | +| 买家确认收货 | A308 | 本人 `Shipped → Completed`、二次确认、主动 / 自动完成竞争的当前结果 | 接口完整定义,数据库设计已确认,待公开应用签名、OpenAPI、实现与交叉评审 | | 支付状态推进 | Payment 内部应用契约 | 应付金额、截止时间和 `PendingPayment → Paid` 唯一竞争 | 已按流程登记跨模块边界;待公开应用签名、数据库与测试承接 | | 过期取消 | Ordering 内部应用契约 | 系统身份、到期复核、统一取消与幂等回补 | 已按流程登记跨模块边界;待公开应用签名、数据库与测试承接 | | 自动完成 | Ordering 内部应用契约 | 到期复核、`Shipped → Completed` 与主动确认竞争 | 已按流程登记跨模块边界;待公开应用签名、数据库与测试承接 | +| 订单售后摘要 | AfterSales 批量内部应用契约 | 当前页订单与订单项全量返回、统一评估时间、聚合/明细投影、资格与数量事实;缺 Key 视为失败 | 已由流程完整定义,待公开应用签名、实现与测试承接 | +| 订单评价事实 | Review 批量内部应用契约 | 当前买家订单项集合的唯一评价存在性;整批返回且禁止 N+1 | 已由流程完整定义,待公开应用签名、实现与测试承接 | A305~A307 由 M06-02 商家履约流程派生,本文件不以旧商家接口反向定义商家页面。 接口阶段必须满足: -1. A301 的幂等标识为必填;请求只传选中购物车条目标识和地址标识,不传最终数量清单、价格、总额或商家。 +1. A301 的幂等标识为必填;请求只传选中购物车条目标识、地址标识与确认时地址版本,不传最终数量清单、价格、总额或商家。 2. A301 同内容重放首次确定成功或确定业务拒绝,换内容复用拒绝;依赖中断、连接失败和事务结果未知不得固化,可用同一标识安全重试。 3. A304 对已 `Cancelled` 的本人订单重放首次成功,对 `Paid` / `Shipped` / `Completed` 明确拒绝;调用时达到截止时间则使用过期原因。 4. A303 在过期但尚未取消成功时必须表达“不可支付、取消待重试”,不能仅凭 `PendingPayment` 展示支付按钮。 5. Payment 内部契约同时校验状态和截止时间;成功来源可为小金库或受控模拟通道。 6. A308 返回唯一完成时间和方式;已完成重试不重复通知。 7. 所有资源归属都由服务端当前身份判断,错误响应不泄露他人订单存在性。 +8. A302/A303 的核心状态筛选仍只使用五种订单状态,不因派生摘要新增筛选或核心状态。 +9. A302/A303 必须区分完整组合与摘要降级;AfterSales 未知时不开放售后,Review 未知时不开放评价,订单项动作必须携带明确目标。 ## 十三、验收证据清单 @@ -378,7 +487,10 @@ A305~A307 由 M06-02 商家履约流程派生,本文件不以旧商家接口 - [ ] 任一商品不可售、库存不足、地址无效、默认商家不可用或可靠事实失败时整单不成立,库存和购物车不留部分变化。 - [ ] 同幂等标识同内容重放首次确定成功或确定业务拒绝,换内容拒绝;瞬态失败不固化,未知结果先查询原订单再用同一标识重试,不重复扣库存。 - [ ] 默认商家分配与账号禁用并发时只有一个合法结果,不产生无人处理订单。 -- [ ] 买家列表和详情严格隔离,五种状态筛选、快照、金额、时间线和真实支付来源正确。 +- [ ] 买家列表和详情严格隔离,五种状态筛选、快照、金额、时间线和真实支付来源正确;履约、售后、评价摘要按固定优先级和数量公式派生。 +- [ ] 同时覆盖正常待发货、售后处理中、部分退款待发剩余数量、全部退款、部分评价和全部评价;不新增订单核心状态。 +- [ ] 列表页最多 50 张订单时 AfterSales 与 Review 各使用一次批量公开应用能力,完整返回所有请求 Key,不出现逐单/逐项 N+1。 +- [ ] 任一扩展摘要失败、缺 Key 或数量不变量破坏时只返回明确降级结果,不伪造 `None`、`Reviewable` 或 `ReadyToShip`,也不开放对应写动作。 - [ ] 截止时间前可支付;达到截止时间后即使 C03 未运行也不能支付。 - [ ] 买家取消、M05 过期触发和 C03 Worker 复用同一取消能力;重复取消不重复回补或通知。 - [ ] 普通和秒杀订单均按订单项原库存来源回补;活动结束或取消不阻止历史取消回补,也不重新开放抢购。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M06-02-\345\225\206\345\256\266\345\261\245\347\272\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M06-02-\345\225\206\345\256\266\345\261\245\347\272\246\346\265\201\347\250\213.md" index a7ef39c..40ad8a7 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M06-02-\345\225\206\345\256\266\345\261\245\347\272\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/wqq/M06-02-\345\225\206\345\256\266\345\261\245\347\272\246\346\265\201\347\250\213.md" @@ -60,8 +60,9 @@ flowchart TD B --> C["按可选核心状态筛选,按创建时间倒序稳定分页"] C --> D{"本页有订单?"} D -- "否" --> E["展示正常空状态和清除筛选入口"] - D -- "是" --> F["展示订单号、买家最小摘要、总额、状态、创建 / 支付 / 发货 / 完成 / 取消相关时间"] - F --> G["Paid 且可能可履约的订单显示查看详情入口;是否可发货由详情重新判断"] + D -- "是" --> H["通过 M10 批量公开能力读取本页订单售后数量"] + H --> F["展示订单号、买家最小摘要、总额、核心状态、履约摘要、售后摘要和相关时间"] + F --> G["Paid 且履约摘要完整的订单显示当前可发货性;正式发货仍由详情和 A307 重检"] ``` 列表规则: @@ -70,7 +71,10 @@ flowchart TD - 结果按创建时间倒序,并使用稳定次序保证翻页不重复、不跳项。 - 列表只返回买家必要摘要,不返回完整地址、手机号、钱包、支付流水、售后原因或无关资料。 - `Paid` 只表示可以进入履约复核,不保证一定可发货;售后申请和退款可能在打开详情前发生。 +- 每个列表项同时返回 M04 统一定义的履约摘要和售后摘要;不得为商家页面另建一套状态或数量公式。 +- 当前页最多 50 张订单,只调用一次 AfterSales 批量公开应用能力;不得逐订单调用 HTTP,也不得直接读取 AfterSales 内部表。 - 空状态、加载、分页、筛选非法和查询失败都有明确页面反馈,不把失败伪装为空集合。 +- Ordering 核心查询失败时整个接口失败;AfterSales 摘要失败、缺少请求 Key 或数量不变量破坏时,核心订单可返回,但组合状态必须标记为降级,履约/售后摘要置为未知,并且不得显示“可发货”。 - 商家账号禁用后所有查询和主动发货均被拒绝;M06-03 必须在存在待处理订单或售后时阻止正常禁用。 ## 四、履约详情与服务端发货数量 @@ -81,16 +85,17 @@ flowchart TD flowchart TD A["商家打开授权订单"] --> B["M04 校验订单仍分配给当前商家"] B --> C["读取订单项、地址快照、金额、状态和时间线"] - C --> D["读取 M10 当前履约快照"] - D --> E["按订单项展示购买数量、处理中售后数量、已退款数量、剩余可履约数量"] - E --> F["按最新状态和阻断原因派生发货入口"] + C --> D["通过 M10 ItemDetail 批量能力读取当前履约快照"] + D --> E["展示订单级履约/售后摘要及每项购买、处理中、已退款、剩余可履约和历史已发数量"] + E --> F["只有完整摘要明确 canShip 时展示发货入口"] ``` 详情只展示履约所需信息: - 订单号、核心状态、金额和已提交支付摘要; - 商品名称、图片、成交单价、购买数量; -- 当前售后阻断摘要、已退款数量和剩余可履约数量; +- M04 统一定义的订单级履约摘要和售后摘要; +- 每项处理中售后数量、已退款数量、剩余可履约数量和历史实际已发数量; - 下单时地址快照,包括发货所需收件人、联系电话和完整配送地址;联系电话只在当前授权订单详情展示,不进入列表或其他订单响应; - 创建、支付、发货、完成或取消时间线; - 已发货时的实际发货数量和必要说明。 @@ -117,6 +122,17 @@ flowchart TD - 非终态申请结束为 `Refunded` 后,已退款数量永久从可履约数量中扣除。 - 部分退款完成后可以一次发出所有剩余可履约数量;全部数量已退款时不允许发货。 - 商家或客户端不能提交一套自选发货数量覆盖服务端结果。本期不做拆单、分批发货或部分收货。 +- `shippedQuantity` 是唯一发货成功时冻结的历史事实;发货后的退款不得递减、覆盖或重算该字段。退款影响售后摘要,不伪造“当初未发货”。 +- 订单级履约摘要、售后摘要、`remainingFulfillableQuantity` 和 `canShip` 全部复用 M04 第 5.1 节固定公式。它们只用于读取与页面提示,不持久化为第二份状态。 + +### 4.3 详情组合完整性与降级 + +- A306 的 AfterSales `ItemDetail` 批量结果必须覆盖当前订单的每个订单项,并携带统一 `evaluatedAt`;少返回任何订单项都按整个售后摘要失败处理,不能按零笔售后补齐。 +- 订单、订单项、归属、地址快照或已支付事实读取失败时,详情整体失败,不返回半张订单。 +- AfterSales 读取失败、缺 Key,或出现 `processingAfterSalesQuantity + refundedQuantity > quantity`、负数等不变量破坏时,可以返回核心订单详情,但必须明确标记 `Degraded`,订单级履约/售后摘要置为未知,所有订单项售后数量也不得伪造。 +- 摘要未知时不返回发货动作,页面显示“履约摘要暂不可用,请刷新”;不得显示“无售后”“可发货”或把未知数量归零。 +- 模块化单体共用 PostgreSQL 时,Ordering 与 AfterSales 公开应用能力加入同一个短生命周期只读事务快照;共享事务不允许 M06-02 直接读取其他模块 DbSet 或仓储。 +- 页面快照之后可能变化;无论详情是否完整,A307 都必须重新锁定订单并读取最新售后事实。读取降级绝不成为绕过写入校验的理由。 ## 五、发货主流程 @@ -192,7 +208,7 @@ flowchart TD - **M04 Ordering**:拥有 `assignedMerchantUserId`、核心状态和 `Paid → Shipped`。M06-02 不直接写订单内部数据。 - **M05 / C08 Payment**:只有已经提交的 `Paid` 订单可履约;发货不检查或修改买家钱包。 -- **M10 AfterSales**:提供非终态阻断、已退款数量和剩余可履约数量;发货与新申请串行复核。 +- **M10 AfterSales**:通过批量公开能力为列表提供 `Aggregate`、为详情提供 `ItemDetail`,完整返回非终态阻断、已退款数量、剩余可履约数量和统一评估时间;发货与新申请再串行复核。 - **M09 Messaging**:发货完整结果提交后通知订单买家;通知失败独立重试。 - **M06-03 Identity**:默认商家始终拒绝禁用。非默认商家只要仍有 `PendingPayment`、仍有可履约数量的 `Paid`、`Shipped`、完成后 7 天售后窗口内订单、非终态售后或未结束秒杀活动,均仍负有业务责任并拒绝禁用;已全量退款且无非终态售后的 `Paid` 不得被状态名永久阻断。禁用后不能主动查询或发货。 - **M04-04**:发货后由买家确认或满 7 天自动完成;M06-02 不代替买家确认、不自行推进 `Completed`。 @@ -201,10 +217,10 @@ flowchart TD | 已确认业务能力 | 派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 商家订单列表 | A305 | 当前 `assignedMerchantUserId` 强制范围、五状态筛选、稳定分页和最小买家摘要 | 接口完整定义,待数据库、公开应用签名、OpenAPI 与交叉评审 | -| 商家订单详情 | A306 | 授权快照、最小敏感信息、售后阻断、已退款和剩余可履约数量 | 接口完整定义,待数据库、公开应用签名、OpenAPI 与交叉评审 | -| 商家发货 | A307 | 必填稳定请求身份、服务端实际发货数量、售后竞争、`Paid → Shipped` 和幂等重放 | 接口完整定义,待数据库、公开应用签名、OpenAPI 与交叉评审 | -| 售后履约快照 | AfterSales 内部应用契约 | 非终态申请、已退款数量、剩余可履约数量和同订单串行复核 | 已按流程登记跨模块边界;待公开应用签名、数据库与测试承接 | +| 商家订单列表 | A305 | 当前 `assignedMerchantUserId` 强制范围、五状态筛选、稳定分页、最小买家摘要、履约/售后摘要及降级结果 | 接口完整定义,待公开应用签名、OpenAPI、实现与交叉评审 | +| 商家订单详情 | A306 | 授权快照、最小敏感信息、订单级履约/售后摘要、每项处理中/已退款/剩余可履约/历史已发数量及降级结果 | 接口完整定义,待公开应用签名、OpenAPI、实现与交叉评审 | +| 商家发货 | A307 | 必填稳定请求身份、服务端实际发货数量、售后竞争、`Paid → Shipped` 和幂等重放 | 接口完整定义,数据库设计已确认,待公开应用签名、OpenAPI、实现与交叉评审 | +| 售后履约快照 | AfterSales 批量内部应用契约 | `Aggregate` / `ItemDetail` 投影、请求 Key 全量返回、统一评估时间、非终态申请、已退款数量、剩余可履约数量和同订单串行复核 | 已由流程完整定义,待公开应用签名、实现与测试承接 | | 发货状态推进 | Ordering 内部应用契约 | 指定商家、`Paid → Shipped`、时间 / 数量 / 说明和可靠通知事实 | 已按流程登记跨模块边界;待公开应用签名、数据库与测试承接 | 接口阶段必须满足: @@ -215,12 +231,16 @@ flowchart TD 4. 已 `Shipped` 的同一动作返回首次结果;非同一内容不能修改已经提交的发货事实。 5. A306 的手机号和地址只为当前授权订单履约展示,列表不返回完整敏感字段。 6. HTTP 状态与错误码必须区分未认证、角色不符、资源不在范围、非法状态、售后阻断、全部退款和并发结果,但不得泄露其他商家的订单。 +7. A305/A306 的核心状态筛选仍只有五种订单状态;履约与售后摘要是派生读取结果,不新增筛选、状态或可写字段。 +8. A305/A306 必须表达完整或降级组合;AfterSales 摘要未知时不得返回 `canShip=true` 或发货动作。 ## 十、验收证据清单 - [ ] 商家列表和详情只出现 `assignedMerchantUserId` 为当前账号的订单,其他商家、买家、管理员和游客无法访问。 -- [ ] 列表支持五种核心状态筛选、稳定分页、创建时间倒序、真实空状态和失败反馈。 -- [ ] 详情只展示履约所需快照和最小买家信息,包含非终态售后、已退款和剩余可履约数量。 +- [ ] 列表支持五种核心状态筛选、稳定分页、创建时间倒序、真实空状态和失败反馈,并按 M04 统一公式展示履约/售后摘要。 +- [ ] 详情只展示履约所需快照和最小买家信息,包含订单级摘要及每项非终态售后、已退款、剩余可履约和历史已发数量。 +- [ ] 当前页最多 50 张订单时只调用一次 AfterSales 批量公开能力,详情一次返回全部订单项;缺 Key 不按零售后处理。 +- [ ] AfterSales 不可用或数量不变量破坏时返回明确降级结果,不显示可发货、不开放发货动作;核心订单查询失败则整体失败。 - [ ] 只有授权 `Paid` 订单可首次发货;其他状态明确拒绝,已 `Shipped` 的相同动作重放已有结果。 - [ ] 发货数量完全由服务端按购买数量减已退款数量计算,客户端不能指定。 - [ ] 任一非终态售后阻断整单发货;`Rejected`、`Cancelled` 不阻断。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" index 5016bf0..a36ed69 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/C01-\347\247\222\346\235\200\346\265\201\347\250\213.md" @@ -19,8 +19,8 @@ | 本文业务流程 | 已校准、待交叉评审 | 明确状态、动作、原子结果、异常与模块边界 | | M04/M05/M06-02/M09/M10 核心流程 | 完整定义,已完成统稿校准 | 复用其公开业务出入口,不建立第二套订单链路 | | C03/C07/C08/C10 挑战流程 | 完整定义,相交边界已冻结 | 只承接与秒杀直接相交的责任 | -| A220~A228 接口 | 已按本文重建、未冻结 | 待数据库、OpenAPI、公开应用签名与交叉评审 | -| 秒杀相关数据设计 | 模板/占位 | 待全部流程完成后从零统一设计 | +| A220~A228 接口 | 已按本文重建、未冻结 | 数据映射已承接,待 OpenAPI、公开应用签名、实现、测试与交叉评审 | +| 秒杀相关数据设计 | 完整定义,已确认 | DB042~DB044、订单来源、幂等和可靠事件已从本文派生;实现不得反向改写流程 | | X04 售后退款 | 独立扩展 | 不并入“待支付订单取消回补”流程 | ## 二、参与者与模块直接出入口 @@ -29,7 +29,7 @@ flowchart LR MERCHANT["已认证且账号正常的商家"] -->|"从统一经营目录选择商品,维护本人创建的秒杀活动"| SEC["C01 秒杀
活动生命周期、独立库存、个人限购"] PUBLIC["游客 / 买家"] -->|"浏览即将开始或进行中的活动"| SEC - BUYER["已认证且账号正常的买家"] -->|"立即抢购:活动、数量、收货地址、稳定请求标识"| SEC + BUYER["已认证且账号正常的买家"] -->|"立即抢购:活动、数量、addressId + addressVersion、稳定请求标识"| SEC CAT["M02 Catalog
统一经营目录、销售状态、普通可售库存与当前价格"] -->|"创建、发布前校验与库存划拨输入"| SEC SEC -->|"权威活动状态、倒计时、剩余库存、已售数量"| PUBLIC @@ -105,8 +105,9 @@ stateDiagram-v2 ``` - 业务状态统一为 `Draft`(草稿)、`Published`(已发布)、`Ongoing`(进行中)、`Ended`(已结束)、`Cancelled`(已取消);“已售罄”仅由剩余库存为 0 派生,不是第六种活动状态。 -- 状态推进以数据库权威 UTC 时间为准。`Published` 在开始时间到达后转为 `Ongoing`,`Ongoing` 在结束时间到达后转为 `Ended`;抢购时仍必须再次同时校验状态与时间窗口。 -- `Draft`、`Published`、`Ongoing` 可以取消;`Ended` 或 `Cancelled` 再次取消必须拒绝。取消后不再接受新抢购,已有订单继续走订单状态机。 +- 状态推进以数据库权威 UTC 时间为准。Worker 负责把常规 `Published → Ongoing → Ended` 持久化;GET 列表和详情只用一次权威 `serverTime` 计算 `effectiveStatus`,不得为了追赶生命周期而写库。计算规则固定为:`Draft` / `Cancelled` 保持存储状态;已划拨且 `serverTime >= endAt` 为 `Ended`;否则已划拨且 `serverTime >= startAt` 为 `Ongoing`;其余为 `Published`。公开 `resultVersion` 不是直接返回存储列,而按 `storedResultVersion * 8 + effectivePhaseCode` 形成单调有效快照版本,其中 `Draft/Published/Ongoing/Ended/Cancelled` 的阶段码固定为 `0/1/2/3/4`;因此即使 Worker 尚未持久化时间阶段,旧阶段响应也不能覆盖新阶段响应,后续任一真实写入递增存储版本后仍严格更大。 +- 发布、取消、抢购等写命令取得活动锁后调用一次数据库 `clock_timestamp()` 形成 `decisionTime`,按上述规则把非终态持久状态追赶到当前应有状态,再用同一时间完成本次裁决。请求到达时间、应用时间、事务开始时的旧时间和 Worker 扫描时间都不能替代锁后 `decisionTime`。 +- `Draft` 无论计划时间是否已过都可以取消;`Published`、`Ongoing` 只有在锁后 `decisionTime < endAt` 时可以取消;`Ended` 或 `Cancelled` 的新取消请求必须拒绝。取消后不再接受新抢购,已有订单继续走订单状态机。 - 进行中取消与并发抢购通过同一权威活动状态竞争:取消先提交时后续抢购条件不再命中;抢购先整体提交时该订单属于“已有订单”,随后取消活动不撤销它。 - 草稿取消时没有库存划拨。已发布或进行中的活动取消后,未售库存仍隔离保留在原活动,不回到普通库存。 - 活动自然结束后剩余库存,以及结束后因待支付订单取消而回补的库存,仍保留在原活动并停止销售,不自动回到普通库存;本期不增加二次处置流程。 @@ -140,7 +141,16 @@ flowchart TD ```mermaid flowchart TD - A["买家提交活动、正整数数量、本人有效收货地址和稳定请求标识"] --> B{"身份与固定输入有效?"} + A0["游客/买家在 A227 点击立即抢购"] --> A01{"是否已登录 Buyer?"} + A01 -- "否" --> A02["保存一次性 SeckillActivityDetail(activityId) 安全返回目标
登录后重读 A227,不自动下单"] + A01 -- "是" --> A03["进入秒杀确认页,复用 A010/A011 加载或新增地址"] + A03 --> A04{"存在唯一默认地址?"} + A04 -- "是" --> A05["预选并突出默认地址;仍可切换"] + A04 -- "否" --> A06["保持未选择,不取列表第一条"] + A05 --> A07["买家明确确认地址版本与正整数数量"] + A06 --> A07 + A07 --> A["生成稳定请求标识,提交活动、数量、addressId + addressVersion"] + A --> B{"身份与固定输入有效?"} B -- "否" --> X["拒绝,不进入库存与订单处理"] B -- "是" --> C{"该买家、活动与稳定标识是否已有结果?"} C -- "同标识同请求" --> R["重放首次确定结果,不重复扣减或下单"] @@ -148,25 +158,30 @@ flowchart TD C -- "全新请求" --> D{"当前流量是否在可承载上限内?"} D -- "否" --> Z["形成过载的确定业务结果;普通商品入口继续可用"] D -- "是" --> E["读取活动、商品与买家当前限购占用"] - E --> F{"活动为 Ongoing、权威时间在窗口内、商品匹配且请求未超限?"} - F -- "否" --> G["形成未开始 / 已结束 / 已取消 / 超限等确定业务结果"] - F -- "是" --> H["开启短事务"] - H --> I["以活动、状态、时间窗口和剩余量为条件原子扣减独立秒杀库存"] + E --> F["在同一事务先锁默认商家责任门,再锁本人地址
复核 addressVersion 并形成 trustedCheckoutContext"] + F --> F0{"地址仍存在、归属本人且版本一致?"} + F0 -- "否" --> G0["形成地址不存在或版本冲突的确定业务结果
零扣减、零建单并要求刷新确认"] + F0 -- "是" --> F1 + F1["保持责任门与地址行锁,再按固定顺序锁活动和买家配额;锁后形成唯一 decisionTime"] + F1 --> F3["追赶活动持久状态,并重检商品、时间窗口和限购"] + F3 --> F2{"活动为 Ongoing、startAt ≤ decisionTime < endAt、商品匹配且请求未超限?"} + F2 -- "否" --> G["形成未开始 / 已结束 / 已取消 / 超限等确定业务结果"] + F2 -- "是" --> H["在当前短事务继续处理"] + H --> I["以活动、Ongoing、同一 decisionTime 窗口和剩余量为条件原子扣减独立秒杀库存"] I --> J{"扣减是否成功?"} J -- "否" --> K["回滚主事务,形成库存 / 状态竞争或订单输入失败等确定业务结果"] J -- "是" --> L["原子增加当前买家的活动限购占用,且不得超过上限"] L --> M{"限购占用是否成功?"} M -- "否" --> K - M -- "是" --> N["在同一原子边界调用 M04 统一订单创建能力"] - N --> N0{"M04 对地址归属、唯一启用默认商家和订单输入复核是否通过?"} - N0 -- "否" --> K - N0 -- "是" --> N1["M04 生成共享待支付订单,以唯一启用的默认商家写入 assignedMerchantUserId,保存固定支付截止时间、地址与订单项快照,并记录秒杀来源、活动和成交价"] + M -- "是" --> N["在同一原子边界把既有 trustedCheckoutContext 交给 M04 统一订单创建能力"] + N --> N1["M04 不重新解析地址/商家、不放锁且不另开事务;生成共享待支付订单并保存固定支付截止时间、地址与订单项快照、秒杀来源、活动和成交价"] N1 --> O["可靠记录订单已创建事实,并绑定本次稳定请求结果"] O --> P{"库存、限购、订单、快照、可靠事实与请求结果是否整体提交?"} P -- "否" --> T["整体回滚;属于未形成确定结果的瞬态失败,原标识可重试"] P -- "是" --> Q["返回同一订单号、购买结果、支付截止时间和提交后的权威剩余库存,进入 M05 支付"] Z --> W["先把确定业务结果与稳定请求标识持久绑定"] G --> W + G0 --> W K --> W W --> U{"结果绑定是否成功?"} U -- "是" --> V["返回已绑定的首次结果;以后同标识直接重放"] @@ -175,13 +190,15 @@ flowchart TD 不可变核心事实: -- 秒杀库存扣减必须使用数据库条件更新,一次同时约束目标活动、`Ongoing` 状态、权威时间窗口与剩余量;未命中就失败,禁止在应用层“先读取、后递减”。 -- 秒杀库存扣减、买家限购占用、M04 共享订单、唯一启用的默认 `assignedMerchantUserId`、固定支付截止时间、地址与订单项快照、可靠订单事实和幂等结果属于同一个原子成功结果;任一步失败全部回滚,不产生支付前置记录。 +- 秒杀库存扣减必须使用数据库条件更新,一次同时约束目标活动、`Ongoing` 状态、锁后同一 `decisionTime` 时间窗口与剩余量;未命中就失败,禁止在应用层“先读取、后递减”或在等待锁前预取时间。 +- 秒杀库存扣减、买家限购占用、M04 共享订单、唯一启用的默认 `assignedMerchantUserId`、固定支付截止时间、由锁定且版本一致的地址形成的地址快照、订单项快照、可靠订单事实和幂等结果属于同一个原子成功结果;任一步失败全部回滚,不产生支付前置记录。 - 活动创建人只决定活动管理范围,不决定订单履约归属。普通订单和秒杀订单都由 M04 在提交时解析同一唯一启用默认商家;默认商家缺失、重复、禁用或与下单并发禁用时,按唯一顺序拒绝或成立,不得创建无人负责订单。 - 完成身份与固定输入校验后,必须先查询稳定请求结果,再进入限流、时间、库存和限购判断。同标识同请求重放首次确定结果,不得因当前活动、库存、限购或流量变化重新裁决。 -- 成功、未开始、已结束、已取消、售罄、超限、地址无效、默认商家不可用和已正式返回的过载结果都属于可重放的确定业务结果,必须先与稳定请求标识持久绑定再返回。数据库连接中断、事务提交失败等无法确认结果的瞬态故障不得伪装成确定业务失败,也不得固化;客户端使用原标识重试。 +- 请求指纹固定绑定规范化 `activityId + quantity + addressId + addressVersion`。成功、未开始、已结束、已取消、售罄、超限、地址不存在/越权、地址版本冲突、默认商家不可用和已正式返回的过载结果都属于可重放的确定业务结果,必须先与稳定请求标识持久绑定再返回。数据库连接中断、事务提交失败等无法确认结果的瞬态故障不得伪装成确定业务失败,也不得固化;客户端使用原标识重试。 +- 秒杀确认页与普通结算页复用同一 A010/A011 地址事实:唯一默认地址可以预选并突出展示,无默认地址不得自动选第一条;新地址固定非默认且创建成功只刷新确认页。买家必须主动确认地址和数量,任何地址创建、登录恢复或页面返回都不得自动调用 A228。 - 买家限购以“同一活动下当前有效占用量”作为唯一并发事实;待支付与已支付订单都占用名额,只有取消成功才释放。 - 事务保持短小,只处理单个活动和本次订单;事务内不调用外部 HTTP、不等待用户输入、不发送即时消息、不做长计算或全表扫描。 +- Worker 扫描只能把活动选为候选;每条活动取得锁后必须重新形成 `decisionTime` 并复核。一次事务可按顺序追赶 `Published → Ongoing → Ended`,但不能跳过取消终态,也不能推进 Draft。公开 GET 只返回等价有效状态,不执行这些写入。 - 商品有效性、秒杀价、订单金额和快照都由服务端重读并计算;客户端价格只能用于展示,不能决定成交金额。 - 请求充足且没有身份、时间、限购等业务失败时,系统必须持续接受可处理请求直至库存售罄;限流配置不得导致库存仍有剩余却提前停止销售。 @@ -235,7 +252,7 @@ flowchart TD - **M02 Catalog / M06-01 商家运营**:提供统一经营目录中的商品存在性、销售状态、普通库存和当前价格;发布时完成普通库存到秒杀库存的原子划拨,之后两个通道互不混用。 - **M03 Cart**:秒杀立即抢购绕过购物车,成功、失败、取消和回补均不读写购物车条目。 -- **M01 Identity / M04 Ordering**:C01 在同一原子边界调用 M04 统一订单创建能力;M04 从 M01 解析唯一启用的默认商家并写入 `assignedMerchantUserId`,生成共享 `PendingPayment` 订单、固定支付截止时间、地址与订单项快照,并保留秒杀来源、活动和成交价,供查询、取消与追溯。活动创建人不替代履约商家,C01 不建立第二套订单创建路径。 +- **M01 Identity / M04 Ordering**:C01 在同一 `DbConnection + DbTransaction` 中,先经公开契约锁定唯一启用默认商家责任门,再锁定并复核本人 `addressId + addressVersion`,形成 `trustedCheckoutContext`,其后才锁活动与配额;商家与地址行锁保持到整体提交。库存和限购更新成功后,M04 只消费该可信上下文创建共享 `PendingPayment` 订单、固定支付截止时间、地址与订单项快照,不重新解析地址/商家、不释放既有锁,也不另开事务。活动创建人不替代履约商家,C01 不建立第二套订单创建路径。 - **M05 Payment / C08 回调**:秒杀订单沿用统一支付和幂等回写;支付成功与取消竞争由订单状态条件推进裁决。 - **C03 超时取消**:只触发 M04 公开取消入口,由订单来源决定回补普通库存还是原秒杀库存,不得直接改写 C01 数据。 - **M06-02 履约、M09 消息、M10 售后**:分别沿用发货、可靠消息和售后流程,不为秒杀建立第二套通道。 @@ -248,27 +265,27 @@ flowchart TD | 已确认流程能力 | 派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 商家创建草稿 | A220 | 校验统一经营目录中的商品存在性、可售性与活动规则,只保存草稿和计划量,不划拨库存 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 商家更新草稿 | A221 | 仅 `Draft` 可修改;发布后拒绝编辑 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 商家发布活动 | A222 | 重新校验并原子划拨普通库存,成功后进入 `Published`;重复请求不重复划拨 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 商家取消活动 | A223 | 仅 `Draft` / `Published` / `Ongoing` 可取消;不回收已划拨库存 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 商家活动列表 | A224 | 仅本人有权管理的活动,支持状态、时间和关键词筛选 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 商家活动详情 | A225 | 返回本人活动、库存与订单汇总;越权不泄露存在性 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 买家公开活动列表 | A226 | 返回 `Published` / `Ongoing` 活动及需求规定的完整展示信息 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 买家公开活动详情 | A227 | 返回权威倒计时、剩余库存、已售数量及当前买家限购提示 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 买家秒杀下单 | A228 | 稳定请求重放、过载保护、条件扣减、限购占用,并由 M04 统一生成共享订单、唯一启用的默认 `assignedMerchantUserId`、固定支付截止时间与快照 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 商家创建草稿 | A220 | 校验统一经营目录中的商品存在性、可售性与活动规则,只保存草稿和计划量,不划拨库存 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 商家更新草稿 | A221 | 仅 `Draft` 可修改;发布后拒绝编辑 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 商家发布活动 | A222 | 重新校验并原子划拨普通库存,成功后进入 `Published`;重复请求不重复划拨 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 商家取消活动 | A223 | 过期 Draft 仍可取消;已划拨活动仅在锁后 `decisionTime < endAt` 时可取消,到期先自然结束;不回收已划拨库存 | 接口已按流程校准,待 OpenAPI、实现、测试与交叉评审 | +| 商家活动列表 | A224 | 仅本人有权管理的活动,支持状态、时间和关键词筛选 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 商家活动详情 | A225 | 返回本人活动、库存与订单汇总;越权不泄露存在性 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 买家公开活动列表 | A226 | 返回 `Published` / `Ongoing` 活动及需求规定的完整展示信息 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 买家公开活动详情 | A227 | 返回权威倒计时、剩余库存、已售数量及当前买家限购提示 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 买家秒杀下单 | A228 | 复用 A010/A011 的显式地址确认并提交版本;稳定请求重放、过载保护、条件扣减、限购占用,并由 M04 统一生成共享订单、唯一启用的默认 `assignedMerchantUserId`、固定支付截止时间与快照 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | | 秒杀订单查询 | 复用 A302 / A303 | 按买家归属查询共享订单和秒杀追溯信息 | 由 M04 契约承载 | | 取消与秒杀回补 | 复用 M04 公开取消契约 | 首次成功取消时按原通道回补并释放限购 | 由 M04 / C03 契约承载 | | 历史秒杀订单列表 / 详情编号 | A229 / A230 已取消 | 不建立第二套秒杀订单查询;统一由 A302 / A303 返回共享订单及秒杀追溯信息 | 已登记历史取消号 | -HTTP 路径、方法、请求响应字段、状态码、错误码、幂等标识传递方式已由本表派生到《接口设计》;真实 OpenAPI Schema、数据库和实现仍须继续承接,不得把旧草案中的字段或错误码反向写回业务流程。 +HTTP 路径、方法、请求响应字段、状态码、错误码和幂等标识传递方式已由本表派生到《接口设计》,表、约束和并发协议已由《数据库设计》承接;后续 OpenAPI、公开应用签名、实现和测试必须继续承接,不得把旧草案或实现便利反向写回业务流程。 ## 十、接口与数据库后续设计必须承接的事实 1. 接口必须区分草稿保存、发布划拨、状态取消、公开浏览和立即抢购,不能把多个原子边界拼成一个含糊动作。 2. 发布契约必须返回“划拨成功且状态已推进”或“全部未发生”中的一种结果;数据库据此保证同一活动最多成功划拨一次。 -3. 抢购契约必须携带活动、正整数数量、本人收货地址和稳定请求标识;商品有效性、成交价、限购和库存全部由服务端确定。 -4. 数据库必须表达活动状态、计划量、已划拨总量、剩余量、已售量、每名买家当前占用量、共享订单追溯信息、稳定请求结果和取消是否已回补;具体表名与字段在统一数据库设计中确定。 +3. 抢购契约必须携带活动、正整数数量、本人 `addressId + addressVersion` 和稳定请求标识;地址通过 Identity 共享事务锁内复核,商品有效性、成交价、限购和库存全部由服务端确定。 +4. 数据库已用 DB042~DB044 表达活动状态、计划量、已划拨总量、剩余量、已售量、每名买家当前占用量和库存流水,并由共享订单、DB104 与可靠事件表达订单追溯、稳定请求结果和取消回补;实现必须逐项遵守已确认字段、约束、锁顺序和版本推进规则。 5. 活动期间必须满足“剩余量 + 已售量 = 已划拨总量”;本期不引入冻结量。取消成功时剩余量增加、已售量减少,二者仍保持恒等。 6. 同一活动的买家当前占用量不得超过单用户限购;同一订单最多释放一次,同一稳定请求最多形成一个确定订单结果。 7. 共享订单必须由 M04 统一解析唯一启用的默认 `assignedMerchantUserId`、生成固定支付截止时间和快照,能区分普通购买与秒杀购买,并能追溯原活动、成交价和原库存通道;活动创建人不决定履约归属,取消时不得依赖客户端告诉系统回补到哪里。 @@ -278,13 +295,14 @@ HTTP 路径、方法、请求响应字段、状态码、错误码、幂等标识 ## 十一、验收证据清单 -- [ ] 生命周期:`Draft → Published → Ongoing → Ended` 按权威 UTC 时间推进;`Draft` / `Published` / `Ongoing` 可取消,`Ended` / `Cancelled` 拒绝重复状态变更。 +- [ ] 生命周期:GET 只计算有效状态且无写副作用;Worker 和写命令按锁后 `decisionTime` 幂等推进 `Published → Ongoing → Ended`。过期 Draft 仍可取消;已划拨活动到期先结束,不得误取消。 - [ ] 发布原子性:普通库存足够时一次性划拨并进入 `Published`;任一步失败时库存和活动状态均不变化;重复发布不重复划拨。 - [ ] 公开展示:列表和详情包含剩余库存与已售数量;最后一份库存被抢走后,相关响应使页面同一交互内变为“已售罄”并禁用入口。 - [ ] 100 并发抢 10 份库存:成功订单数恰好为 10、剩余为 0、已售为 10、无负库存、无孤立订单或孤立订单项;其余请求有明确失败原因。 - [ ] 不少卖:请求充足且不存在身份、时间、限购等业务失败时,10 份库存全部形成 10 笔成功订单,限流配置不提前截断全部有效请求。 - [ ] 单用户限购:同一买家的当前有效秒杀数量不超过上限;并发请求不能绕过;取消成功后按数量释放。 - [ ] 幂等:同一买家、活动和稳定标识重复提交只形成一笔订单并返回同一结果;同标识不同请求被拒绝。 +- [ ] 地址确认:唯一默认地址只预选、无默认地址不自动选第一条;并发编辑/删除/切换默认时,版本不符或地址无效均零扣库存、零占限购、零建单并要求刷新确认。 - [ ] 订单统一创建:C01 只负责活动库存和限购;M04 在同一原子边界生成共享订单、指定商家、固定支付截止时间和快照,不存在第二套秒杀订单创建路径。 - [ ] 事务回滚:库存、限购、订单、订单项快照、可靠订单事实或请求结果任一步失败时,全部恢复原状。 - [ ] 时间窗口:开始前、结束后、取消后的请求均不扣库存;应用实例时钟偏差不改变结果。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" index 3d7c685..fbfb991 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhh/M03-\350\264\255\347\211\251\350\275\246\346\265\201\347\250\213.md" @@ -3,22 +3,22 @@ > 负责人:朱惠惠 > 覆盖:M03-01、F07 > 基础核心流程:F01、F02、M02 公开浏览、F08 下单、M09 消息 -> 直接协作:韦乾强(M04 Ordering)、顾欣月(M02 Catalog)、张海洋(秒杀边界 C01) -> 文档状态:已按需求校准,可作为接口与数据库设计输入;待 Catalog/Ordering 交叉评审 +> 直接协作:韦乾强(M04 Ordering)、顾欣月(M02 Catalog);C01 秒杀边界与本模块同由朱惠惠负责 +> 文档状态:业务流程已校准,接口与数据库设计已承接;待 OpenAPI、实现、测试和 Catalog/Ordering 交叉评审 > 需求事实源:[需求规格说明书 M03-01](../../../01-需求文档/需求规格说明书.md) 的“M03-01 购物车管理(F07)”完整七节 ## 一、范围与事实来源 本模块负责买家在登录态下维护本人购物车,覆盖查看列表、加入商品、修改数量、删除条目、单选 / 全选、选择失效处理、服务端计价和下单前 / 下单事务内的购物车清理。购物车只承担“下单前的暂存区”,不承载营销、优惠、推荐、凑单,也不维护独立状态机;选中状态、价格、库存上限由服务端实时派生,客户端不得越权决定订单金额。 -本文先依据需求确定业务参与者、上游输入、状态派生口径、原子结果和模块出入口,再由这些动作派生接口契约。A2xx(A201 加购、A202 查看、A203 改数量、A204 删除、A205 批量删除、A206 切换选中、A207 清空、A208 结算预览)只用于流程完成后的契约映射与缺口检查,不能反向拼接流程,也不能用现有 Axxx 反向覆盖业务语义。A208 之前的“幂等记录”草表属于加购主接口内嵌能力,不作为独立 HTTP 契约单独列出。 +本文先依据需求确定业务参与者、上游输入、状态派生口径、原子结果和模块出入口,再由这些动作派生接口契约。A2xx(A201 加购、A202 查看、A203 改数量、A204 删除、A205 批量删除、A206 切换选中、A207 清空、A208 结算预览)只用于流程完成后的契约映射与缺口检查,不能反向拼接流程,也不能用现有 Axxx 反向覆盖业务语义。幂等记录是 M00 公共能力,不单独形成购物车 HTTP 契约;仅 A201 的可选 Key、A205 批量删除和 A206 非天然幂等选择动作使用它,A204/A207 采用条件删除的天然幂等结果。 | 设计对象 | 当前成熟度 | 本文处理 | |---|---|---| | M03-01/F07 需求 | 完整定义 | 作为购物车业务语义事实源 | | 本文业务流程 | 完整定义,已完成统稿校准 | 参与者、上游输入、状态派生、原子结果和模块出入口已闭合,可作为下游设计输入 | | A2xx 接口 | 完整定义,待交叉评审 | 已由本文流程派生核心契约;后续评审不得反向改写业务语义 | -| 购物车相关表(条目、幂等记录) | 模板/占位 | 本文不发明表名、字段、约束或索引 | +| 购物车数据设计 | 完整定义,已确认 | DB041 保存条目;DB104 仅承接 A201 可选 Key、A205 和 A206 的确定结果,A204/A207 不持久化多余幂等记录 | | X02 收藏与浏览历史 | 独立扩展 | 仅登记边界,不混入 F07 主流程 | | C01 秒杀 | 独立扩展 | 立即抢购绕过购物车,C01 仅与本文确定“不读写购物车”的边界 | @@ -26,12 +26,12 @@ ```mermaid flowchart LR - ID["M01 Identity
已认证且账号状态正常的买家"] -->|"身份校验通过"| CART["M03 Cart
条目、选中状态、服务端金额"] + ID["M01 Identity
已认证且账号状态正常的买家"] -->|"身份校验通过"| CART["M03 Cart
条目、选中状态、服务端预览金额"] CAT["M02 Catalog
销售状态、实时价格、实时可售库存"] -->|"加购 / 改数量 / 结算校验输入"| CART - BUYER["买家购物车页、加购入口、收银台"] -->|"维护 / 选择 / 去结算动作"| CART - CART -->|"本人选中条目、数量、选中状态、服务端金额"| ORD["M04 Ordering
服务端重读、计价、原子扣减"] + BUYER["买家购物车页、加购入口、结算预览页"] -->|"维护 / 选择 / 去结算动作"| CART + CART -->|"本人选中条目标识、数量与预览金额;预览不成为订单事实"| ORD["M04 Ordering
服务端重读、最终计价、原子扣减"] CART -->|"本人全部条目(含失效)"| BUYER - CART -->|"本人可结算与服务端金额"| CHECKOUT["结算预览 → M04 提交入口"] + CART -->|"本人可结算集合与服务端预览金额"| CHECKOUT["结算预览 → M04 提交入口"] ORD -->|"提交事务成功:清理已下单条目"| CART ORD -->|"事务回滚:购物车条目原状保留"| CART @@ -44,7 +44,7 @@ flowchart LR 边界约束: -- M03 不直接接受前端传入的最终金额或处理商家,所有计价与归属以 M02 / M04 服务端重读为准。 +- M03 不直接接受前端传入的最终金额或处理商家。Cart 只计算当前请求时点的预览金额;M04 下单事务必须重新读取商品事实并形成最终订单项单价与订单总额。 - M03 不预留库存;库存扣减由 M04 在下单事务内条件更新完成。 - 下单事务成功后由 M04 在同一事务内删除已下单条目;事务回滚时购物车条目原状保留。 - C01 秒杀绕过 M03,不读取、不写入购物车条目;普通购物车条目不受秒杀扣减 / 回补影响。 @@ -78,7 +78,7 @@ flowchart TD 关键约束: -- 同一买家的同一商品在购物车中只允许一个条目;重复加购按数量累加,禁止生成多个并行条目。对应唯一性约束由后续数据库设计派生。 +- 同一买家的同一商品在购物车中只允许一个条目;重复加购按数量累加,禁止生成多个并行条目。DB041 已通过 `(buyer_id,product_id)` 唯一约束承接该规则。 - 数量上下限 `1 ≤ 数量 ≤ 商品当前实时可售库存`,调小 / 删除不受上限约束,但不允许设为 0 或负数。 - 加购、改数量、累加均按实时库存拒绝越界请求,并返回当前最大可设值;前端据此截断,不依赖前端控制。 - 完成身份与固定字段校验后,必须先查稳定请求标识,再读取当前商品与库存;同标识同请求直接重放首次确定结果,不得因之后的下架、改价或库存变化改变结果。 @@ -94,9 +94,9 @@ flowchart TD A["M01:买家进入购物车页"] --> B["按当前买家与分页条件查询本人条目"] B --> C["服务端读取每个商品的实时销售状态与可售库存"] C --> D{"商品仍可售、库存 > 0 且条目数量不超过实时库存?"} - D -- "是" --> E["标记为可结算,返回实时单价、当前数量、小计、是否选中"] - D -- "否" --> F["标记失效并写明失效原因(下架 / 售罄 / 禁用 / 数量超过实时库存)"] - E --> G["购物车页统一渲染:选中、未选中、失效三类状态"] + D -- "是" --> E["标记为可结算,返回实时单价、当前数量、小计和持久化选择意图"] + D -- "否" --> F["标记失效并写明失效原因;GET 不覆盖原选择意图"] + E --> G["购物车页分别渲染可用/失效与已选/未选两个维度"] F --> G G --> H["返回最大可设库存和失效原因,由服务端按请求分页"] ``` @@ -127,31 +127,29 @@ flowchart TD ```mermaid flowchart TD - A["买家提交单条删除 / 批量删除 / 清空及稳定请求标识"] --> R{"该买家与标识是否已有结果?"} - R -- "同标识同请求" --> S["重放首次确定结果,不重新执行"] - R -- "同标识不同请求" --> T["拒绝标识复用,不产生副作用"] - R -- "没有结果" --> B{"动作类型?"} - B -- "单条删除" --> C{"该条目存在且属于当前买家?"} - C -- "否" --> X["形成不存在 / 无权限的确定结果,不泄露归属"] - C -- "是" --> D["删除该条目"] - B -- "批量删除" --> E["一次读取并校验全部目标条目"] + A["已认证买家提交删除动作"] --> B{"动作类型?"} + B -- "单条删除" --> C["按 cartItemId + 当前买家条件删除"] + C --> D{"实际命中 1 行还是 0 行?"} + D -- "任一结果" --> S["返回相同 204;最多删除本人一行,不暴露目标存在性"] + B -- "批量删除" --> R{"稳定请求标识是否已有结果?"} + R -- "同标识同请求" --> R1["重放首次确定结果,不重新执行"] + R -- "同标识不同请求" --> R2["拒绝标识复用,不产生副作用"] + R -- "没有结果" --> E["对 1~100 个目标去重,一次读取并校验全部目标"] E --> F{"每个目标都存在且属于当前买家?"} F -- "否" --> Y["形成整批拒绝结果,任何条目都不删除"] F -- "是" --> G["在一个原子操作中删除全部目标条目"] - B -- "清空" --> H["删除当前买家的全部条目,含失效条目"] - D --> I["形成最新购物车或空状态结果"] - G --> I - H --> I - X --> J["副作用与确定结果原子提交;无副作用的确定失败先保存再返回"] + G --> J["删除结果与稳定请求结果原子提交"] Y --> J - I --> J + B -- "清空" --> H["按当前买家删除全部条目,含失效条目"] + H --> H1["无论原有 0 行还是多行都返回 204;无需稳定请求标识"] ``` 关键说明: -- 单条删除、批量删除与清空均按当前买家过滤并携带稳定请求标识;同标识同请求重放首次结果,同标识不同请求拒绝。若使用新的请求标识删除已不存在条目,仍按不存在 / 无权限形成新的确定失败。 -- 删除副作用与成功结果必须原子提交;不存在、越权、整批校验失败等无副作用的确定失败也要先保存结果再返回。瞬态基础设施失败不保存确定结果,允许原标识重试。 -- 批量删除采用“全部校验、全部删除”的原子语义:只要任一目标不存在或不属于当前买家,整批拒绝且一个也不删除,不能静默忽略越权条目。 +- A204 单条删除按 `(cartItemId, currentBuyerId)` 条件执行,影响 0 行和 1 行都返回 204。它天然幂等,不要求 `Idempotency-Key`,既避免为 DELETE 建立无必要状态,也避免用响应差异探测他人条目。 +- A205 批量删除必须携带稳定请求标识;同标识同规范请求重放首次结果,同标识换目标集合拒绝。目标集合先去重,去重后数量必须为 1~100;请求指纹使用去重后按 UUID 字节序稳定排序的集合,调用方仅改变输入顺序仍视为同一请求。 +- 批量删除采用“全部校验、全部删除”的原子语义:只要任一目标不存在或不属于当前买家,整批拒绝且一个也不删除,不能静默忽略越权条目。确定成功或失败与 DB104 结果同事务提交;瞬态基础设施失败不固化。 +- A207 清空按当前买家条件删除全部条目,影响 0 行或多行都返回 204,不要求 `Idempotency-Key`;重复清空天然成功。 - 清空只影响当前买家的全部条目;购物车本来为空时仍按成功的空结果返回。 - 查看、修改、删除、清空都不返回他人条目;前端不缓存购物车金额或选中状态作为最终结果。 @@ -162,35 +160,45 @@ flowchart TD A["买家切换单选 / 全选、反选"] --> B{"条目归属当前买家?"} B -- "否" --> X["拒绝切换"] B -- "是" --> C{"条目当前可结算?"} - C -- "否" --> Y["拒绝选中并返回失效原因"] - C -- "是" --> D["服务端写入选中状态,按需触发全选 / 反选批量更新"] - D --> E["购物车页即时刷新选中结果"] - - P["买家请求结算预览"] --> Q["服务端按当前买家+选中条目读取商品实时单价"] + C -- "否且请求设为已选" --> Y["拒绝选中并返回失效原因"] + C -- "否且请求取消选择" --> D1["允许写为未选"] + C -- "是" --> D["服务端写入选中状态;全选 / 反选同时把失效条目规范为未选"] + D1 --> E + D --> E["返回稳定选择变更摘要"] + E --> F["客户端另调 A202 刷新当前分页与实时商品事实"] + + P["买家请求结算预览
全部已选或显式 cartItemId 子集"] --> Q["服务端按当前买家+目标已选条目读取商品实时单价"] Q --> R{"全部条目仍可结算?"} R -- "否" --> S["整体拒绝整次结算,仅标记问题条目并保留全部购物车"] R -- "是" --> T["服务端计算选中总额 = Σ 实时单价 × 当前数量"] - T --> U["返回选中条目与总额给前端,前端只用来展示;提交订单以服务端再次重读为准"] + T --> U["返回条目、总额与 checkoutRevision;提交订单重读重算并比较内容版本"] ``` 关键约束: -- 选中状态保存在服务端;刷新和重新登录后仍按服务端记录渲染;失效条目禁止被选中。 -- 选中总额一律由服务端按实时单价计算,前端展示金额仅供参考;客户端不得指定最终金额。 -- 失效原因必须来自 `下架 / 库存归零 / 禁用 / 当前数量超过实时库存` 四类客观状态,不允许写入主观提示。 +- `isSelected` 是服务端保存的用户选择意图,`isAvailable` 是按 Catalog 当前事实派生的可结算资格,两者不能合并为一个三态字段。合法组合包括“已选且可用、未选且可用、已选但失效、未选且失效”。 +- GET 不产生写操作。条目在已选后失效时保持 `isSelected=true/isAvailable=false`,计入已选数但不计入可结算金额,并阻断包含它的 A208 目标集合;用户仍可取消选择、删除或调小。任何命令都不能把当前失效条目从未选改为已选;`SelectAll` / `Invert` 会将失效条目规范为未选。 +- 如果条目在用户发出新的选择命令前恢复可售,原持久化选择意图重新成为有效选择;这是保留购物车意图的确定结果,不由 GET 暗中改写。 +- A206 只返回 `mode/affectedCount/selectedIntentCount/selectionChangedAt` 的稳定变更摘要,不返回完整购物车、动态金额或库存;纯取消选择完全不依赖 Catalog。客户端随后调用 A202 刷新当前分页,刷新失败不回滚选择结果,也不得为刷新而重新执行 `Invert`。 +- 选中总额一律由服务端按实时单价计算,前端展示金额仅供参考;客户端不得指定最终金额。A208 未带显式 ID 时使用全部已选条目但同样最多 100 个;带 ID 时先去重并校验 1~100 个目标均属于本人、仍被选中且可结算,只返回该原样子集,任一无效时整体不可结算。 +- 有效预览按稳定顺序对买家以及每项 `cartItemId/productId/cartVersion/quantity/productName/mainImageObjectKey/unitPrice` 计算 `checkoutRevision`。是否由“全部已选”或“显式子集”进入预览只决定目标集合,不写入 Revision;A301 只提交规范化后的条目集合,也不携带目标模式。A301 仍锁内重读重算;内容不一致时零建单并返回最新结构化预览,买家确认新版本后使用新 Revision 和新幂等 Key。Revision 不锁价、不预占库存,也不包含仍足够的剩余库存绝对数。 +- 本期没有额外最低结算金额;只有目标集合至少 1 项、全部有效且服务端总额大于 0 才允许继续。 +- 失效原因只允许 `ProductOffSale`(`Draft/OffSale`)、`OutOfStock`(库存为 0)和 `QuantityExceedsStock`(库存大于 0 但小于购物车数量)三类可恢复客观状态;商品受购物车外键保护不能在条目仍存在时物理删除,分类停用也不改变已上架商品可售性,不得另造 `ProductDisabled/ProductMissing/ProductUnpublished` 等同义原因或写入主观提示。 ## 六、与订单模块的协作:结算下单与清理 ```mermaid flowchart TD - SEL["买家在购物车提交选中条目 + 地址 ID + 幂等键"] --> A["M03:从本人购物车读取选中条目"] + SEL["买家提交同一目标条目子集 + checkoutRevision + 地址 ID + 幂等键"] --> A["M03:从本人购物车读取目标已选条目"] CAT["M02 直接输入:销售状态、实时价格、实时库存"] --> B - ID["M01 直接输入:身份与地址归属"] --> C - A --> B["服务端重新校验上下架、库存和归属"] - B --> C["校验地址归属当前买家且状态正常"] - C --> D{"校验全部通过?"} - D -- "否" --> X["整次下单拒绝,仅把问题条目标记失效并保留全部购物车"] - D -- "是" --> E["M04 开启下单事务"] + ID["M01 直接输入:身份、地址归属与唯一启用默认商家"] --> C + A --> C["先形成地址快照与默认商家可信上下文"] + C --> C0{"地址、默认商家与 Identity 依赖是否可确定?"} + C0 -- "否" --> XG["返回全局错误;不返回 latestPreview,不改购物车条目或选择状态"] + C0 -- "是" --> B["服务端重新校验条目归属、选中状态、上下架、库存和内容"] + B --> B2{"条目、数量、展示与单价是否仍匹配 Revision?"} + B2 -- "否" --> XR["整次拒绝并返回 latestPreview;仅在响应中标明问题项,不写回失效或选择状态"] + B2 -- "是" --> E["M04 在当前事务继续下单"] E --> F["条件扣减普通库存(M04 与 M02 内部完成)"] F --> G["M04 创建订单与订单项快照、保存服务端总额、订单状态 PendingPayment"] G --> H["M04 在同一事务内删除本次已结算的购物车条目"] @@ -201,10 +209,17 @@ flowchart TD K --> L["M04 返回订单号、应付金额与 PendingPayment → 进入 M05 收银台"] ``` +结果分层固定如下: + +- 只有仍属于本人且可完整重算的目标集合发生选中、条目版本、数量、商品名称、主图、价格、销售状态、库存可结算性或正金额资格变化时,才返回 `ORDER.CHECKOUT_CHANGED + latestPreview`。问题原因只存在于本次响应,不得借“标记失效”写回 DB041 或自动取消选择。 +- 任一条目不存在或不属于本人返回统一 404,不返回 `latestPreview`,也不泄露具体是哪一种情况。 +- 地址不存在/不属于本人、唯一默认商家结构性不可用、幂等键冲突以及 Identity、Cart、Catalog、数据库依赖未知均是全局错误,不携带 `latestPreview`;所有情况下购物车条目、数量与选择状态保持原样。 + 不变量: - 库存扣减、订单与快照写入、可靠记录订单已创建事实、购物车清理属于同一下单事务,任一失败整体回滚。 - 订单回滚时购物车条目原状保留,禁止“订单失败但条目丢失”。 +- 普通“去结算”默认使用最多 100 个全部已选条目;页面也可把当前准备结算的 1~100 个已选条目作为 A208 显式子集,以固定本次目标集合。A208 响应中的同一 ID 子集与 `checkoutRevision` 一并交给 A301;本期不建立独立“立即购买”或第二套订单流程。 - 买家主动取消订单或 C03 超时取消后,本期不自动恢复购物车条目;用户希望重新购买需手动再次加车。 - C01 秒杀绕过购物车:秒杀成功订单与取消后库存回补均不读写本文购物车条目。 @@ -237,11 +252,11 @@ flowchart TD | 加入购物车(可选幂等) | A201 | 校验数量 + 库存,同 `(买家+商品)` 累加,幂等不重复累加 | 待交叉评审 | | 查看本人购物车 | A202 | 仅返回本人条目,含可结算 / 失效标记与失效原因 | 待交叉评审 | | 修改本人条目数量 | A203 | 调大按实时库存校验并返回最大可设值;调小不验库存上限;返回最新数量与小计 | 待交叉评审 | -| 删除本人条目(按 ID) | A204 | 按当前买家与条目标识过滤;同一稳定请求重放首次结果,新请求删除已不存在条目仍被拒绝 | 待交叉评审 | -| 批量删除本人条目 | A205 | 先校验全部目标均存在且归属本人,再原子删除;任一目标无效或越权时整批拒绝且不删除;同一稳定请求可重放 | 待交叉评审 | -| 切换单选 / 全选 / 反选 | A206 | 服务端持久化选中;失效条目不允许被选中 | 待交叉评审 | -| 一键清空购物车 | A207 | 幂等;只影响本人;失效条目一并清理 | 待交叉评审 | -| 结算预览(获取总价) | A208 | 实时重读单价并计算总额;前端不能指定金额 | 待交叉评审 | +| 删除本人条目(按 ID) | A204 | 按当前买家与条目标识条件删除;影响 0/1 行均返回 204,天然幂等且不泄露存在性,不要求幂等键 | 接口已按流程校准,待 OpenAPI、实现与测试 | +| 批量删除本人条目 | A205 | 1~100 个目标去重并稳定规范化;先校验全部归属再原子删除,任一无效时整批零删除;稳定请求结果可重放 | 接口已按流程校准,待 OpenAPI、实现与测试 | +| 切换单选 / 全选 / 反选 | A206 | 服务端持久化选择意图;失效条目可取消选择但不可设为已选,全选/反选把失效条目规范为未选 | 待交叉评审 | +| 一键清空购物车 | A207 | 按买家条件删除全部条目;空购物车和重复调用均 204,天然幂等且无需幂等键 | 接口已按流程校准,待 OpenAPI、实现与测试 | +| 结算预览(获取总价) | A208 | 完整返回全部已选或显式目标;任一失效时整体不可结算;至少一项、全部有效且服务端总额大于 0 才通过 | 待交叉评审 | | 加购幂等(内嵌) | 不单独分配 Axxx | 调用方携带稳定请求标识,同一标识与同一请求重放首次确定结果;具体传递方式由接口通用约定派生 | 不作为独立 HTTP 契约 | 接口详细定义与实现必须承接上述流程结果。HTTP 状态码、请求字段、错误码与幂等键传递方式不得反向写入业务图,接口设计 1.12 通用幂等规则统一承载。 @@ -260,10 +275,11 @@ flowchart TD 3. 加购幂等键的窗口期需要与库存 / 上限校验配合:同一幂等键只能重放首次确定结果,不同请求体携带同键视为标识复用并被拒绝;瞬态失败不固化,可用原标识重试。 4. 下单成功后,订单模块在事务内清理购物车条目;若现有 A2xx 与订单模块的下单接口跨事务异步清理,必须先改为同事务清理,保证事务回滚不丢条目。 5. 结算预览返回的服务端金额是“可选预览”,下单时订单模块必须再重读一次商品与库存,不能直接复用 A208 的金额作为最终扣款事实。 -6. A204 单条删除、A205 批量删除和 A207 清空必须分别承接本流程语义;其中批量删除须先校验全部归属再原子执行,不得静默忽略无效或越权条目。后台清理需另起保留期规则,不在本文范围。 +6. A204 单条删除、A205 批量删除和 A207 清空必须分别承接本流程语义:A204/A207 是无需持久化请求结果的天然幂等条件删除;A205 先规范化集合、校验全部归属,再把整批副作用与 DB104 结果原子提交。后台清理需另起保留期规则,不在本文范围。 7. 购物车数据归属全部按 `(买家 ID, 商品 ID)` 或 `(买家 ID, 条目 ID)` 双重过滤;A2xx 不能仅按条目 ID 给出可访问性。 8. 价格变动:商品改价后购物车再次展示用实时单价;现有接口若缓存条目的小计或反推金额,需在列表时重算并返回最新单价。 -9. 购物车表、幂等记录表的字段、约束和索引必须由数据库设计任务另行确认,本文档不发明。 +9. 数据库承接已确认:DB041 以独立条目 ID 为物理主键、`(buyer_id, product_id)` 为业务唯一键;DB104 仅承接 A201 可选 Key、A205 与 A206,A204/A207 不写幂等记录。实现不得重新发明购物车专用幂等表。 +10. 被动商品失效不跨模块批量写购物车,也不允许 GET 清除 `is_selected`;A202 必须把持久化选择意图和实时可用性分别返回,A206 再按明确命令规范化。 ## 十一、验收证据清单 @@ -272,13 +288,15 @@ flowchart TD - [ ] 修改数量超过商品实时可售库存时拒绝并返回最大可设值;改为 0、负数或非整数被拒绝并提示原因。 - [ ] 选中条目总额由服务端按实时单价计算,前端篡改金额或数量再提交被服务端拒绝,订单总额与数据库一致。 - [ ] 商品变为 `Draft` / `OffSale` 后,已加入条目在购物车页标记“不可结算”,不可调大、不可累加、不能勾选进入结算;实时可售库存为 0 时同样标记;分类停用不反向改变仍为 `OnSale` 商品的可结算资格。 +- [ ] 已选条目被动失效时 GET 返回“已选但失效”且不写库,结算完整提示并阻断;可显式取消选择,全选/反选会把失效条目规范为未选,恢复可售前后选择意图规则一致。 +- [ ] 结算不存在额外最低金额门槛;零目标、任一失效或服务端总额不大于 0 均不可继续,至少一项全部有效且总额大于 0 才可提交。 - [ ] 失效条目可下调数量、可删除;仅当商品处于 `OnSale` 且下调后的数量不超过实时可售库存时,条目恢复可结算状态。 - [ ] 越权:用买家 B 身份请求买家 A 的条目被拒绝,响应不暴露该条目是否存在及归属信息。 - [ ] 下单成功后,对应购物车条目在同一事务内被清除;订单事务回滚时购物车条目原状保留,未出现“订单失败但条目丢失”。 - [ ] 买家主动取消订单或 C03 超时取消后,对应购物车条目本期不自动恢复。 - [ ] 并发:同一条目同时被改数量和删除,最终只出现删除结果或最新数量,两者不会同时生效导致数据错乱。 - [ ] 幂等:相同请求幂等标识在约定窗口内重复提交不重复累加数量,返回结果一致。 -- [ ] 删除:单条、批量和清空只影响本人;批量目标中任一条目不存在或越权时整批拒绝且零删除,同一稳定删除请求重试返回首次结果。 +- [ ] 删除:A204 首次、重复、不存在和他人条目都返回 204 且最多删除本人一行;A205 任一目标不存在或越权时整批零删除且同 Key 重放首次结果;A207 对非空、空和重复清空均返回 204。 - [ ] C01 衔接:秒杀路径独立执行活动库存与个人限购校验,超卖拒绝且不影响普通购物车条目。 - [ ] 库存与下单协作:购物车不预留库存,订单提交事务内完成扣减;C03 超时取消正确回补库存,购物车无需联动处理。 - [ ] 性能与可用性:购物车页 30~100 条目在常规环境下加载时间低于 2 秒;大量条目启用分页,禁止无上限返回。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" index b833b4a..2a47aa4 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/C08-\346\224\257\344\273\230\345\233\236\350\260\203\344\270\216\345\257\271\350\264\246\346\265\201\347\250\213.md" @@ -21,8 +21,8 @@ F10 的默认买家路径仍为 M05 小金库同步支付。C08 不扣买家小 | C08 需求与教师挑战目标 | 完整定义 | 作为回调、对账和差异闭环事实源 | | F10/F09/M10 流程 | 完整定义,已完成统稿校准 | 作为支付、取消和退款协作边界 | | 本文业务流程 | 已校准、待交叉评审 | 冻结回调终态、原子结果、截止时间与对账闭环 | -| A421~A426 接口 | 已按第十一章重建、未冻结 | 待数据库、OpenAPI、公开应用签名与交叉评审 | -| 回调、支付、对账数据设计 | 模板/占位 | 流程完成后统一派生,不在业务图中预设字段 | +| A421~A426 接口 | 已按第十一章重建、未冻结 | 数据库设计已确认,待 OpenAPI、公开应用签名、实现与交叉评审 | +| 回调、支付、对账数据设计 | 完整定义,已确认设计 | DB084/DB085、DB089~DB096、DB107 已承接回调聚合、最后财务水位、逐日批次、差异证据与处置;待实现与测试 | ## 二、参与者与模块直接出入口 @@ -116,9 +116,10 @@ flowchart TD C -- "是" --> D{"回调标识是否已有确定结果?"} D -- "同标识同内容" --> E["重放该回调的首次确定结果,不重复记账或通知"] D -- "同标识不同内容" --> Z["拒绝回调标识复用并记录安全冲突,不改变业务事实"] - D -- "新回调标识" --> F["开启短事务并取得该回调的唯一处理资格"] - F --> G["读取支付流水聚合、权威订单、应付金额、状态、支付截止时间和已有支付来源"] - G --> H["按第五章结果矩阵确定 ProcessedSuccess / ProcessedFailure / Ignored / Difference"] + D -- "新回调标识" --> F["开启短事务并取得回调标识、支付流水的唯一处理资格"] + F --> G["按统一顺序取得订单、通道尝试、已有成功支付与财务提交水位;财务水位最后取得"] + G --> G1["最后一个可能阻塞的共享事实就绪后
只读取一次数据库 finalTime"] + G1 --> H["按第五章结果矩阵确定 ProcessedSuccess / ProcessedFailure / Ignored / Difference"] H --> I["按结果形成对应原子业务写入"] I --> J{"事务整体提交?"} J -- "否" --> R["全部回滚,不留下已提交处理中状态;通道可安全重试"] @@ -133,16 +134,19 @@ flowchart TD - 支付流水第一次出现时绑定订单、金额和币种;后续回调若改变这些不可变事实,形成 `Difference` 来源并拒绝业务推进,但不能在幂等门口静默丢弃。 - 并发到达的相同回调只有一个取得处理资格;其他请求读取并返回已提交结果。 - 签名错误、固定字段非法和基础设施故障不伪装成支付失败终态;只有业务事务成功提交后才形成四种回调终态之一。 +- 接收层必须先用网关、服务器与端点一致的 16 KiB 原始 UTF-8 Body 上限约束读取;`Content-Length` 与分块传输都不能绕过,超限立即返回 413,不进入 HMAC、JSON、回调幂等或数据库流程。签名头必须唯一且满足接口冻结的 ASCII 格式/长度;回调金额必须处于 PostgreSQL `numeric(18,2)` 可表示的正数范围并禁止指数。 +- 新回调必须先串行化 `callbackId`,再串行化首次可能尚不存在的支付流水绑定;订单存在时锁定订单,随后取得通道尝试和已有成功支付事实,最后取得全局财务提交水位。取得最后一个可能阻塞的共享事实后,立即且仅调用一次数据库 `clock_timestamp()` 形成 `finalTime`。 +- 取得 `finalTime` 后不得再等待任何新的共享锁或调用外部服务,只能按已锁事实重跑矩阵并提交确定结果。订单不存在时可跳过订单锁,但仍必须在支付流水绑定后取得财务水位和 `finalTime`。 - 具体签名算法、密钥轮换、请求字段和响应码在接口与架构阶段派生;流程只固定“来源可信、内容完整、不可重放篡改”的结果。 ## 五、乱序、截止时间与订单竞争 | 权威订单 / 时间事实 | 回调结果 | C08 确定结果 | 订单与资金结果 | |---|---|---|---| -| `PendingPayment` 且早于支付截止时间,金额与订单一致 | 成功 | `ProcessedSuccess` | 原子形成模拟通道支付并推进 `Paid`;不扣小金库 | -| `PendingPayment` 且早于支付截止时间 | 失败 | `ProcessedFailure` | 订单保持待支付;本次通道尝试失败,不发支付成功事实 | -| `PendingPayment` 但已到支付截止时间 | 成功 | `Difference` | 不推进 `Paid`;登记“过期后通道成功”差异并触发 M04/C03 过期取消 | -| `PendingPayment` 但已到支付截止时间 | 失败 | `ProcessedFailure` | 不推进 `Paid`;触发 M04/C03 过期取消 | +| `PendingPayment`、`finalTime < paymentDeadline` 且金额与订单一致 | 成功 | `ProcessedSuccess` | 原子形成模拟通道支付并推进 `Paid`;不扣小金库 | +| `PendingPayment` 且 `finalTime < paymentDeadline` | 失败 | `ProcessedFailure` | 订单保持待支付;本次通道尝试失败,不发支付成功事实 | +| `PendingPayment` 但 `finalTime >= paymentDeadline` | 成功 | `Difference` | 不推进 `Paid`;登记 `expired_success` 来源,第一阶段提交后触发 M04/C03 过期取消 | +| `PendingPayment` 但 `finalTime >= paymentDeadline` | 失败 | `ProcessedFailure` | 登记 `failure_recorded_after_deadline`;第一阶段提交后触发 M04/C03 过期取消 | | 已由 M05 或其他模拟通道支付 | 新的成功 | `Difference` | 不重复入账、不改变订单;登记潜在重复支付差异 | | 已支付 | 新的失败 | `Ignored` | 保留合法 `Paid` 终态,不反向回退 | | 已取消 | 成功 | `Difference` | 不改为 `Paid`;登记迟到成功差异 | @@ -151,13 +155,13 @@ flowchart TD 关键规则: -- 成功回调推进订单时,条件必须同时包含订单仍为 `PendingPayment`、权威时间早于支付截止时间、金额和订单一致。任一条件未命中都不得创建成功支付事实。 +- 成功回调推进订单时,条件必须同时包含订单仍为 `PendingPayment`、`finalTime < paymentDeadline`、金额和订单一致。任一条件未命中都不得创建成功支付事实。 - M05 同步钱包支付先提交时,C08 新成功回调登记差异;C08 先提交时,M05 读取 `Paid` 并返回已有状态,不扣钱包。 - 买家主动取消可在截止时间前与支付竞争;达到截止时间后,M05 与 C08 均不得再支付,M04/C03 的过期取消成为唯一合法推进方向。 - 同一支付流水先失败后成功时,新的成功回调仍要重新执行本章矩阵:订单仍可支付则形成 `ProcessedSuccess`,订单已过期或已有其他终态则形成 `Difference`。 - 同一支付流水先成功后失败时,新的失败回调形成 `Ignored`,不得把支付或订单回退;同一支付流水重复成功且不可变事实一致时也形成无副作用的 `Ignored`。 - 不同支付流水对同一订单宣称成功时,第一笔合法成功来源保留,后续成功回调形成 `Difference`,不得覆盖已提交终态。 -- 回调发生时间用于追踪和对账,是否仍可支付以服务端处理时的权威时间和订单截止时间为准,不能信任调用方时间决定状态。 +- 回调发生时间用于追踪和对账,是否仍可支付只以取得全部可能阻塞事实后的 `finalTime` 和订单截止时间为准。请求接收、验签、事务开始、取得订单锁或回调方 `occurredAt` 均不能授权支付;若等待通道尝试或财务水位时跨过截止点,必须按过期矩阵处理。 ## 六、回调事务原子结果 @@ -178,42 +182,93 @@ flowchart TD 原子性要求: -- `ProcessedSuccess` 的模拟通道支付、订单状态、回调终态和可靠支付事实必须同时成功或同时失败。 +- `ProcessedSuccess` 的模拟通道支付、订单状态、回调终态、财务提交序号和可靠支付事实必须同时成功或同时失败;订单 `paidAt`、支付 `paidAt`、回调 `processedAt`、通道尝试最后处理时间和财务水位时间均使用同一 `finalTime`。 - `ProcessedFailure`、`Ignored`、`Difference` 也必须先保存完整确定结果再返回;`Difference` 回调结果本身保留类型、来源和关联事实,供每日批次读取。 +- 每个合法的四终态结果都按同一财务提交水位分配序号,并以 `finalTime` 作为结果成功提交时间;只有 `ProcessedSuccess` 创建成功支付。这样对账按统一提交序号和时间读取,不依赖回调到达时间。 - 回调事务不直接创建管理员待处理差异条目。每日对账只把一个 `Difference` 来源归入一个有效批次和一个差异条目,避免回调阶段与批次阶段重复建单。 - 任一事务回滚后,不得声称某个处理中状态仍已提交;通道使用相同标识重试时重新取得处理资格。 - 通知发生在业务事务提交之后。通知失败只重试投递,不回滚支付、订单或回调事实。 +- 过期回调采用两个独立事务:第一阶段只提交回调四终态、通道聚合和财务水位;提交后再调用 M04 统一过期取消。第二阶段锁定订单后重新取得独立 `cancelDecisionTime`,仅当仍为 `PendingPayment` 且已到截止时间时取消并原子回补。第二阶段不得改写第一阶段 `finalTime` 或回调终态,失败时由 C03 继续收敛。 ## 七、每日对账批次 ```mermaid flowchart TD - W["Worker 每日触发"] --> A["确定上一完整 UTC 业务提交日的固定范围"] - A --> B{"该日期和范围是否已有有效批次?"} - B -- "是" --> X["返回已有批次,不重复统计"] - B -- "否" --> C["读取范围内已提交的同步钱包支付、模拟通道支付、订单终态和回调终态"] - C --> D["读取范围内 M10 已退款事实、退款操作结果和小金库退款入账"] + W["Worker 00:05 UTC 固定触发
每分钟漏跑检查
进程启动立即补查"] --> A["确定已经到达 00:05 触发点的缺失 UTC 业务日"] + A --> A1["按业务日期升序选择首个缺失日;每次只处理一个日期"] + A1 --> A2["以 YYYY-MM-DD 作为该业务日唯一 runKey,领取当日任务责任"] + A2 --> B{"该日期是否已有 Matched / HasDifferences / Resolved 批次?"} + B -- "是" --> X["跳过既有权威批次,继续下一日期"] + B -- "否" --> C["冻结该日半开范围、watermarkSequence 与 watermarkAt"] + C --> C1["在同一 REPEATABLE READ 快照中只读业务事实
不锁定、不修改来源业务表"] + C1 --> D["读取范围内 M10 已退款事实、退款操作结果和小金库退款入账"] D --> E["逐项比对支付、订单、回调与退款三方事实"] - E --> F["合并指向同一业务差异的来源与比较规则;汇总总数、匹配数与差异数"] - F --> F2["未归属来源逐条形成待处理差异;已有来源复用唯一差异条目"] + E --> F["按财务 postingSequence 生成唯一比较锚点并执行固定 12 类规则"] + F --> F2["同一锚点同一规则合并证据;不同规则分别形成差异行"] F2 --> G["在一个原子结果中生成批次与全部差异条目"] G --> H{"生成是否整体成功?"} - H -- "否" --> R["全部回滚,稍后按相同范围重试"] + H -- "否" --> R["该日全部回滚并停止本轮;下次仍从这个缺失日重试"] H -- "是且无差异" --> M["批次 Matched"] H -- "是且有差异" --> N["批次 HasDifferences,管理员页面可查询"] + M --> NEXT["继续下一个缺失的完整 UTC 日"] + N --> NEXT ``` 对账口径: - 每日范围只按业务结果成功提交时的服务端时间归入 UTC 自然日,不使用回调接收时间、通道发生时间或客户端时间。跨 UTC 零点收到但在零点后提交的结果归入新的一日。 -- 批次使用固定的日界线和一致读取水位,只纳入在范围结束前已经成功提交并可见的事实;未在该水位前提交的事实进入其实际提交日的后续批次,不能回填已冻结旧批次。 -- 支付至少核对:成功支付是否有对应 `Paid` 订单、`Paid` 订单是否有且只有一个成功支付来源、回调成功终态是否与支付和订单一致。 -- 回调中的 `Difference` 是差异来源,不是预先创建的差异条目。批次按“差异业务对象及关联标识 + 比较规则”建立唯一归属;来源类型只作为证据。同一问题同时被回调结果和横向比对发现时合并为一个条目并保留全部证据引用。 +- 每日固定触发点是 `00:05:00Z`。Worker 正常运行时在该时刻触发一次,并每分钟执行同一漏跑检查;进程启动时立即执行一次补查。三种入口只负责发现日期,不改变业务口径,也不得为同一日期创建不同任务。 +- `latestEligibleBusinessDate` 只取“已经到达 00:05 触发点”的最近完整日:当前 UTC 时刻不早于 `00:05:00Z` 时取前一日,早于 `00:05:00Z` 时取前两日。当前尚未结束的 UTC 日,以及已经结束但尚未到次日 `00:05:00Z` 的业务日,都不得提前生成。 +- 每个日期范围固定为 `[businessDate 00:00:00Z, businessDate + 1 day 00:00:00Z)`;DB107 固定使用 `jobName=PaymentDailyReconciliation`、`runKey=YYYY-MM-DD`,其中 `runKey` 就是该 UTC `businessDate`,不得附加实例号、重试次数、触发来源或当前时间。 +- 先计算 `coverageStart = min(latestEligibleBusinessDate, earliestPostingDate(若存在), earliestExistingBatchDate(若存在))`;两项历史日期都不存在时取 `latestEligibleBusinessDate`。Worker 在 `[coverageStart, latestEligibleBusinessDate]` 中找最早缺失日并按日期升序处理。这样首个财务事实只出现在尚未到触发点的业务日时,仍会为最近已到触发点的完整 UTC 日生成空批次;无交易且停机多日时也会从既有最早批次之后逐日补齐,不跳过中间空日。 +- 多日停机恢复必须按日期升序逐日补齐,不合并日期、不只处理昨天、不跳过失败日。某日失败后停止本轮;先前已成功日期保持提交,下次从首个缺失日继续。 +- 对账日任务不得进入永久死信:失败日复用同一 DB107 责任行保持 `RetryWait`,按 1 分钟、5 分钟、15 分钟、1 小时封顶退避并持续告警,成功前阻断后续日期。`runKey` 唯一不能成为无法重建同日任务的闭锁点。 +- 每日批次使用独立范围、独立任务责任、独立事务和生成时的一致财务截止水位。任务取得当日执行资格后,先等待已经分配序号的在途财务事务提交或回滚,再读取 DB096 已提交的 `watermarkSequence` 和同一数据库时钟的 `watermarkAt`;随后开启新的 `REPEATABLE READ` 快照读取业务事实。不能先建立旧快照再等待财务水位,也不能在扫描中途移动水位。 +- 对账对订单、支付、回调、售后、退款和钱包来源表一律只读:不使用 `FOR UPDATE`,不借对账修正状态,不为“让结果匹配”而补造或删除来源事实。生成事务只允许写本日期的批次、差异、证据和任务结果;来源模块纠正必须走第八章的独立受控动作。 +- 截止条件固定为 `postingTime ∈ [rangeFrom,rangeTo)` 且 `postingSequence <= watermarkSequence`。只有同时满足二者的财务结果可以成为本日锚点;`watermarkAt` 只用于展示“何时冻结”,不能代替 `postingSequence` 过滤。 +- 锚点稳定引用的更早事实只用于证明该锚点,不重复计数。正向事实必须带有不超过 `watermarkSequence` 的财务谱系;“记录不存在”必须保存同一快照、同一水位和固定查询条件形成的缺失证据。水位之后才出现的支付、订单支付谱系、回调、退款或钱包事实不能用当前值覆盖旧日 `expectedFacts/actualFacts`,而应按自身真实 `postingTime/postingSequence` 进入后续业务日。 +- 模拟通道截至该日的回调状态必须从不可变 DB089 按 `processedAt < rangeTo AND postingSequence <= watermarkSequence` 重建,同一流水按 `postingSequence/serverSequence` 排序聚合;DB084 只提供首次不可变绑定和当前运行聚合,不能用其可变 `last*` 字段重建历史批次。 +- 订单支付一致性按“已支付谱系”判断:`Paid/Shipped/Completed` 且 `paidAt/paymentPostingSequence` 非空都必须有唯一成功 DB085;成功 DB085 对应订单允许处于这三态。`PendingPayment/Cancelled` 与成功支付不相容,不能把合法履约后的 `Shipped/Completed` 误报成“订单未更新”。 +- 无交易的完整 UTC 日也生成 `totalCount=0`、`differenceCount=0`、状态为 `Matched` 的空批次,以区分“已经对账但无交易”和“任务从未执行”。 +- 已存在的 `Matched`、`HasDifferences` 或 `Resolved` 均是该日期权威批次,不覆盖、不重新生成,也不因比较规则升级静默重写;差异保存生成时的比较规则版本。 +- 唯一 `postingSequence` 是唯一比较锚点。共享同一序号的一组原子财务事实按 `RefundOperation > Payment > Callback` 选择锚点类型:有成功退款操作就归 `RefundOperation`,否则有成功支付就归 `Payment`,只有回调确定终态时才归 `Callback`。缺失事实规则使用携带该序号的权威来源 ID 作为非空锚点 ID,例如支付缺失时使用订单 ID、退款操作缺失时使用售后申请 ID;不得生成空 ID 或临时随机 ID。 +- 同一原子结果中的同步钱包支付与扣款流水、模拟成功回调与支付、退款申请终态与退款操作、钱包入账及支付累计退款共享一个锚点。出现多个成功支付、多个成功退款操作或多个钱包入账时,首个合法结果按自身锚点比较,之后每个新冲突结果以自己的较大 `postingSequence` 作为冲突锚点,不能把多次真实资金结果压成一个无从追踪的计数。 +- `totalCount` 等于本日去重后的锚点数;`matchedCount` 是零条差异规则命中的锚点数;`differenceCount` 是至少命中一条规则的锚点数,必须始终满足 `totalCount = matchedCount + differenceCount`。同一锚点命中多条不同规则时,`differenceCount` 仍只加一。 +- `paymentUnitCount` 统计 `Payment` 与 `Callback` 锚点,`refundUnitCount` 统计 `RefundOperation` 锚点,必须满足 `totalCount = paymentUnitCount + refundUnitCount`;主体类型不参与批次计数。 +- `differenceCountsByType` 按差异行的 `differenceType` 分组计数,不按锚点去重,因此各类型数量之和可以大于 `differenceCount`。同一 `(batchId, anchorPostingSequence, comparisonRuleCode, comparisonRuleVersion)` 只能形成一条差异,重复发现只合并证据;不同规则不得因为主体相同而互相覆盖。 +- 回调中的 `Difference` 是差异来源,不是预先创建的管理员条目。同一锚点同一规则同时被回调结果和横向比对发现时合并为一条差异并保留全部证据引用。 - `ProcessedFailure` 不应被误认作成功支付缺失;同一支付流水后续成功时,以后续成功回调的提交日和最终聚合结果参加对账。 -- 退款至少核对:M10 `Refunded` 终态、成功退款操作和本人小金库入账三方是否一一对应且金额一致。 - Worker 重复执行同一范围只返回已有批次;任务中断时批次和差异必须同时不存在或同时完整。 - 本期不自动修复资金和订单,也不通过对账任务发送管理员站内消息;管理员在对账页面通过查询或轮询看到新批次。 +### 7.1 十二类差异的固定检测矩阵 + +下表是差异生成和后续复核的同一事实源。`规则` 固定为 `comparisonRuleCode / comparisonRuleVersion`;版本 `1` 的 `expectedFacts`、`actualFacts` 只能出现表中列出的键,缺失值写 JSON `null`,不得省略键或塞入自由文本。金额统一为两位小数的 CNY 字符串,UUID 使用小写 D 格式,时间使用 UTC,ID 数组按 `postingSequence`、ID 升序,枚举使用本文英文值。规则升级必须新增版本,不能改写已生成批次。 + +`anchorKind` 只允许 `Payment`、`Callback`、`RefundOperation`,`anchorId` 和 `anchorPostingSequence` 按矩阵固定;`subjectType` 只允许 `Order`、`Payment`、`Callback`、`AfterSalesRequest`、`RefundOperation`、`WalletTransaction`,每类差异的 `subjectType/subjectId` 也由矩阵固定。实现不得为方便查询在这些类型之间改选,也不得用 `orderId` 统一替代所有主体。 + +| 差异类型 | 锚点 | 主体 | 规则 | `expectedFacts` 固定键 | `actualFacts` 固定键 | 必须保存的水位内证据 | +|---|---|---|---|---|---|---| +| `payment_succeeded_order_not_updated` | `Payment / paymentId / payment.postingSequence` | `Order / orderId` | `C08.PAYMENT_ORDER_LINEAGE / 1` | `paymentId,orderId,success,amount,currency,paymentPostingSequence,allowedOrderStatuses` | `orderStatus,paidAt,paymentPostingSequence,orderAmount,orderCurrency` | DB085 成功支付、DB061 订单支付谱系;支付序号与批次水位 | +| `order_paid_payment_missing` | `Payment / orderId / order.paymentPostingSequence`,支付缺失时以订单 ID 作为锚点 ID | `Order / orderId` | `C08.ORDER_PAYMENT_PRESENCE / 1` | `orderId,orderPaidLineage,paymentPostingSequence,successfulPaymentCount,amount,currency` | `orderStatus,paidAt,paymentPostingSequence,successfulPaymentCount,paymentIds,paymentPostingSequences` | DB061 已支付谱系;同一订单 DB085 成功支付缺失的水位查询条件与空结果 | +| `multiple_successful_payment_sources` | `Payment / laterPaymentId / laterPayment.postingSequence` | `Payment / laterPaymentId` | `C08.ORDER_SINGLE_SUCCESS_SOURCE / 1` | `orderId,successfulPaymentCount,authoritativePaymentId,amount,currency` | `successfulPaymentCount,paymentIds,sourceTypes,postingSequences,amounts,currencies` | DB061 订单;DB085 全部成功来源;存在模拟回调时追加 DB089 | +| `late_success_callback` | `Callback / callbackId / callback.postingSequence` | `Callback / callbackId` | `C08.CALLBACK_LATE_SUCCESS / 1` | `acceptedSuccess,paymentCreated,walletDelta,successNotificationCreated` | `callbackResult,callbackDisposition,orderId,orderStatus,paymentDeadline,finalTime,paymentCreated,walletDelta,successNotificationCreated` | DB089 成功回调确定终态、DB061 截止与取消事实、DB085/DB083/可靠消息副作用存在或缺失证明 | +| `callback_binding_mismatch` | `Callback / callbackId / callback.postingSequence` | `Callback / callbackId` | `C08.CALLBACK_BINDING_IMMUTABLE / 1` | `channelTransactionNo,boundOrderId,boundAmount,boundCurrency` | `callbackId,claimedOrderId,claimedAmount,claimedCurrency,mismatchedFields` | DB084 首次不可变绑定、DB089 当前回调与主裁决码;两者序号和指纹 | +| `processed_success_payment_missing` | `Callback / callbackId / callback.postingSequence` | `Callback / callbackId` | `C08.CALLBACK_SUCCESS_PAYMENT_PRESENCE / 1` | `callbackDisposition,successfulPaymentCount,orderPaidLineage,orderId,amount,currency` | `callbackDisposition,successfulPaymentCount,paymentIds,orderStatus,paidAt,paymentPostingSequence` | DB089 `ProcessedSuccess` 回调、DB061 订单;DB085 成功支付缺失的水位查询条件与空结果 | +| `refunded_operation_missing` | `RefundOperation / afterSalesRequestId / afterSales.refundPostingSequence`,退款操作缺失时以申请 ID 作为锚点 ID | `AfterSalesRequest / afterSalesRequestId` | `C08.AFTER_SALES_REFUND_OPERATION_PRESENCE / 1` | `afterSalesRequestId,afterSalesStatus,successfulRefundOperationCount,approvedRefundAmount,currency,refundPostingSequence` | `afterSalesStatus,closedAt,refundPostingSequence,successfulRefundOperationCount,refundOperationIds` | DB086 `Refunded` 事实及 DB087 时间线;DB088 成功操作缺失的水位查询条件与空结果 | +| `duplicate_refund_operation` | `RefundOperation / laterRefundOperationId / laterRefundOperation.postingSequence` | `RefundOperation / laterRefundOperationId` | `C08.REFUND_OPERATION_SINGLE_SUCCESS / 1` | `afterSalesRequestId,successfulRefundOperationCount,authoritativeRefundOperationId,amount,currency` | `successfulRefundOperationCount,refundOperationIds,postingSequences,amounts,currencies` | DB086 申请、DB088 全部成功退款操作及各自序号 | +| `refund_succeeded_after_sales_not_updated` | `RefundOperation / refundOperationId / refundOperation.postingSequence` | `AfterSalesRequest / afterSalesRequestId` | `C08.REFUND_AFTER_SALES_TERMINAL / 1` | `refundOperationId,refundOperationStatus,afterSalesStatus,amount,currency,refundPostingSequence` | `refundOperationStatus,completedAt,afterSalesStatus,closedAt,refundPostingSequence` | DB088 成功退款操作、DB086 当前终态与 DB087 状态时间线 | +| `refund_succeeded_wallet_credit_missing` | `RefundOperation / refundOperationId / refundOperation.postingSequence` | `RefundOperation / refundOperationId` | `C08.REFUND_WALLET_CREDIT_PRESENCE / 1` | `refundOperationId,buyerId,walletCreditCount,walletCreditAmount,currency,refundPostingSequence` | `walletCreditCount,walletTransactionIds,walletCreditAmounts,walletPostingSequences` | DB088 成功退款、DB086 买家与金额;DB083 退款入账缺失的水位查询条件与空结果 | +| `duplicate_wallet_credit` | `RefundOperation / refundOperationId / laterWalletTransaction.postingSequence`,锚点类别仍归退款业务,序号取新增冲突入账 | `WalletTransaction / laterWalletTransactionId` | `C08.REFUND_WALLET_SINGLE_CREDIT / 1` | `refundOperationId,buyerId,walletCreditCount,walletCreditAmount,currency` | `walletCreditCount,walletTransactionIds,walletCreditAmounts,walletPostingSequences` | DB088 退款操作、DB083 指向同一退款业务身份的全部入账;DB081 当前余额只作辅助证据、不作金额是否匹配的唯一依据 | +| `refund_amount_mismatch` | `RefundOperation / refundOperationId / refundOperation.postingSequence` | `RefundOperation / refundOperationId` | `C08.REFUND_AMOUNT_CONSISTENCY / 1` | `afterSalesRequestId,refundOperationId,approvedRefundAmount,refundOperationAmount,walletCreditAmount,paymentRefundedTotalDelta,currency` | `approvedRefundAmount,refundOperationAmount,walletCreditAmount,paymentRefundedTotalDelta,currencies,walletCreditCount` | DB086 批准退款金额、DB088 操作金额、DB083 入账、DB085 累计退款变化及共同财务序号 | + +矩阵执行补充规则: + +- `allowedOrderStatuses` 固定为 `["Paid","Shipped","Completed"]`;`success=true`、`orderPaidLineage=true`、各单数期望值 `1`、`acceptedSuccess=false`、`paymentCreated=false`、`walletDelta="0.00"` 和 `successNotificationCreated=false` 都必须作为结构化期望值保存,不能只写进说明。 +- 一个锚点可同时命中多条规则。例如 `ProcessedSuccess` 回调缺 DB085 且订单仍未进入已支付谱系时,同一 Callback 锚点同时生成 `processed_success_payment_missing` 和适用的订单一致性差异行;批次 `differenceCount` 只增加一,`differenceCountsByType` 两类各增加一。 +- 缺失证据不是“没有 evidence”。生成器必须保存批次 ID、规则码/版本、`rangeFrom/rangeTo/watermarkSequence`、规范查询主体和空结果哈希;解决时用同一规则和当前权威水位重新查询。截图、日志文本或管理员口述不能替代缺失证明。 +- DB084 当前聚合、DB081 当前余额和页面读模型都只能作为辅助证据,不能覆盖 DB089 不可变回调、DB083 不可变钱包流水、DB085 支付、DB087 售后时间线及冻结财务序号形成的历史结论。 + ## 八、差异领取与闭环 ```mermaid @@ -221,11 +276,13 @@ flowchart TD A["管理员打开 HasDifferences 批次"] --> B["查看差异类型、关联业务事实和时间线"] B --> C{"领取 Pending 差异?"} C -- "状态已变化" --> X["返回当前处理人和状态,不重复领取"] - C -- "成功" --> D["差异进入 InProgress,并记录当前领取人"] - D --> T{"当前领取人仍有效且持有领取权?"} - T -- "否" --> U["其他管理员显式接管或原领取人释放;保留领取与转交历史"] + C -- "成功" --> D["差异进入 InProgress
领取固定 30 分钟且不可续期"] + D --> T{"当前管理员仍在固定领取期内?"} + T -- "否" --> U["先保存未提交草稿并刷新详情"] + U --> U1{"A425 Takeover 是否成功?"} + U1 -- "否,版本或领取人已变化" --> X + U1 -- "是,重新取得固定 30 分钟" --> E T -- "是" --> E["通过所属模块核实;必要纠正必须使用该模块受控业务动作"] - U --> E E --> F{"选择受控处置结果"} F -- "已纠正" --> F1["引用所属模块已完成的纠正动作"] F -- "确认无业务影响" --> F2["引用可验证的重复、迟到或无资金变更规则"] @@ -243,11 +300,43 @@ flowchart TD - C08 差异处理本身不直接改订单、支付、退款或钱包;如确需纠正,管理员先通过事实所属模块的受控动作完成,再在差异中引用结果。 - 差异只有两类可关闭结果:“所属模块已纠正且复核一致”或“按固定规则确认没有未决资金与订单影响”。仅填写文字说明、承诺稍后处理或接受仍存在的不一致,都不能进入 `Resolved`。 - 关闭时必须重新读取权威业务事实并执行产生该差异的同一比较规则;复核仍不一致时保持 `InProgress`。 -- 当前领取人被禁用、主动释放或领取权失效时,其他管理员可显式接管;接管不清空原处理记录。领取有效期和续期方式由接口与配置派生,但任何时刻只有当前有效领取人可以提交关闭。 +- A425 `Claim` 或 `Takeover` 成功时使用同一数据库权威时间设置 `claimedAt`,并固定 `claimExpiresAt = claimedAt + 30 minutes`。本期不提供 `Renew` 动作,不发送心跳续期,不因打开页面、查询详情、保存本地草稿或正在编辑而延长;部署配置也不得改变 30 分钟业务规则。 +- 到达 `claimExpiresAt` 后差异仍为 `InProgress`,但原领取权立即失效,原处理人不能继续 `Resolve`。页面必须保留其尚未提交的说明和证据为本地草稿、刷新 A426 最新版本,再使用新的 `Idempotency-Key` 和最新 `expectedVersion` 调用 `Takeover` 重新认领;若已被其他管理员接管,只能保留或复制草稿,不得覆盖新领取人。 +- 当前领取人被禁用、主动 `Release` 或领取过期时,其他管理员也可显式 `Takeover`;接管必须写新 `claimedAt/claimExpiresAt` 并保留原领取、失效、释放和转交历史。任何时刻只有当前管理员、领取未过期且请求版本匹配时可以提交关闭。 +- 领取权只控制谁可以提交 A425 处置,不是订单、支付、售后或钱包业务锁;领取期间业务模块仍可按自身流程推进,解决时必须以最新权威事实重跑原规则。 - 处理说明、处理人、处理时间、证据引用、差异终态和最后一个差异触发的批次终态必须形成可恢复的一致结果。 - 不建设需求外的通用后台审计框架;差异记录自身的状态时间线就是本流程所需追踪证据。 - 管理员并发领取或关闭同一差异时只有一人成功,其他请求返回最新状态。 +### 8.1 十二类差异的固定关闭矩阵 + +“确认无业务影响”必须由系统重新读取权威事实并按固定规则证明,不接受管理员仅凭文字说明、截图或手写编号确认。“受控纠正”只能复用事实所属模块已经确认的业务动作;C08 不直接修改订单、支付、退款、钱包或库存,且对账 Worker 不自动纠正下列任何差异。 + +| 差异类型 | `ConfirmedNoBusinessImpact` 条件 | `CorrectedByControlledAction` 条件 | 明确禁止 | +|---|---|---|---| +| `payment_succeeded_order_not_updated` | 不允许;成功支付对应订单未进入或未保留 `Paid/Shipped/Completed + paidAt/paymentPostingSequence` 已支付谱系是实质不一致 | Payment/Ordering 已按完整事实完成合法结果或既有补偿,且原比较规则重新匹配 | C08 自动改 `Paid`、自动取消或自动退款 | +| `order_paid_payment_missing` | 不允许;处于 `Paid/Shipped/Completed` 已支付谱系却缺唯一成功支付会破坏履约与审计 | 所属模块从不可伪造证据恢复唯一成功支付事实,或完成已确认补偿,且复核匹配 | 凭订单状态伪造支付、自动回退订单 | +| `multiple_successful_payment_sources` | 仅当一个来源是唯一权威支付,其他来源均无额外扣款、入账、履约或通知副作用 | 真实存在多个结算结果时,由 Payment 对非权威来源执行已确认补偿并复核 | C08 删除/覆盖记录或自行选择权威来源 | +| `late_success_callback` | 订单保持 `Cancelled`,回调为 `Difference`,且无成功支付、钱包变化、重复履约或成功通知 | 通常不需要;若有真实资金影响,应改按相应资金差异处理 | 将订单恢复 `Paid`,或用本类型掩盖资金影响 | +| `callback_binding_mismatch` | 错误绑定已被拒绝,首次绑定未变,且未创建支付、修改订单或产生钱包副作用 | 通常不需要;若业务事实已污染,改按对应支付差异处理 | 按新回调重绑流水、覆盖首次绑定或删除冲突证据 | +| `processed_success_payment_missing` | 不允许;回调成功终态缺少支付事实是实质不一致 | Payment 按回调、订单、流水和提交水位恢复或补偿并复核 | 仅凭回调终态补造支付 | +| `refunded_operation_missing` | 不允许;`Refunded` 缺少成功退款操作会破坏资金链 | M10/Payment 使用既有退款恢复核实并补齐原结果,且三方复核匹配 | 新建无法证明来源的退款或盲目再次入账 | +| `duplicate_refund_operation` | 仅当多条记录最终对应同一资金结果,且无重复钱包入账、库存回补或退款累计 | 真实重复结算由 Payment 已确认的补偿动作处理并复核 | 删除历史、自动冲销或再次执行退款 | +| `refund_succeeded_after_sales_not_updated` | 不允许;资金成功但售后未完成会影响用户状态和资格 | M10 退款恢复完成售后状态、时间线及相关事实后复核 | C08 直接推进售后状态 | +| `refund_succeeded_wallet_credit_missing` | 不允许;买家应收资金缺失有直接业务影响 | Payment 使用原稳定退款操作身份核实并完成既有退款恢复 | 无来源充值或使用新退款身份重复退款 | +| `duplicate_wallet_credit` | 不允许;重复入账有直接资金影响 | 本期没有通用扣款/余额调整动作;只有未来需求明确确认的 Payment 冲正动作才能处理 | 自动扣余额、删流水、按当前余额推测修复;本期保持 `InProgress` | +| `refund_amount_mismatch` | 不允许;真实金额不一致不能按无影响关闭 | 金额权威来源明确且所属模块已有合法动作修正非权威事实时,动作完成后复核 | 选择最大/最小/任一记录,或自动补差、扣差 | + +“有条件允许”不等于 C08 自己执行动作。若差异既没有合法的所属模块动作,又不满足确认无影响的硬条件,必须保持 `InProgress`,不能为了让批次变为 `Resolved` 强行关闭。M10 退款恢复 Worker 属于退款主流程恢复,不是对账自动纠正;恢复成功后,C08 只引用结果并复核。 + +### 8.2 受控动作引用与双事务边界 + +- 本期不新增通用纠正接口,也不在差异接口中增加 `Correct` 动作。管理员先通过事实所属模块既有的合法动作完成纠正;所属模块成功提交后签发稳定 `controlledActionRef`,当前领取管理员再以“解决”动作引用它。 +- `CorrectedByControlledAction` 必须提供 `controlledActionRef`;`ConfirmedNoBusinessImpact` 必须不提供。其他辅助证据使用规范化的 `evidenceRefs`,不能把文字说明当成系统事实。 +- C08 必须通过所属模块只读公开能力验证引用真实存在、已成功提交、模块与差异类型匹配,且订单/支付/售后/退款/钱包主体、买家、金额、币种和稳定业务身份与当前差异一致。失败、处理中、结果未知、已回滚、日志文本、截图链接和手写编号都不是合法引用。 +- 纠正与闭环是两个独立事务:所属模块事务先完成自身资金/订单/库存/Outbox 和幂等结果;C08 闭环事务再锁定差异与批次,校验当前领取、请求版本和幂等指纹,只读验证动作引用、重跑原 `comparisonRuleCode + comparisonRuleVersion`,随后原子保存证据、复核时间线、差异终态和必要的批次终态。 +- 解决请求必须携带 `expectedVersion`。状态、领取或版本已变化时返回最新事实,不覆盖其他管理员结果;复核仍不一致时保持 `InProgress`、记录 `verification_failed` 并返回剩余原因。 + ## 九、退款对账 ```mermaid @@ -277,10 +366,16 @@ flowchart TD | 成功与失败乱序 | 按唯一标识、支付流水和订单终态裁决 | 不让 `Paid` 反向跳变 | | 过期或已取消订单收到成功回调 | 登记 `Difference` | 不推进 `Paid` | | 已由 M05 支付后收到新成功回调 | 登记 `Difference` | 不重复支付,不扣钱包 | +| 回调等待共享锁时跨过支付截止点 | 以最后共享锁后的 `finalTime` 执行过期矩阵 | 不创建成功支付;第一阶段提交后独立触发过期取消 | | 回调事务失败 | 整体回滚 | 无已提交处理中记录,可安全重试 | -| Worker 重复或中断 | 返回已有批次或整体重试 | 不生成重复 / 半批次 | +| Worker 重复或单日中断 | 返回既有日期批次,或仅回滚失败日期 | 不生成重复 / 半批次;后续从首个缺失日继续 | +| Worker 停机多日 | 按日期升序逐日补齐所有完整 UTC 日,空日也建批 | 不合并、不跳日,不重开历史批次 | +| Worker 在 `00:05Z` 前启动或检查 | 最近结束日尚未到触发点,不提前生成 | 只补更早且已到触发点的缺失日 | +| 同一锚点命中多条规则 | 每条规则形成一条差异、证据分别保存 | `differenceCount` 只计一个锚点,类型汇总按多条差异行计 | | 差异并发领取或解决 | 一次状态推进成功 | 其他请求返回当前状态 | +| 原领取人编辑期间超过 30 分钟 | 本地保存草稿、刷新详情并显式 `Takeover` | 不自动续期;他人已接管时不得覆盖 | | 管理员直接改库企图隐藏差异 | 不属于合法流程 | 必须使用所属模块受控动作并留证 | +| 差异没有合法纠正动作且不能证明无影响 | 保持 `InProgress` | 不得为关闭批次而强制解决 | | M09 通知暂时失败 | 保留支付成功事实 | 可靠投递重试,不修改回调结果 | ## 十一、由流程派生的接口契约映射 @@ -289,53 +384,60 @@ flowchart TD | 已确认流程能力 | 派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 接收受控模拟支付回调 | A421 | 来源鉴别、回调标识幂等、支付流水聚合与乱序、截止时间和状态竞争、四种确定终态 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 对账批次列表 | A422 | 管理员按日期与状态分页查看 `Matched` / `HasDifferences` / `Resolved` 批次 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 对账批次详情 | A423 | 返回范围、总数、匹配数、差异数、类型汇总和当前闭环状态 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 差异列表 | A424 | 管理员按批次、类型和状态分页查询差异摘要 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 差异领取、转交与解决 | A425 | 条件领取 / 接管、受控处置类型、权威事实复核、原子关闭差异并按需关闭批次 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 差异详情 | A426 | 返回比较规则、期望与实际事实、全部来源证据、领取/转交/处置和复核时间线 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | +| 接收受控模拟支付回调 | A421 | 来源鉴别、回调标识/支付流水串行、最后共享锁后的唯一 `finalTime`、两阶段过期取消、四种确定终态 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 对账批次列表 | A422 | 管理员按日期与状态分页查看 `Matched` / `HasDifferences` / `Resolved` 批次,并返回固定 UTC 范围和冻结水位 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 对账批次详情 | A423 | 返回锚点 `totalCount/matchedCount/differenceCount`、按差异行统计的 `differenceCountsByType`、范围、水位和闭环状态;类型汇总和可大于 `differenceCount` | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 差异列表 | A424 | 管理员按批次、类型和状态分页查询差异摘要;摘要稳定暴露锚点、主体和规则版本,不得让调用方重新选择比较主体 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 差异领取、转交与解决 | A425 | `Claim/Release/Takeover/Resolve`;领取固定 30 分钟且无 `Renew`,过期后用最新版本显式接管;解决时校验 `expectedVersion`、所属模块 `controlledActionRef` 或无影响规则,重跑原规则并原子关闭差异及必要批次;不提供通用 `Correct` | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 差异详情 | A426 | 返回固定锚点、主体、规则码/版本、期望/实际 JSON、全部来源证据、领取到期、转交/处置和复核时间线 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | A421 以回调标识和请求指纹保证同一次投递幂等,以支付流水聚合一次支付尝试的多个时序信号,不要求客户端另造独立幂等语义;A422~A426 只向管理员开放。具体 HTTP 方法、路径、签名头、请求响应、状态码和错误码在接口阶段由本表派生。 ## 十二、跨模块边界 - **M05/F10**:小金库同步支付是默认买家路径;C08 不扣小金库。两者只通过 M04 订单状态竞争,不能同时形成成功支付。 -- **M04/F09/C03**:订单状态和支付截止时间是权威裁决。到期后即使 C03 尚未扫描,C08 也不能确认支付;过期回调触发同一内部取消能力。 +- **M04/F09/C03**:订单状态和支付截止时间是权威裁决。A421 以全部回调共享事实就绪后的 `finalTime` 决定回调终态;到期后即使 C03 尚未扫描也不能确认支付。回调第一阶段提交后,M04 在独立事务以 `cancelDecisionTime` 重检并取消,失败由 C03 继续收敛。 - **M09**:只通知已提交的支付成功结果。失败、忽略和差异不作为买家 / 商家支付成功通知;管理员通过对账页面查看差异。 -- **M10**:只把 `Refunded` 终态、成功退款操作和钱包入账纳入成功三方对账;`RefundFailed` 仍由 M10 重试。 +- **M10**:只把 `Refunded` 终态、成功退款操作和钱包入账纳入成功三方对账;退款恢复属于 M10 主流程,不是 C08 自动纠正。恢复成功后可签发受控动作引用供差异复核。 - **C10**:多个 API 实例可并发接收回调,业务唯一处理资格与共享事务确保只处理一次;不能依赖进程内记忆去重。 - **C06**:不向管理员建立需求外实时推送;对账页面使用查询或轮询即可。 -## 十三、接口与后续数据设计必须承接的事实 +## 十三、接口与数据库设计承接事实 1. 回调终态只有 `ProcessedSuccess`、`ProcessedFailure`、`Ignored`、`Difference`;基础设施处理失败不占用这四个业务终态。 2. 同一回调标识只能绑定一个请求内容和一个首次确定结果;同一支付流水绑定稳定的订单、金额和币种,但允许多个回调标识形成有序聚合,必须支持失败后成功和成功后失败。 -3. 成功回调必须同时满足待支付状态、截止时间、金额、币种和订单关联,且模拟通道支付、订单 `Paid`、回调终态与可靠支付事实原子提交。 +3. 成功回调必须在取得该路径全部可能阻塞共享事实后的唯一 `finalTime` 同时满足待支付状态、截止时间、金额、币种和订单关联,且模拟通道支付、订单 `Paid`、回调终态、财务序号与可靠支付事实原子提交;成功相关时间都使用同一 `finalTime`。 4. 回调失败事务整体回滚后不存在已提交 `Processing` 记录;不得设计“同事务回滚但 Processing 仍保留”的不可能状态。 5. 对账批次直接生成 `Matched` 或 `HasDifferences`,不增加 `Pending`;差异自身使用 `Pending` / `InProgress` / `Resolved`。 6. 最后一个差异关闭时,差异处理结果和批次 `Resolved` 必须在一致边界内完成,不能先返回成功再异步补批次状态。 -7. C08 的 `Difference`、每日比对发现的支付差异和退款差异都进入统一差异闭环,但同一业务差异与比较规则只能归属一个有效差异条目,并保留所有发现来源和证据。 +7. C08 的 `Difference`、每日比对发现的支付差异和退款差异都进入统一差异闭环;唯一财务 `postingSequence` 是计数锚点,同一锚点同一规则版本只能归属一个有效差异条目并合并全部证据,同一锚点命中不同规则时分别建行但 `differenceCount` 只计一次。 8. 支付成功来源必须区分小金库与受控模拟通道;同一订单最多一个成功来源,回调不得生成钱包流水。 -9. 管理员差异处理不直接更新业务表;需要纠正时必须引用所属模块的受控操作结果,并由系统重跑原比较规则确认已经一致。 -10. 对账归属只使用成功提交时间和固定 UTC 水位;领取转交、处置类型、复核失败和当前领取人权限必须由接口承载。 -11. A421~A426 已从本文逐项复核鉴别、幂等、截止时间、乱序、事务、批次和差异状态;后续数据库、OpenAPI 与实现继续承接,不能用旧接口草案反向修改流程。 +9. 管理员差异处理不直接更新业务表;不提供通用纠正动作。需要纠正时必须引用所属模块已成功提交且可验证的 `controlledActionRef`,并由系统重跑原比较规则确认一致;无合法动作且不能证明无影响时保持处理中。 +10. 对账归属只使用成功提交 `finalTime`、财务提交序号和固定 UTC 水位;来源业务事实采用取得水位之后的新 `REPEATABLE READ` 快照只读,不能移动截止点或用后续当前值污染旧日结论。 +11. 调度固定为每日 `00:05Z`、每分钟漏跑检查和启动补查,`runKey=YYYY-MM-DD`;多日缺失按日期升序逐日补齐,空日也生成权威批次,失败日阻断后续日期,历史批次不得重开。 +12. 领取转交、请求版本、处置类型、受控动作引用、证据、复核失败和当前领取人权限必须由接口承载;领取固定 30 分钟且不可续期,过期草稿不得绕过重新接管;最后一个差异关闭与批次终态在同一一致边界完成。 +13. A421~A426 与 DB084/DB085、DB089~DB096、DB107 必须逐项承接本文的鉴别、幂等、截止时间、乱序、事务、批次、锚点、12 类规则矩阵和差异状态;后续 OpenAPI 与实现不能用旧接口草案或预想代码反向修改流程。 ## 十四、验收证据清单 - [ ] 同一回调标识重复提交只返回该回调首次结果;同标识换内容被拒绝且首次结果不被覆盖。 - [ ] 同一支付流水的失败后成功、成功后失败和重复成功均按聚合规则处理,既不被幂等门口误拒绝,也不重复支付。 -- [ ] 合法成功回调仅在订单仍为 `PendingPayment` 且早于截止时间时推进 `Paid`,并且不扣小金库。 +- [ ] 合法成功回调仅在取得全部可能阻塞共享事实后的 `finalTime` 仍满足订单 `PendingPayment` 且早于截止时间时推进 `Paid`,并且不扣小金库。 +- [ ] 人为阻塞通道尝试或财务水位直至跨过截止点后再放行,A421 按过期矩阵提交确定回调结果且不创建成功支付;随后独立取消失败时由 C03 继续收敛。 - [ ] M05 同步支付与 C08 成功回调并发时最多一个成功来源;另一方不重复入账。 - [ ] 订单已取消、已过期或已支付后收到新的成功回调,订单终态不变并登记差异。 - [ ] 成功与失败回调乱序不会让 `Paid` 反向跳变。 - [ ] 回调事务任一步失败时无支付、订单、回调或可靠事实半提交,原回调可安全重试。 - [ ] 回调业务终态中不存在含义冲突的 `Processing` / `Failed`;失败通道结果与处理器故障可以明确区分。 -- [ ] Worker 只按成功提交时间和固定 UTC 水位归属事实;跨零点与迟到提交不遗漏,重复执行只得到一个有效批次。 -- [ ] 回调 `Difference` 与横向比对发现的同一问题只形成一个有效差异条目,不重复计数或处理。 +- [ ] `00:05Z` 固定触发、每分钟漏跑检查和启动补查都只创建 `runKey=YYYY-MM-DD` 的同一日期责任;`00:05Z` 前启动不会提前生成最近结束日。 +- [ ] Worker 先取得已提交水位,再开启新的 `REPEATABLE READ` 快照只读来源业务事实;跨零点与迟到提交不遗漏,水位后的当前值不污染旧日快照,重复执行只得到一个有效批次。 +- [ ] 停机多日后按日期升序补齐每个已经到触发点的完整 UTC 日;无交易日生成空 `Matched` 批次,失败日之后不跳跃,历史批次不重开。 +- [ ] 12 类检测分别验证固定锚点、主体、规则码/版本、期望/实际 JSON 键和证据来源;同锚点同规则多来源只形成一行,不同规则分别建行。 +- [ ] `totalCount=matchedCount+differenceCount=paymentUnitCount+refundUnitCount` 且全部按唯一 `postingSequence` 锚点统计;构造一个锚点同时命中两条规则时 `differenceCount=1`,两类 `differenceCountsByType` 各为 1。 - [ ] 批次只使用 `Matched` / `HasDifferences` / `Resolved`,差异只使用 `Pending` / `InProgress` / `Resolved`。 -- [ ] 管理员并发领取、接管或关闭同一差异仅一次成功;领取人失效后可追踪转交。 -- [ ] 仍存在资金或订单不一致时不能仅凭文字说明关闭;处置类型、权威事实复核、证据和批次收敛一致。 +- [ ] 管理员并发领取、接管或关闭同一差异仅一次成功;`claimExpiresAt=claimedAt+30 minutes` 且没有续期入口,过期原处理人保留草稿、刷新版本并重新 `Takeover`,他人已接管时不能覆盖。 +- [ ] 仍存在资金或订单不一致时不能仅凭文字说明关闭;十二类差异逐类遵守固定关闭矩阵,`expectedVersion`、受控动作引用、权威事实复核、证据和批次收敛一致。 +- [ ] 本期没有合法纠正动作的真实重复钱包入账或无法纠正金额差异保持 `InProgress`,系统不自动扣余额、删流水或为关闭批次伪造解决结果。 - [ ] 对账能发现支付成功但订单未更新、订单已支付但无成功支付、重复成功来源和回调差异。 - [ ] 退款对账能发现 M10 `Refunded`、成功退款操作与小金库入账之间的缺失、重复和金额不一致。 - [ ] `RefundFailed` 不伪造成功退款记录,M10 重试成功后才进入成功三方匹配。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" index b11b6a7..066d404 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M05-\346\224\257\344\273\230\346\265\201\347\250\213.md" @@ -9,7 +9,7 @@ ## 一、范围与事实来源 -本模块负责买家“小金库”的余额查询、模拟充值、充值记录、统一收银台、模拟支付和支付记录查询。它不接入真实支付渠道,不处理银行卡、第三方支付账户、真实资金结算、售后退款或 C08 异步回调。 +本流程负责买家“小金库”的余额查询、模拟充值、充值记录、统一收银台、模拟支付和支付记录查询。它不接入真实支付渠道,不处理银行卡、第三方支付账户或真实资金结算;买家售后退款入口和审核状态由 M10 编排,C08 异步回调由挑战流程处理,但两者形成资金结果时统一调用 Payment 公开应用契约,不能另建第二套钱包入账规则。 本文先依据需求确定业务参与者、状态、判断分支、事务结果和模块出入口,再由这些流程步骤派生接口能力。A401~A408 只用于流程完成后的契约映射和缺口检查,不能反向决定或拼接业务流程。 @@ -18,7 +18,7 @@ | M05-01/F10 需求 | 完整定义 | 作为业务语义事实源 | | 本文业务流程 | 完整定义,已完成统稿校准 | 角色、状态、分支、事务边界和模块出入口已闭合,可作为下游设计输入 | | A401~A408 接口 | 完整定义,待交叉评审 | 已按本文流程派生并闭合核心契约;后续评审不得反向改写业务语义 | -| DB081~DB100 数据库设计 | 模板/占位 | 本文不发明表字段、枚举编码和索引 | +| DB081~DB096 及公共表数据库设计 | 完整定义,已确认设计 | 本文以已确认业务语义约束数据库,不反向用字段或索引改写流程 | | X04/C08 | 独立扩展 | 只登记接入点,不混入 F10 核心状态机 | ## 二、模块直接出入口 @@ -93,10 +93,11 @@ flowchart TD V -- "是" --> G{"该买家与稳定标识是否已有确定结果?"} G -- "同标识同请求" --> P0 G -- "同标识不同请求" --> Y["拒绝标识被不同请求复用"] - G -- "没有结果" --> H["开启支付事务并重新读取订单归属、金额、状态、截止时间和余额"] - H --> I{"仍属于本人、仍为 PendingPayment 且权威时间仍早于截止时间?"} - I -- "否" --> RSTATE["整体回滚,形成已支付 / 已取消 / 已过期等确定业务结果"] - I -- "是" --> J["对本人钱包执行余额充足条件扣减"] + G -- "没有结果" --> H["开启支付事务;按统一顺序取得订单、既有支付、本人钱包和财务提交水位"] + H --> I["取得最后一个可能阻塞的共享事实后
只读取一次数据库 finalTime"] + I --> I2{"仍属于本人、仍为 PendingPayment、金额一致、余额充足
且 finalTime < paymentDeadline?"} + I2 -- "否" --> RSTATE["不产生资金副作用,形成已支付 / 已取消 / 已过期 / 余额不足等确定结果"] + I2 -- "是" --> J["对本人钱包执行余额充足条件扣减"] J --> K{"余额扣减条件命中?"} K -- "否" --> RBAL["整体回滚,形成余额不足的确定业务结果"] K -- "是" --> L["条件推进本人订单 PendingPayment → Paid"] @@ -122,13 +123,22 @@ flowchart TD + 支付钱包流水已写入 + 支付记录已写入 + 首次处理结果已写入 -+ 订单在截止时间前由 PendingPayment → Paid ++ 订单以 finalTime 在截止时间前由 PendingPayment → Paid ++ 财务提交序号已分配,订单、支付和钱包流水时间均等于同一 finalTime + 待发布的支付成功事实已写入 = 同一事务提交成功 ``` 任一步失败时整体回滚,不允许出现“余额已扣但订单未支付”或“订单已支付但没有支付记录”的部分结果。完成身份与固定字段校验后,稳定请求结果检查必须早于实时状态、余额和截止时间判断:同标识同请求重放首次确定结果;余额不足、已取消、已过期等已返回业务结果先持久绑定再返回;事务或基础设施故障未形成确定结果时不固化,允许原标识重试。 +最终时间规则: + +- 新支付事务必须先按统一锁顺序取得稳定请求处理资格、目标订单、既有成功支付、本人钱包和财务提交水位;财务提交水位是最后一个可能阻塞的共享锁。 +- 取得最后一个锁后立即且仅调用一次数据库 `clock_timestamp()` 形成 `finalTime`。取得 `finalTime` 后不得再等待新的共享锁或调用外部服务,只能重检已锁事实并完成确定写入。 +- 只有 `finalTime < paymentDeadline` 才能扣款并推进 `Paid`;请求到达、打开收银台、事务开始、取得订单锁或预检查的时间都不能授权支付。等待任一锁期间跨过截止点时必须转为确定的过期拒绝。 +- 成功支付的订单 `paidAt`、支付记录 `paidAt`、钱包流水时间和财务水位时间必须使用同一 `finalTime`。不得先用旧时间判定成功,再以截止点后的新时间入账。 +- 到期拒绝先把无副作用的幂等结果提交,再尽力调用 M04 统一过期取消;取消暂时失败时由 C03 继续收敛。同一请求标识始终重放原过期结果。 + ## 五、核心状态与并发竞争 F10 不增加“支付中”等订单状态。同步模拟支付提交前,订单仍是 `PendingPayment`;事务成功后直接成为 `Paid`。 @@ -146,7 +156,7 @@ stateDiagram-v2 ```mermaid flowchart LR - A["订单 PendingPayment"] --> T{"权威时间是否早于支付截止时间?"} + A["订单 PendingPayment"] --> T{"取得全部支付共享事实后的 finalTime
是否早于支付截止时间?"} T -- "是" --> B["F10 支付事务
条件推进为 Paid"] T -- "否" --> C["M04/C03 过期取消事务
条件推进为 Cancelled"] A --> ACTIVE["F09 买家主动取消事务
条件推进为 Cancelled"] @@ -166,7 +176,7 @@ flowchart TD B -- "钱包余额" --> C["返回本人实时余额"] B -- "充值记录" --> D["分页返回本人充值记录"] B -- "收银台" --> E["返回本人订单金额、状态、余额、截止时间和按权威时间派生的可支付性"] - B -- "订单支付结果" --> F["只返回本人已确定支付事实;没有记录时明确返回 paymentResult=None,不按订单状态猜测支付成功"] + B -- "订单支付结果" --> F["与支付写事务同步后返回 Succeeded / NotPaid / Confirming / ExpiredOrCancelled / Indeterminate"] B -- "支付记录/详情" --> G["仅返回本人支付记录"] C --> H["页面展示确定状态和下一步"] D --> H @@ -176,7 +186,7 @@ flowchart TD A -->|"资源非本人"| X["404/403,不泄露他人记录"] ``` -网络中断或结果未知时,客户端使用原防重复标识重试,或执行“查询订单支付结果”动作;当前该动作映射为 A406。不得生成新标识诱导重复付款。 +网络中断或结果未知时,客户端优先使用原防重复标识重试 A405;也可执行 A406 查询。A405、C08 回调和 A406 共用订单级 `payment-result:{orderId}` 事务锁:A406 未取得锁时只返回 `Confirming`,取得后才读取订单和唯一支付事实并返回 `Succeeded/NotPaid/ExpiredOrCancelled/Indeterminate`。`PendingPayment + paymentResult=null` 不再脱离 `resultState` 解释,任何 `Confirming/Indeterminate` 都禁止生成新标识盲目重复付款。 ## 七、异常、回滚与责任 @@ -188,7 +198,8 @@ flowchart TD | 余额不足 | 不开启或回滚支付事务 | 订单保持 `PendingPayment` | | 订单已支付 | 返回已有确定结果 | 订单保持 `Paid`,不重复扣款 | | 订单已取消 | 拒绝支付 | 订单保持 `Cancelled` | -| 订单仍为待支付但权威时间已到截止点 | 拒绝扣款并触发过期取消通道 | 钱包不扣款;M04/C03 条件推进订单并回补原库存 | +| 订单仍为待支付但最终 `finalTime` 已到截止点 | 拒绝扣款并触发过期取消通道 | 钱包不扣款;M04/C03 使用独立取消裁决时间条件推进订单并回补原库存 | +| 支付等待钱包或财务水位锁时跨过截止点 | 以锁后的 `finalTime` 裁决为过期,禁止沿用旧预检查时间 | 不创建成功支付、不递增资金副作用;过期取消继续收敛 | | 同一防重复标识、同一请求重试 | 返回首次确定结果 | 不重复产生副作用 | | 同一防重复标识、不同请求 | 返回标识复用冲突 | 不执行新副作用 | | 支付与取消并发 | 截止时间与状态条件共同裁决 | 截止时间前支付可与主动取消竞争;截止时间到达后支付不得胜出 | @@ -201,14 +212,14 @@ flowchart TD | 已确认流程能力 | 当前派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 查询本人钱包余额 | A401 | 仅返回当前买家的确定余额 | 待交叉评审 | -| 对本人钱包模拟充值 | A402 | 校验金额并幂等、原子地产生余额和充值流水结果 | 待交叉评审 | -| 查询本人充值记录 | A403 | 按当前买家隔离并分页返回充值记录 | 待交叉评审 | -| 打开本人订单收银台 | A404 | 基于 M04 持久化订单事实和权威时间返回金额、状态、余额、截止时间与可支付性 | 待交叉评审 | -| 确认模拟支付 | A405 | 先重放确定结果;新请求仅在截止时间前幂等提交原子支付事务,并处理与取消的状态竞争 | 待交叉评审 | -| 查询订单支付结果 | A406 | 返回当前买家该订单的确定支付结果 | 待交叉评审 | -| 查询本人支付记录 | A407 | 按当前买家隔离并分页返回支付记录 | 待交叉评审 | -| 查询本人支付详情 | A408 | 仅返回当前买家可访问的单笔支付详情 | 待交叉评审 | +| 查询本人钱包余额 | A401 | 仅返回当前买家的确定余额 | 接口已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 对本人钱包模拟充值 | A402 | 校验金额并幂等、原子地产生余额和充值流水结果 | 接口已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 查询本人充值记录 | A403 | 按当前买家隔离并分页返回充值记录 | 接口已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 打开本人订单收银台 | A404 | 基于 M04 持久化订单事实和权威时间返回金额、状态、余额、截止时间与可支付性 | 接口已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 确认模拟支付 | A405 | 先重放确定结果;新请求取得全部可能阻塞的共享事实后,以唯一 `finalTime` 同时裁决截止资格和提交资金时间,并处理与取消的状态竞争 | 接口已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 查询订单支付结果 | A406 | 与同订单支付裁决同步,区分成功、当前未支付、确认中、已过期/取消和数据不确定;确认中/不确定不得诱导新支付 | 接口已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 查询本人支付记录 | A407 | 按当前买家隔离并分页返回支付记录 | 接口已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 查询本人支付详情 | A408 | 仅返回当前买家可访问的单笔支付详情 | 接口已完整定义、数据库设计已确认,待 OpenAPI、实现与交叉评审 | 接口详细定义与实现必须承接上述流程结果。当前接口设计使用 `Idempotency-Key` 承载“防重复标识”,并按接口设计 1.12、1.12.1 的通用规则和资金类专用保留期,把充值、支付及其确定结果持久化;HTTP 状态码、请求字段和错误码不得反向写入业务图。 @@ -218,17 +229,17 @@ flowchart TD - C08 是不扣小金库的受控挑战模拟通道,不替换 F10 默认同步钱包路径。C08 成功回调与 F10、F09、C03 共同以订单状态和支付截止时间竞争:最多一方把待支付订单推进为已支付或已取消;回调先成功时后续钱包支付返回已有已支付结果且不扣款,钱包支付先成功时新的成功回调登记差异且不得重复入账。 - X03/M09 只消费支付事务提交后的支付成功事实;消息失败不能反向修改支付或订单状态。具体事件名和可靠投递机制由系统架构设计派生。 -## 十、接口承接结果与剩余数据待评审项 +## 十、接口与数据库承接结论 1. A404 已按流程统一收银台结果:已支付订单返回现有确定支付结果;已取消或其他不可支付状态使用明确结果,不把“已支付”误报为普通状态冲突。 2. A405 已区分同 Key、同请求重放和新 Key 再次支付:原请求重放首次确定结果;订单已有成功支付时返回既有支付结果且不重复扣款。 3. A405 的最终扣款金额只来自 M04 持久化订单事实;`expectedAmount` 仅承担客户端旧值冲突保护,不能成为扣款事实。 4. A402/A405 的幂等结果按接口设计 1.12.1 与资金事实一起持久化并长期保留;Redis 不保存唯一幂等事实。 -5. F10 同步钱包流程与 C08 回调已经分开:A406 只查询当前订单的确定支付结果;迟到回调、聚合和差异登记由 C08/A421~A426 承接。 +5. F10 同步钱包流程与 C08 回调已经分开:A406 通过共享订单级事务锁与 A405/A421 同步,只查询当前订单的安全支付结果状态;迟到回调、聚合和差异登记由 C08/A421~A426 承接。 6. A401 已冻结无钱包记录的处理:返回逻辑零余额且 GET 不写库;首次充值或扣款才在对应写事务内按需创建钱包记录,并用买家唯一约束防止并发重复创建。 7. A403 仅列出已成功提交的充值事实,A407 仅列出已确认支付事实;本期不虚构 `Pending/Failed` 资金记录状态机。 -8. A404/A405 已把数据库权威时间与固化的 `paymentDeadline` 纳入可支付条件;到期后即使 C03 尚未扫描也拒绝支付,并复用 M04/C03 的过期取消结果。 -9. DB081~DB100 尚未形成可实施的完整表定义,数据库字段、约束和索引仍由后续数据库设计任务按上述已确认流程与接口统一派生。 +8. A404 以查询时数据库权威时间派生页面可支付性;A405 不能复用该预览时间,而须在取得全部可能阻塞的订单、钱包和财务提交水位后形成唯一 `finalTime`。到期后即使 C03 尚未扫描也拒绝支付,并复用 M04/C03 的过期取消结果。 +9. DB081~DB096 及 DB102/DB104 已按上述流程与接口形成完整字段、约束、索引、财务水位和事务定义;当前尚未实现 EF Core、Migration、OpenAPI 或测试,不得把设计完成描述为数据库已落地。 ## 十一、验收证据清单 @@ -239,6 +250,7 @@ flowchart TD - [ ] 已支付订单重复提交不重复扣款,返回既有结果。 - [ ] 已取消订单不能通过重试进入 `Paid`。 - [ ] 截止时间前支付与主动取消并发时只有一个最终状态;截止时间达到后即使 C03 尚未扫描,支付也被拒绝且钱包不扣款。 +- [ ] 人为阻塞钱包或财务提交水位直至跨过截止点后再放行,A405 仍按最终 `finalTime` 拒绝支付;不存在截止后入账或时间字段互相矛盾。 - [ ] 模拟任一步失败时事务整体回滚。 -- [ ] 网络结果未知时使用原防重复标识或结果查询动作得到确定结果。 +- [ ] 网络结果未知时使用原防重复标识,或由 A406 明确返回 Confirming 后继续轮询,最终得到 Succeeded/NotPaid/ExpiredOrCancelled;任何中间 `paymentResult=null` 不被当成失败并触发新 Key 重付。 - [ ] 保留页面、接口响应、数据库事务结果和可靠消息重试证据。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" index 5c9da37..1b188f1 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/zhy/M10-\345\224\256\345\220\216\346\265\201\347\250\213.md" @@ -17,10 +17,10 @@ M10 负责订单项售后资格、申请数量占用、商家审核、退货说 |---|---|---| | M10 / X04 需求 | 完整定义 | 作为业务语义事实源 | | M04 / M06-02 订单与履约边界 | 完整定义,已完成统稿校准 | 已冻结订单状态、指定商家、可履约数量和发货竞争 | -| M05 退款能力 | 应用契约已按本文重建、未冻结 | 只承接稳定退款操作,不决定售后资格与库存;待数据库、公开签名与交叉评审 | +| M05 退款能力 | 应用契约已按本文重建、未冻结 | 只承接稳定退款操作,不决定售后资格与库存;数据库设计已确认,待公开签名、实现与交叉评审 | | M02 / C01 / C07 库存通道 | 完整定义 | 售后成功按原来源回补;普通库存触发 C07,秒杀原活动库存不触发 | | M09 通知 | 完整定义,固定接收矩阵已校准 | 只消费已经提交的售后事实 | -| 本文业务流程 | 已重构、待交叉评审 | 作为接口与后续数据设计输入 | +| 本文业务流程 | 已重构、待交叉评审 | 已派生接口与 DB086~DB088、DB092/DB093 数据设计;后续实现不得反向改写业务语义 | ## 二、参与者、事实归属与直接出入口 @@ -62,8 +62,8 @@ stateDiagram-v2 PendingReturn --> PendingReceipt: 买家提交退货说明 PendingReceipt --> Refunding: 指定商家确认收货 Refunding --> Refunded: 退款原子结果成功 - Refunding --> RefundFailed: 退款得到确定失败结果 - RefundFailed --> Refunding: 对同一退款操作安全重试 + Refunding --> RefundFailed: 需要商家重试或系统关注的确定失败 + RefundFailed --> Refunding: 商家对同一退款操作安全重试 Cancelled --> [*] Rejected --> [*] Refunded --> [*] @@ -76,7 +76,7 @@ stateDiagram-v2 | 待收货 | `PendingReceipt` | 买家已提交退货说明,等待指定商家确认收到该申请全部数量 | | 退款中 | `Refunding` | 审核或收货事实已提交,唯一退款操作正在执行或核实 | | 已退款 | `Refunded` | 钱包、退款、必要库存回补和售后终态已形成完整成功结果 | -| 退款失败 | `RefundFailed` | 得到确定失败结果,未增加余额、未回补库存,可安全重试 | +| 退款失败 | `RefundFailed` | 已确认未增加余额、未回补库存;恢复处置为需要商家重试或需要系统关注,只有前者开放人工重试 | | 已拒绝 | `Rejected` | 商家拒绝并释放申请数量,终止状态 | | 已撤销 | `Cancelled` | 买家在审核前撤销并释放申请数量,终止状态 | @@ -85,8 +85,8 @@ stateDiagram-v2 - 只有表中箭头是合法转换;接口或实现不得自行增加跳转、回退或“已退款订单”核心状态。 - `PendingReview`、`PendingReturn`、`PendingReceipt`、`Refunding`、`RefundFailed` 都是非终态并继续占用申请数量。 - `Refunding` 是可查询、可恢复的真实状态,不是假设必定瞬时完成的代码步骤。 -- 退款结果未知时保持 `Refunding`,先核实同一退款操作;只有得到确定失败结果时才进入 `RefundFailed`。 -- `RefundFailed` 不是终止状态。重试只能回到 `Refunding`,并继续使用同一退款操作身份。 +- 退款结果未知时保持 `Refunding`,先核实同一退款操作;瞬态确定失败仍有自动重试额度时也保持 `Refunding`。只有进入 `MerchantRetryRequired` 或 `OperatorAttentionRequired` 才转换为 `RefundFailed`。 +- `RefundFailed` 不是退款成功终态。`MerchantRetryRequired` 可由指定商家在冷却后重试并回到 `Refunding`;`OperatorAttentionRequired` 表示原尝试已确认无资金或库存副作用,但故障不适合自动或商家重试,系统只持续告警并保留完整证据,等待本期范围外的受控运维处置,不再把它加入 Unknown 核实或自动重试扫描。两者都继续使用同一退款操作身份。 - `Cancelled`、`Rejected`、`Refunded` 是终止状态,不能再次审核、退货、退款或撤销。 ### 3.2 可申请数量 @@ -125,7 +125,10 @@ stateDiagram-v2 ```mermaid flowchart TD A["状态正常的买家从本人订单详情选择订单项"] --> B["选择申请类型、数量并填写原因"] - B --> C["服务端重新读取订单归属、核心状态、完成时间、指定商家、实付单价和原库存来源"] + B --> B1["取得 A412 的 DB104 售后幂等范围并重查首次结果"] + B1 --> B2["无锁预读 DB061 不可变 assignedMerchantUserId
只用于定位责任商家门"] + B2 --> B3["锁定该商家的 DB001 责任门并重检 Merchant + Normal"] + B3 --> C["锁定 DB061 订单并重新读取归属、核心状态、完成时间、指定商家、实付和原库存来源"] C --> D{"订单项属于当前买家?"} D -- "否" --> X["拒绝且不泄露他人订单内容"] D -- "是" --> E{"状态、时限与申请类型符合 4.1?"} @@ -133,7 +136,7 @@ flowchart TD E -- "是" --> F["重新核算处理中、已退款和剩余可申请数量"] F --> G{"数量为正且不超过剩余数量?"} G -- "否" --> Z["提示最新可申请数量,不占用额度"] - G -- "是" --> H["与同订单发货动作串行复核最新履约事实"] + G -- "是" --> H["锁定/聚合 DB086 既有申请和目标 DB062 订单项
与同订单发货动作串行复核最新履约事实"] H --> I{"复核后仍满足资格?"} I -- "否" --> R["按最新已发货或售后事实重新返回可选类型与数量"] I -- "是" --> J["形成原子申请结果:PendingReview、数量占用、服务端金额、状态时间线、指定商家通知事实"] @@ -147,6 +150,8 @@ flowchart TD - 买家只能为本人订单项提交,商家、管理员和游客不能代为创建。 - 买家提交申请类型、原因和正整数数量,不提交最终退款金额。 - 每次用户动作具有稳定请求身份。同一身份、同一内容重放原申请;同一身份换内容拒绝;新的合法部分申请使用新的请求身份。 +- A412 固定锁序为:`DB104 售后幂等范围 → 无锁预读 DB061 不可变 assignedMerchantUserId → DB001 责任商家门 → DB061 订单 → DB086 既有申请 / DB062 目标订单项`。预读只定位门行,不能授权;锁定 DB061 后必须重新核对买家归属和 `assignedMerchantUserId`,DB001/DB061/DB086 锁保持到申请、时间线、Outbox 与幂等结果同事务提交。 +- A412 不得照搬 A307 的“先锁订单”顺序。商家禁用先提交时 A412 在 DB001 门失败且不能新建责任;A412 先提交时 A016 必须在同一门行之后看到新增售后责任。预读订单不存在、责任商家变化、商家非正常或重检归属不一致时整次返回确定失败,不继续取得后续业务锁。 - 已有一笔非终态申请不禁止同一订单项继续申请,但新的申请只能使用尚未被占用或退款的数量。 - 创建申请与商家发货必须对同一订单履约事实串行复核: - 申请先提交时,非终态售后阻断后续发货; @@ -204,7 +209,7 @@ flowchart TD C -- "是" --> D["填写审核意见并选择同意或拒绝"] D --> E{"审核决定?"} E -- "拒绝" --> F["原子推进 Rejected、释放申请数量、记录意见与买家通知事实"] - E -- "同意仅退款" --> G["原子记录同意意见、唯一退款操作和 PendingReview → Refunding"] + E -- "同意仅退款" --> G["原子记录同意意见、唯一退款操作、initial 尝试和 PendingReview → Refunding"] E -- "同意退货退款" --> H["原子记录同意意见并推进 PendingReview → PendingReturn"] G --> I["进入第八章退款处理"] H --> J["通知买家提交退货说明"] @@ -215,7 +220,7 @@ flowchart TD - 只有订单指定商家可审核,不能按 Merchant 角色全局操作。 - 商家只能填写意见和决定,不得修改买家申请类型、原因、数量、订单快照或退款金额。 -- `RefundOnly` 同意后先可靠形成“已同意 + `Refunding` + 唯一退款操作”,再执行退款。即使后续退款失败,审核事实也不能丢失或回到待审核。 +- `RefundOnly` 同意后先可靠形成“已同意 + `Refunding` + 唯一退款操作 + `attemptNumber=1/Initial/Executing` 尝试”,再执行退款。四项必须同事务提交;即使进程随后崩溃,Worker 也能从该尝试的执行租约恢复,不能留下 `Refunding + attemptCount=0`。后续退款失败也不能丢失审核事实或回到待审核。 - `ReturnAndRefund` 同意后只进入 `PendingReturn`,不能提前退款或回补库存。 - 两个审核请求并发时只有一个决定生效;相同请求重放当前结果,相反决定不得覆盖首次结果。 - 审核通过、拒绝和待退货结果通知买家。退款最终结果另按第九章通知。 @@ -244,13 +249,14 @@ flowchart TD flowchart TD A["状态正常的指定商家打开 PendingReceipt 申请"] --> B["核对申请全部数量与退货说明"] B --> C{"是否确认收到该申请全部数量?"} - C -- "否" --> X["保持 PendingReceipt,填写沟通备注但不退款"] + C -- "否" --> X["不调用 A417,保持 PendingReceipt;本期不另存否定动作或争议备注"] C -- "是" --> D["重新校验归属、状态和退款金额"] - D --> E["原子记录收货事实、唯一退款操作并推进 PendingReceipt → Refunding"] + D --> E["原子记录收货事实、唯一退款操作、initial 尝试并推进 PendingReceipt → Refunding"] E --> F["进入第八章退款处理"] ``` - 本期一笔申请按申请数量整体确认,不引入部分收货、拆分退款或新的子状态。数量有争议时保持 `PendingReceipt`,不得先退部分金额。 +- A417 只表达“确认已收到”的正向状态命令;商家尚未收到时关闭确认操作即可,系统不新增否定确认、沟通备注、客服工单或争议状态。需要沟通不等于已经发生可持久化状态变化。 - 只有订单指定商家可确认;买家、管理员和其他商家不能代替确认。 - 收货确认后不得回到 `PendingReturn`,退款失败也保留已经确认收货的事实。 - 库存不会在点击确认时单独回补;符合条件的回补与退款成功在第八章形成一个完整结果,避免退款失败却先增加库存。 @@ -261,14 +267,27 @@ flowchart TD 每笔售后申请最多对应一个退款操作身份,金额、收款买家、订单项和申请数量一经确定不得改变: -- 首次进入 `Refunding` 时建立该退款操作身份。 +- 首次进入 `Refunding` 时必须在同一事务建立退款操作身份和唯一 `attemptNumber=1/Initial/Executing` 尝试,生成固定 `refundAttemptId + executionToken + executionLeaseExpiresAt`,并令退款操作 `attemptCount=1`。申请、退款操作和首个尝试任何一个写入失败都整体回滚。 +- `Initial`、`AutomaticRetry`、`ManualRetry` 三类真实执行使用同一执行租约:创建尝试时以同一个数据库 `startedAt` 写 `executionLeaseExpiresAt=startedAt+60 秒`;执行器每 20 秒只可按 `refundAttemptId + executionToken + Executing` 条件续租,单次最多续到数据库当前时间后 60 秒,且绝不能超过 `startedAt+5 分钟`。 +- 续租或最终写入影响 0 行表示执行租约已经丢失、状态已被恢复 Worker 收敛或 Token 不匹配。旧执行器必须立即停止、丢弃本地结果并重读 DB093;成功、确定失败和 Unknown 的最终更新都必须携带原 `executionToken`,不能覆盖新恢复责任。 - 同一退款操作可以有多次受控尝试,但任何时刻最多一个尝试执行。 - 已成功时,任何审核重放、确认收货重放、人工重试或系统恢复都返回首次成功结果,不再次增加余额或库存。 -- 已得到确定失败时,申请进入 `RefundFailed`;新尝试仍关联原退款操作,不创建第二笔业务退款。 +- 已得到确定失败时,必须先由服务端固定策略判断 `AutomaticRetry`、`MerchantRetryRequired` 或 `OperatorAttentionRequired`。自动重试仍保持 `Refunding`;后两类才进入 `RefundFailed`。任何新尝试仍关联原退款操作,不创建第二笔业务退款。 - 结果未知时保持 `Refunding`,先查询或恢复原尝试;不得把“超时未收到响应”直接当失败,也不得立即发起无法去重的新退款。 +- 初始退款不计入自动重试额度;瞬态且可安全重试的确定失败最多自动重试 3 次,退避固定为 30 秒、2 分钟、10 分钟。自动额度耗尽后进入 `MerchantRetryRequired`;数据不变量或商家无法解决的问题进入 `OperatorAttentionRequired`。 ### 8.2 退款主流程 +`RefundOrchestrator` 是本流程唯一外层事务编排器,但不拥有任何业务表。A416、A417、A419 和恢复 Worker 只提交受信任触发意图;编排器按“订单 → 售后申请 → 退款操作 → 最新退款尝试 → 原支付 → 钱包 → 原库存聚合 → 财务水位”锁定,通过各模块公开应用能力完成: + +- Ordering 返回锁定的订单/订单项、买家、实付、支付和原库存来源快照; +- AfterSales 独占申请资格、申请数量、`stockReturnPolicy`、状态与时间线; +- Payment 独占 DB088/DB093 的创建与状态、钱包、资金流水、原支付退款累计和财务水位; +- Catalog/Seckill 只按服务端可信 `productId + quantity + inventorySource + seckillActivityId + refundOperationId` 回补自己拥有的库存通道; +- 编排器把上述公开能力加入一个受控共享 PostgreSQL 事务,绝不让任一模块直接取得其他模块仓储。 + +首次/人工/自动尝试分别固定为 `Initial/ManualRetry/AutomaticRetry`。每次执行都使用已经持久化的 `refundAttemptId + executionToken + attemptKind`;结算上下文由编排器在锁内从 Ordering 与 AfterSales 公开快照派生,至少包含 `orderId/orderItemId/productId/quantity/amount/buyerId/originalPaymentId/stockReturnPolicy/inventorySource/seckillActivityId`。HTTP、商家或 Worker 都不能自报金额、数量、库存来源或是否回补。 + ```mermaid flowchart TD A["申请已可靠进入 Refunding"] --> B["读取本人、服务端退款金额、订单原库存来源和本次库存规则"] @@ -276,13 +295,16 @@ flowchart TD C -- "是" --> R["重放 Refunded,不重复入账或回补"] C -- "否" --> D{"是否有结果未知的在途尝试?"} D -- "是" --> E["核实原尝试;保持 Refunding,不开启第二笔退款"] - D -- "否" --> F["M05 对同一退款操作执行小金库退款"] + D -- "否" --> F["RefundOrchestrator 以已持久化 attemptId + executionToken 执行同一退款操作"] F --> G{"退款得到什么结果?"} G -- "确定成功" --> H["形成完整原子结果:退款操作成功、本人余额增加、必要库存回补、申请 Refunded、时间线和买家通知事实"] - G -- "确定失败" --> I["确认未入账且未回补后,记录失败原因并推进 RefundFailed"] + G -- "确定失败" --> I["确认未入账且未回补后
按固定恢复策略分类"] G -- "未知" --> E H --> J["余额立即可用;C08 后续核对三方一致"] - I --> K["通知买家退款失败;允许指定商家或系统安全重试"] + I --> I1{"恢复处置?"} + I1 -- "AutomaticRetry" --> AR["保持 Refunding,按 30秒/2分钟/10分钟安排下一次系统重试"] + I1 -- "MerchantRetryRequired" --> MR["推进 RefundFailed;通知买家与指定商家,冷却后允许 A419"] + I1 -- "OperatorAttentionRequired" --> OR["推进 RefundFailed;通知买家并立即运维告警,不开放商家重试"] ``` 成功原子结果必须满足: @@ -300,21 +322,58 @@ flowchart TD 库存回补属于上述成功原子结果的一部分:原来源为普通库存时,完整结果提交后触发 C07 失效目标详情和固定首页;原来源为秒杀活动时只回补原活动库存,不触发 C07。缓存失效失败不回滚已提交退款。 -### 8.3 失败重试 +### 8.3 商家失败重试 ```mermaid flowchart TD - A["指定商家或系统选择 RefundFailed 申请"] --> B["重新读取申请状态和原退款操作"] - B --> C{"是否仍为 RefundFailed 且没有成功 / 未知尝试?"} - C -- "否" --> X["返回当前 Refunding 或 Refunded 结果,不开启新尝试"] - C -- "是" --> D["唯一推进 RefundFailed → Refunding,并为同一退款操作开启受控重试"] + A["指定商家选择 RefundFailed 申请"] --> B["重新读取申请状态、原退款操作、最新尝试与冷却时间"] + B --> C{"当前稳定结果?"} + C -- "已成功或已恢复执行" --> X["200 返回 Refunded / Refunding 当前结果,不开启新尝试"] + C -- "Unknown 或自动重试排队" --> U["409 RetryNotAvailable + nextActionAt"] + C -- "MerchantRetryRequired 但仍在冷却" --> W["409 RetryCooldown + manualRetryAvailableAt"] + C -- "OperatorAttentionRequired" --> O["409 RetryNotAllowed,不展示商家重试入口"] + C -- "MerchantRetryRequired 且冷却结束、无未决尝试" --> D["唯一推进 RefundFailed → Refunding,并开启受控 ManualRetry"] + C -- "其他状态" --> Z["409 InvalidStatus"] D --> E["复用 8.2 退款主流程"] ``` -- 买家不能主动执行退款重试;指定商家可手动重试,系统恢复任务也可重试确定失败的申请。 -- 商家重试与系统重试并发时只有一个进入执行,其他调用读取当前结果。 +- 买家不能主动执行退款重试;指定商家只可重试 `MerchantRetryRequired`,且上一次确定失败后至少冷却 60 秒。`OperatorAttentionRequired` 返回不可重试,不把运维问题伪装成商家按钮。 +- 商家重试与系统自动重试/核实必须先按候选 ID 只读解析关联关系,再统一按“订单 → 售后申请 → 退款操作 → 最新退款尝试”取得业务锁;只有一个执行器创建下一尝试,其他调用读取当前结果。不得从退款尝试或退款操作反向先锁,避免与退款成功事务形成逆序死锁。 +- A419 和 Worker 只请求“为原退款操作开始某类后继尝试”;Payment 通过 RefundOrchestrator 在固定锁序事务中把旧处置改为 `Consumed`、递增 DB088 `attemptCount` 并创建 DB093。AfterSales 不直接写 Payment 的退款操作或尝试表。 - 重试不重新审核、不要求买家再次申请,也不改变原金额、数量、库存通道或收款人。 - 系统无法确认旧尝试结果时继续保持 `Refunding` 并进入核实,不得伪造 `RefundFailed`。 +- 人工重试不设置永久总次数上限,避免合法退款因固定次数永远无法完成;但稳定请求标识、60 秒冷却、单一在途尝试和固定退款操作身份必须同时满足。 + +### 8.4 系统退款恢复 + +系统恢复属于原退款主流程,不调用 A419 HTTP、不伪装 Merchant JWT,也不新增管理员售后入口。Worker 每 5 秒扫描最多 50 条到期责任,以共享数据库事实恢复,进程重启后不依赖内存队列。 + +```mermaid +flowchart TD + A["Worker 领取执行租约过期、Unknown 到期核实或 AutomaticRetry 到期的尝试"] --> B["只读解析关联后按订单 → 申请 → 退款操作 → 最新尝试锁定"] + B --> C{"候选类型?"} + C -- "Executing 租约过期" --> U["将原尝试收敛为 Unknown,绝不直接新建尝试"] + C -- "Unknown 到期" --> V["携带原 attemptId + executionToken 核实原尝试"] + C -- "AutomaticRetry 到期" --> R["确认仍无成功/未知尝试后,为同一退款操作创建下一自动尝试"] + U --> V + V --> D{"核实结果?"} + D -- "发现完整原子成功事实" --> S["同一尝试收敛 Succeeded,申请收敛 Refunded"] + D -- "确认所有参与事实均无副作用" --> F["同一尝试收敛 DefiniteFailure,再按固定恢复策略分类"] + D -- "仍无法判断" --> N["保持 Unknown,递增核实次数并安排下一核实"] + R --> E["复用 8.2 退款主流程"] +``` + +恢复调度与责任: + +- 候选领取严格使用两阶段:第一短事务只对 DB093 使用 `FOR UPDATE SKIP LOCKED`,原子写入/接管短期恢复租约和必要的 `Executing → Unknown` 后立即提交,绝不在持有子行时等待订单、申请或退款操作;第二业务事务重新按“订单 → 售后申请 → 退款操作 → 最新退款尝试”锁定并重检租约 Token,再核实、创建后继或确认结果。领取顺序不能变成业务状态迁移的锁顺序。 + +- Unknown 核实退避固定为 10 秒、30 秒、2 分钟、10 分钟、30 分钟、60 分钟;第 6 次仍未知后标记需关注并转为每小时安全核实,绝不伪造成失败或开启第二笔退款。 +- **执行租约与恢复租约严格分离**:`executionToken/executionLeaseExpiresAt` 只围栏一次真实退款调用;`recoveryLeaseToken/recoveryLeaseAcquiredAt/recoveryLeaseExpiresAt` 只围栏 Worker 的原尝试核实或自动重试调度。两者使用不同 Token、字段和持有阶段,不能把执行续租当恢复续租,也不能由一个执行器同时提交两类结果。 +- 恢复租约同样首次为 60 秒、每 20 秒按当前 `recoveryLeaseToken` 续租;DB093 保存首次 `recoveryLeaseAcquiredAt`,任何续租都不得超过首次取得后 5 分钟。接管不同 Token 的过期恢复租约递增连续接管计数,正常完成并释放一轮后归零;连续 3 次接管触发告警。 +- Worker 发现 `executionLeaseExpiresAt` 已到或达到 `startedAt+5 分钟` 时,锁定同一 DB093 并原子执行 `Executing → Unknown`、建立新的恢复租约和核实责任;不得直接创建下一退款尝试。原退款执行器提交结果必须同时匹配原 `attemptId + executionToken + Executing`,恢复完成更新必须匹配当前 `recoveryLeaseToken`。两者谁先提交谁生效,条件更新失败的一方必须停止并重读,迟到结果不能越过 Unknown 核实或接管者。 +- 多实例使用有界批次与跳过已锁候选领取;一次退款操作最多一个未消费自动重试调度,也最多一个 `Executing/Unknown` 尝试。Worker、A419 竞争时遵守同一锁顺序。 +- `Unknown` 持续 15 分钟产生 Warning、持续 1 小时产生 Critical;三次自动重试耗尽产生 Warning;`OperatorAttentionRequired` 立即 Critical;同一操作连续 3 次租约接管产生 Warning;到期积压超过 100 条或最老到期超过 10 分钟产生 Critical。 +- 运维告警不进入 M09,也不新增管理员消息中心。业务状态仍由申请详情展示;Worker 单轮运行记录只用于运维追踪,逐笔退款责任始终保存在退款尝试事实中。 ## 九、库存、履约和订单状态协作 @@ -360,9 +419,12 @@ flowchart TD | 退货退款审核通过并进入 `PendingReturn` | 申请买家 | 买家售后详情 | | 买家提交退货说明并进入 `PendingReceipt` | 订单指定商家 | 商家售后详情 | | 退款成功 | 申请买家 | 买家售后详情 / 小金库记录 | -| 退款确定失败 | 申请买家 | 买家售后详情 | +| `MerchantRetryRequired` 最终失败状态 | 申请买家 + 订单指定商家 | 买家售后详情 + 商家售后详情/退款重试入口 | +| `OperatorAttentionRequired` 最终失败状态 | 申请买家 | 买家售后详情;另产生运维告警但不进入 M09 | +| `Unknown` 或单次自动重试中 | 无新增失败消息 | 申请详情显示核实中或预计下次处理时间 | -- 退款成功和失败只通知申请买家,不向无关商家或管理员广播。 +- 退款成功通知申请买家。进入 `RefundFailed` 时申请买家收到状态通知;只有 `MerchantRetryRequired` 才额外通知订单指定商家,且消息入口必须重新校验仍可执行 A419。无关商家和管理员不接收私人消息。 +- `Unknown`、单次自动确定失败以及自动重试排队不发送失败消息,避免“退款中/失败”反复抖动和重复提醒;`OperatorAttentionRequired` 的运维告警走监控系统,不建设管理员消息中心。 - 消息只描述已经提交的状态和必要业务标识;打开详情时由 M10 重新校验归属和最新状态。 - 通知失败不改变售后结果,由 M09 可靠重试;页面查询始终以 M10 当前事实为准。 - 页面明确展示加载、空状态、处理中、失败、冲突和最终结果;不能在退款结果未知时显示成功或失败。 @@ -381,7 +443,9 @@ flowchart TD | 两个商家审核或相反决定并发 | 指定商家的首次合法决定胜出 | 不重复审核 | | 退货说明重复或试图覆盖 | 同内容重放,已提交后拒绝换内容 | 时间线稳定 | | 重复确认收货 | 返回当前退款状态 | 不重复退款或回补 | -| 退款确定失败 | 进入 `RefundFailed` | 余额和库存均未增加 | +| 瞬态确定失败且仍有自动额度 | 保持 `Refunding` 并按固定退避自动重试 | 余额和库存均未增加,不发送失败消息 | +| 自动额度耗尽或需要商家触发 | 进入 `RefundFailed + MerchantRetryRequired` | 买家收到状态通知,指定商家收到可操作提醒 | +| 数据不变量或商家无法解决 | 进入 `RefundFailed + OperatorAttentionRequired` | 买家收到状态通知,立即运维告警,不开放商家重试 | | 退款结果未知 | 保持 `Refunding` 并核实原尝试 | 不并发创建第二笔退款 | | 手动与系统退款重试并发 | 一次执行,其他读取当前结果 | 同一操作最多退款一次 | | 通知失败 | 保留已提交业务事实 | M09 重试,不反向修改 | @@ -394,24 +458,25 @@ flowchart TD | 已确认业务能力 | 派生接口 | 接口必须承载的业务结果 | 当前状态 | |---|---|---|---| -| 售后资格预检 | A411 | 当前可选类型、截止时间、剩余可申请数量;明确预检不是提交承诺 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 提交售后申请 | A412 | 本人归属、最新订单状态、类型、数量、服务端金额、发货竞争和请求幂等 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 买家 / 商家申请列表 | A413 | 本人或 `assignedMerchantUserId` 授权范围、状态筛选和分页 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 申请详情 | A414 | 快照、金额、申请内容、审核意见、退货说明、退款结果和状态时间线 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 买家撤销 | A415 | 仅本人 `PendingReview` 可撤销;与审核竞争并释放数量 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 商家审核 | A416 | 指定商家、同意 / 拒绝、意见、状态竞争;仅退款同意后进入 `Refunding` | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 商家确认收货 | A417 | 指定商家确认整笔申请数量,`PendingReceipt → Refunding` | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 退款失败重试 | A419 | 仅 `RefundFailed`,复用同一退款操作,返回当前确定或在途状态 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 买家提交退货说明 | A434 | 仅本人 `PendingReturn`,同内容重放,提交后不得静默覆盖 | 接口已按流程重建,待数据库、OpenAPI 与交叉评审 | -| 小金库退款 | Payment 内部应用契约 | 一个售后申请一个退款操作;成功重放、未知核实、确定失败可安全重试 | 已按流程重建,待数据库、公开签名与交叉评审 | -| 履约快照 | AfterSales 内部应用契约 | 非终态阻断、已退款数量、剩余可履约数量和同订单串行复核 | 已按流程重建,待数据库、公开签名与交叉评审 | +| 售后资格预检 | A411 | 当前可选类型、截止时间、剩余可申请数量;明确预检不是提交承诺 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 提交售后申请 | A412 | 本人归属、最新订单状态、类型、数量、服务端金额、发货竞争和请求幂等 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 买家 / 商家申请列表 | A413 | 本人或 `assignedMerchantUserId` 授权范围、状态筛选和分页 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 申请详情 | A414 | 快照、金额、申请内容、审核意见、退货说明、退款结果、恢复状态/下一处理时间、角色可用动作和状态时间线 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 买家撤销 | A415 | 仅本人 `PendingReview` 可撤销;与审核竞争并释放数量 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 商家审核 | A416 | 指定商家、同意 / 拒绝、意见、状态竞争;仅退款同意后进入 `Refunding` | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 商家确认收货 | A417 | 指定商家确认整笔申请数量,`PendingReceipt → Refunding` | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 退款失败重试 | A419 | 仅 `RefundFailed + MerchantRetryRequired` 且冷却结束;复用同一退款操作,与 Worker 串行并返回当前确定或在途状态 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 买家提交退货说明 | A434 | 仅本人 `PendingReturn`,同内容重放,提交后不得静默覆盖 | 接口已按流程重建,数据库设计已确认,待 OpenAPI、实现与交叉评审 | +| 小金库退款与原尝试核实 | Payment 内部应用契约 | 一个售后申请一个退款操作;执行和 `Verify` 均使用原操作/尝试身份,成功重放、Unknown 只核实原尝试、确定失败返回固定恢复处置 | 已按流程重建,数据库设计已确认,待公开签名、实现与交叉评审 | +| 退款恢复 Worker | Mall.Worker 内部任务 | 租约领取、Unknown 核实、最多三次自动重试、商家/系统关注分流和可恢复告警;不调用 A419 HTTP | 已按流程完整定义并由 DB088/DB093/DB107 承接,待 Worker 实现与测试 | +| 履约快照 | AfterSales 内部应用契约 | 非终态阻断、已退款数量、剩余可履约数量和同订单串行复核 | 已按流程重建,数据库设计已确认,待公开签名、实现与交叉评审 | 接口阶段必须特别修正: 1. 售后撤销状态使用 AfterSales 作用域内的 `Cancelled`,不得与订单 `Cancelled` 混成同一个状态机。 2. A416 和 A417 的响应必须返回最新已提交状态;退款可继续处于 `Refunding`,不能为了同步响应伪造 `Refunded`。 -3. A419 不创建新业务退款,只重试原退款操作;若原尝试结果未知,返回 `Refunding` 并先核实。 -4. A412 不接受最终退款金额或商家归属;A411 的可申请数量不能替代 A412 提交时复核。 +3. A419 不创建新业务退款,只允许指定商家在 `MerchantRetryRequired` 且 60 秒冷却结束后重试原退款操作;自动重试仍排队、结果未知、冷却未结束或 `OperatorAttentionRequired` 必须返回各自稳定结果,不能强行启动。 +4. A412 不接受最终退款金额或商家归属;A411 的可申请数量不能替代 A412 提交时复核。实现固定遵守 `DB104 → 无锁预读 assignedMerchantUserId → DB001 → DB061 → DB086/DB062`,预读结果不得替代锁后归属与商家状态重检。 5. A413、A414 的商家范围统一使用订单 `assignedMerchantUserId`,不允许全局 Merchant 查询。 6. A417 不接收任意“收到数量”改变申请金额;本期只确认整笔申请数量。 7. A434 不把运单号设为全局业务唯一键。 @@ -421,10 +486,10 @@ flowchart TD ## 十三、跨模块整合必须承接的事实 - **M04 / M06-02**:订单提供状态、指定商家、实付与来源快照;履约在发货前读取非终态售后和已退款数量。售后与发货必须串行复核。 -- **M05**:对一个售后申请形成一个退款操作;钱包入账、退款成功和必要库存回补必须是完整结果。结果未知时先核实,不盲目重试。 +- **M05**:对一个售后申请形成一个退款操作;钱包入账、退款成功和必要库存回补必须是完整结果。除执行能力外还必须提供“按原 attemptId + executionToken 核实”能力,核实不得创建新退款;确定失败返回服务端固定恢复处置。 - **M02 / C01**:只接收已经满足回补条件的原通道数量;秒杀活动结束或取消后回补数量仍留在原活动,不恢复抢购。 - **C07**:普通库存售后回补提交后失效商品详情与固定首页;秒杀原活动回补不失效普通商品缓存。 -- **M09**:申请提交通知指定商家;审核、待退货、退款成功和失败通知申请买家;退货说明通知指定商家。退款失败不额外通知商家。 +- **M09**:申请提交通知指定商家;审核、待退货、退款成功和最终失败通知申请买家;退货说明通知指定商家。只有 `MerchantRetryRequired` 额外通知订单指定商家,`Unknown`、自动重试和系统运维告警不生成额外消息。 - **C08**:只把 M10 `Refunded`、成功退款操作和本人钱包入账纳入成功三方对账;`RefundFailed` 和结果未知的 `Refunding` 不伪造成功记录。 - **M06-03**:存在任一非终态售后申请时不得禁用负责处理的商家;禁用买家不能发起主动动作,但系统退款恢复可继续。 @@ -435,16 +500,21 @@ flowchart TD - [ ] 同一订单项可按剩余数量分次申请;非终态占用与已退款数量不会重复使用或超出购买数量。 - [ ] 同一提交动作重试不重复创建;并发申请总占用不超额。 - [ ] 申请与发货并发时结果可解释,同一数量不同时被当作未发货退款和待发货数量。 +- [ ] A412 与 A016、A307 分别并发时遵守固定锁序,无逆序死锁;商家禁用先提交则拒绝新申请,售后先提交则禁用/发货看到已提交责任或占用。 - [ ] 买家撤销与商家审核竞争只有一个结果;审核后不能撤销或回退。 - [ ] 指定商家才能查询、审核、确认收货和人工重试;其他商家看不到申请内容。 - [ ] 仅退款审核通过后保留审核事实并进入 `Refunding`;退货退款严格经过 `PendingReturn → PendingReceipt → Refunding`。 - [ ] 买家退货说明可安全重放,进入待收货后不能换内容覆盖;商家只确认整笔申请数量。 - [ ] 退款成功按服务端金额只增加一次本人余额;同一退款操作重复执行不重复入账。 -- [ ] 退款确定失败时余额和库存均未增加并进入 `RefundFailed`;重试继续使用原退款操作。 -- [ ] 退款结果未知时保持 `Refunding` 并核实原尝试,不误报失败或开启第二笔退款。 +- [ ] 退款确定失败时钱包、支付累计、库存和售后终态均无本次成功副作用,并按固定恢复处置分流:`AutomaticRetry` 保持 `Refunding`,只有 `MerchantRetryRequired` / `OperatorAttentionRequired` 进入 `RefundFailed`;所有恢复继续使用原退款操作。 +- [ ] 瞬态确定失败最多按 30 秒、2 分钟、10 分钟自动重试三次,自动恢复期间保持 `Refunding`;额度耗尽才进入 `MerchantRetryRequired`。 +- [ ] 退款结果未知时按原 `attemptId + executionToken` 核实并保持 `Refunding`;六次快速核实后转每小时安全核实,不误报失败或开启第二笔退款。 +- [ ] A419 与 Worker 并发时只创建一个下一尝试;冷却未结束、自动重试排队、结果未知或 `OperatorAttentionRequired` 均不能由商家强制重试。 - [ ] 未发货仅退款、已发货仅退款、退货退款分别按第九章矩阵处理库存,且只回补一次原库存通道。 - [ ] 秒杀售后回补不转入普通库存,活动结束或取消后不重新开放抢购。 - [ ] 部分退款不改变订单核心状态;已退款数量从可履约数量中扣除,全部退款后不能发货。 -- [ ] 通知接收人正确:申请和退货说明到指定商家,审核和退款结果到申请买家;通知失败不修改业务事实。 +- [ ] 通知接收人正确:申请和退货说明到指定商家,审核和退款最终结果到申请买家;只有 `MerchantRetryRequired` 再通知指定商家,Unknown/自动恢复/运维告警不生成额外 M09 消息。 +- [ ] 多实例 Worker 只领取一次,租约丢失的旧执行器不能覆盖接管结果;重启后从持久退款尝试恢复,不依赖内存队列。 +- [ ] `Initial`、`AutomaticRetry`、`ManualRetry` 均验证执行租约首次 60 秒、每 20 秒条件续租、`startedAt+5 分钟` 绝对上限;执行租约与恢复租约使用不同 Token,任一旧执行器条件更新为 0 后都不能提交迟到结果。 - [ ] C08 能核对 `Refunded`、成功退款操作和本人小金库入账;`RefundFailed` 不伪装成功。 - [ ] 保存申请、撤销、审核、退货、确认收货、退款成功、确定失败、结果未知、重试、越权和并发场景的真实证据。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" index 025c6a5..f4d7eeb 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" @@ -24,6 +24,51 @@ 设计顺序为“需求确认 → 业务流程 → 接口/数据库/架构落地”。流程先确定业务要发生什么,接口再承载流程中的动作与结果;现有 Axxx 只能用于流程完成后的映射和缺口检查,不能用接口清单反向拼接业务流程。“接口文档先行”只约束代码实现阶段,即接口契约必须先于实现与调用方变更确认。 +### 1.1 京东、淘宝购物主链路对标与范围门槛 + +本轮于 2026-07-25 复核京东帮助中心和淘宝开放平台公开资料,只把两者共同采用、且已经落在教师要求与本项目需求范围内的成熟做法作为流程校准依据。事实优先级仍是“教师只读基线与用户已确认需求 > 本节共同做法 > 两个平台存在差异时的项目内最优选择”;外部平台的多商户、拆单、优惠、运费、发票、客服仲裁、延长收货、换货、维修等能力不会因此自动进入本期。 + +参考资料: + +- [京东新手购物教学](https://help.jd.com/user/guide.html)与[京东下单步骤](https://help.jd.com/user/issue/34-18.html):挑选商品、购物车/结算、提交订单、付款、收货、评价构成连续主链路。 +- [京东购物车库存说明](https://help.jd.com/user/issue/45-80.html):加入购物车不代表预定商品,以成功提交订单为准。 +- [京东取消订单说明](https://help.jd.com/user/issue/317-979.html)与[京东售后申请说明](https://help.jd.com/user/issue/118-4278.html):正向取消和付款后的售后入口按订单阶段分流。 +- [淘宝开放平台交易状态公开定义](https://developer.alibaba.com/docs/doc.htm?articleId=102856&docType=1&treeId=796):待付款、待发货、待收货、交易成功、交易关闭形成正向交易阶段。 +- [淘宝退款退货接入说明](https://developer.alibaba.com/docs/doc.htm?articleId=102594&docType=1):未发货退款、已发货仅退款/退货退款使用独立逆向申请及状态。 +- [淘宝订单评价说明](https://developer.alibaba.com/docs/doc.htm?articleId=119661&docType=1&treeId=1):交易成功后才开放评价,并对退款/退货申请中的订单限制评价。 + +| 共同成熟做法 | 本项目采用的流程语义 | 本期边界 | +|---|---|---| +| 游客先浏览,执行个人写操作时再登录 | 分类、列表、搜索、详情允许匿名;收藏、历史、购物车、下单、钱包、订单、售后和消息必须按本人身份处理 | 不增加第三方登录、游客购物车或匿名下单 | +| 购物车表达购买意图,不等于锁货 | Cart 保存条目、选中集合和服务端预览;不预占库存;Ordering 提交时重新锁定并校验状态、价格、库存和数量 | 不增加购物车保价、优惠、凑单或预占库存 | +| 结算确认地址与订单内容后才创建待付款订单 | 结算预览可返回失效原因;提交订单必须整单原子成功或失败,成功后形成地址与成交价快照及固定支付截止时间 | 单店 B2C,不拆商家子订单,不增加运费、发票和优惠分摊 | +| 待付款可支付或取消,付款后进入履约 | `PendingPayment` 只竞争进入 `Paid` 或 `Cancelled`;商家只对 `Paid` 发货,买家或系统只对 `Shipped` 完成 | 前台支付仍只有钱包;C08 模拟回调是受控测试通道,不扩成真实第三方收银台 | +| 可用操作由订单阶段决定 | 列表和详情按五态订单、支付截止时间、履约/售后/评价派生摘要区分整单动作和订单项动作,正式提交时再次校验 | 不靠前端隐藏按钮授权,不接受客户端自行声明状态或摘要 | +| 付款后的退款/退货走独立逆向单 | 售后申请按订单项和数量独立推进;退款、退货物流和状态时间线不回退订单核心状态 | 本期只做退款/退货退款,不增加换货、维修、平台仲裁 | +| 收货完成后进入评价 | 手动确认或到期自动完成后,按订单项重检本人、`Completed` 和唯一评价资格 | 是否评价只按本项目 X01;不照搬外部平台未写入需求的售后限制、期限或治理能力 | + +当京东与淘宝在自动确认时长、售后响应时限、支付方式或平台仲裁上不一致时,本项目不复制任一平台的具体数字,而使用主需求已经冻结的 30 分钟支付截止、7 天自动完成、7 天售后窗口及单店角色边界。只有需求发生正式变化时才重新评审流程,不能以“大厂这么做”为理由绕过范围控制。 + +### 1.2 基础购物闭环覆盖审计 + +本节用于回答“是否存在完全没有覆盖的基础购物流程”。覆盖判断以用户能够从一个已知入口走到确定结果、异常有返回路径、跨模块交接有唯一责任为准;只有页面名称而没有状态、异常或出入口,不算已覆盖。 + +| 基础阶段 | 当前入口与确定结果 | 本轮审计结论 | 范围控制 | +|---|---|---|---| +| 游客发现商品 | 首页/商品列表 → 分类、关键词、组合筛选 → 商品详情 | 已覆盖;补齐条件恢复、售罄仍展示、下架不可购买、游客收藏/加购登录后至多恢复一次 | 不增加推荐、广告、热词榜、SKU 或多规格 | +| 登录与账号 | 统一登录/注册 → 按服务端角色落地 → 刷新恢复/退出失效 | 已覆盖;角色错误、账号禁用、撤销事实不可确认均有失败关闭结果 | 不增加第三方登录、刷新令牌、找回密码 | +| 加购与购物车 | 商品详情 → 加购 → 数量/选择/删除/清空 → 可结算集合 | 已覆盖;本轮冻结“选择意图”和“当前可结算性”两个维度,失效已选项阻断结算且不被静默丢弃 | 不增加游客购物车合并、凑单、优惠券;不另建立即购买 | +| 结算与下单 | 购物车已选集合 → 结算预览 → 明确选择本人地址 → 主动提交 → 唯一待付款订单 | 已覆盖;预览与提交通过 checkoutRevision 防止静默换单,无地址时复用 A010/A011 在同一结算上下文新增后显式选择;最终金额、地址/商品快照、库存扣减、默认商家、截止时间和购物车清理形成原子结果 | 不自动设默认地址或自动下单,不增加拆单、运费、发票 | +| 收银台与支付结果 | 待付款订单 → 统一收银台 → 小金库充值/支付 → 支付结果或保留待付款 | 已覆盖;补齐收银台、支付结果、小金库余额/充值及资金记录页面范围,并冻结截止点最后共享锁后的唯一裁决时间 | C08 仍是受控挑战回调,不新增第二个买家支付入口或真实支付渠道 | +| 订单查询与取消 | 订单列表/详情 → 核心状态、快照、时间线 → 待付款主动取消/超时取消 | 已覆盖;列表和详情可恢复稳定分页,取消按原库存通道原子回补,支付与取消最多一方成功 | 不增加订单改址、删除、导出、打印 | +| 商家履约与买家完成 | 已支付 → 指定商家复核售后摘要后发货 → 买家确认或 7 天自动完成 | 已覆盖;本轮补齐履约/售后组合摘要、部分退款实际发货量和摘要依赖降级关闭动作 | 不接真实物流,不做多商家履约、拆单或结算 | +| 评价 | 已完成订单项 → 订单详情评价入口 → 提交时重检 → 商品详情公开评价 | 已覆盖;每订单项最多一条,公开总数/均分实时计算,售后不会被擅自扩成评价禁用条件 | 不增加追评、匿名切换、审核治理或整单评价 | +| 售后与退款 | 合格订单项 → 申请/审核/寄回说明/收货 → 原退款操作成功、自动恢复或明确失败处置 | 已覆盖;本轮补齐 Unknown 原尝试核实、三次自动重试、商家冷却重试、系统关注和多实例围栏恢复 | 不增加换货、维修、平台仲裁、真实退货物流接口 | +| 消息与返回业务 | 已提交订单/支付/履约/售后事实 → 固定接收人消息 → 权威列表/未读/详情 → 重新校验业务跳转 | 已覆盖;实时推送失败由 HTTP 补查,不把角标或 WebSocket 当业务事实 | 不增加客服、群聊、管理员消息中心 | +| 商家与管理员后台 | 商品/分类维护、指定商家订单履约与售后;管理员买家/商家账号治理 | 已覆盖教师要求内的后台闭环;跨角色与跨资源访问均由服务端重检 | 不增加平台型商户入驻、店铺、结算、运营营销和通用审计后台 | + +审计结果:教师要求和当前已确认需求内,没有仍完全缺失的基础购物阶段。本轮发现并已回填的不是新业务,而是四类会造成实现歧义的横向缺口:购物车失效选择语义、历史商品图片保留、订单组合摘要降级、退款与多日对账恢复。后续若发现新的接口或数据库缺口,仍须先回到对应需求与流程判断是否属于上表现有阶段;不能借“参考大厂”扩大本期范围。 + ## 二、绘图与维护约定 ### 2.1 图的边界 @@ -112,6 +157,7 @@ flowchart LR | 商品 | 草稿/未上架、已上架、已下架 | 持久化销售状态;只有已上架进入公开列表和搜索;删除是满足约束后的终止结果,不是继续保留的销售状态 | | 购物车条目 | 可结算、不可结算 | 根据商品上下架、实时库存、数量和归属实时派生,不新增独立业务状态机 | | 订单 | `PendingPayment`、`Paid`、`Shipped`、`Completed`、`Cancelled` | 持久化状态;合法转换以 3.6 为唯一流程基线 | +| 订单组合读模型 | 履约摘要、售后摘要、评价摘要 | 根据订单、售后和评价已提交事实查询时派生;不持久化、不增加核心状态、不接受客户端回写 | | 钱包与支付 | 余额、不可变流水、确定支付记录 | F10 不定义“支付中”等额外业务状态;支付成功以支付记录落库且订单进入 `Paid` 为准 | 账号状态: @@ -160,8 +206,11 @@ flowchart LR M04 -->|"assignedMerchantUserId 匹配的 Paid 订单与履约快照"| M06O["M06-02 商家履约入口"] M06O -->|"条件推进 Paid → Shipped"| M04 M04 -->|"订单项、实付快照与履约状态"| M10["M10 AfterSales"] + M10 -->|"批量售后数量、资格和评估时间"| M04 M10 -->|"处理中申请、已退款数量与剩余可履约数量"| M06O M10 -->|"幂等退款命令"| M05 + M04 -->|"当前买家与订单项集合"| M07["M07 Review"] + M07 -->|"批量评价存在性事实"| M04 M04 -. "事务提交后的订单事实" .-> EXT["M09/C03 等扩展入口"] M05 -. "事务提交后的支付事实" .-> EXT @@ -179,8 +228,10 @@ flowchart LR | M05 Payment | 幂等支付命令和确定支付事实 | M04 Ordering | `PendingPayment → Paid` 的唯一条件更新结果 | 扣款、记录和状态必须形成一个原子结果 | | M04 Ordering | `assignedMerchantUserId` 等于当前商家的 `Paid` 订单和必要履约快照 | M06-02 | 经售后快照复核后的 `Paid → Shipped` 结果 | 本期不建设店铺、拆单或结算,但每单仍唯一归属一个履约商家;商家不能修改金额、支付事实、地址或订单项快照 | | M04 Ordering | 订单项归属、实付快照、履约状态与完成时间 | M10 AfterSales | 售后资格、申请占用与退款结果 | M10 不覆盖订单核心状态,也不直接修改订单、支付或库存内部数据 | +| M10 AfterSales | 当前页订单和订单项的批量售后数量、资格、类型、截止时间及统一评估时间 | M04 Ordering | 履约与售后派生摘要、订单项售后入口 | 必须全量返回请求 Key;缺 Key 或失败只能降级,不得按“无售后”处理 | | M10 AfterSales | 非终态申请、已退款数量和剩余可履约数量 | M06-02 | 允许、部分允许或阻断发货 | 发货与售后在同一订单事实边界串行化,不能由页面缓存决定 | | M10 AfterSales | 已确认退款金额、买家和幂等标识 | M05 Payment | 唯一退款流水与钱包入账结果 | M10 不直接修改钱包;退款失败保留可重试状态 | +| M07 Review | 当前买家订单项集合的批量唯一评价存在性 | M04 Ordering | 订单级与订单项级评价派生摘要 | 必须全量返回请求 Key;Ordering 结合自身 `Completed` 状态计算,不直接读取 Review 内部表 | | M06-01 | 分类、商品、上下架维护命令 | M02 Catalog | 最新商品销售状态 | 后台入口不拥有第二份商品事实 | | M06-03 | 买家/商家账号禁用或启用命令 | M01 Identity | 最新账号状态和令牌失效结果 | 不允许修改角色或管理员账号 | @@ -209,7 +260,16 @@ flowchart TD L -- "是" --> N{"账号状态正常?"} N -- "否" --> O["拒绝登录并提示账号停用"] N -- "是" --> P["M01 签发登录凭证并返回服务端确认的角色"] - P --> V["M01 直接出口:认证主体、角色和账号状态"] + P --> P1{"是否存在登录恢复上下文?"} + P1 -- "否" --> V["M01 直接出口:认证主体、角色和账号状态"] + P1 -- "是" --> P2{"returnDestination 与 actionIntent 组合是否合法?"} + P2 -- "否" --> P3["整组清除;不导航、不重放动作"] + P3 --> V + P2 -- "是" --> P4["只导航到白名单 returnDestination
或原商品详情"] + P4 --> P5{"是否有同商品详情的受控 actionIntent?"} + P5 -- "否" --> V + P5 -- "是" --> P6["重新校验并仅重放收藏或加入购物车
复用原 actionKey"] + P6 --> V V --> Q["M03/M04/M05/M06 按各自资源规则继续校验"] Q --> R{"刷新、访问受保护资源或退出?"} R -- "刷新/访问" --> S{"令牌有效且账号仍正常?"} @@ -226,6 +286,10 @@ flowchart TD - 用户主动退出只撤销当前令牌;管理员禁用账号的全部旧令牌失效流程见 3.7。 - 前端路由只改善体验,服务端仍按 JWT、Policy 和资源归属校验权限。 - 令牌失效状态无法确认时,受保护请求必须失败关闭,不能因依赖异常继续放行。 +- 登录恢复上下文把“返回哪里”和“登录后补做哪个受控动作”分成两个字段:`returnDestination` 只能使用白名单页面枚举及该页面允许的可选 UUID,不接受任意 URL;`actionIntent` 只允许从同一商品详情产生的 `Favorite(productId)` 或 `AddToCart(productId,quantity,actionKey)`,加入购物车必须继续把原 `actionKey` 作为幂等键。 +- 只要存在 `actionIntent`,`returnDestination` 就只能为空或精确为同一 `productId` 的 `ProductDetail`;来自购物车、结算、订单、支付或售后页面的动作意图,以及商品 ID 不一致、任意 URL、未知页面或未知动作,全部整组清除,不导航也不重放,避免登录后在后台修改另一上下文。 +- 登录成功绝不自动结算、提交订单、取消订单、发起支付、确认收货或提交售后。独立 `returnDestination` 白名单精确为 `ProductDetail(productId)`、`SeckillActivityDetail(activityId)`、`Cart`、`Checkout`、`Cashier(orderId)`、`PaymentResult(orderId)`、`Wallet`、`TopupRecords`、`PaymentRecords`、`PaymentDetail(paymentId)`、`Profile`、`Addresses`、`Orders`、`OrderDetail(orderId)`、`Favorites`、`BrowsingHistory`、`Messages`、`MessageDetail(messageId)`、`AfterSalesList`、`AfterSalesDetail(requestId)`;括号内只接受一个标准 UUID,不接受查询串、片段或其他参数。目标页必须重新鉴权并读取最新事实,其中 Checkout 重调 A208、Cashier/PaymentResult 重读 A404/A406、秒杀详情重读 A227,恢复页面不等于恢复提交动作。 +- 受控动作只有得到确定 2xx 或确定业务 4xx 后才清除;网络中断、超时、503 或结果未知时保留同一 `actionIntent/actionKey` 供用户显式重试,不能换 Key 盲目再做一次。 ### 3.2 个人资料与收货地址 @@ -258,7 +322,7 @@ flowchart TD ```mermaid flowchart TD - A["M01 Address:已登录买家进入地址管理"] --> B["按当前买家查询本人地址"] + A["M01 Address:已登录买家进入地址管理"] --> B["按当前买家查询本人地址
返回每条地址 version"] B --> C{"新增、编辑、设默认或删除?"} C -- "新增" --> D["校验收件人、联系电话、地区和详细地址"] C -- "编辑/设默认/删除" --> E{"地址是否属于当前买家?"} @@ -274,7 +338,7 @@ flowchart TD K -- "是" --> M["保存并返回最新地址"] G --> N["返回唯一默认地址结果"] H --> O["刷新本人地址列表"] - B -. "后续下单选择任一本人地址" .-> P["M04 直接入口:重新校验地址归属并生成地址快照"] + B -. "后续下单显式提交 addressId + addressVersion" .-> P["M04 直接入口:锁定后重检归属与版本并生成地址快照"] ``` 关键说明: @@ -283,6 +347,8 @@ flowchart TD - 手机号修改属于敏感操作,必须验证当前密码;成功后修改前签发的全部令牌失效。 - 地址必须按买家隔离,不能查看或修改他人地址。 - 删除默认地址后不自动指定其他地址;“最多一个默认地址”允许当前没有默认地址。 +- 结算页有默认地址时只把该地址预选并醒目标明;没有默认地址时保持“未选择”,不得自动选择列表第一条。无地址时可在原结算上下文调用 A011 新增非默认地址,返回后仍由买家显式选中。 +- A010/A011/A012/A014 返回地址当前 `version`;编辑内容、切换默认等成功变化都会形成新版本。A301 必须提交预览时选定的 `addressId + addressVersion`,锁后版本不一致统一返回 `IDENTITY.ADDRESS_VERSION_CONFLICT` 和最新结算预览,不静默使用新地址内容。 - 订单保存收货信息快照,后续修改地址不改变历史订单。 ### 3.3 商品维护、上架与购物端浏览 @@ -370,12 +436,15 @@ flowchart TD E --> F["服务端重读归属、销售状态、实时价格和库存并计算总额"] F --> G{"全部条目可结算?"} G -- "否" --> GX["整次结算失败,仅标记问题条目并保留全部购物车数据"] - G -- "是" --> H["展示服务端金额,买家选择本人地址并提交下单幂等键"] + G -- "是" --> G1["返回规范已选集合、服务端金额、checkoutRevision
以及带 version 的本人地址"] + G1 --> H["买家明确选择地址并主动提交
checkoutRevision + addressId/version + Idempotency-Key"] ADDR["M01 直接输入:本人地址记录与归属"] --> J H --> I{"该买家+幂等键已有成功订单?"} I -- "是" --> IX["返回原订单号,不重复扣库存或清理购物车"] - I -- "否" --> J["再次校验身份、地址、购物车、商品、库存和正数总额"] - J --> K{"最终校验通过?"} + I -- "否" --> J["按固定锁序重检身份、地址版本、购物车版本、商品、库存和正数总额"] + J --> J1{"checkoutRevision 与 addressVersion 是否仍匹配?"} + J1 -- "否" --> JC["零建单、零扣库存、零清理
返回最新预览并要求买家重新确认"] + J1 -- "是" --> K{"最终校验通过?"} K -- "否" --> KX["拒绝提交并保留购物车条目"] K -- "是" --> K1["M01 解析唯一启用的默认履约商家"] K1 --> K2{"是否得到唯一 assignedMerchantUserId?"} @@ -400,7 +469,10 @@ flowchart TD - 前端显示的价格和库存不能作为下单事实,提交时必须由服务端重新校验。 - 商品价格变化只刷新服务端计价,不自动把条目标为失效;商品未上架、资源归属错误或库存不足才阻止结算。分类停用不影响既有已上架商品继续公开和购买。 -- 购物车条目的“可结算/不可结算”是实时派生结果,提交瞬间必须再次校验。 +- A208 只在目标集合非空、全部当前可结算且服务端总额为正时生成 `checkoutRevision`。规范输入固定为买家 ID,以及按稳定顺序排列的每项 `cartItemId/productId/cartVersion/quantity/productName/mainImageObjectKey/unitPrice`;不纳入页面入口、客户端金额或仍足够的瞬时剩余库存数字。A301 必须原样提交该 Revision,不能让前端重新拼出另一个集合。 +- 购物车条目的“可结算/不可结算”是实时派生结果,提交瞬间必须再次校验。任一条目选择、版本、数量、名称、主图、价格或结算集合变化,商品已不可售,或最终库存不足,统一返回 `ORDER.CHECKOUT_CHANGED + latestPreview`,整单拒绝;不能静默换价、删除失效条目或只提交可用子集。库存数字虽不进入 Revision,事务内仍必须按最新库存条件扣减;库存变化后仍足够时无需仅因数字变化强迫重复确认。 +- A301 同时提交显式选择的 `addressId + addressVersion`。地址不存在/越权按不存在处理;地址版本已变化返回 `IDENTITY.ADDRESS_VERSION_CONFLICT + latestPreview`。两类冲突都要求用户在页面重新确认后再次主动下单,系统不得自动提交。 +- 有默认地址时结算页预选默认地址;无默认地址时不自动选择第一条。无地址可在原上下文新增非默认地址,但新增完成后仍须显式选择;任何登录恢复都最多回到结算/购物车页面,绝不自动下单或支付。 - 普通订单创建时由 M01 解析唯一启用的默认商家并保存 `assignedMerchantUserId`;无法得到唯一结果时整次下单失败,不产生无人负责或多商家竞争的订单。 - 订单创建时按当时生效的正式配置写入固定 `paymentDeadline`(本期正式值为创建后 30 分钟);后续配置变化不重算历史订单截止时间。 - 库存扣减、订单和快照、履约商家与支付截止时间、待发布订单创建事实、已结算购物车清理属于一个原子业务结果。 @@ -449,20 +521,28 @@ stateDiagram-v2 flowchart TD ID["M01 直接输入:已认证买家"] --> A["M04:买家进入订单列表"] A --> B["按本人、状态和分页查询,创建时间倒序"] - B --> C["查看订单摘要或进入详情"] - C --> D{"订单是否属于当前买家?"} + B --> C{"查看列表摘要或订单详情?"} + C -- "列表" --> S["取得本页最多 50 张订单及订单项权威快照"] + C -- "详情" --> D{"订单是否属于当前买家?"} D -- "否" --> X["返回不存在或无权限,不泄露订单内容"] - D -- "是" --> E["展示地址和商品快照、金额、状态时间线及支付信息"] - E --> F{"当前状态?"} + D -- "是" --> S + S --> AS["M10 一次批量返回售后聚合或订单项明细"] + S --> RV["M07 一次批量返回订单项评价存在性"] + AS --> SUM["按固定优先级组合履约、售后和评价摘要"] + RV --> SUM + SUM --> E["展示核心状态、快照、时间线、三个摘要及完整/降级结果"] + E --> F{"当前核心状态与订单项资格?"} F -- "PendingPayment" --> G["显示去支付和主动取消入口"] F -- "Shipped" --> H["显示确认收货入口"] - H -. "符合售后规则" .-> AS2["X04:从 Shipped 订单项接入独立售后流程"] - F -- "Completed" --> I["显示评价入口"] - I -. "X01" .-> RV["从 Completed 订单项接入评价流程"] - I -. "符合售后期限" .-> AS3["X04:从 Completed 订单项接入独立售后流程"] + H -. "订单项符合售后规则" .-> AS2["X04:从目标 Shipped 订单项接入独立售后流程"] + F -- "Completed" --> I["按订单项分别显示评价与售后入口"] + I -. "未评价订单项" .-> RV2["X01:从目标 Completed 订单项接入评价流程"] + I -. "符合售后期限的订单项" .-> AS3["X04:从目标 Completed 订单项接入独立售后流程"] F -- "Paid" --> J["显示等待商家发货"] - J -. "符合售后规则" .-> AS1["X04:从 Paid 订单项接入独立售后流程"] + J -. "订单项符合售后规则" .-> AS1["X04:从目标 Paid 订单项接入独立售后流程"] F -- "Cancelled" --> K["不显示支付、发货或完成入口"] + AS -. "失败或缺 Key" .-> DG["核心订单可返回但摘要明确降级;售后和发货动作关闭"] + RV -. "失败或缺 Key" .-> DG2["评价摘要明确降级;不把未知伪装成未评价"] ``` 支付与取消竞争: @@ -529,6 +609,9 @@ flowchart TD 关键说明: - 买家列表和详情只返回本人订单;商家列表、详情和发货只返回 `assignedMerchantUserId` 等于当前商家的订单。本期虽不建设店铺和拆单,但不能把全部订单授权给所有商家。 +- 订单核心状态始终只有五种。履约、售后和评价摘要只按当前已提交事实查询时派生,用于解释 `Paid` 是否被售后阻断、是否已部分/全部退款以及 `Completed` 订单项评价进度,不写回订单表。 +- 当前页售后与评价事实分别通过一次批量公开应用能力读取,不能逐订单或逐订单项调用 HTTP,也不能因共用 PostgreSQL 直接读取其他模块内部表。 +- 摘要调用失败、缺 Key 或数量不变量破坏时可以返回核心订单,但必须明确降级并关闭依赖该摘要的动作;不能用 `None`、`ReadyToShip` 或“未评价”替代未知。所有写动作仍在提交时重检。 - 买家支付、主动取消和 C03 超时取消竞争 `PendingPayment`,数据库状态条件决定唯一胜出结果。 - 取消、原库存通道回补和待发布订单取消事实处于同一事务,重复取消不能重复回补。 - 发货前必须读取 M10 权威履约快照:任一非终态售后申请阻断整单发货;已退款数量从可履约数量中扣除,部分退款只发剩余数量,全部退款不允许发货。发货与售后提交在同一订单事实边界串行化。 @@ -665,17 +748,24 @@ flowchart LR flowchart LR ID["M01 Identity
买家/商家身份和账号状态"] -->|"本人消息查询与已读操作鉴权"| MSG["M09 Messaging"] ID -->|"WebSocket 建连及存续期间鉴权"| RT["C06 实时推送"] - ORD["M04 Ordering
F08/F09/F12 已提交事实"] --> MATRIX["按事件类型解析固定接收人集合"] - PAY["M05 Payment
F10 已提交支付事实"] --> MATRIX - AFTER["M10 AfterSales
X04 已提交事实"] --> MATRIX + ORD["M04 Ordering
F08/F09/F12 已提交事实"] --> OUTBOX["来源事务 DB102 Outbox"] + PAY["M05 Payment
F10 已提交支付事实"] --> OUTBOX + AFTER["M10 AfterSales
X04 已提交事实"] --> OUTBOX + OUTBOX --> WORKER["Mall.Worker 校验封闭事件、规范哈希
并按事件类型解析固定接收人集合"] + WORKER --> MATRIX["固定接收人集合"] MATRIX --> VALID{"全部必需接收人都存在、角色正确且归属匹配?"} VALID -- "否" --> REJECT["整事件零消息并告警
不得只写部分接收人"] - VALID -- "是" --> MSG - MSG -->|"首次处理"| STORED["Inbox 结果与全部接收人消息同事务提交"] - MSG -->|"重复事件"| EXISTING["返回既有整事件结果
不新增消息或未读数"] - MSG -->|"临时处理或事务失败"| RETRY["可靠重试
不改变来源业务结果"] - STORED -. "提交后轻提示" .-> RT + VALID -- "是" --> STORED["DB103 + 全部 DB101 + 每消息一条 DB102 实时提示
同一事务提交"] + WORKER -->|"同 eventId/同 hash 重放"| EXISTING["返回既有整事件结果
不新增消息、提示或未读数"] + WORKER -->|"同 eventId/异 hash"| CONFLICT["保留首次结果;当前投递告警并死信"] + WORKER -->|"临时处理或事务失败"| RETRY["可靠重试
不改变来源业务结果"] + STORED --> MSG + STORED -. "Outbox Publisher 发布 60 秒提示" .-> HINTQ["RabbitMQ 共享提示队列"] + HINTQ -. "任一 API 入口消费者竞争消费一次" .-> INGRESS["Mall.Api 实时提示入口消费者"] + INGRESS -. "发布版本化频道" .-> REDIS["Redis
eshop:{environment}:signalr:message-hints:v1"] + REDIS -. "每个 API 各接收一次" .-> FANOUT["各 API RealtimeFanoutHostedService"] + FANOUT -. "逐本地 connectionId 复核
Clients.Client(connectionId)" .-> RT RT -->|"在线连接可用"| ONLINE["本人全部在线连接收到消息标识"] RT -->|"断线、Redis 降级或推送失败"| QUERY["通过 M09 权威未读数、列表与详情补查"] ONLINE --> QUERY @@ -689,41 +779,57 @@ flowchart LR | 订单创建、取消、发货、完成 | 订单买家 | | 支付成功 | 订单买家 + 订单 `assignedMerchantUserId` | | 售后申请、买家提交退货说明 | 订单 `assignedMerchantUserId` | -| 售后审核结果、退款成功、确定退款失败 | 订单买家 | +| 售后审核结果、退款成功 | 订单买家 | +| `RefundFailed + MerchantRetryRequired` | 订单买家 + 订单 `assignedMerchantUserId` | +| `RefundFailed + OperatorAttentionRequired` | 订单买家;运维告警不进入 M09 | +| 退款 Unknown 或自动重试中 | 不生成失败消息 | | 支付失败、被忽略回调、回调差异、对账差异及管理员对账处置 | 不生成 M09 消息 | 交接约束: -- Ordering、Payment 和 AfterSales 只能提交已经完成事务的确定事实;事件类型决定固定接收人,调用方不能把 `recipients[]` 当作任意广播名单。 +- Ordering、Payment 和 AfterSales 只能在来源事务写封闭 `MessagingSourceEventV1` Outbox;事件类型决定固定接收人和精确字段,调用方不能把 `recipients[]`、开放 `ownership/data` 或任意广播名单交给 Messaging。 - 任一必需接收人不存在、角色错误或订单归属不匹配时,整事件零消息并告警;账号已禁用但身份与归属仍有效时消息仍持久化,查询、已读和实时连接在禁用期间被拒绝,重新启用后可见。 -- M09 完成整事件校验、幂等判断和全部消息持久化后才能进入 C06;来源模块不得直接向客户端广播未落库的成功事实。 +- Mall.Worker 必须把 Processed DB103、整事件全部 DB101 和每消息一条 DB102 `MessagingRealtimeHintRequestedV1` 同事务提交;来源模块和 Worker 都不得直接向客户端广播。Outbox Publisher 只把提示发布到 RabbitMQ 共享队列,由任一 API 入口消费者竞争取得一次,再发布到 `eshop:{environment}:signalr:message-hints:v1`;每个 API 的 `RealtimeFanoutHostedService` 各收一次,只复核本实例连接并调用 `Clients.Client(connectionId)`,禁止入口直接 `Clients.User` 绕过远端复核,也不要求自定义 `HubLifetimeManager`。 +- 提示在 `message.createdAt+60 秒` 过期。入口消费者发布 Redis 失败且尚未过期时不得 ACK,必须重投同一 RabbitMQ 消息;到期后确认丢弃。Redis Pub/Sub 不补发 API 离线期间的提示,至少一次重复由客户端按 `messageId` 去重,所有遗漏统一由 A501/A503 权威 HTTP 查询补偿。 +- 同 `eventId` 同规范哈希只返回既有整事件结果;同 ID 异哈希保留首次结果、当前投递零消息并告警死信,不允许覆盖或换新 ID 掩盖冲突。 - C06 当前 PC Web 只使用 WebSockets 并跳过协商,不启用 SSE 或长轮询。重连顺序固定为立即、2 秒、5 秒、10 秒,四次失败后暂停并保留 HTTP 补查。 - C06 事件只是“有新消息”的轻提示;客户端收到后以 M09 权威未读数校正角标,按需查询列表或详情,不能直接执行 `badge + 1`。 -- “全部标记已读”在事务开始捕获本人已提交消息的稳定高水位,只更新该高水位及以前的未读消息;并发新消息保持未读,不使用客户端时间或墙上时钟界定批次。 +- “全部标记已读”在本人消息写入锁内捕获稳定高水位,只更新 `read_at IS NULL` 且不高于水位的消息;无历史返回 `highWatermark=0`,零更新返回 `markedCount=0/readAt=null`,并发新消息保持未读。 - 消息历史正文始终可读;目标资源已删除、不可用或当前权限无法确认时,操作入口为空并显示“目标暂不可用”。进入目标模块时仍须重新校验当前身份、归属和状态。 -- 主动退出、JWT 到期、手机号修改、账号禁用或无法确认撤销/账号状态时,既有实时连接必须关闭;Redis 恢复不重放历史推送,由 M09 查询补偿。 +- Hub 固定 `CloseOnAuthenticationExpiration=true、ClockSkew=0`,严格使用唯一 UUID `sub` 作为用户标识。每 API 实例独立消费安全失效广播并按本地 `jti/userId/tokenVersion` 登记 Abort;每次推送和最长 30 秒复核失败也必须关闭。Redis 恢复不重放历史推送,由 M09 查询补偿。 #### 3.8.6 Catalog、C07 与购物端读取的交接 ```mermaid -flowchart LR - HOME["A102 固定首页
无筛选、第一页12条、createdAt/productId 倒序"] --> CACHE["C07 Cache-Aside"] - DETAIL["A103 商品自身公开详情"] --> CACHE - DIRECT["其他列表/分类/搜索/筛选、M07 评分评价、秒杀活动事实"] --> CAT["M02 Catalog / PostgreSQL"] - CACHE -->|"有效命中"| RESPONSE["返回原公开商品响应"] - CACHE -->|"未命中或损坏"| FILL{"取得跨实例唯一回填资格?"} - FILL -- "是" --> CAT - FILL -- "否" --> WAIT["最多等待 500 ms"] - WAIT -->|"仍未命中"| CAT - CACHE -->|"Redis 降级"| CAT - CAT -->|"已上架商品事实"| RESPONSE - CAT -. "仅唯一回填者且查询在2秒窗口内完成时回填
正常60秒,空结果10秒" .-> CACHE - - WRITE["商品公开字段、销售状态或普通库存变更"] -->|"事务提交成功"| INVALIDATE["立即失效详情和受影响的固定首页缓存"] - INVALIDATE --> DELAY["提交后第3秒再次失效相同 Key"] - WRITE -->|"事务回滚"| KEEP["不产生成功失效结果"] - - ORDER["F08 提交订单"] -->|"重读销售状态、价格和库存"| CAT +flowchart TD + HOME["固定首页商品摘要查询
无筛选、第一页12条、createdAt/productId 倒序"] --> CACHE["C07 Cache-Aside"] + DETAIL["商品自身公开详情查询"] --> CACHE + CACHE -->|"有效命中"| RESPONSE["返回与 PostgreSQL 同口径的公开响应"] + CACHE -->|"未命中或损坏"| FILL{"SET lockKey randomToken NX PX 3000
是否取得唯一回填资格?"} + FILL -- "是" --> NEWTX["取得锁后才开启新的短
READ COMMITTED 只读事务"] + NEWTX --> FILLREAD["读取 PostgreSQL 事实"] + FILLREAD --> CHECK{"从开始回填未超过2秒
且 Lua 比较仍持有同一 Token?"} + CHECK -- "是" --> STORE["同一 Lua 原子写缓存与 TTL
正常60秒,空结果10秒"] + STORE --> RELEASE["Lua 比较 Token 后释放锁"] + RELEASE --> RESPONSE + CHECK -- "否" --> NOFILL["只返回数据库结果,不得迟到回填"] + NOFILL --> RELEASE + FILL -- "否" --> WAIT["最多等待 500 ms 再查缓存"] + WAIT -->|"命中"| RESPONSE + WAIT -->|"仍未命中"| DIRECTDB["直读 PostgreSQL,不再竞争回填"] + CACHE -->|"Redis 降级"| DIRECTDB + DIRECT["其他列表/分类/搜索/筛选、M07 评分评价、秒杀活动事实"] --> DIRECTDB + DIRECTDB --> RESPONSE + + WRITE["商品公开字段、销售状态或普通库存变更"] -->|"来源业务事务同时写入"| BOTH["Immediate + Delayed 两条独立 DB102
共享 operationId,不同 eventId"] + BOTH -->|"Immediate 固定立即调度,可用即执行"| FIRST["幂等 DEL 目标 Key"] + BOTH -->|"Delayed 初始未武装"| ARM["提交可见后以数据库时间原子武装
available_at=armed_at+3秒"] + ARM -->|"到期执行,不依赖 Immediate 成败"| SECOND["幂等 DEL 同一组 Key"] + FIRST --> CACHE + SECOND --> CACHE + WRITE -->|"事务回滚"| KEEP["业务事实和两条失效责任均不提交"] + + ORDER["F08/C01/M10 等交易动作"] -->|"重读并修改 PostgreSQL 事实"| DIRECTDB CACHE -. "不得作为交易事实" .-> ORDER ``` @@ -731,10 +837,13 @@ flowchart LR - 固定首页仅指 A102 无用户筛选、第一页 12 条、`OnSale`、`createdAt DESC, productId DESC` 的公开摘要;普通库存为 0 的商品仍展示为售罄。A103 只缓存商品自身公开字段,不缓存 M07 评分或评价。 - 其他 A102 列表、分类、关键词、价格/库存筛选、排序,M07 评分评价和 C01 秒杀活动事实全部直读 PostgreSQL,不得以接口实现方便反向扩大缓存范围。 -- 跨实例只允许一个回填者;其他请求最多等待 500 ms,仍未命中就直读 PostgreSQL且不竞争回填。取得资格的查询只有在 2 秒有效窗口内完成才可回填。 -- 正常结果 TTL 为 60 秒,空结果 TTL 为 10 秒;事务提交后立即删除并在第 3 秒二次删除。若两次删除都失败,计入最长 2 秒有效回填窗口后,正常旧值最迟 62 秒、空结果最迟 12 秒自然消失。 +- 填充锁固定为 `SET NX PX 3000`,Token 使用密码学安全随机源且熵不少于 128 bit,禁止续租。只有取得锁后才开启新的短 `READ COMMITTED` 事务;其他请求最多等待 500 ms,仍未命中就直读 PostgreSQL且不再竞争回填。 +- 唯一回填者只有在 2 秒有效窗口内完成查询、且 Lua 比较确认锁值仍等于本 Token 时,才能在同一脚本写缓存和 TTL;释放锁也必须 Lua 比较 Token 后删除。超过窗口、锁丢失、Token 不匹配或脚本结果未知时只返回数据库结果,不能覆盖后继回填者。 +- 正常结果 TTL 为 60 秒,空结果 TTL 为 10 秒。每个会改变 C07 响应的来源事务必须同时提交 Immediate 与 Delayed 两条独立 DB102:共享稳定 `operationId/invalidationBaseTime`,分别使用独立事件 ID。Immediate 使用固定立即调度;Delayed 以 `after_commit_delay` 模式初始未武装,提交可见后由 Outbox 调度器用数据库 `clock_timestamp()` 原子写入 `armed_at` 和 `available_at=armed_at+3 秒`,最早不早于提交可见后 3 秒,调度延迟只会更晚。两条责任独立消费、独立幂等 DEL、失败各自恢复,不能由 Immediate 消费者或调度器事后创建 Delayed,Immediate 也不能武装、取消或推迟 Delayed。 +- 若 Delayed 武装或两次删除持续失败,计入最长 2 秒有效回填窗口后,正常旧值最迟在业务提交可见后 62 秒、空结果最迟 12 秒自然消失;任何交易写入仍直接使用 PostgreSQL,不等待缓存收敛。 - 触发详情和受影响固定首页失效的事实包括:名称、价格、普通库存、描述、分类展示、图片/主图/排序、销售状态和删除;普通订单扣减/取消回补;M10 普通库存售后回补;C01 发布时普通库存划转为秒杀配额。 - C01 活动内部秒杀库存变化、原活动库存取消/售后回补以及 M07 评价和评分变化不触发 C07;它们本来不属于本期缓存响应。 +- A102/A103/A226 JSON 响应固定返回 `Cache-Control: no-store`;A227 因有效 Buyer Token 可增加本人限购字段,固定返回 `Cache-Control: private, no-store`。Nginx、CDN、浏览器和 Service Worker 均不得形成第二层业务 JSON 缓存。只有不可变、版本化媒体 URL 可以按独立媒体策略长期缓存。 - 事务提交后才触发失效,回滚不触发;Redis 故障时所有公开读取回退 PostgreSQL。F08、C01、M10 等交易动作始终重读并修改 PostgreSQL 事实,缓存只影响性能。 #### 3.8.7 C10 统一入口与单 API 实例故障交接 @@ -744,7 +853,7 @@ flowchart TD PG["PostgreSQL 可连接"] --> MIG["一次性 Migrator
唯一 Migration 执行者"] MIG -->|"目标 Migration 成功"| APPS["同版本 API 1、API 2 与 Worker 启动"] MIG -->|"失败或版本不兼容"| STOP["API/Worker 不进入就绪"] - APPS --> READY{"安全配置、运行版本、Migration 与 PostgreSQL 均通过?"} + APPS --> READY{"HTTP/Hub/期望/双 API 认证摘要一致
安全配置、版本、Migration 与 PostgreSQL均通过?"} READY -- "否" --> STOP READY -- "是" --> NGINX["Nginx 只向就绪 API 转发"] WEB["PC Web"] --> NGINX @@ -765,13 +874,15 @@ flowchart TD 交接约束: - 同一版本只启动一次一次性 Migrator;API 和 Worker 不并发自动迁移。启动顺序固定为 PostgreSQL 就绪 → Migrator 成功 → API/Worker 启动并通过就绪检查。 -- 全局就绪只由安全配置、运行版本兼容、目标 Migration 匹配和 PostgreSQL 决定。Redis、RabbitMQ 和 SeaweedFS 是能力级状态,单项故障不把整个 API 误判为不可用。 +- 每个 API 必须分别从实际生效的 HTTP Bearer 与 SignalR Hub 选项生成规范 JSON SHA-256 认证摘要,字段精确为 `Issuer`、`Audience`、非秘密验签材料 `keyFingerprint`、`accessTokenLifetimeSeconds`、`clockSkewSeconds=0`、`tokenVersionValidationRule`。摘要不得包含原始 Secret、私钥或对称密钥哈希。 +- A507 必须证明本实例 HTTP 摘要、Hub 摘要、Compose 注入的同版本期望摘要三者相等,部署门还必须证明两个 API 摘要相同;任一项不一致时 C10 全局 `NotReady`,不能只摘除错误实例后把单实例运行伪装成双实例验收通过。运行版本兼容、目标 Migration 匹配和 PostgreSQL 同样属于全局门槛;Redis、RabbitMQ 和 SeaweedFS 才是能力级状态。 - Redis 故障时 C07 公开读取回退 PostgreSQL,C06 实时能力关闭;任何需要令牌撤销、账号禁用、手机号变更或旧凭证失效事实的受保护 HTTP/Hub 请求无法确认时均失败关闭。 - Redis 基础连通恢复后可恢复公开缓存;受保护请求和实时连接只有在撤销事实重建完成且安全健康检查通过后才能恢复。恢复过程不得短暂放行未知旧凭证。 - RabbitMQ 故障时已提交业务事实和 Outbox 保留、投递暂停;SeaweedFS 故障时上传和对象写入失败,其他不依赖对象写入的能力继续。 -- 查询请求可以有限重试;提交类请求不得盲目重放,结果未知时复用原幂等标识或回到所属业务权威查询确认。C06 仅使用 WebSockets 并跳过协商,不依赖会话亲和。 -- 停止单个 API 时,Nginx 先停止向目标实例转发,目标实例在有界时间内排空请求、关闭本实例 Hub、刷新日志后退出;Worker 和共享依赖继续运行。 -- 停止整个系统时,先停止全部新流量,再排空 API、关闭 Hub、让 Worker 停止领取并完成或安全释放任务、刷新遥测,最后停止应用和共享依赖;PostgreSQL、Redis、RabbitMQ 和对象存储卷必须保留。 +- 查询请求可以有限重试;提交类请求不得盲目重放,结果未知时复用原幂等标识或回到所属业务权威查询确认。C06 仅使用 WebSockets 并跳过协商,不依赖会话亲和;Hub 固定 `CloseOnAuthenticationExpiration=true、ClockSkew=0`。 +- C06 的共享 RabbitMQ 提示队列只竞争产生一个 Redis 发布入口;`eshop:{environment}:signalr:message-hints:v1` 让每个 API 的 `RealtimeFanoutHostedService` 各接收一次。各实例只能向本地复核通过的 `connectionId` 执行 `Clients.Client(connectionId)`,不能使用入口 `Clients.User` 替代远端复核。 +- 停止单个 API 时,目标实例先变为 `NotReady` 并由 Nginx 停止转发,再停止取得新实时提示/安全广播、让未 ACK 的共享提示交给其他实例、有界排空 HTTP,最后按本地登记逐一 Abort Hub 连接并刷新遥测后退出;Worker 和共享依赖继续运行。 +- 停止整个系统时,先停止全部新流量,再让 API 停领共享提示并排空、逐实例 Abort 全部本地 Hub、停止安全队列,让 Worker 停止领取并完成或安全释放任务,刷新遥测后才停止应用和共享依赖;PostgreSQL、Redis、RabbitMQ 和对象存储卷必须保留。 - 本节只承诺 C10 的双 API 分发和单 API 实例故障能力,不宣称 Nginx、PostgreSQL、Redis、RabbitMQ 或对象存储具备集群高可用或跨机房容灾。 #### 3.8.8 C01 下单、支付、取消、履约与缓存交接 @@ -793,7 +904,13 @@ flowchart LR ```mermaid flowchart LR - ORDER["M04 订单项、实付快照与履约状态"] --> AFTER["M10 售后资格与独立状态机"] + SUBMIT["A412 提交售后申请"] --> IDEMP["DB104 售后幂等范围"] + IDEMP --> PRE["无锁预读 DB061 不可变
assignedMerchantUserId"] + PRE --> GATE["锁 DB001 责任商家门
重检 Merchant + Normal"] + GATE --> ORDERLOCK["锁 DB061 订单
重检买家、指定商家、状态与时限"] + ORDERLOCK --> ITEMLOCK["锁/聚合 DB086 既有申请
与 DB062 目标订单项"] + ITEMLOCK --> AFTER["M10 售后资格与独立状态机"] + ORDER["M04 订单项、实付快照与履约状态"] --> AFTER AFTER -->|"非终态申请/已退款数量"| SHIP["M06-02 发货准入"] AFTER -->|"稳定退款操作"| PAY["M05 幂等退回买家钱包"] PAY -->|"成功"| STOCK{"原库存来源?"} @@ -803,6 +920,10 @@ flowchart LR PAY -. "退款资金事实" .-> C08["C08 每日退款对账"] ``` +- A412 固定锁序为 `DB104 售后幂等范围 → 无锁预读 DB061 不可变 assignedMerchantUserId → DB001 责任商家门 → DB061 订单 → DB086 既有申请/DB062 目标订单项`。预读只定位门行,不能授权;锁定 DB061 后必须重新确认买家归属和指定商家。商家禁用先提交时拒绝新申请,A412 先提交时 A016/A307 必须在同一门行/订单事实后看到已经提交的售后责任,禁止按 A307 的“先订单”锁序反向实现。 +- `Initial`、`AutomaticRetry`、`ManualRetry` 的退款执行租约统一为创建时 60 秒、每 20 秒携带 `refundAttemptId + executionToken + Executing` 条件续租、绝对不超过数据库 `startedAt+5 分钟`。续租或提交影响 0 行时旧执行器必须停止并重读,不能提交迟到结果。 +- Worker 的 Unknown 核实和自动重试调度使用独立 `recoveryLeaseToken/recoveryLeaseAcquiredAt/recoveryLeaseExpiresAt`,同样为 60 秒、20 秒、5 分钟上限,但不得与真实退款执行租约复用 Token、字段或责任。执行租约到期只先原子收敛为 `Unknown` 并建立恢复责任,不能直接开启下一笔退款。 + #### 3.8.10 C08 回调、订单、支付、对账与消息交接 ```mermaid @@ -815,11 +936,21 @@ flowchart LR ATOMIC -. "支付成功" .-> MSG["M09 买家 + assignedMerchantUserId"] IGNORE -. "不生成 M09" .-> MSG DIFF -. "不生成 M09" .-> MSG - WORKER["每日对账批次"] -->|"固定 UTC 范围与水位"| RECON["聚合支付、订单、钱包退款与售后事实"] + WORKER["Worker 每日 00:05Z 触发
每分钟漏跑检查 + 启动补查"] --> RUN["按缺失日期升序领取
jobName + runKey=YYYY-MM-DD"] + RUN --> WATERMARK["等待已分配序号事务结束
冻结 watermarkSequence/watermarkAt"] + WATERMARK --> SNAPSHOT["此后开启新的 REPEATABLE READ
读取固定 UTC 范围"] + SNAPSHOT --> RECON["按 postingSequence 锚点聚合
支付、订单、钱包退款与售后事实"] DIFF --> RECON RECON --> ADMIN["管理员领取、举证、受控处置并重新核验后关闭"] ``` +C08 对账交接约束: + +- 三个调度入口只发现同一日期责任:固定 `00:05:00Z` 触发、每分钟漏跑检查、Worker 启动立即补查;`jobName=PaymentDailyReconciliation`、`runKey=YYYY-MM-DD`,只处理已经到达次日 00:05 的完整 UTC 日。多日缺失从最早日期开始升序逐日补齐,失败日复用原任务责任并阻断后续日期,空日也生成 `Matched` 批次。 +- 每日任务先等待已分配财务序号的在途事务提交或回滚,再读取已提交 `watermarkSequence/watermarkAt`,此后才开启新的 `REPEATABLE READ` 只读快照。固定范围为 `[businessDate 00:00:00Z, businessDate+1 day 00:00:00Z)` 且 `postingSequence <= watermarkSequence`;不得先建旧快照再等水位,也不得用水位后的当前值改写旧日证据。 +- 唯一 `postingSequence` 是计数锚点;同一原子财务结果按 `RefundOperation > Payment > Callback` 选取锚点类型。`totalCount` 为去重锚点数,`matchedCount` 为零差异锚点数,`differenceCount` 为至少命中一条规则的锚点数,固定满足 `totalCount=matchedCount+differenceCount`;同一锚点命中多条规则仍只让 `differenceCount` 加一。 +- `differenceCountsByType` 按实际差异行分组,不按锚点去重,因此各类型合计允许大于 `differenceCount`。同一锚点、比较规则码和规则版本只生成一条差异并合并证据,不同规则不得互相覆盖。 + 直接交接约束: - 调用方只传业务标识和必要命令,目标模块重新校验当前身份、归属和状态。 @@ -831,19 +962,19 @@ flowchart LR | 教师编号 | 核心状态或确定结果 | 直接入口 → 直接出口 | 本文流程 | 主责人 | 当前成熟度 | |---|---|---|---|---|---| -| F01 | 创建 `Normal` 买家账号,不自动登录 | 游客注册 → F02 登录 | [M01-01 用户注册流程](tyh/M01-01-用户注册流程.md)、3.1 | 唐宇昊 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | -| F02 | 有效 JWT + 服务端角色;退出后当前 JWT 失效 | M01 → 全部受保护入口 | [M01-02 用户登录与退出流程](tyh/M01-02-用户登录与退出流程.md)、3.1、3.8.1 | 唐宇昊 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | -| F03 | 本人资料与地址;敏感修改后全部旧凭证失效 | M01 Address → M04 地址快照 | [M01-03 个人信息与收货地址流程](tyh/M01-03-个人信息与收货地址流程.md)、3.2、3.8.1 | 唐宇昊 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | -| F04 | 只返回 `OnSale` 商品的稳定分页列表 | M02 → 购物端列表 | [M02 分类与商品流程](gxy/M02-分类与商品流程.md)、3.3 | 顾欣月 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | -| F05 | 强制公开过滤下的安全关键词与组合查询 | 查询条件 → M02/C04 → F04 列表 | [M02 分类与商品流程](gxy/M02-分类与商品流程.md)、[C04 中文搜索流程](gxy/C04-中文搜索流程.md)、3.3 | 顾欣月 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | -| F06 | 商品自身公开详情、最新价格库存和明确可售状态 | F04 → M02 详情 → M03/M07/M08 | [M02 分类与商品流程](gxy/M02-分类与商品流程.md)、3.3、3.8.1 | 顾欣月 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | -| F07 | 本人购物车;选中与可结算状态由服务端实时派生 | M01/M02 → M03 → M04 | [M03 购物车流程](zhh/M03-购物车流程.md)、3.4、3.8.1 | 朱惠惠 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | -| F08 | 唯一 `PendingPayment` 订单、快照、默认商家、固定截止时间、库存扣减与购物车清理 | M01/M02/M03 → M04 → M05 | [M04 订单流程](wqq/M04-订单流程.md)、3.4、3.8.2 | 韦乾强 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | -| F09 | 本人订单可查;合法到达 `Cancelled` 或 `Completed` | M04/M05/系统任务 → M04 | [M04 订单流程](wqq/M04-订单流程.md)、[C03 订单超时流程](wqq/C03-订单超时流程.md)、3.6、3.8.2~3.8.3 | 韦乾强 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | -| F10 | `Wallet` 或 `SimulatedChannel` 形成唯一确定支付事实并推进 `Paid` | M04 → M05/C08 → M04/M06-02 | [M05 支付流程](zhy/M05-支付流程.md)、[C08 支付回调与对账流程](zhy/C08-支付回调与对账流程.md)、3.5、3.8.2、3.8.10 | 张海洋 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | -| F11 | 商品处于 `Draft`、`OnSale`、`OffSale`,或满足约束后完成删除 | M06-01 → M02 → F04~F06/C07 | [M06-01 后台商品管理流程](gxy/M06-01-后台商品管理流程.md)、3.3、3.7、3.8.4 | 顾欣月 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | -| F12 | 责任商家经售后快照复核后合法 `Paid → Shipped` | M04/M10 → M06-02 → M04 | [M06-02 商家履约流程](wqq/M06-02-商家履约流程.md)、3.6、3.7、3.8.3 | 韦乾强 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | -| F13 | 买家/商家账号 `Normal ↔ Disabled`,旧凭证和商家责任结果明确 | M06-03 → M01/M04/M10/C01 → 全部受保护入口 | [M06-03 后台用户管理流程](tyh/M06-03-后台用户管理流程.md)、3.1、3.7、3.8.4 | 唐宇昊 | 完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| F01 | 创建 `Normal` 买家账号,不自动登录 | 游客注册 → F02 登录 | [M01-01 用户注册流程](tyh/M01-01-用户注册流程.md)、3.1 | 唐宇昊 | 完整定义,已校准;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | +| F02 | 有效 JWT + 服务端角色;退出后当前 JWT 失效 | M01 → 全部受保护入口 | [M01-02 用户登录与退出流程](tyh/M01-02-用户登录与退出流程.md)、3.1、3.8.1 | 唐宇昊 | 完整定义,已校准;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | +| F03 | 本人资料与地址;敏感修改后全部旧凭证失效 | M01 Address → M04 地址快照 | [M01-03 个人信息与收货地址流程](tyh/M01-03-个人信息与收货地址流程.md)、3.2、3.8.1 | 唐宇昊 | 完整定义,已校准;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | +| F04 | 只返回 `OnSale` 商品的稳定分页列表 | M02 → 购物端列表 | [M02 分类与商品流程](gxy/M02-分类与商品流程.md)、3.3 | 顾欣月 | 完整定义,已校准;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | +| F05 | 强制公开过滤下的安全关键词与组合查询 | 查询条件 → M02/C04 → F04 列表 | [M02 分类与商品流程](gxy/M02-分类与商品流程.md)、[C04 中文搜索流程](gxy/C04-中文搜索流程.md)、3.3 | 顾欣月 | 完整定义,已校准;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | +| F06 | 商品自身公开详情、最新价格库存和明确可售状态 | F04 → M02 详情 → M03/M07/M08 | [M02 分类与商品流程](gxy/M02-分类与商品流程.md)、3.3、3.8.1 | 顾欣月 | 完整定义,已校准;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | +| F07 | 本人购物车;选中与可结算状态由服务端实时派生 | M01/M02 → M03 → M04 | [M03 购物车流程](zhh/M03-购物车流程.md)、3.4、3.8.1 | 朱惠惠 | 完整定义,已校准;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | +| F08 | 唯一 `PendingPayment` 订单、快照、默认商家、固定截止时间、库存扣减与购物车清理 | M01/M02/M03 → M04 → M05 | [M04 订单流程](wqq/M04-订单流程.md)、3.4、3.8.2 | 韦乾强 | 完整定义,已校准;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | +| F09 | 本人订单可查;合法到达 `Cancelled` 或 `Completed` | M04/M05/系统任务 → M04 | [M04 订单流程](wqq/M04-订单流程.md)、[C03 订单超时流程](wqq/C03-订单超时流程.md)、3.6、3.8.2~3.8.3 | 韦乾强 | 完整定义,已校准;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | +| F10 | `Wallet` 或 `SimulatedChannel` 形成唯一确定支付事实并推进 `Paid` | M04 → M05/C08 → M04/M06-02 | [M05 支付流程](zhy/M05-支付流程.md)、[C08 支付回调与对账流程](zhy/C08-支付回调与对账流程.md)、3.5、3.8.2、3.8.10 | 张海洋 | 完整定义,已校准;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | +| F11 | 商品处于 `Draft`、`OnSale`、`OffSale`,或满足约束后完成删除 | M06-01 → M02 → F04~F06/C07 | [M06-01 后台商品管理流程](gxy/M06-01-后台商品管理流程.md)、3.3、3.7、3.8.4 | 顾欣月 | 完整定义,已校准;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | +| F12 | 责任商家经售后快照复核后合法 `Paid → Shipped` | M04/M10 → M06-02 → M04 | [M06-02 商家履约流程](wqq/M06-02-商家履约流程.md)、3.6、3.7、3.8.3 | 韦乾强 | 完整定义,已校准;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | +| F13 | 买家/商家账号 `Normal ↔ Disabled`,旧凭证和商家责任结果明确 | M06-03 → M01/M04/M10/C01 → 全部受保护入口 | [M06-03 后台用户管理流程](tyh/M06-03-后台用户管理流程.md)、3.1、3.7、3.8.4 | 唐宇昊 | 完整定义,已校准;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | ## 五、选做与挑战流程登记 @@ -851,16 +982,16 @@ flowchart LR | 编号 | 基础核心流程 | 直接扩展入口 → 出口 | 不可变核心结果 | 主责人 | 当前状态 | |---|---|---|---|---|---| -| X01 | F09、F06 | 本人 `Completed` 订单项 → 提交时资格重检 → 唯一公开评价 | 订单保持 `Completed`;不修改商品状态、价格、库存,不触发 C07 或 M09 | 顾欣月 | [M07 商品评价流程](gxy/M07-商品评价流程.md):完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | -| X02 | F02、F06,展示衔接 F04 | 已登录买家查看详情 → 本人收藏/最近 200 条浏览记录 | 不修改商品事实;游客不产生个人记录;关闭历史只阻止未来写入 | 唐宇昊 | [M08 商品收藏与浏览历史流程](tyh/M08-商品收藏与浏览历史流程.md):完整定义,已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | -| X03 | F02、F13;F08、F09、F10、F12、X04 | 固定来源事实 → 整事件消息落库 → 查询/高水位已读/离线补查 | 消息和推送失败不回滚核心事务;接收人不能由调用方任意扩张 | 罗皓晨 | [M09 站内消息流程](lhc/M09-站内消息流程.md):完整定义,接收矩阵与原子性已冻结;接口已按流程重建,待数据库、实现和测试承接 | -| X04 | F09、F10、F12 | 本人合格订单项 → 独立售后状态 → 稳定退款操作 → 原库存通道回补 | 不覆盖订单核心状态和快照;退款不超实付且不重复入账/回补 | 张海洋 | [M10 售后流程](zhy/M10-售后流程.md):完整定义,履约竞争和退款闭环已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | -| C01 | F11 + F04/F06 → F08 → F10/F09/F12 | 发布时普通库存原子划转 → 独立库存扣减 → M04 统一 `PendingPayment` | 后续复用核心支付和履约;取消/售后只回原活动库存;活动创建人不决定履约商家 | 朱惠惠 | [C01 秒杀流程](zhh/C01-秒杀流程.md):完整定义,库存归属和默认商家已冻结;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | -| C03 | F08、F10、F09 | 到达固定 `paymentDeadline` → Worker 复用 M04 统一取消 → `Cancelled` | 与 Wallet/C08 只能一个胜出;原库存通道回补原子且幂等 | 韦乾强 | [C03 订单超时流程](wqq/C03-订单超时流程.md):完整定义,固定截止时间已冻结;接口与 Worker 契约已按流程重建,待数据库、实现和测试承接 | -| C04 | F04、F05、F06 | 同一查询入口选择高级搜索 → 失败时安全回退基础搜索 | 只公开 `OnSale`;权限、强制筛选、下单重校验和性能口径不变 | 顾欣月 | [C04 中文搜索流程](gxy/C04-中文搜索流程.md):完整定义,降级与验收口径已校准;接口已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| X01 | F09、F06 | 本人 `Completed` 订单项 → 提交时资格重检 → 唯一公开评价 | 订单保持 `Completed`;不修改商品状态、价格、库存,不触发 C07 或 M09 | 顾欣月 | [M07 商品评价流程](gxy/M07-商品评价流程.md):完整定义,已校准;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | +| X02 | F02、F06,展示衔接 F04 | 已登录买家查看详情 → 本人收藏/最近 200 条浏览记录 | 不修改商品事实;游客不产生个人记录;关闭历史只阻止未来写入 | 唐宇昊 | [M08 商品收藏与浏览历史流程](tyh/M08-商品收藏与浏览历史流程.md):完整定义,已校准;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | +| X03 | F02、F13;F08、F09、F10、F12、X04 | 固定来源事实 → 整事件消息落库 → 查询/高水位已读/离线补查 | 消息和推送失败不回滚核心事务;接收人不能由调用方任意扩张 | 罗皓晨 | [M09 站内消息流程](lhc/M09-站内消息流程.md):完整定义,接收矩阵与原子性已冻结;接口已按流程重建,数据库设计已确认,待实现和测试承接 | +| X04 | F09、F10、F12 | 本人合格订单项 → 独立售后状态 → 稳定退款操作 → 原库存通道回补 | 不覆盖订单核心状态和快照;退款不超实付且不重复入账/回补 | 张海洋 | [M10 售后流程](zhy/M10-售后流程.md):完整定义,履约竞争和退款闭环已校准;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | +| C01 | F11 + F04/F06 → F08 → F10/F09/F12 | 发布时普通库存原子划转 → 独立库存扣减 → M04 统一 `PendingPayment` | 后续复用核心支付和履约;取消/售后只回原活动库存;活动创建人不决定履约商家 | 朱惠惠 | [C01 秒杀流程](zhh/C01-秒杀流程.md):完整定义,库存归属和默认商家已冻结;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | +| C03 | F08、F10、F09 | 到达固定 `paymentDeadline` → Worker 复用 M04 统一取消 → `Cancelled` | 与 Wallet/C08 只能一个胜出;原库存通道回补原子且幂等 | 韦乾强 | [C03 订单超时流程](wqq/C03-订单超时流程.md):完整定义,固定截止时间已冻结;接口与 Worker 契约已按流程重建,数据库设计已确认,待实现和测试承接 | +| C04 | F04、F05、F06 | 同一查询入口选择高级搜索 → 失败时安全回退基础搜索 | 只公开 `OnSale`;权限、强制筛选、下单重校验和性能口径不变 | 顾欣月 | [C04 中文搜索流程](gxy/C04-中文搜索流程.md):完整定义,降级与验收口径已校准;接口已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | | C06 | F02、F13;经 X03 接入核心与售后事实 | M09 消息提交 → WebSocket 轻提示/固定重连 → M09 权威补查 | 推送失败不改变消息与业务事实;未知撤销状态必须关闭连接 | 罗皓晨 | [C06 实时推送流程](lhc/C06-实时推送流程.md):完整定义,传输与连接边界已冻结;Hub 与事件契约已按流程重建,待部署、实现和测试承接 | -| C07 | F04、F06、F11;普通库存变更扩展至 F08/F09/C01/X04 | 固定首页/A103 Cache-Aside;提交后立即及第 3 秒失效 | PostgreSQL 是事实源;范围、60/10 秒 TTL、2 秒回填窗、500 ms 等待和 62/12 秒旧值上限固定 | 罗皓晨、顾欣月 | [C07 缓存流程](lhc/C07-缓存流程.md):完整定义,范围、参数与失效矩阵已冻结;缓存契约已按流程重建,待实现和压测承接 | -| C08 | F10、F09;退款对账关联 X04 | 受控回调 → 四种终态 → 每日固定范围对账 → 领取/举证/复核闭环 | 不扣 Wallet;迟到成功只形成 Difference;不重复支付、退款或关闭差异 | 张海洋 | [C08 支付回调与对账流程](zhy/C08-支付回调与对账流程.md):完整定义,通道竞争与差异闭环已冻结;接口与 Worker 契约已按流程重建,待数据库、OpenAPI、实现和测试承接 | +| C07 | F04、F06、F11;普通库存变更扩展至 F08/F09/C01/X04 | 固定首页/A103 Cache-Aside;Immediate 尽快失效,Delayed 提交可见后武装并等待 3 秒失效 | PostgreSQL 是事实源;范围、60/10 秒 TTL、2 秒回填窗、500 ms 等待和 62/12 秒旧值上限固定 | 罗皓晨、顾欣月 | [C07 缓存流程](lhc/C07-缓存流程.md):完整定义,范围、参数与失效矩阵已冻结;缓存契约已按流程重建,待实现和压测承接 | +| C08 | F10、F09;退款对账关联 X04 | 受控回调 → 四种终态 → 每日固定范围对账 → 领取/举证/复核闭环 | 不扣 Wallet;迟到成功只形成 Difference;不重复支付、退款或关闭差异 | 张海洋 | [C08 支付回调与对账流程](zhy/C08-支付回调与对账流程.md):完整定义,通道竞争与差异闭环已冻结;接口与 Worker 契约已按流程重建,数据库设计已确认,待 OpenAPI、实现和测试承接 | | C10 | F01~F13 全部横切;支撑 C06/C07 与 Worker | Migrator → 全局就绪 → 能力级降级/恢复 → 有序停止 | 状态机和数据库结果不变;未知安全事实失败关闭;单实例切换不重复写或越权 | 罗皓晨 | [C10 高可用流程](lhc/C10-高可用流程.md):完整定义,迁移、就绪、降级、恢复和停止边界已冻结;接口与运行契约已按流程重建,待部署、实现和验收承接 | 流程层已关闭的原阻断项: -- Gitee From fb4f1b390f7917106d7d14c3ade835343f07faaa Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Sat, 25 Jul 2026 16:58:20 +0800 Subject: [PATCH 113/118] =?UTF-8?q?docs(design):=20=E6=8C=89=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E6=A0=A1=E5=87=86=E6=8E=A5=E5=8F=A3=E5=92=8C=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=EF=BC=9B=E7=BB=9F=E4=B8=80=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E4=B8=8E=E8=AF=BB=E6=A8=A1=E5=9E=8B=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interface/interface-gxy.md" | 2 + .../interface/interface-lhc.md" | 2 + .../interface/interface-tyh.md" | 2 + .../interface/interface-wqq.md" | 4 +- .../interface/interface-zhh.md" | 2 + .../interface/interface-zhy.md" | 2 + ...75\345\220\215\350\247\204\350\214\203.md" | 6 +- ...45\345\217\243\350\256\276\350\256\241.md" | 1741 +++++++++++------ ...56\345\272\223\350\256\276\350\256\241.md" | 587 ++++-- ...66\346\236\204\350\256\276\350\256\241.md" | 96 +- 10 files changed, 1690 insertions(+), 754 deletions(-) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" index 9317d3d..5e7c597 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" @@ -1,5 +1,7 @@ # 接口设计(顾欣月)— Catalog、Review +> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`../接口设计.md`](../接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 + > 组别:24级1班第7组 编写人:顾欣月 编写日期:2026-07-24 版本:v0.1 > 编号区间:`A101`~`A200` 负责模块:Catalog(商品目录)、Review(评价) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" index da1231a..92a416d 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" @@ -1,5 +1,7 @@ # 罗皓晨接口设计 +> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`../接口设计.md`](../接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 + > 负责人:罗皓晨 > 负责范围:M00 公共基建与集成、M09 站内消息通知(X03)、C06 实时消息推送、C07 缓存与性能优化、C10 容器化部署与负载均衡 > 接口编号范围:`A501`~`A600` diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" index da13199..a2a8836 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" @@ -1,5 +1,7 @@ # 个人接口文件 — 唐宇昊(Identity、Engagement) +> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`../接口设计.md`](../接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 + > 组别:24级1班第7组 负责人:唐宇昊(tyh) 接口编号区间:`A001`~`A100` > 负责模块:Identity(注册、登录退出、JWT、用户资料、收货地址、后台账号治理)、Engagement(收藏、浏览历史) > 关联教师验收编号:F01、F02、F03、F13、X02 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" index 64c4ac8..bea6026 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" @@ -1,5 +1,7 @@ # 韦乾强 - 订单模块接口详细定义 +> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`../接口设计.md`](../接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 + > 负责人:韦乾强 > 模块:Ordering(含买家订单与商家订单管理) > 接口编号范围:A301~A308 @@ -10,7 +12,7 @@ ## 接口清单 | 编号 | 模块 | 需求编号 | 名称 | 方法 | 路径 | operationId | 请求Schema | 响应Schema | 鉴权 | 关联DB | 状态 | -|---|---|---|---|---|---|---|---|---|---|---|---|---| +|---|---|---|---|---|---|---|---|---|---|---|---| | A301 | Ordering | F08 | 提交订单 | POST | /api/orders | Ordering_CreateOrder | CreateOrderRequest | CreateOrderResponse | BuyerOnly | DB061,DB062 | 部分定义 | | A302 | Ordering | F09、C01 | 查询订单列表 | GET | /api/orders | Ordering_ListOrders | - | OrderListResponse | BuyerOnly | DB061,DB062 | 部分定义 | | A303 | Ordering | F09、C01 | 查询订单详情 | GET | /api/orders/{orderId} | Ordering_GetOrder | - | OrderDetailResponse | BuyerOnly | DB061,DB062 | 部分定义 | diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" index 1a6529e..6a23b64 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" @@ -1,5 +1,7 @@ # 个人接口文件 — 朱惠惠(Cart、Seckill) +> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`../接口设计.md`](../接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 + > 组别:24级1班第7组 负责人:朱惠惠(zhh) 接口编号区间:`A201`~`A300` > 负责模块:Cart(购物车 CRUD、选中、结算预览、清空)、Seckill(商家活动维护、买家抢购下单;订单查询复用 Ordering 的 A302/A303) > 关联教师验收编号:F07、C01 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" index b42eeaa..d90c732 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" @@ -1,5 +1,7 @@ # 张海洋个人接口文件(A401-A500) +> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`../接口设计.md`](../接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 + > **模块**:Payment(含支付对账) / AfterSales > **负责人**:张海洋(zhy) > **范围**:M05-01 模拟支付 + M10 售后流程 + C08 支付回调幂等与对账 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" index 519c794..8ba637b 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" @@ -95,8 +95,8 @@ - 图片和附件使用小写 `kebab-case`,例如 `order-checkout-flow.png`,不使用 `截图1.png`、`最终版2.png`。 - 文件名不得包含姓名、日期或版本,除非日报、周报、Migration、发布材料等规则明确要求。 - 接口并行设计阶段统一在 `docs/02-设计文档/interface/` 保存个人原稿,固定使用 `interface-<姓名拼音首字母>.md`,例如 `interface-tyh.md`;首字母必须全小写。 -- 数据库并行设计阶段统一在 `docs/02-设计文档/database/` 保存个人原稿,固定使用 `database-<姓名拼音首字母>.md`,例如 `database-lhc.md`;首字母必须全小写。 -- 个人接口和数据库原稿汇总后继续保留,但不得覆盖对应的 `接口设计.md` 和 `数据库设计.md` 主事实源。 +- 数据库不按成员拆分个人原稿,只维护 `docs/02-设计文档/数据库设计.md`;不得新建 `database-<姓名拼音首字母>.md`,也不得等待个人稿后再实现。 +- 个人接口原稿汇总后继续保留,仅用于贡献追踪,不能覆盖 `接口设计.md`;数据库只以统一主文档为事实源。 ## 四、Vue 3、TypeScript、Vite、Pinia、Axios与UI @@ -310,7 +310,7 @@ Android约定: | 主键约束 | `pk_
` | `pk_orders` | - 不使用`user`、`order`等保留字作为单数表名,使用`users`、`orders`。 -- `DBxxx`只用于《数据库设计》的表追踪与分工,不进入真实表名、实体名、DbSet、约束名或Migration名;编号合入`dev`后不得复用。 +- `DBxxx`只用于《数据库设计》的表设计追踪和跨文档引用,不代表成员分工,也不进入真实表名、实体名、DbSet、约束名或Migration名;编号合入`dev`后不得复用。 - 布尔字段使用`is_`或`has_`:`is_enabled`。 - 时间点使用`_at`:`created_at`、`paid_at`;自然日期使用`_date`。 - 金额使用`_amount`,单价使用`unit_price`,数量使用`quantity`。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" index 8398c0c..7c3034a 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" @@ -3,7 +3,7 @@ > 组别:24级1班第7组 编写人:罗皓晨 编写日期:2026-07-24 版本:v1.0 > 截止:第 2 周周三(开发过程中持续更新,保持与代码一致) > -> 当前状态:已按业务流程 v1.0 重建统一契约;共 109 个追踪编号,其中 99 个活动 HTTP 定义、10 个历史取消编号;待 OpenAPI、数据库、实现、测试和正式交叉评审承接 +> 当前状态:已按业务流程 v1.0 重建并完成与统一数据库设计的全链路校准;共 109 个追踪编号,其中 99 个活动 HTTP 定义、10 个历史取消编号;待 OpenAPI、实现、测试和正式交叉评审承接 ## 一、通用约定 @@ -139,10 +139,24 @@ - 受保护接口使用 `Authorization: Bearer `。 - JWT 至少包含用户 ID、角色、`jti`、过期时间和账号令牌版本;不得包含密码、余额、完整手机号等敏感业务数据。 -- API 依次校验签名、Issuer、Audience、有效期、撤销状态、账号状态和账号令牌版本。 +- API 依次校验签名、Issuer、Audience、有效期、撤销状态、账号状态和账号令牌版本;有效期验证固定 `ClockSkew=0`,到达 `exp` 即失效。 - Token 缺失、格式错误、签名无效、过期、已撤销或版本失效时返回 `401 Unauthorized`;凭据有效但账号状态为 `Disabled` 时返回 `403 AUTH.ACCOUNT_DISABLED`。 - 退出接口只使当前 Token 失效时,应以 `jti` 为范围执行;是否退出全部设备由专用接口另行定义。 - 撤销状态、账号状态、手机号变更或全部旧凭证失效事实无法安全确认时返回 `503 AUTH.TOKEN_SERVICE_UNAVAILABLE`,不得先读取或修改受保护资源。 +- 两个 API 与 SignalR Hub 必须使用同一 Issuer、Audience、签名材料标识、有效期、`ClockSkew` 和令牌版本规则,并暴露不含秘密的认证配置摘要参与 A507 就绪校验;摘要不一致的实例不得承接受保护流量。 + +受保护接口统一继承以下认证基线错误,详细 Axxx 的失败表只列该接口新增的参数、归属、状态和依赖错误;没有重复列出不表示可以省略: + +| HTTP 状态 | 业务错误码 | 统一含义 | +|---:|---|---| +| 401 | `AUTH.UNAUTHENTICATED` | 缺少凭据,或格式、签名、Issuer、Audience 无效 | +| 401 | `AUTH.TOKEN_EXPIRED` | 已到 JWT `exp` | +| 401 | `AUTH.TOKEN_REVOKED` | 当前 JTI 已退出或账号令牌版本失效 | +| 403 | `AUTH.ACCOUNT_DISABLED` | 凭据可验证但账号已禁用 | +| 403 | `AUTH.FORBIDDEN` | 当前角色不满足接口 Policy | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销、账号状态、令牌版本或认证安全水位无法确认 | + +前端全局 401 处理只响应上述 Bearer 失效结果并清理登录态;业务请求中的密码、验证码或其他字段校验不得借用 401。各接口如果有“当前密码不正确”等业务校验,必须使用其明确的 400 错误码并保持现有登录态。 #### 1.6.2 Policy 与资源授权 @@ -281,7 +295,7 @@ COMMON.VALIDATION_FAILED AUTH.INVALID_CREDENTIALS AUTH.TOKEN_EXPIRED CATALOG.PRODUCT_NOT_FOUND -CART.ITEM_NOT_AVAILABLE +CART.ITEM_UNAVAILABLE ORDER.INVALID_STATUS PAYMENT.INSUFFICIENT_BALANCE AFTER_SALES.INVALID_STATUS @@ -314,6 +328,7 @@ AFTER_SALES.INVALID_STATUS | `RESOURCE.NOT_FOUND` | 404 | 目标资源不存在 | | `RESOURCE.CONFLICT` | 409 | 当前资源状态与操作冲突 | | `IDEMPOTENCY.KEY_REUSED` | 409 | 同一幂等键被用于不同请求内容 | +| `IDEMPOTENCY.REQUEST_IN_PROGRESS` | 409 | 同一幂等请求仍在受控处理中,调用方应使用原键稍后重试 | ### 1.11 分页、筛选、搜索与排序 @@ -383,10 +398,11 @@ AFTER_SALES.INVALID_STATUS 规则: - 客户端生成 UUID 作为幂等键,并在不确定首次请求结果时使用原键重试。 -- 服务端幂等范围默认至少包含“已认证用户或可信调用方 + 接口/业务动作 + 幂等键”。A016/A017 是显式例外:二者共享“管理员 + 账号治理 + 幂等键”唯一范围,再把目标账号与禁用/启用动作绑定到请求指纹;同键换目标或换动作必须拒绝,不能因路径不同而分别接受。A301 也按 M04 上游规则采用订单创建事实内的 `(buyerId, Idempotency-Key)` 唯一范围,请求指纹绑定地址与购物车条目;该订单唯一范围不扩展为跨模块共享 Key。 +- 服务端幂等范围默认至少包含“已认证用户或可信调用方 + 接口/业务动作 + 幂等键”。A016/A017 是显式例外:二者共享“管理员 + 账号治理 + 幂等键”唯一范围,再把目标账号与禁用/启用动作绑定到请求指纹;同键换目标或换动作必须拒绝,不能因路径不同而分别接受。A301 也按 M04 上游规则采用订单创建事实内的 `(buyerId, Idempotency-Key)` 唯一范围,请求指纹绑定 `addressId + addressVersion + 按 UUID 字节升序的 cartItemIds + checkoutRevision`;该订单唯一范围不扩展为跨模块共享 Key。 - 完成身份、Header 和固定字段格式校验后,必须先查询持久化幂等结果,再读取库存、状态、资格、余额、限购、截止时间等会变化的事实;同键同指纹直接重放首次确定结果。 - 同一范围、同一幂等键、相同请求内容重复提交时,不重复执行副作用,返回首次已确认结果。 - 同一幂等键对应不同请求内容时返回 `409` 和 `IDEMPOTENCY.KEY_REUSED`。 +- 同一键、同一请求命中仍在有效租约内的跨对象 `Processing` 记录时,服务端可在本次请求内最多等待 2 秒读取确定结果;仍未完成则返回 `409 IDEMPOTENCY.REQUEST_IN_PROGRESS` 与 `Retry-After: 1`,该响应不是确定业务结果且不得保存为 Completed。只有租约到期后,新的执行者才可在同一工作清单上换发 fencing token 接管;旧执行者不得继续完成或清理对象。 - 已正式返回的成功和确定业务失败都应按具体接口保存为可重放结果;依赖中断、事务提交未知等瞬态结果不得伪装为确定失败或固化。 - 幂等记录的保留时间、唯一约束和响应重放范围由具体接口定义;资金、订单等关键结果不能只依赖短期内存缓存。 - 客户端按钮置灰、防抖只能改善体验,不能替代服务端幂等、唯一约束和状态条件。 @@ -398,11 +414,13 @@ AFTER_SALES.INVALID_STATUS |---|---|---|---| | A016/A017 | 共享 `(adminUserId, 账号治理, Idempotency-Key)`,指纹绑定目标账号与动作 | 账号治理与幂等事实留存期;本期不自动清理 | 首次确定的启用/禁用响应或确定失败 | | A122/A142/A220 | 已认证用户 + 接口 + `Idempotency-Key`,并由商品、订单项评价或活动唯一事实兜底 | 对应资源与幂等事实留存期;本期不自动清理 | 首次创建响应、`Location`(存在时)及确定失败 | +| A141 | 买家 + A141 + `Idempotency-Key`,指纹绑定订单项与文件内容 SHA-256 | 首次预留起 24 小时;Attached 后随评价图片生命周期保留 | 首次 `imageId`;同键续传同一对象工作,不重复占用图片名额 | | A201 | 买家 + A201 + `Idempotency-Key` | 首次确定结果提交后 24 小时 | 首次 HTTP 状态、响应体及确定失败;窗口内不得再次累加 | -| A204~A207 | 买家 + 具体接口 + `Idempotency-Key` | 首次确定结果提交后 24 小时 | 首次 204/200 或确定失败,不重复删除、清空或翻转 | +| A204/A207 | 不使用幂等键;分别按 `(currentBuyerId,cartItemId)` 与 `currentBuyerId` 条件删除 | 不建立幂等记录 | 影响 0/1 行或 0/N 行均返回 204,依靠最终状态天然幂等 | +| A205/A206 | 买家 + 具体接口 + `Idempotency-Key` | 首次确定结果提交后 24 小时 | 首次 200 或确定失败,不重复批量删除或翻转 | | A222/A223 | 商家 + 具体接口 + `Idempotency-Key` | 活动与幂等事实留存期;本期不自动清理 | 首次发布/取消响应及确定失败,不重复划拨或改写状态 | -| A228 | 买家 + A228 + `Idempotency-Key` | 秒杀订单与幂等事实留存期;本期不自动清理 | 首次秒杀下单响应及确定失败,不重复扣活动库存、占用限购或建单 | -| A301 | `(buyerId, Idempotency-Key)`,指纹绑定地址与购物车条目 | 订单与幂等事实留存期;本期不自动清理 | 首次普通订单响应及确定失败,不重复扣普通库存、建单或清理购物车 | +| A228 | 买家 + A228 + `Idempotency-Key`,指纹绑定规范化 `activityId + quantity + addressId + addressVersion` | 秒杀订单与幂等事实留存期;本期不自动清理 | 首次秒杀下单响应及确定失败,不重复扣活动库存、占用限购或建单 | +| A301 | `(buyerId, Idempotency-Key)`,指纹绑定规范化 `addressId + addressVersion + 按 UUID 字节升序的 cartItemIds + checkoutRevision` | 订单与幂等事实留存期;本期不自动清理 | 首次普通订单响应及确定失败,不重复扣普通库存、建单或清理购物车 | | A307 | 商家 + A307 + `Idempotency-Key` | 订单履约与幂等事实留存期;本期不自动清理 | 首次发货响应及确定失败,不重复发货 | | A402/A405 | 买家 + 具体接口 + `Idempotency-Key` | 资金流水、支付与幂等事实留存期;本期不自动清理 | 首次充值/支付结果或确定失败,不重复入账或扣款 | | A412/A415~A417/A419/A434 | 已认证用户 + 具体接口 + `Idempotency-Key` | 售后申请、退款操作、状态时间线与幂等事实留存期;本期不自动清理 | 首次申请/状态动作结果或确定失败,不重复占用数量、退款或写时间线 | @@ -421,7 +439,8 @@ AFTER_SALES.INVALID_STATUS - PostgreSQL 始终是业务事实来源;Redis 缓存不改变 API 契约和权限规则。 - 只有 A102 的固定首页形态和 A103 商品自身公开详情可以使用 C07 Cache-Aside:固定首页必须无筛选、`page=1`、`pageSize=12`、`OnSale`、`createdAt desc, productId desc`;A103 不含评分、评价或个人字段。分类、普通列表、搜索/筛选、评价、秒杀及其他接口不得缓存。 - 用户私人数据、钱包、支付结果和敏感管理数据默认不得被共享 HTTP 缓存。 -- 鉴权响应默认建议使用 `Cache-Control: no-store`;公开资源是否允许浏览器/CDN缓存由具体接口和部署方案决定。 +- A102/A103 的全部 JSON 响应固定 `Cache-Control: no-store`;A226 固定 `no-store`,A227 因有效 Buyer Token 会增加本人限购字段而固定 `private, no-store`。Nginx/CDN/Service Worker 不得保存这些动态 JSON,服务端 Redis Cache-Aside 不受此响应头影响。 +- 只有由不可变对象 Key 派生、内容变化必换 URL 且仍受公开引用保护的媒体响应可使用 `Cache-Control: public, max-age=31536000, immutable`;不得把该策略扩展到商品、活动、库存、评价或个人 JSON。 - 商品改价、库存或上下架后,即使客户端仍持有旧展示数据,提交订单时也必须以服务端数据库最新校验为准。 - 当前不全局启用 ETag、`Last-Modified` 或 CDN缓存;启用前必须补充一致性、权限和失效规则。 @@ -432,8 +451,8 @@ AFTER_SALES.INVALID_STATUS - 商品图片和评价图片的格式、单文件大小、总数量及尺寸限制在对应接口中明确。 - 原始文件名只用于安全展示,不直接作为对象存储 Key,不允许路径穿越字符影响存储位置。 - 对象存储失败时不得写入指向不存在对象的成功业务记录;数据库与对象存储无法组成单一事务时必须说明补偿和清理方式。 -- 数据库保存对象 Key、受控访问 URL 或必要元数据,不保存图片二进制。 -- 私有文件下载必须重新校验身份和资源归属,不能仅依赖不可预测 URL。 +- 数据库只保存对象 Key 与必要媒体元数据,不把访问 URL 作为长期业务事实,也不保存图片二进制。DB023 的 Attached/Detached 商品图与 DB025 的 Attached 评价图才是本期公开内容,响应 URL 固定由环境配置的只读媒体源和不可变对象 Key 派生,不携带存储签名、上传凭据或短于业务响应缓存的过期参数;A102/A103 缓存 62 秒最坏窗口内 URL 必须始终可读,历史订单引用的 Detached 商品图也沿用同一稳定读 URL。 +- 只读媒体源仅开放受控 Key 的 `GET/HEAD`,读取时确认 Key 仍由 DB023 Attached/Detached 或 DB025 Attached 事实引用;DB025 Uploading/Pending 必须返回 404,不能因为 Key 随机就当作私有保护。媒体源关闭目录枚举、写入和任意 MIME 嗅探;对象写入仍只能经服务端 `IObjectStorage`。未来若增加其他私有文件,必须另行定义身份/归属校验和禁止进入 C07 的短效 URL 契约。 - 上传失败使用 ProblemDetails;部分文件成功、部分失败是否允许由具体接口定义,默认采用整次请求失败。 ### 1.15 多客户端约定 @@ -452,8 +471,8 @@ AFTER_SALES.INVALID_STATUS - 当前 PC Web 只使用 WebSockets 并跳过协商,不启用 SSE、长轮询或会话亲和;Hub 路径、连接鉴权、事件名称和载荷结构在 4.3 固定。 - 连接身份来自有效 JWT,服务端不接受客户端自行声明接收用户 ID、角色或消息组。 - 推送载荷只提供消息 ID、类型、创建时间和安全跳转提示;客户端收到后先调用 A503 校正权威未读数,再按需调用 A501/A502,不做 `badge + 1`,也不直接据此修改订单最终状态。 -- 客户端固定按立即、2 秒、5 秒、10 秒重连,四次失败后暂停;重连后查询未读数和消息列表补偿,不要求服务端重放历史实时事件。 -- 多 API 实例通过 Redis Backplane 共享实时通道,但 Redis 不保存唯一消息事实。 +- 客户端固定按立即、2 秒、5 秒、10 秒重连,四次失败后暂停;重连后查询未读数和消息列表补偿,不要求服务端重放历史实时事件。已认证且页面可见时,连接正常每 60 秒安全校正 A503,持续断线时每 15 秒补查;隐藏时暂停,重新可见或恢复在线时立即补查。 +- 多 API 实例通过 4.3.7 固定的版本化 Redis 频道共享实时分发命令,各实例只向本地复核通过的连接发送;Redis 不保存唯一消息、在线或认证事实。 - 建连、重连和存续期间都校验账号状态与撤销事实;退出、JWT 到期、手机号修改、账号禁用、全部旧凭证失效或安全事实无法确认时关闭既有连接。 - Hub 错误不得泄漏内部异常;需要用户处理的稳定业务失败通过 HTTP ProblemDetails 表达。 @@ -739,13 +758,13 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 | A504 | Messaging | X03-FR06 | 标记本人单条消息已读 | POST | `/api/messages/{messageId}/read` | `Messaging_MarkMessageRead` | BuyerOnly / MerchantOnly | 待交叉评审 | | A505 | Messaging | X03-FR07 | 标记本人当前消息全部已读 | POST | `/api/messages/read-all` | `Messaging_MarkAllMessagesRead` | BuyerOnly / MerchantOnly | 待交叉评审 | | A506 | Infrastructure | C10-FR06 | API 存活检查 | GET | `/health/live` | `Infrastructure_GetLiveness` | Anonymous | 待交叉评审 | -| A507 | Infrastructure | C10-FR06 | API 就绪检查 | GET | `/health/ready` | `Infrastructure_GetReadiness` | Anonymous | 待交叉评审 | +| A507 | Infrastructure | C10-FR06、C10-FR12 | API 就绪检查 | GET | `/health/ready` | `Infrastructure_GetReadiness` | Anonymous | 待交叉评审 | ## 三、统一接口详细定义 本章只收录 99 个活动 HTTP 定义。已取消编号和内部应用契约统一放在第四章,避免被误实现为公开端点;本章“活动”表示未取消,不等于已经完成正式冻结。 -> 来源:[`interface-tyh.md`](interface/interface-tyh.md)。F03 已统一为买家专属;A024/A025 已闭合浏览记录写入与设置查询,仍待 DBxxx、OpenAPI 和交叉评审。 +> 来源:[`interface-tyh.md`](interface/interface-tyh.md)。F03 已统一为买家专属;A024/A025 已闭合浏览记录写入与设置查询并由 DB006/DB007 承接,待 OpenAPI、实现和交叉评审。 ### A001 买家注册 @@ -840,7 +859,7 @@ RegisteredUserResponse { - 模块 / Tag:Identity - 需求编号:F02、M01-02 - 负责人:唐宇昊 -- 关联数据表:DB001、DB004 +- 关联数据表:DB001(`users`) - 当前状态:待交叉评审 - 用途:用户使用手机号和密码登录,签发一个由任一 API 实例可验证的 JWT;登录结果在多实例间一致。 - 方法与路径:`POST /api/auth/login` @@ -865,6 +884,7 @@ LoginRequest { #### 成功响应 - HTTP 状态:`200 OK` +- Response Header:`Cache-Control: no-store`(令牌与账号摘要不得进入浏览器、代理或 Service Worker 持久缓存) - 响应 Schema:`LoginResponse` ```text @@ -902,9 +922,14 @@ CurrentUserResponse { - 账号不存在和密码错误统一返回 `401 / AUTH.INVALID_CREDENTIALS`,不泄露账号是否存在。 - 禁用账号返回 `403 / AUTH.ACCOUNT_DISABLED` 并明确说明联系管理员。 -- 签发的 JWT 至少包含 `sub`、`role`、`jti`、过期时间与 `tokenVersion`;签名、Issuer、Audience、过期、撤销、账号状态、版本号任一校验失败即拒绝。 +- 签发的 JWT 至少包含 `sub`、`role`、`jti`、过期时间与 `tokenVersion`;签名、Issuer、Audience、`ClockSkew=0` 的有效期、撤销、账号状态、版本号任一校验失败即拒绝。 - JWT 使用独立 `jti`;仅退出、手机号修改、账号禁用或全部旧凭证失效时把相应 `jti`/账号令牌版本加入撤销事实,刚签发的有效令牌不得写入撤销集合。 - 当令牌服务或 Redis 撤销校验不可用时,宁可拒绝登录也不放过无法确认的请求(`503 / AUTH.TOKEN_SERVICE_UNAVAILABLE`)。 +- A002 只返回认证结果,不接收 `actionIntent`、`returnDestination` 或任意 URL。PC Web 登录编排在当前会话分别保存最多一份动作意图和安全页面目标: + - Buyer 登录后先消费合法的 `returnDestination`,其白名单与必要 UUID 以需求 M01-02 为准;目标页必须重新鉴权和读取,Checkout 重调 A208,Cashier/PaymentResult 重读 A404/A406,SeckillActivityDetail 重读 A227,绝不自动调用 A228/A301/A405。 + - 合法 `actionIntent` 仅允许 `Favorite/AddToCart + productId + quantity? + actionKey`。恢复前重读 A103;Favorite 调 A019,AddToCart 调 A201 且原 `actionKey` 原样作为 `Idempotency-Key`。 + - `actionIntent` 只能由同一商品详情页产生。存在该意图时,`returnDestination` 只能为空,或精确为同一 `productId` 的 `ProductDetail`;不同商品,或与 Checkout、Cashier、PaymentResult、Orders、AfterSales 等其他目标并存,都视为整组恢复上下文非法,必须同时清除两者,不导航也不重放动作。 + - 动作只在确定 `2xx` 或确定业务 `4xx` 后清除;网络中断、超时、503 或提交结果未知时保留同一意图和 Key。页面目标在完成一次安全导航后清除。非 Buyer 登录、显式退出或字段非法时清除两者。 #### 缓存、事件或外部依赖 @@ -927,7 +952,7 @@ CurrentUserResponse { - 模块 / Tag:Identity - 需求编号:F02、M01-02 - 负责人:唐宇昊 -- 关联数据表:DB001、DB004 +- 关联数据表:DB001、DB004、DB102 - 当前状态:待交叉评审 - 用途:使当前 JWT 在自然过期前不可继续使用;只影响本令牌,不影响同一账号其他设备。 - 方法与路径:`POST /api/auth/logout` @@ -947,8 +972,10 @@ CurrentUserResponse { ```text LogoutResponse { - revoked: true - revokedAt: string // UTC ISO 8601 + invalidated: true + invalidationMode: "revoked" | "already_expired" + invalidatedAt: string // UTC ISO 8601;撤销时间或 JWT exp + revokedAt: string | null // revoked 时等于 invalidatedAt;already_expired 时为 null } ``` @@ -957,24 +984,28 @@ LogoutResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 401 | `AUTH.UNAUTHENTICATED` | 缺少令牌,或令牌格式、签名、Issuer、Audience、有效期无效;已明确撤销的同一令牌重放按成功处理 | -| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态共享不可用 | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 无法取得数据库裁决时间,或在裁决时仍需写 DB004 但撤销权威事实无法提交/无法确认 | #### 业务规则与并发 -- 当前 JWT 的 `jti` 被写入共享撤销事实,撤销事实的过期时间不得早于原 JWT 的自然过期时间,至少覆盖令牌全部剩余有效期(通常与 `exp` 一致);没有刷新令牌需要处理。 -- A003 使用退出专用认证顺序:先验证令牌格式、签名、Issuer、Audience 和自然有效期并提取 `jti`,再读取撤销事实;同一 `jti` 已明确撤销时直接重放首次 `200` 与原 `revokedAt`,不在普通受保护接口的撤销拦截处提前返回 401。 -- 撤销事实无法确认写入时返回 `503`,不得向客户端报告退出成功。 +- A003 使用退出专用认证顺序:请求进入处理前先以 `ClockSkew=0` 验证令牌格式、签名、Issuer、Audience 和自然有效期并提取 `jti/exp`,再读取 DB004;同一 `jti` 已明确撤销时直接重放 `invalidationMode=revoked`、首次 `revokedAt`,不在普通受保护接口的撤销拦截处提前返回 401。进入处理前已经到期仍返回 401。 +- 尚无 DB004 时,在数据库事务内只读取一次 `decisionTime=clock_timestamp()`:若 `decisionTime < exp`,以该时间执行 `INSERT ... ON CONFLICT (jti) DO NOTHING`;首次插入还在同一事务写一条 DB102 `IdentityRealtimeCredentialInvalidatedV1`,其 `type="TokenRevoked"` 且 `invalidatedAt=revokedAt=decisionTime`。并发冲突时读取既有 DB004 并重放其结果。DB004 `expiresAt` 精确等于原 JWT `exp`,满足 `expiresAt > revokedAt`。 +- 若 `decisionTime >= exp`,签名有效期已经确定令牌失效,固定返回 `200`、`invalidationMode=already_expired`、`invalidatedAt=exp`、`revokedAt=null`;不得写 DB004、DB102 或 Redis 镜像,不得把 `revokedAt` 倒填到 `exp` 之前,也不得把正常自然到期误报为 503。 +- 无法取得数据库 `decisionTime`,或 `decisionTime < exp` 时 DB004/DB102 原子事务无法提交、提交结果未知,返回 `503` 且不得报告退出成功。DB004 已确定提交后固定返回 200;随后 Redis 镜像同步失败不能反向宣称数据库已回滚,也不能把成功改成 503。 - 同一账号在其他设备的有效令牌不受影响。 - 退出后前端必须清理本地令牌和登录态;后续 `A004` 使用已退出的令牌必须返回 `401 / AUTH.TOKEN_REVOKED`。 #### 缓存、事件或外部依赖 -- Redis Key:`auth:revoked:{jti}`。 +- Redis Key `auth:revoked:{jti}` 只是 DB004 的可重建镜像。DB004 提交后删除/更新镜像;镜像同步完成前,对该安全水位无法确认的受保护请求按 C10 失败关闭,不能接受旧缓存未命中。后台按 DB004 有效期内记录重建镜像并推进安全水位。 +- A003 不发布业务集成事件;只有 `decisionTime < exp` 且首次建立 DB004 时,才发布 4.3.7 定义的安全失效事件。相同 `jti` 重放既有退出结果或裁决为 `already_expired` 时不创建 Outbox;广播失败由既有 DB102 责任重试。 #### 验证场景 -- 已登录用户调用 → 200;同一令牌再次访问 `A004` 返回 401 / `AUTH.TOKEN_REVOKED`。 -- 同一仍处于自然有效期但已明确撤销的令牌再次调用 A003 → 200,返回与首次相同的 `revokedAt`;伪造、过期或撤销事实未知分别返回 401 / 503。 +- 已登录用户在 `decisionTime < exp` 时调用 → 200 / `revoked`;同一令牌再次访问 `A004` 返回 401 / `AUTH.TOKEN_REVOKED`。 +- 同一仍处于自然有效期但已明确撤销的令牌再次调用 A003 → 200,返回与 DB004 首次记录相同的 `revokedAt`;伪造、过期或 DB004 权威事实未知分别返回 401 / 503。 +- 令牌进入处理时有效、但数据库 `decisionTime >= exp` → 200 / `already_expired`,`invalidatedAt=exp` 且 `revokedAt=null`,DB004、DB102 和 Redis 均不新增记录。 +- DB004 已提交但 Redis 镜像写入失败 → A003 仍返回 200;该 JTI 后续请求在镜像安全水位恢复前失败关闭,恢复后固定返回 `AUTH.TOKEN_REVOKED`。 - 第二个设备登录后的令牌仍可正常使用。 ### A004 获取当前用户 @@ -985,7 +1016,7 @@ LogoutResponse { - 模块 / Tag:Identity - 需求编号:F02、M01-02 - 负责人:唐宇昊 -- 关联数据表:DB001 +- 关联数据表:DB001、DB004 - 当前状态:待交叉评审 - 用途:返回当前登录账号的简要信息,用于登录态恢复与前端路由守卫。 - 方法与路径:`GET /api/auth/me` @@ -1020,7 +1051,7 @@ LogoutResponse { #### 缓存、事件或外部依赖 -- 不缓存;直接读取数据库与 Redis 撤销状态。 +- 不缓存业务响应;DB001/DB004 是账号状态、令牌版本与撤销权威事实,Redis 只作可重建安全镜像。镜像水位未知时按 C10 失败关闭。 #### 验证场景 @@ -1036,7 +1067,7 @@ LogoutResponse { - 模块 / Tag:Identity - 需求编号:F03、M01-03 - 负责人:唐宇昊 -- 关联数据表:DB001、DB004 +- 关联数据表:DB001、DB102 - 当前状态:待交叉评审 - 用途:买家修改本人手机号,提交后修改前签发的全部登录态失效并要求重新登录。 - 方法与路径:`POST /api/users/me/phone` @@ -1064,31 +1095,33 @@ ChangePhoneRequest { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 400 | `COMMON.VALIDATION_FAILED` | 字段缺失或新手机号格式错误 | +| 400 | `AUTH.CURRENT_PASSWORD_INCORRECT` | 当前密码不正确;这是敏感业务动作校验失败,不使 Bearer 或当前登录态失效 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | -| 401 | `AUTH.INVALID_CREDENTIALS` | 当前密码错误 | | 401 | `AUTH.TOKEN_REVOKED` | 访问令牌版本失效 | | 403 | `AUTH.ACCOUNT_DISABLED` | 当前账号已禁用 | | 409 | `AUTH.PHONE_ALREADY_REGISTERED` | 新手机号已被他人使用 | -| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态共享不可用,无法签发新令牌 | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | DB001 手机号与令牌版本事务无法提交或提交结果未知 | #### 业务规则与并发 -- 使用“当前手机号/账号版本仍等于读取值”的条件更新处理并发;手机号变更、账号令牌版本号 +1 和全部旧凭证失效结果形成一个确定提交。并发请求至多一个成功。 -- 修改成功后当前及其他旧 JWT 立即失效;撤销事实无法可靠建立时整次不提交手机号变更。 +- 当前密码校验失败固定返回 400,当前登录态、手机号和页面输入保持不变;不得进入全局 401 清理流程。 +- 使用“当前手机号/账号版本仍等于读取值”的条件更新处理并发;手机号变更、DB001 `token_version + 1` 与一条 DB102 `IdentityRealtimeCredentialInvalidatedV1`(`type="AccountCredentialsInvalidated"`、`accountStatus="Normal"`、`currentTokenVersion` 为递增后的值)在一个 PostgreSQL 事务形成权威确定提交。并发请求至多一个成功。 +- DB001 事务无法提交或提交未知时整次返回 503;事务一旦确定提交,当前及其他旧 JWT 立即失效并返回成功。Redis 镜像传播失败不能回滚手机号或令牌版本,也不能把已提交结果伪装成失败;镜像安全水位确认前,相关受保护请求按 C10 失败关闭。 - 成功后强制要求重新登录;前端需要清理本地登录态。 - 新手机号唯一性由 PostgreSQL 唯一约束 `ux_users_phone` 保证。 #### 缓存、事件或外部依赖 -- Redis Key:`auth:token-version:{userId}`。 -- 不发布集成事件。 +- Redis Key `auth:token-version:{userId}` 是 DB001 `token_version` 的可重建镜像;数据库提交后更新/失效该 Key,失败则进入重建与安全水位恢复流程。 +- 不发布业务集成事件;手机号与 `tokenVersion` 事务只同时写 4.3.7 定义的 `AccountCredentialsInvalidated` 安全失效 Outbox,由每 API 实例广播队列加速关闭旧连接。 #### 验证场景 - 正确当前密码 + 未占用新手机号 → 200;旧令牌立即返回 401 / `AUTH.TOKEN_REVOKED`。 -- 错误当前密码 → 401 / `AUTH.INVALID_CREDENTIALS`,手机号不变。 +- 错误当前密码 → 400 / `AUTH.CURRENT_PASSWORD_INCORRECT`,手机号不变且当前登录态保持有效。 - 新手机号已被使用 → 409 / `AUTH.PHONE_ALREADY_REGISTERED`,手机号不变。 -- Redis 撤销不可用 → 503,提示用户暂不可用,不修改手机号。 +- DB001 提交前数据库不可用或结果未知 → 503,不报告手机号已修改。 +- DB001 已提交但 Redis 镜像更新失败 → 仍返回 200 并要求重新登录;旧 JWT 在镜像水位恢复前失败关闭,恢复后固定因版本不符返回 401。 ### A007 重置用户名 @@ -1203,7 +1236,7 @@ MyProfileResponse { #### 验证场景 - 已登录买家 → 200。 -- 已禁用账号的旧令牌 → 401 / `AUTH.TOKEN_REVOKED`。 +- 账号仍为 `Disabled` 时,禁用前签发且格式有效的旧令牌 → 403 / `AUTH.ACCOUNT_DISABLED`;账号重新启用后,该旧令牌因版本失效 → 401 / `AUTH.TOKEN_REVOKED`。 - 商家或管理员调用 → 403 / `AUTH.FORBIDDEN`。 ### A010 我的地址列表 @@ -1250,8 +1283,8 @@ AddressListResponse { #### 业务规则与并发 -- 严格按 `user_id = current_user_id` 过滤;不允许查询他人地址。 -- 默认地址按 `is_default = true` 标记;同一用户最多一个默认地址。 +- 严格按 `buyer_id = current_user_id` 过滤;不允许查询他人地址。 +- 默认地址按 `is_default = true` 标记;同一用户最多一个默认地址。排序固定为 `isDefault desc, updatedAt desc, addressId desc`,因此唯一默认地址置顶;没有默认地址时只按后两列排序,客户端不得把第一条推断为默认。 #### 缓存、事件或外部依赖 @@ -1285,9 +1318,9 @@ AddressListResponse { CreateAddressRequest { recipientName: string // 必填,1~50 字 phone: string // 必填,^1[3-9]\d{9}$ - province: string // 必填,省份名称 - city: string // 必填,城市名称 - district: string // 必填,区/县名称 + province: string // 必填,省份名称 1~100 字 + city: string // 必填,城市名称 1~100 字 + district: string // 必填,区/县名称 1~100 字 detail: string // 必填,详细地址 5~120 字 } ``` @@ -1301,6 +1334,7 @@ CreateAddressRequest { ```text AddressResponse { addressId: uuid + version: integer // 当前地址事实并发版本,创建为 1 recipientName: string phoneMasked: string province: string @@ -1323,7 +1357,8 @@ AddressResponse { #### 业务规则与并发 -- 新增地址固定 `isDefault=false`;默认地址只能通过 A014 显式设置。本期不增加需求外的固定地址数量上限。 +- 新增地址固定 `version=1,isDefault=false`;默认地址只能通过 A014 显式设置。本期不增加需求外的固定地址数量上限。 +- `recipientName/province/city/district/detail` 先做 Unicode NFC 并去除首尾 Unicode 空白,再按 Unicode 标量值计数;规范化后分别要求 1~50、1~100、1~100、1~100、5~120,且不得包含 `Cc/Cf` 控制或格式字符。`phone` 不做宽松纠错,必须原样匹配 `^1[3-9][0-9]{9}$`。Body 是封闭对象,重复/未知属性、显式 `null`、非字符串值和客户端提交 `isDefault` 均返回 `COMMON.VALIDATION_FAILED`。 #### 缓存、事件或外部依赖 @@ -1352,7 +1387,21 @@ AddressResponse { - Route 参数:`addressId: uuid` - Header:`Authorization: Bearer `(必填) -- Body:`recipientName`、`phone`、`province`、`city`、`district`、`detail` 均可选但至少传一个;不接受 `isDefault`。 +- Body: + +```text +UpdateAddressRequest { + recipientName?: string + phone?: string + province?: string + city?: string + district?: string + detail?: string +} +``` + +- Body 是区分大小写的封闭 JSON 对象:上述字段均可省略,但至少出现一个;省略表示保持原值,出现时不得为 `null`。重复属性、未知属性、非字符串值、空对象以及 `isDefault` 均返回 `COMMON.VALIDATION_FAILED`。 +- 每个出现字段完全复用 A011 的规范化、长度和电话格式:`recipientName/province/city/district/detail` 做 Unicode NFC、去首尾 Unicode 空白、拒绝 `Cc/Cf` 后分别为 1~50、1~100、1~100、1~100、5~120 个 Unicode 标量值;`phone` 必须原样匹配 `^1[3-9][0-9]{9}$`。不得把非法值解释成“未修改”。 #### 成功响应 @@ -1371,8 +1420,9 @@ AddressResponse { #### 业务规则与并发 -- 严格按 `user_id = current_user_id AND address_id = :addressId` 过滤;不存在的地址返回 404。 +- 严格按 `buyer_id = current_user_id AND address_id = :addressId` 过滤;不存在的地址返回 404。 - 不允许通过此接口直接修改 `isDefault`;默认地址切换使用 A014。 +- 地址字段成功变化时 `version + 1` 并在响应返回新版本;未实际改变规范化字段的请求返回当前事实且不制造虚假版本。下单端因此能够区分买家确认后的并发编辑。 #### 缓存、事件或外部依赖 @@ -1382,6 +1432,8 @@ AddressResponse { - 编辑本人地址 → 200,字段更新。 - 编辑他人地址 → 404,不泄露归属。 +- 省略字段保持不变;显式 `null`、未知字段、空对象或 `isDefault` → 400,地址和版本均不改变。 +- 规范化后与当前值完全相同 → 200 返回当前事实,`version` 不递增;至少一个规范字段实际变化 → 只递增一次。 ### A013 删除地址 @@ -1468,6 +1520,7 @@ AddressResponse { - 默认地址切换必须在同一事务内形成一个原子结果:目标地址设为默认、当前买家的其他地址取消默认;任一步失败整体不生效。 - 并发设置不同默认地址时,必须通过“每个买家至多一个默认地址”的数据库唯一约束、按买家串行化或等效可执行机制兜底,并按已提交顺序返回最终结果;普通条件更新本身不作为唯一性证明。 +- 成功切换时,目标地址和被取消默认的原地址都对各自实际状态变化执行 `version + 1`;目标本来就是唯一默认时返回当前结果且不递增。A014 返回目标地址提交后的版本。 #### 缓存、事件或外部依赖 @@ -1564,7 +1617,7 @@ AdminUserResponse { - 模块 / Tag:Identity - 需求编号:F13、M06-03 - 负责人:唐宇昊 -- 关联数据表:DB001、DB004 +- 关联数据表:DB001(`users`)、DB002(`user_status_histories`)、DB102(`outbox_messages`)、DB104(`idempotency_records`) - 当前状态:待交叉评审 - 用途:管理员禁用指定买家或商家账号;账号禁用前签发的全部令牌立即失效。 - 方法与路径:`POST /api/admin/users/{userId}/disable` @@ -1593,7 +1646,7 @@ AdminUserResponse { | 409 | `IDENTITY.DEFAULT_MERCHANT_PROTECTED` | 目标是本期唯一默认商家运营账号,不允许直接禁用 | | 409 | `IDENTITY.MERCHANT_HAS_ACTIVE_WORK` | 非默认商家仍有关联待履约订单、售后窗口/申请或未结束秒杀活动 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一治理幂等键被用于不同目标或在禁用/启用动作间复用 | -| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | 撤销状态共享不可用 | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | DB001、DB002 与 DB104 的账号治理事务无法提交或提交结果未知 | | 503 | `IDENTITY.MERCHANT_RESPONSIBILITY_UNAVAILABLE` | Ordering、AfterSales 或 Seckill 任一责任复核/受理门槛无法确认 | #### 业务规则与并发 @@ -1604,23 +1657,25 @@ AdminUserResponse { - 买家禁用不查询订单、支付、售后等业务责任,也不改写这些事实。 - 禁用非默认商家前,通过 Ordering、AfterSales、Seckill 公开应用契约检查固定清单:`PendingPayment`;仍有可履约数量的 `Paid`;`Shipped`;完成后 7 天窗口内 `Completed`;任一非终态售后;任一未结束秒杀活动。已全量退款、无剩余可履约数量且无非终态售后的 `Paid` 不单独阻断。 - 任一责任存在时,`IDENTITY.MERCHANT_HAS_ACTIVE_WORK` 的 ProblemDetails `extensions.blockingReasons[]` 只允许 `PendingPaymentOrder`、`PaidOrderWithFulfillableQuantity`、`ShippedOrder`、`CompletedOrderWithinAfterSalesWindow`、`NonTerminalAfterSales`、`UnendedSeckillActivity`,不返回订单、申请或活动私人明细;任一责任契约不可用或结果未知时返回 `503 IDENTITY.MERCHANT_RESPONSIBILITY_UNAVAILABLE`,不得按“无责任”继续禁用。 -- 商家责任检查与新的普通/秒杀订单、售后责任接收形成唯一提交顺序;禁用后不得并发写入新责任。 -- 账号状态、全部旧凭证失效、追踪信息和幂等结果形成一个确定结果;任一部分无法确认时不返回成功。 +- 商家责任检查与新的普通/秒杀订单、售后责任接收形成唯一提交顺序:A016 与 A220/A222/A301/A228/A412 都先在各自幂等范围内锁定目标商家的 DB001 `users` 行作为统一责任门,再复核 `status=Normal`,随后才锁业务聚合。A016 持有该行锁后查询固定责任清单;新责任路径持有同一行锁后才允许创建责任。禁用先提交时后续新责任必须返回 `403 AUTH.ACCOUNT_DISABLED`;新责任先提交时 A016 必须看到阻断事实。 +- 首次从 `Normal → Disabled` 时,DB001 的账号状态与 `tokenVersion + 1`、DB002 状态历史、DB102 `IdentityRealtimeCredentialInvalidatedV1`(`type="AccountCredentialsInvalidated"`,`accountStatus="Disabled"`)和 DB104 幂等结果必须在同一 PostgreSQL 事务形成一个确定结果;已禁用重放不重复递增版本或写安全事件。事务无法提交或提交结果未知时返回 503。事务一旦确定提交即返回成功,Redis 镜像或广播传播失败不能回滚已提交治理事实,也不能把成功伪装成 503。 #### 缓存、事件或外部依赖 -- Redis Key:`auth:token-version:{userId}`。 +- Redis Key `auth:token-version:{userId}` 只是 DB001 `token_version` 的可重建镜像;事务提交后更新/失效该 Key,失败则进入重建与安全水位恢复流程。安全水位恢复前,目标账号的受保护请求按 C10 失败关闭。 +- A016 不发布业务集成事件;只发布 4.3.7 定义的账号级安全失效 Outbox,由每 API 实例的独立广播队列关闭目标账号旧连接。 #### 验证场景 -- 禁用正常买家 → 200,旧令牌 401 / `AUTH.TOKEN_REVOKED`。 +- 禁用正常买家 → 200;账号仍禁用时旧令牌先因账号状态返回 403 / `AUTH.ACCOUNT_DISABLED`,重新启用后同一旧令牌再因版本失效返回 401 / `AUTH.TOKEN_REVOKED`。 - 重复禁用 → 200,返回当前禁用状态。 - 禁用管理员账号 → 404。 - 禁用默认商家 → 409 / `IDENTITY.DEFAULT_MERCHANT_PROTECTED`。 - 禁用仍有待处理业务的非默认商家 → 409 / `IDENTITY.MERCHANT_HAS_ACTIVE_WORK`。 - 责任复核任一模块不可用 → 503 / `IDENTITY.MERCHANT_RESPONSIBILITY_UNAVAILABLE`,账号状态不变。 - A016 使用某键后,A017 对同一或另一账号复用该键 → 409 / `IDEMPOTENCY.KEY_REUSED`。 -- 禁用过程中 Redis 撤销不可用 → 503,不返回虚假成功。 +- DB001/DB002/DB104 提交前数据库不可用或结果未知 → 503,账号治理不报告成功。 +- PostgreSQL 事务已提交但 Redis 镜像更新失败 → A016 仍返回 200;目标账号后续请求在镜像安全水位恢复前失败关闭,恢复后固定按 Disabled 与新 `tokenVersion` 拒绝旧凭证。 ### A017 启用账号 @@ -1630,7 +1685,7 @@ AdminUserResponse { - 模块 / Tag:Identity - 需求编号:F13、M06-03 - 负责人:唐宇昊 -- 关联数据表:DB001、DB004 +- 关联数据表:DB001(`users`)、DB002(`user_status_histories`)、DB104(`idempotency_records`) - 当前状态:待交叉评审 - 用途:管理员启用被禁用的买家或商家账号;启用前已签发令牌不恢复,用户必须重新登录。 - 方法与路径:`POST /api/admin/users/{userId}/enable` @@ -1657,22 +1712,24 @@ AdminUserResponse { | 403 | `AUTH.FORBIDDEN` | 当前账号不是管理员 | | 404 | `RESOURCE.NOT_FOUND` | 目标账号不存在或不是买家/商家 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一治理幂等键被用于不同目标或在禁用/启用动作间复用 | +| 503 | `AUTH.TOKEN_SERVICE_UNAVAILABLE` | DB001、DB002 与 DB104 的账号治理事务无法提交或提交结果未知 | #### 业务规则与并发 - 与 A016 共用 `(adminUserId, Idempotency-Key)` 治理作用域并绑定 `targetUserId + action`;同键同目标同动作重放首次结果,同键换目标或从禁用换成启用返回 `409 IDEMPOTENCY.KEY_REUSED`。 - 先查询持久化幂等结果;条件更新仅当目标为 `Disabled` 时改为 `Normal`,目标已是 `Normal` 时返回并绑定当前正常结果。 - 启用不回退 `tokenVersion`;禁用前签发的旧令牌仍不可用,需重新登录。 -- 账号状态、最小安全追踪信息(管理员、目标账号、动作、提交时间、`traceId`)与稳定幂等结果必须在同一 PostgreSQL 事务形成;任一步无法确认时不得返回启用成功。 +- DB001 账号状态、DB002 最小安全追踪信息(管理员、目标账号、动作、提交时间、`traceId`)与 DB104 稳定幂等结果必须在同一 PostgreSQL 事务形成;事务无法提交或提交结果未知时返回 503。事务一旦确定提交即返回成功,Redis 镜像传播失败不能回滚已提交治理事实,也不能把成功伪装成 503。 #### 缓存、事件或外部依赖 -- 不修改 Redis 撤销集合。 +- 不修改按 JTI 保存的 Redis 撤销集合;更新/失效 `auth:token-version:{userId}` 可重建镜像。镜像更新失败进入重建与安全水位恢复流程,恢复前目标账号的受保护请求按 C10 失败关闭。 #### 验证场景 - 启用已禁用账号 → 200,旧令牌仍 401 / `AUTH.TOKEN_REVOKED`;新登录可用。 - 启用正常账号 → 200,返回当前正常状态。 +- DB001/DB002/DB104 提交前数据库不可用或结果未知 → 503;已确定提交但 Redis 镜像更新失败 → A017 仍返回 200,目标账号请求在安全水位恢复前失败关闭。 ### A018 收藏列表 @@ -1736,7 +1793,7 @@ EngagementProductSnapshot { #### 业务规则与并发 -- 严格按 `user_id = current_user_id` 过滤;不允许跨用户访问。 +- 严格按 `buyer_id = current_user_id` 过滤;不允许跨买家访问。 - 默认按 `createdAt desc, favoriteId desc` 稳定分页。 - 收藏商品摘要来自 Catalog 模块;库存为零、商品已下架或防御性缺失时仍返回收藏占位,并分别标记 `OutOfStock`、`ProductOffSale`、`ProductMissing`,不得丢失记录或把实体缺失伪装成下架。 @@ -1792,7 +1849,7 @@ AddFavoriteRequest { #### 业务规则与并发 -- 先按 `(user_id, product_id)` 查询本人既有收藏;已存在时直接返回原记录,即使商品后来下架或防御性缺失。 +- 先按 `(buyer_id, product_id)` 查询本人既有收藏;已存在时直接返回原记录,即使商品后来下架或防御性缺失。 - 只有新建收藏时才要求商品存在且为 `OnSale`;唯一约束保证并发最多新增一条。 #### 缓存、事件或外部依赖 @@ -1840,7 +1897,7 @@ AddFavoriteRequest { #### 业务规则与并发 -- 删除按 `(user_id, product_id)` 过滤;不存在记录时返回 204,保持幂等。 +- 删除按 `(buyer_id, product_id)` 过滤;不存在记录时返回 204,保持幂等。 #### 缓存、事件或外部依赖 @@ -1902,7 +1959,7 @@ BrowsingHistoryItemResponse { #### 业务规则与并发 -- 按 `user_id = current_user_id` 过滤并默认使用 `viewedAt desc, browsingHistoryId desc` 稳定分页。 +- 按 `buyer_id = current_user_id` 过滤并默认使用 `viewedAt desc, browsingHistoryId desc` 稳定分页。 - 开关关闭只阻止未来写入,不能隐藏或删除已有历史;库存为零、下架或防御性缺失商品仍以 A018 定义的明确不可用占位返回,不能把“已不存在”伪装为“已下架”。 #### 缓存、事件或外部依赖 @@ -1922,7 +1979,7 @@ BrowsingHistoryItemResponse { - 模块 / Tag:Engagement - 需求编号:X02、M08 - 负责人:唐宇昊 -- 关联数据表:DB006 +- 关联数据表:DB007 - 当前状态:待交叉评审 - 用途:买家开启或关闭后续浏览记录写入;关闭不等于删除已有历史。 - 方法与路径:`PATCH /api/browsing-history/settings` @@ -1957,7 +2014,7 @@ UpdateBrowsingHistorySettingRequest { #### 业务规则与并发 - 关闭开关不影响已有浏览记录;重新开启后恢复写入。 -- A022 开关切换与 A024 浏览写入/裁剪必须经过同一买家级串行化门槛,可锁定稳定的设置/配额行、使用可重试的 Serializable 事务、事务级 advisory lock 或等效机制;设置记录尚不存在时也必须先安全建立默认 `enabled=true` 的稳定门槛,不能只做一次无锁读取。 +- A022 开关切换与 A024 浏览写入/裁剪固定使用同一个 PostgreSQL **事务级买家 advisory lock**;逻辑锁名为 `engagement:browsing-history:{buyerId}`,由公共锁键函数稳定映射为 64 位锁值。两个接口不得改用设置行锁、`Serializable` 或各自的锁名;即使 DB007 设置记录尚不存在,也先取该锁,再按逻辑默认 `enabled=true` 处理并由 A022 UPSERT。 - 以该串行化门槛的数据库提交顺序裁决:关闭先提交则后续写入返回 `recorded=false`;写入先提交则本次记录保留,随后关闭只影响未来请求。 #### 缓存、事件或外部依赖 @@ -1978,7 +2035,7 @@ UpdateBrowsingHistorySettingRequest { - 模块 / Tag:Engagement - 需求编号:X02、M08、M08-FR04 - 负责人:唐宇昊 -- 关联数据表:DB006 +- 关联数据表:DB006、DB007 - 当前状态:待交叉评审 - 用途:买家成功打开已上架商品详情后,由前端显式记录或更新最近浏览时间;A103 商品详情 GET 本身不产生写入副作用。 - 方法与路径:`POST /api/browsing-history/records` @@ -2025,8 +2082,8 @@ RecordBrowsingHistoryResponse { - 浏览记录开关关闭时返回 `200`、`recorded=false`,不写入记录;这属于用户偏好,不是权限错误。 - 游客身份不持久化记录;本接口仅 BuyerOnly;前端在游客访问商品详情时引导登录,登录后再调用本接口。 -- 同一买家同一商品只保留一条记录;按 `(user_id, product_id)` 唯一约束写入或更新,`viewedAt` 始终取服务端时间。 -- A024 与 A022 共用同一买家级串行化门槛;取得门槛后才复核当前 `enabled`、校验本次商品资格、执行同一商品 upsert,并按 `viewedAt, browsingHistoryId` 裁剪最早记录。开关复核、写入和裁剪在同一事务提交,不能先读 `enabled=true` 后让关闭先提交、自己再补写。 +- 同一买家同一商品只保留一条记录;按 `(buyer_id, product_id)` 唯一约束写入或更新,`viewedAt` 始终取服务端时间。 +- A024 与 A022 共用逻辑锁名 `engagement:browsing-history:{buyerId}` 的事务级 advisory lock;取得门槛后才复核当前 `enabled`、校验本次商品资格、执行同一商品 UPSERT,并按 `viewedAt, browsingHistoryId` 裁剪最早记录。开关复核、写入和裁剪在同一事务提交,不能先读 `enabled=true` 后让关闭先提交、自己再补写。 - 默认单买家最多保留 200 条记录;同一买家的并发写入被上述门槛串行化,任一提交后都必须满足总数 `≤ 200`,并在 `trimmedCount` 返回本次清理数量。 - 新写入仅接受当前已上架商品;商品后来下架时保留既有历史记录,并由列表标记为不可购买。 @@ -2053,7 +2110,7 @@ RecordBrowsingHistoryResponse { - 模块 / Tag:Engagement - 需求编号:X02、M08、M08-FR07 - 负责人:唐宇昊 -- 关联数据表:DB006 +- 关联数据表:DB007 - 当前状态:待交叉评审 - 用途:买家查询本人"是否记录浏览历史"开关当前值,前端用于初始化控件状态。 - 方法与路径:`GET /api/browsing-history/settings` @@ -2084,7 +2141,7 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 -- 严格按 `user_id = current_user_id` 过滤;不存在设置记录时直接按 `enabled=true, updatedAt=null` 返回,GET 不得为了默认值写数据库。 +- 严格按 `buyer_id = current_user_id` 过滤;不存在设置记录时直接按 `enabled=true, updatedAt=null` 返回,GET 不得为了默认值写数据库。 - 与 A022 配对:GET 读取当前值,PATCH 修改值;同一资源不重复定义写入入口。 #### 缓存、事件或外部依赖 @@ -2104,7 +2161,7 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Catalog - 需求编号:M02-01-FR02 - 负责人:顾欣月 -- 关联数据表:DB021 `categories` +- 关联数据表:DB021 `categories`、DB022 `products` - 当前状态:待交叉评审 - 用途:为购物端商品筛选提供当前有效分类,供列表页分类入口使用。 - 方法与路径:`GET /api/categories` @@ -2169,7 +2226,7 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Catalog - 需求编号:M02-01(F04、F05)、C04 - 负责人:顾欣月 -- 关联数据表:DB022 `products`、DB023 `product_images` +- 关联数据表:DB021 `categories`、DB022 `products`、DB023 `product_images`、DB027 `product_search_documents` - 当前状态:待交叉评审 - 用途:购物端商品发现入口,支持分页、分类筛选、关键词模糊/分词搜索、价格区间、仅看有货与白名单排序,翻页保持条件。 - 方法与路径:`GET /api/products` @@ -2204,6 +2261,7 @@ BrowsingHistorySettingResponse { #### 成功响应 - HTTP 状态:`200 OK` +- Response Header:`Cache-Control: no-store`(固定首页即使由服务端 Redis 命中也不允许浏览器、Nginx/CDN 或 Service Worker 保存动态 JSON) - 响应 Schema:`ProductListResponse`,`items` 元素 `ProductSummary` 含 `productId`、`name`、`categoryId`、`price`、`stockStatus`(`InStock`/`SoldOut`)、`thumbnailUrl`、`createdAt`。 - 示例: @@ -2244,7 +2302,8 @@ BrowsingHistorySettingResponse { - 依赖 Catalog 搜索能力契约 `IProductSearch`(C04),基础与进阶搜索都读取同一 PostgreSQL 商品事实。 - A102 只有固定首页摘要形式可进入 C07:无 `keyword`、`categoryId`、价格和库存筛选,固定 `page=1&pageSize=12&sortBy=createdAt&sortOrder=desc`,并只返回 `OnSale` 商品摘要;其余普通列表、关键词、组合筛选和用户自选排序全部直读 PostgreSQL,不生成参数化缓存 Key。 -- 固定首页正常值 TTL 60 秒、空值 TTL 10 秒;事务提交后立即及 3 秒二次失效,双删失败时正常旧值最坏不超过提交后 62 秒、旧空值不超过 12 秒。缓存不可用时回退 PostgreSQL。 +- 固定首页正常值 TTL 60 秒、空值 TTL 10 秒;来源事务同时建立 Immediate 与初始未武装的 Delayed 两条独立失效责任。Delayed 提交可见后由数据库时间武装,`availableAt=armedAt+3 秒`,任一阶段失败均恢复本 eventId。即使武装或两阶段删除持续失败,正常旧值最坏不超过提交可见后 62 秒、旧空值不超过 12 秒。缓存不可用时回退 PostgreSQL。 +- Nginx/CDN 对 A102 禁用代理缓存,Service Worker 不保存响应;不得用浏览器缓存绕过服务端一致性窗口。 #### 验证场景 @@ -2261,7 +2320,7 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Catalog - 需求编号:M02-02(F06) - 负责人:顾欣月 -- 关联数据表:DB022 `products`、DB023 `product_images` +- 关联数据表:DB021 `categories`、DB022 `products`、DB023 `product_images` - 当前状态:待交叉评审 - 用途:展示已上架商品的名称、图片、描述、当前价格、库存与分类,供购买决策。 - 方法与路径:`GET /api/products/{productId}` @@ -2283,6 +2342,7 @@ BrowsingHistorySettingResponse { #### 成功响应 - HTTP 状态:`200 OK` +- Response Header:`Cache-Control: no-store` - 响应 Schema:`ProductDetailResponse`,`data` 含 `productId`、`name`、`categoryId`、`categoryName`、`description`、`price`、`stock`、`stockStatus`、`images`(数组:`imageId`、`url`、`sortOrder`、`isPrimary`、`altText`)、`createdAt`、`updatedAt`。评分汇总与评价列表由 A140 单独获取,本响应不内联。 - 示例: @@ -2292,7 +2352,7 @@ BrowsingHistorySettingResponse { "message": "ok", "data": { "productId": "3f0e…", "name": "示例手机", "categoryId": "6f1d…", "categoryName": "手机数码", - "description": "受控富文本或纯文本描述", "price": 1999.00, "stock": 12, "stockStatus": "InStock", + "description": "仅按纯文本展示的商品描述", "price": 1999.00, "stock": 12, "stockStatus": "InStock", "images": [ { "imageId": "a1…", "url": "https://…/1.webp", "sortOrder": 1, "isPrimary": true, "altText": "正面图" } ], "createdAt": "2026-07-20T02:00:00Z", "updatedAt": "2026-07-22T06:00:00Z" } @@ -2310,14 +2370,15 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 - 价格、库存、状态以服务端最新数据为准,前端缓存不得作为下单依据。 -- 商品描述按受控内容返回,不含脚本;不返回内部备注或未公开状态字段。 +- 商品描述按规范化纯文本返回:写入时把 `CRLF/CR` 统一为 `LF`、去除首尾 Unicode 空白,空串存为 `null`,非空按 Unicode 标量值最多 2000 个;Vue 只能使用文本插值并以 `white-space: pre-wrap` 保留内部换行,禁止 `v-html`。不返回内部备注或未公开状态字段。 - 已下架商品旧链接返回 404,不提供购买操作;历史订单快照不受影响(由 Ordering 保存)。 - `OnSale` 且库存为 0 的商品仍返回 200,`stockStatus=SoldOut`,前端禁用加购和购买入口;库存为 0 不自动下架。 #### 缓存、事件或外部依赖 - 图片 `url` 由对象存储(S3 兼容 / SeaweedFS)受控访问地址提供。 -- A103 只缓存商品自身公开字段,不包含 M07 评价、评分、收藏、购物车或任何身份化字段;正常值 TTL 60 秒、不可公开空值 TTL 10 秒。相关商品事务提交后立即失效并在 3 秒后对同一 Key 二次失效;双删失败时正常旧值最坏不超过提交后 62 秒、旧空值不超过 12 秒,缓存不可用时回退 PostgreSQL。 +- A103 只缓存商品自身公开字段,不包含 M07 评价、评分、收藏、购物车或任何身份化字段;正常值 TTL 60 秒、不可公开空值 TTL 10 秒。相关商品事务同时写 Immediate/Delayed 两条独立失效责任,Delayed 初始未武装,提交可见后由数据库时间武装并在 `armedAt+3 秒` 可投递,不依赖首删成功;即使武装或两阶段删除持续失败,正常旧值最坏不超过提交可见后 62 秒、旧空值不超过 12 秒,缓存不可用时回退 PostgreSQL。 +- A103 JSON 在 Nginx/CDN/Service Worker 层固定不缓存。`images[].url` 是内容不可变的稳定对象 URL时,媒体源可单独返回长期 `public, immutable`,但 JSON 仍为 `no-store`。 #### 验证场景 @@ -2330,7 +2391,7 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Catalog - 需求编号:M06-01-FR01 - 负责人:顾欣月 -- 关联数据表:DB021 `categories` +- 关联数据表:DB021 `categories`、DB022 `products` - 当前状态:待交叉评审 - 用途:商家维护商品时查看全部(含停用)分类及层级、排序、存储启停状态和派生的购物端有效状态。 - 方法与路径:`GET /api/merchant/categories` @@ -2352,6 +2413,7 @@ BrowsingHistorySettingResponse { #### 成功响应 - HTTP 状态:`200 OK` +- Response Header:`Cache-Control: no-store` - 响应 Schema:`MerchantCategoryListResponse`,元素为 `MerchantCategoryResponse`,含 `categoryId`、`name`、`parentId`、`sortOrder`、`status`(存储状态:`Enabled`/`Disabled`)、`isEffectiveForStorefront`(派生布尔值)和 `directProductCount`(直接归属该分类、尚未物理删除的全状态商品数)。 - 示例:`{ "categoryId": "6f1d…", "name": "手机", "parentId": "08af…", "sortOrder": 1, "status": "Enabled", "isEffectiveForStorefront": false, "directProductCount": 12 }` 表示分类自身启用,但父分类停用。 @@ -2439,7 +2501,7 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Catalog - 需求编号:M06-01-FR02 - 负责人:顾欣月 -- 关联数据表:DB021 `categories` +- 关联数据表:DB021 `categories`、DB022 `products`、DB027 `product_search_documents`、DB102 `outbox_messages` - 当前状态:待交叉评审 - 用途:商家修改分类名称、父级、排序。 - 方法与路径:`PUT /api/merchant/categories/{categoryId}` @@ -2591,7 +2653,7 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Catalog - 需求编号:M06-01-FR03 - 负责人:顾欣月 -- 关联数据表:DB021 `categories` +- 关联数据表:DB021 `categories`、DB022 `products` 及所有历史引用 - 当前状态:待交叉评审 - 用途:在分类没有任何商品或历史引用时物理删除分类;存在引用时拒绝并引导停用。 - 方法与路径:`DELETE /api/merchant/categories/{categoryId}` @@ -2642,7 +2704,7 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Catalog - 需求编号:M06-01-FR04 - 负责人:顾欣月 -- 关联数据表:DB022 `products` +- 关联数据表:DB021 `categories`、DB022 `products`、DB023 `product_images` - 当前状态:待交叉评审 - 用途:商家按关键词、分类、上下架状态分页查询统一经营目录中的商品,展示价格、库存与状态。 - 方法与路径:`GET /api/merchant/products` @@ -2695,7 +2757,7 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Catalog - 需求编号:M06-01-FR04、FR06 - 负责人:顾欣月 -- 关联数据表:DB022 `products`、DB023 `product_images` +- 关联数据表:DB021 `categories`、DB022 `products`、DB023 `product_images` - 当前状态:待交叉评审 - 用途:商家编辑前获取商品完整信息(含并发版本号 `version`)。 - 方法与路径:`GET /api/merchant/products/{productId}` @@ -2743,7 +2805,7 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Catalog - 需求编号:M06-01-FR05 - 负责人:顾欣月 -- 关联数据表:DB022 `products` +- 关联数据表:DB021(`categories`)、DB022(`products`)、DB023(`product_images`)、DB026(`catalog_inventory_movements`,初始库存大于 0 时)、DB027(`product_search_documents`)、DB102(`outbox_messages`,仅 A103 短空值两阶段失效)、DB104(`idempotency_records`)、DB105(`object_cleanup_tasks`,需要补偿时) - 当前状态:待交叉评审 - 用途:商家录入商品基础信息,创建为草稿状态。 - 方法与路径:`POST /api/merchant/products` @@ -2764,7 +2826,7 @@ BrowsingHistorySettingResponse { | `categoryId` | uuid | 是 | 分类存在且在购物端有效(自身及父级均启用) | | `price` | number | 是 | ≥ 0,最多两位小数 | | `stock` | integer | 是 | ≥ 0 非负整数 | -| `description` | string | 否 | ≤ 2000,受控内容 | +| `description` | string | 否 | 纯文本;换行统一为 LF、去首尾 Unicode 空白,规范化后为空按 null,非空按 Unicode 标量值 ≤ 2000 | | `images` | file[] | 是 | 1~8 张,第一张固定为主图 | | `altTexts` | string[] | 否 | 与 `images` 同序;每项 ≤ 100 | @@ -2785,19 +2847,25 @@ BrowsingHistorySettingResponse { | 404 | `CATALOG.CATEGORY_NOT_FOUND` | 分类不存在 | | 409 | `CATALOG.CATEGORY_DISABLED` | 分类自身或父级已停用,当前不是购物端有效分类,不能用于新建 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同请求内容 | +| 409 | `IDEMPOTENCY.REQUEST_IN_PROGRESS` | 同一键、同一内容仍在有效租约内上传或收敛对象;返回 `Retry-After: 1`,调用方必须使用原键重试 | | 413 | `COMMON.PAYLOAD_TOO_LARGE` | 任一图片超过大小限制 | | 415 | `CATALOG.INVALID_IMAGE` | 图片格式或尺寸不合规 | | 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 对象存储或数据库暂时不可用,未形成确定创建结果 | #### 业务规则与并发 -- 完成身份、幂等键格式和 multipart 结构校验后,先读取持久化幂等结果,再读取分类等可变事实;同键换内容返回 409。 -- 服务端预生成 `productId`,依次写入受控对象并在一个数据库事务中原子保存 `Draft` 商品、图片记录、第一张主图和确定幂等结果;任一对象上传或数据库写入失败时不得返回成功。 -- 对象已写入但数据库事务失败时立即尝试清理本次全部对象;清理失败登记可追踪补偿。创建成功后可用 A127 增加图片、A128 删除图片,最后经 A125 完整性校验上架。 +- 请求指纹固定由**校验并规范化后的** `name/categoryId/price/stock/description`,以及按 multipart `images` 出现顺序排列的 `(图片内容 SHA-256, 对应规范化 altText)` 计算;不包含 multipart boundary、Content-Disposition、原始文件名、临时路径或传输 Header。服务端必须在占用幂等工作前完成全部文件哈希和基础格式校验;同一图片字节与业务字段即使文件名或 boundary 不同,也视为同一请求。 +- 完成身份、幂等键格式和 multipart 固定结构校验后,先按 `(merchantUserId,A122,Idempotency-Key)` 查询 PostgreSQL 幂等记录;同键换指纹返回 409。同键同指纹命中 Completed 时原样重放;命中有效 Processing 租约时最多等待 2 秒,仍未完成返回 `IDEMPOTENCY.REQUEST_IN_PROGRESS`,不得并发开始第二组对象写入。 +- 跨数据库与对象存储的创建固定为三段协议: + 1. **建立持久工作清单**:短事务取得幂等 advisory lock,预生成唯一 `productId`、每张 `imageId`、原图 Key 与缩略图 Key;DB104 `workPayload` 固定保存规范标量、图片顺序、内容哈希、altText、全部不可变 Key 和当前阶段,同时写入新 `leaseToken`。默认租约 60 秒、每 20 秒续租,`workExpiresAt=createdAt+24 小时`;这些值由统一配置覆盖,但同一部署所有 API/Worker 必须一致。 + 2. **执行外部对象工作**:只按原工作清单上传原图并生成缩略图;每次继续或更新检查点前都要匹配当前 fencing token 且租约未失效。接管只能在租约到期后基于原清单换发新 token,不能生成第二套 ID/Key;旧执行者一旦失去 token,就不能完成 DB104、删除对象或覆盖接管者检查点。 + 3. **原子提交商品事实**:最终事务以 `DB104.id + status=Processing + leaseToken` 为条件锁定工作,重新校验分类与所有对象元数据,创建 Draft DB022、按请求顺序创建 Attached DB023(第一张为主图)、同步 DB027;`stock>0` 时写 DB026 初始流水,写 DB102 的 A103 短空值失效事实,并把 DB104 原子改为 Completed。商品、图片、搜索投影、库存流水、失效责任和首次 HTTP 结果要么全部提交,要么全部不提交。 +- 对象存储或数据库瞬态失败不得返回创建成功,也不得把 503 固化成业务失败;保留 Processing 工作清单供原 Key 续传或接管。确定的分类不存在/停用等 4xx 若在对象写入后才裁决,必须在保存 BusinessFailure 的同一事务为全部可能已写 Key UPSERT DB105,之后才返回;不能先丢弃工作清单。超过 24 小时仍未完成时停止新对象写入,由恢复任务按清单登记/复核清理;只有全部 Key 已确认不存在或删除后,才能在原幂等 advisory lock 下移除该 Processing 记录,让原 Key 重新建立新清单。 +- 创建成功后可用 A127 增加图片、A128 删除图片,最后经 A125 完整性校验上架。 #### 缓存、事件或外部依赖 -- `pg_trgm`/GIN 索引随 PostgreSQL 商品数据同步维护,不通过 Outbox 或 Worker 复制搜索索引。新商品固定为 `Draft`,不进入固定首页;创建事务提交后仍须立即清理该 `productId` 的 A103 短空值并在第 3 秒二次删除,不失效固定首页。 +- `pg_trgm`/GIN 索引随 PostgreSQL 商品数据同步维护,不通过 Outbox 或 Worker 复制搜索索引。新商品固定为 `Draft`,不进入固定首页;创建事务同时建立该 `productId` 的 Immediate 与初始未武装 Delayed,提交可见后前者尽快清理 A103 短空值,后者由数据库时间武装并等待 3 秒再删除,不失效固定首页。 #### 验证场景 @@ -2810,7 +2878,7 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Catalog - 需求编号:M06-01-FR06、M06-01-FR10 - 负责人:顾欣月 -- 关联数据表:DB022 `products` +- 关联数据表:DB021 `categories`、DB022 `products`、DB023 `product_images`、DB026 `catalog_inventory_movements`、DB027 `product_search_documents`、DB102 `outbox_messages` - 当前状态:待交叉评审 - 用途:修改允许变更的商品信息,使用并发标记防止静默覆盖。 - 方法与路径:`PUT /api/merchant/products/{productId}` @@ -2850,7 +2918,7 @@ BrowsingHistorySettingResponse { #### 缓存、事件或外部依赖 -- 商品事务提交后按受影响字段精确失效 A103 商品详情;名称、价格、普通库存、主图或首页排序/成员资格变化时同时失效唯一固定首页摘要,并在 3 秒后对同一 Key 二次失效。普通列表和搜索不缓存。 +- 商品事务按受影响字段原子建立 C07 两条责任:提交可见后 Immediate 尽快失效 A103 商品详情,名称、价格、普通库存、主图或首页排序/成员资格变化时同时失效唯一固定首页摘要;Delayed 提交可见后由数据库时间武装并等待 3 秒,对同一 Key 二次失效。普通列表和搜索不缓存。 - `pg_trgm`/GIN 索引由 PostgreSQL 随数据同步维护,不发布“同步搜索索引”事件。 #### 验证场景 @@ -2864,7 +2932,7 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Catalog - 需求编号:M06-01-FR08 - 负责人:顾欣月 -- 关联数据表:DB022 `products` +- 关联数据表:DB022 `products`、DB023 `product_images`、DB026 `catalog_inventory_movements`、DB027 `product_search_documents`、DB102 `outbox_messages`、DB105 `object_cleanup_tasks`;外部历史引用只经所属模块公开检查能力读取 - 当前状态:待交叉评审 - 用途:仅在商品为草稿或已下架且不存在任何历史关联时物理删除;其他情况拒绝破坏性删除并建议下架。 - 方法与路径:`DELETE /api/merchant/products/{productId}` @@ -2897,18 +2965,23 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 -- 仅 `Draft` 或 `OffSale` 商品可进入删除判断;存在订单、购物车、收藏、浏览、评价、秒杀等任何历史关联时均拒绝物理删除,不按关联当前状态排除已取消订单或失效记录。 +- 仅 `Draft` 或 `OffSale` 商品可进入删除判断;存在订单、购物车、收藏、浏览、评价、秒杀、售后等任何外部业务关联时均拒绝物理删除,不按关联当前状态排除已取消订单或失效记录。商品自身的图片、搜索投影和普通库存维护流水属于 Catalog 聚合内部数据,按下述成功协议收敛,不能让一个从未进入外部业务链的草稿商品因“自己引用自己”而永远无法删除。 - 下架(A126)不删除购物车、收藏、浏览记录、评价、秒杀关联与历史订单快照。 -- 删除资格、全量引用检查和物理删除在一个受控提交序列中复核,并由数据库引用约束兜底;检查后并发产生新引用时,删除失败而不能留下悬空引用。 +- 固定成功协议为: + 1. 锁定 DB022 商品行并重检状态;通过各所属模块公开能力完成外部引用检查,任一能力未知则返回 503,不能把未知当“无引用”。 + 2. 读取该商品全部 DB023 原图与缩略图 Key,在同一事务逐个 UPSERT DB105 清理责任;不得先删数据库 Key 再尝试登记补偿。 + 3. 删除 DB023 当前/归档图片关联;DB026 普通库存内部流水和 DB027 搜索投影随 DB022 聚合删除收敛,不触碰任何跨模块交易事实。 + 4. 写入 DB102 的 C07 Immediate 失效事实,目标为 A103 和可能包含该商品的固定首页,再删除 DB022 并提交;对象只在提交后由 Worker 异步清理。 +- 删除资格、内部清理清单、可靠失效事实和物理删除属于一个数据库事务。外部 FK 插入与父商品删除由 PostgreSQL 形成唯一先后:检查后并发新建外部引用时,要么引用先成立并使删除返回 409,要么删除先成立并使引用方失败,绝不能留下悬空引用或漏失对象清理责任。 #### 缓存、事件或外部依赖 -- 历史关联检查通过各所属模块的公开应用契约或已确认只读检查能力完成,不跨模块修改内部表。 -- 物理删除事务提交后失效目标 A103 正常值/空值与可能包含该商品的固定首页摘要,并在 3 秒后二次失效;对象清理失败进入可追踪补偿,不把已提交的删除伪装为失败。 +- 历史关联检查通过各所属模块的公开应用契约或已确认只读检查能力完成,不跨模块修改内部表;DB 外键仍是并发最终保护。 +- 来源事务内同时写入 C07 Immediate 与初始未武装 Delayed 两条 DB102;提交可见后由 Immediate 尽快失效目标 A103 正常值/空值与可能包含该商品的固定首页摘要,Delayed 由数据库时间独立武装并等待 3 秒后再次失效,不得由 Immediate 或调度器派生。DB105 Worker 删除对象前再次确认 DB023/DB062 都无引用;清理失败持续重试或告警,不把已提交的商品删除伪装为失败。 #### 验证场景 -- 存在订单、购物车、收藏、浏览、评价或秒杀任一引用时删除返回 409;仅 `Draft`/`OffSale` 且无任何引用时返回 204。 +- 存在订单、购物车、收藏、浏览、评价、秒杀或售后任一外部引用时删除返回 409;仅 `Draft`/`OffSale`、无外部引用但存在自有图片/搜索/库存内部记录时,仍按完整清理协议返回 204。模拟任一步数据库失败必须证明商品、关联、清理任务和失效事实整体回滚;模拟对象存储失败则商品删除保持成功且 DB105 可恢复。 --- @@ -2917,7 +2990,7 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Catalog - 需求编号:M06-01-FR07 - 负责人:顾欣月 -- 关联数据表:DB022 `products` +- 关联数据表:DB021 `categories`、DB022 `products`、DB023 `product_images`、DB102 `outbox_messages` - 当前状态:待交叉评审 - 用途:完成商品销售前校验并将商品设为已上架。 - 方法与路径:`POST /api/merchant/products/{productId}/publish` @@ -2946,22 +3019,23 @@ BrowsingHistorySettingResponse { | 403 | `AUTH.FORBIDDEN` | 非商家 | | 404 | `CATALOG.PRODUCT_NOT_FOUND` | 商品不存在 | | 409 | `CATALOG.PRODUCT_INCOMPLETE` | 必填项或主图缺失 | +| 409 | `CATALOG.PRODUCT_PRICE_INVALID` | 当前价格不大于 0;零价只允许保留为未上架草稿 | | 409 | `CATALOG.CATEGORY_DISABLED` | 商品分类自身或父级已停用,不是购物端有效分类 | | 409 | `CATALOG.PRODUCT_VERSION_CONFLICT` | 非目标态且提交 `version` 与当前版本不一致 | #### 业务规则与并发 -- 上架前必须满足:名称、购物端有效分类(自身及父级均启用)、价格、库存以及至少一张主图;只允许 `Draft` 或 `OffSale` 进入 `OnSale`。 +- 上架前必须满足:名称、购物端有效分类(自身及父级均启用)、`price > 0`、非负库存以及至少一张主图;只允许 `Draft` 或 `OffSale` 进入 `OnSale`。价格为 0 的商品可继续保留和编辑为草稿,但不能进入会创建正金额订单的公开销售链路。 - 当前已为 `OnSale` 时直接返回当前结果;否则使用 `productId + version + 当前状态` 条件更新,确保编辑、上架和下架竞争只提交一个基于最新版本的结果。 #### 缓存、事件或外部依赖 -- 上架事务提交后立即失效目标 A103 空值和唯一固定首页摘要,并在 3 秒后对同一 Key 二次失效;普通列表与搜索不缓存。 +- 上架事务同时建立 C07 Immediate 与初始未武装 Delayed;提交可见后前者尽快失效目标 A103 空值和唯一固定首页摘要,后者由数据库时间武装并等待 3 秒后对同一 Key 二次失效;普通列表与搜索不缓存。 - `pg_trgm`/GIN 索引由 PostgreSQL 随商品数据同步维护。 #### 验证场景 -- 缺主图、分类自身停用或父级停用时返回 409;重复上架幂等成功。 +- 缺主图、价格为 0、分类自身停用或父级停用时返回 409;重复上架幂等成功。 --- @@ -2970,7 +3044,7 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Catalog - 需求编号:M06-01-FR07 - 负责人:顾欣月 -- 关联数据表:DB022 `products` +- 关联数据表:DB022 `products`、DB102 `outbox_messages` - 当前状态:待交叉评审 - 用途:停止商品销售,使购物端列表、详情和搜索不再公开该商品。 - 方法与路径:`POST /api/merchant/products/{productId}/unpublish` @@ -3008,7 +3082,7 @@ BrowsingHistorySettingResponse { #### 缓存、事件或外部依赖 -- 下架事务提交后立即失效目标 A103 正常值和唯一固定首页摘要,并在 3 秒后二次失效;普通列表与搜索直读 PostgreSQL,公开查询始终过滤商品状态。 +- 下架事务同时建立 C07 Immediate 与初始未武装 Delayed;提交可见后前者尽快失效目标 A103 正常值和唯一固定首页摘要,后者由数据库时间武装并等待 3 秒后二次失效;普通列表与搜索直读 PostgreSQL,公开查询始终过滤商品状态。 #### 验证场景 @@ -3021,7 +3095,7 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Catalog - 需求编号:M06-01-FR09 - 负责人:顾欣月 -- 关联数据表:DB023 `product_images` +- 关联数据表:DB022(`products`)、DB023(`product_images`)、DB102(`outbox_messages`);对象已写但关联失败时使用 DB105(`object_cleanup_tasks`) - 当前状态:待交叉评审 - 用途:为商品上传图片到 S3 兼容对象存储,返回图片记录。 - 方法与路径:`POST /api/merchant/products/{productId}/images` @@ -3059,15 +3133,18 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 - 原始文件名只用于安全展示,不作为对象存储 Key;对象 Key 采用 `products/{productId}/{fileId}.` 格式。 -- 对象存储失败时不写入指向不存在对象的成功记录。 -- 商品没有图片时服务端强制首图为主图;已有图片且 `isPrimary=true` 时在同一事务取消旧主图并设新图为主图;`isPrimary=false` 或省略时按当前最大 `sortOrder + 1` 追加。 -- 有图片的商品任一时刻恰有一张主图。图片记录、排序和主图切换必须形成一个一致结果;并发上传第 9 张时由受控计数/约束只允许前 8 张成功,其余稳定返回 409。 -- 对象已写入但数据库事务失败时立即尝试删除对象,删除失败则登记可追踪补偿任务。 +- A127 固定采用三阶段上传,不得退化成“先写对象、最后才首次保存 Key”: + 1. **预留**:短事务锁定 DB022 商品,统计 `attached + 未过期 uploading`;达到 8 张即返回 409。服务端预生成 `imageId`、原图/缩略图不可变 Key,插入 DB023 `status=uploading`,`uploadExpiresAt=reservationTime+15 分钟` 后提交。预留不进入当前图库、主图或公开详情。 + 2. **写对象**:只向预留 Key 写原图和缩略图;每个对象调用最长 2 分钟,且开始前必须确认预留仍存在、未过期。对象存储失败时不得产生 Attached 成功记录。 + 3. **关联**:新事务再次锁定商品和预留,要求 `status=uploading AND uploadExpiresAt>decisionTime`,重新校验总数、对象存在性和媒体元数据,再切换为 `attached`、清空到期时间并原子设置排序/主图、写 DB102 失效事实。商品没有图片时强制该图为主图;已有图片且 `isPrimary=true` 时取消旧主图并设新图;否则按当前最大 `sortOrder+1` 追加。 +- 有图片的商品任一时刻恰有一张主图。`attached + 未过期 uploading ≤ 8`,预留和最终事务都在同一商品锁下重检;并发上传第 9 张时最多 8 张可进入有效预留,其余稳定返回 409。 +- 失败收敛固定如下:尚未写任何对象时,可在锁定预留后直接删除该 DB023 行;任一对象可能已写时,必须在同一数据库事务为原图和缩略图分别 UPSERT DB105,再删除失败预留。数据库不可用时保留预留作为最后一份 Key 清单。过期扫描默认每 1 分钟、每批 100 条,只处理 `uploadExpiresAt` 已到期的行;扫描锁定预留后登记两项 DB105,DB105 在删除前重查 DB023/DB062 引用。晚到执行者因预留过期或消失不能 Attached,并必须重用相同 DB105 Key 触发再次核验,不能把迟到对象遗留为无主对象。 +- A127 每次调用生成新图片记录,调用方在网络结果不确定时应先重新读取 A121 当前图库再决定是否重试;本接口不以“重复文件内容”自动去重,也不允许用原文件名判断同一图片。 #### 缓存、事件或外部依赖 - 依赖 M00 提供的 `IObjectStorage` 公共接口与 SeaweedFS 开发环境。 -- 图片事务提交后立即失效目标 A103;主图或首页摘要字段变化时同时失效唯一固定首页,并在 3 秒后二次失效。缓存失效失败不回滚已提交图片事实。 +- 图片事务同时建立 C07 Immediate 与初始未武装 Delayed;提交可见后前者尽快失效目标 A103,主图或首页摘要字段变化时同时失效唯一固定首页,后者由数据库时间武装并等待 3 秒后二次失效。缓存失效失败不回滚已提交图片事实。 #### 验证场景 @@ -3080,9 +3157,9 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Catalog - 需求编号:M06-01-FR09 - 负责人:顾欣月 -- 关联数据表:DB023 `product_images` +- 关联数据表:DB022(`products`)、DB023(`product_images`)、DB062(`order_items`)、DB102(`outbox_messages`)、DB105(`object_cleanup_tasks`) - 当前状态:待交叉评审 -- 用途:删除某张商品图片。 +- 用途:把某张图片从当前商品图库移除;历史订单已快照引用时保留归档图片对象,无历史引用时才登记安全清理。 - 方法与路径:`DELETE /api/merchant/products/{productId}/images/{imageId}` - operationId:`Catalog_DeleteProductImage` - 请求 Schema:无(Route) @@ -3114,15 +3191,24 @@ BrowsingHistorySettingResponse { - 删除主图且仍有其他图片时,服务端固定按 `sortOrder asc, imageId asc` 自动提升下一张为主图;本接口不接受客户端临时指定替代主图。 - `OnSale` 商品不得删除最后一张图片;`Draft`/`OffSale` 商品允许删空,后续上架仍须重新满足主图完整性。 -- 图片关联删除和新主图选择在同一数据库事务内完成;对象存储删除在提交后执行,失败时记录可重试补偿,不能恢复已删除的数据库关联或返回虚假失败。 +- 事务先锁定 DB022 商品行和目标 `status=attached` 图片行,再校验图片属于商品。删除主图时固定按 `sortOrder asc, imageId asc` 选定并切换替代主图;`OnSale` 删除后无主图仍拒绝。 +- 在同一事务检查 DB062 是否通过 `productImageObjectKeySnapshot` 引用目标对象 Key: + - 存在历史订单引用:把 DB023 图片改为 `detached`,清空当前排序、取消主图并写入 `detachedAt`;原图、缩略图 Key 和媒体元数据保持不可变,不创建 DB105 清理任务。A101/A102/A103/A120/A121 和当前图库图片上限均不再返回/统计该图,历史订单与售后仍从 DB062 快照读取。 + - 不存在历史订单引用:为原图和缩略图分别登记 DB105 清理任务,再物理删除 DB023 关联;图库变化、替代主图和两条清理责任一起提交。 +- 目标图片原 `sortOrder=k`。先完成目标 Detach/Delete 释放序号 k,再按 `sortOrder asc,imageId asc` 对其后图片逐行执行 `k+1→k、k+2→k+1…`;不得用无序集合式 `sortOrder-1` 更新撞击非可延迟唯一索引。目标原为主图时,压缩后把最小 `sortOrder,imageId` 的剩余图片设为唯一主图;最多 8 张使逐行算法有界且可直接验证。 +- Ordering 下单与 A128 必须锁定同一商品行。下单先提交时,DB062 快照引用及数据库 RESTRICT 约束迫使删图走 `detached`;删图先提交时,下单只能读取新的 `attached` 主图,不能快照已移除图片。 +- DB105 Worker 真正删除对象前再次检查同 Key 不存在 DB023 当前/归档关联和 DB062 历史快照引用;仍被引用时不得删除对象,以 `Succeeded + ObjectStillReferenced` 记录安全无操作结果并告警设计期不变量偏离。对象已不存在按 `ObjectNotFound` 幂等成功处理,实际删除按 `ObjectDeleted` 完成;查询依赖失败不能当作无引用。 #### 缓存、事件或外部依赖 -- 数据库事务提交后立即失效目标 A103;主图或首页摘要字段变化时同时失效唯一固定首页,并在 3 秒后二次失效。对象清理失败记录可追踪错误,不阻断已提交主流程。 +- 数据库事务同时建立 C07 Immediate 与初始未武装 Delayed;提交可见后前者尽快失效目标 A103,主图或首页摘要字段变化时同时失效唯一固定首页,后者由数据库时间武装并等待 3 秒后二次失效。对象是否因历史引用保留属于内部生命周期,不改变本接口 204 语义;清理失败记录可追踪错误,不阻断已提交主流程。 #### 验证场景 - 跨商品删图返回 404;已上架商品删至无主图返回 409。 +- 删除有历史订单快照引用的图片 → 204;当前商品不再展示该图,历史订单仍能读取原图,且不生成对象删除任务。 +- 删除无历史引用的图片 → 204;图库和主图先提交,原图/缩略图各有一条可重试清理任务。 +- 并发下单与删图 → 下单先提交则图片归档保留,删图先提交则订单使用新主图;不存在订单引用已被物理清理对象的结果。 --- @@ -3200,37 +3286,40 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Review - 需求编号:M07-FR03 - 负责人:顾欣月 -- 关联数据表:DB025 `review_images` +- 关联数据表:DB025 `review_images`、DB104 `idempotency_records`;失败或过期清理时使用 DB105 `object_cleanup_tasks` - 当前状态:待交叉评审 - 用途:买家在提交评价前逐张上传晒图,返回图片标识供 A142 引用。 - 方法与路径:`POST /api/reviews/images` - operationId:`Review_UploadReviewImage` - 请求 Schema:`UploadReviewImageRequest`(`multipart/form-data`) -- 响应 Schema:`ReviewImageResponse` +- 响应 Schema:`ReviewImageUploadResponse` - 身份与 Policy:BuyerOnly。 -- 幂等要求:非幂等;每次上传生成新的暂存图片。 +- 幂等要求:必需 `Idempotency-Key`;同键同订单项同文件字节重放同一 `imageId`,同键换订单项或文件内容返回冲突。 #### 请求 -- Header:`Authorization`、`Content-Type: multipart/form-data`。 +- Header:`Authorization`、`Idempotency-Key`(uuid,必需)、`Content-Type: multipart/form-data`。 - Body(form-data):`UploadReviewImageRequest`,含 `orderItemId`(uuid,必填)、`file`(图片文件,必填)。 -- 校验规则:先校验订单项属于当前买家、所属订单为 `Completed` 且尚无评价,再校验累计图片数不超过 6;仅接受 JPEG、PNG、WebP;单图 ≤ 5 MB;宽高均 200~4096 像素;同时校验扩展名、声明 MIME 与实际特征。该资格只用于允许上传,A142 正式提交时仍完整重检。 +- 校验规则:先完整读取并计算文件 SHA-256,校验订单项属于当前买家、所属订单为 `Completed` 且尚无评价,再校验累计未过期 Uploading/Pending 图片数不超过 6;仅接受 JPEG、PNG、WebP;单图 ≤ 5 MB;宽高均 200~4096 像素;同时校验扩展名、声明 MIME 与实际特征。请求指纹固定为 `buyerId + orderItemId + 文件内容 SHA-256`,不包含 multipart boundary、原始文件名或传输 Header;A142 正式提交时仍完整重检资格。 #### 成功响应 - HTTP 状态:`201 Created` -- 响应 Schema:`ReviewImageResponse`,含 `imageId`、`url`。 +- 响应 Schema:`ReviewImageUploadResponse { imageId }`。A141 不返回公共 URL;前端只用本次本地文件的浏览器 object URL 预览,刷新后不承诺恢复暂存预览。 #### 失败响应 | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失/格式错误,或 `orderItemId/file` 缺失 | | 401 | `AUTH.UNAUTHENTICATED` | 未登录 | | 403 | `AUTH.FORBIDDEN` | 非买家 | | 404 | `REVIEW.ORDER_ITEM_NOT_FOUND` | 订单项不存在或不属于当前买家 | | 409 | `REVIEW.ORDER_NOT_COMPLETED` | 订单未完成 | | 409 | `REVIEW.ALREADY_REVIEWED` | 订单项已经评价 | | 409 | `REVIEW.IMAGE_LIMIT_EXCEEDED` | 该订单项累计可提交图片超过 6 张 | +| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一键被用于不同订单项或不同文件内容 | +| 409 | `IDEMPOTENCY.REQUEST_IN_PROGRESS` | 同键同文件仍在有效上传租约内;返回 `Retry-After: 1` | | 413 | `COMMON.PAYLOAD_TOO_LARGE` | 文件超限 | | 415 | `REVIEW.INVALID_IMAGE` | 格式或尺寸不符合要求 | | 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Ordering 评价资格或对象存储暂时不可用 | @@ -3238,8 +3327,13 @@ BrowsingHistorySettingResponse { #### 业务规则与并发 - 上传结果同时绑定当前买家与 `orderItemId`,只能被同一买家针对同一订单项的 A142 请求引用;不得仅凭 `imageId` 跨买家或跨订单项占用图片。 -- 同一订单项的图片计数必须串行化或由等效数据库约束保护;并发上传第 7 张时只允许一方成功,其余稳定返回 `REVIEW.IMAGE_LIMIT_EXCEEDED`。 -- 对象写入成功但上传结果持久化失败时立即尝试清理对象;清理失败登记可追踪补偿。未被成功评价引用的上传结果不得公开。 +- 完成固定格式、文件哈希与基础媒体校验后,先读取 DB104:同键换指纹返回 409;同键同指纹 Completed 重放同一 `imageId`;有效 Processing 最多有界等待 2 秒,仍未完成返回 `IDEMPOTENCY.REQUEST_IN_PROGRESS`,不得另建图片。 +- 上传固定为三阶段: + 1. **持久预留**:短事务取得 A141 幂等 advisory lock 与 `(buyerId,orderItemId)` 事务 advisory lock,重检资格和“未过期 Uploading/Pending < 6”,预生成同一 `imageId/objectKey`;DB104 保存文件哈希、ID、Key、60 秒租约 Token 和 24 小时工作期限,DB025 同事务插入 Uploading。 + 2. **对象写入**:只向工作清单的不可变 Key 写入;单次对象调用最长 2 分钟,每 20 秒以 DB104 当前 Token 续租,失去围栏后不得完成旧工作。租约到期后同键重试只能换发 Token 接管同一 ID/Key,不能生成第二张。 + 3. **完成暂存**:新事务同时匹配 DB104 Processing + 当前 Token 和 DB025 Uploading + 未过期,补齐媒体元数据并切为 Pending,把 DB104 改为 Completed 并保存首次 201 响应。DB025 与 DB104 要么一起完成,要么都保持可恢复状态。 +- `uploadExpiresAt` 固定为首次预留时间 + 24 小时,续租不延长可引用期限。对象失败或数据库瞬态失败不固化 503,保留原工作清单供同键续传/接管;过期后禁止完成,改由 `review.expired_image_upload_cleanup` 登记 DB105。若旧执行者在失去 Token 后才确认对象写入,必须以 DB105 `Supersede` 模式登记新代清理责任,不能让旧 NotFound 结果吞掉迟到对象。 +- DB025 Uploading/Pending 不被 A140 或只读媒体源公开;只有 A142 在同一评价事务切为 Attached 后,A140/A142 才派生稳定公开 URL。 #### 缓存、事件或外部依赖 @@ -3247,7 +3341,7 @@ BrowsingHistorySettingResponse { #### 验证场景 -- 非法格式/尺寸返回 415;超大返回 413;返回可被 A142 引用的 `imageId`。 +- 非法格式/尺寸返回 415;超大返回 413;同键同文件响应丢失后重试仍返回同一 `imageId` 且只占一个名额;同键换文件返回 409;暂存 Key 经公共媒体源读取返回 404,A142 成功后才可公开访问。 --- @@ -3256,7 +3350,7 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Review - 需求编号:M07-FR04、M07-FR05 - 负责人:顾欣月 -- 关联数据表:DB024 `reviews`、DB025 `review_images` +- 关联数据表:DB024 `reviews`、DB025 `review_images`、DB104 `idempotency_records` - 当前状态:待交叉评审 - 用途:买家对本人已完成订单项提交一次评分、文字与可选图片评价。 - 方法与路径:`POST /api/reviews` @@ -3276,10 +3370,10 @@ BrowsingHistorySettingResponse { |---|---|---|---| | `orderItemId` | uuid | 是 | 属于当前买家且订单已完成的订单项 | | `rating` | integer | 是 | 1~5 整数 | -| `content` | string | 是 | 1~500,纯文本/受控内容 | +| `content` | string | 是 | 纯文本;换行统一为 LF、去首尾 Unicode 空白,规范化后按 Unicode 标量值 1~500 | | `imageIds` | uuid[] | 否 | 引用 A141 暂存图片,≤ 6 | -- 校验规则:服务端重新校验身份、订单项归属、订单完成状态、评分范围、文字长度、图片数量与是否已评价。 +- 校验规则:服务端重新校验身份、订单项归属、订单完成状态、评分范围、规范化纯文本长度、图片数量与是否已评价。`content` 不接受 HTML 语义,响应始终为普通 JSON string;前端仅文本插值并以 `white-space: pre-wrap` 保留换行,禁止 `v-html`。 #### 成功响应 @@ -3308,7 +3402,7 @@ BrowsingHistorySettingResponse { - 同一订单项只能形成一条评价:唯一约束 `ux_reviews_order_item_id` 作为最终保障;重复点击、不同窗口并发或不同幂等键命中同一订单项时返回既有已评价结果,不新增记录。 - 订单完成状态、订单项归属由 Ordering 提供的应用契约校验,不直接改订单表。 - 创建评价时通过 Identity 公开应用契约读取当前买家的安全展示名并完成脱敏,将结果保存为 `buyerDisplayName` 快照;没有展示名时回退到自动用户名的脱敏值。后续用户资料变化不改写历史评价快照,公开列表和详情不逐条查询 Identity。 -- 正式提交重新校验当前买家、订单项归属、`Completed` 状态、尚未评价、字段,以及全部 `imageIds` 均绑定当前买家和该订单项。评价、脱敏展示名快照、图片关联和确定幂等结果在一个原子事务中提交;任一必要写入失败时整体回滚。 +- 正式提交重新校验当前买家、订单项归属、`Completed` 状态、尚未评价、字段,以及全部 `imageIds` 均为当前买家/订单项下未过期 Pending。评价、脱敏展示名快照、图片 `Pending → Attached`、清空 `uploadExpiresAt`、将对应 A141 DB104 `expiresAt` 清空以延长至图片生命周期,以及 A142 确定幂等结果在一个原子事务中提交;任一必要写入失败时整体回滚。 - 事务提交成功后评价立即公开并进入 A140 的总数与平均分;M04 订单和订单项状态保持 `Completed`,本期不发送站内消息。 #### 缓存、事件或外部依赖 @@ -3375,7 +3469,7 @@ BrowsingHistorySettingResponse { - 已评价返回 `eligible=false, reason=AlreadyReviewed`;未完成返回 `eligible=false, reason=OrderNotCompleted`。A143 只决定订单详情入口提示,A142 仍执行完整资格重检。 -> 来源:[`interface-zhh.md`](interface/interface-zhh.md)。A229/A230 已取消并由 A302/A303 承接秒杀订单查询;活动、库存与 Ordering 创建契约仍待数据库设计和联调确认。 +> 来源:[`interface-zhh.md`](interface/interface-zhh.md)。A229/A230 已取消并由 A302/A303 承接秒杀订单查询;活动、库存与 Ordering 创建契约已反查统一数据库设计,待 OpenAPI、实现和联调确认。 ### A201 加入购物车 @@ -3385,7 +3479,7 @@ BrowsingHistorySettingResponse { - 模块 / Tag:Cart - 需求编号:F07、M03-01 - 负责人:朱惠惠 -- 关联数据表:DB041 +- 关联数据表:DB041;提供 `Idempotency-Key` 时使用 DB104 - 当前状态:待交叉评审 - 用途:已登录买家将商品加入购物车;同一买家同一商品只保留一条记录,重复加入按累加处理;服务端实时校验上下架、库存与数量上限。 - 方法与路径:`POST /api/cart/items` @@ -3413,7 +3507,7 @@ AddCartItemRequest { #### 成功响应 - HTTP 状态:`201 Created`(新增条目)或 `200 OK`(重复加入累加) -- Response Header:`Location: /api/cart/items/{cartItemId}` +- Response Header:无。当前契约没有购物车单条 GET;创建结果直接由响应体返回,不发布无法访问的 `Location`。 - 响应 Schema:`CartItemResponse` ```text @@ -3426,13 +3520,17 @@ CartItemResponse { subtotal: number // unitPrice × quantity,由服务端计算 isSelected: boolean isAvailable: boolean - unavailableReason: string? // 例如 "ProductUnpublished"、"OutOfStock" - maxAllowedQuantity: integer // 商品当前实时可售库存,供前端截断 + unavailableReason: "ProductOffSale" | "OutOfStock" | "QuantityExceedsStock" | null + maxAllowedQuantity: integer // ProductOffSale/OutOfStock 为 0;其余为当前普通库存 createdAt: string updatedAt: string } ``` +其中 `productSummary` 完整复用 A102 的 `ProductSummary`:`productId/name/categoryId/price/stockStatus/thumbnailUrl/createdAt`;A201 不得为购物车另造字段同名但口径不同的商品摘要。 + +`CartItemResponse` 的可用性固定为 `OnSale && stock >= quantity && quantity > 0`。`Draft/OffSale` 返回 `ProductOffSale`;`OnSale && stock=0` 返回 `OutOfStock`;`OnSale && 0 0 } ``` @@ -3531,7 +3631,9 @@ CartListResponse { - 严格按 `buyer_id = current_user_id` 过滤;不允许跨用户查看。 - 排序固定按 `updatedAt desc, cartItemId desc`,避免同一商品事实变化或同时间戳导致翻页重复、遗漏。 -- 商品为 `Draft` / `OffSale` 或实时可售库存归零时,条目仍可见但标记 `isAvailable=false` 并附 `unavailableReason`;不参与 `selectedTotalAmount` 与 `availableSelectedCount` 计算。分类停用不反向改变仍为 `OnSale` 商品的可结算资格。 +- `isSelected` 原样返回 DB041 持久化选择意图,`isAvailable` 按本次 Catalog 实时事实派生。GET 不写库,因此商品在已选后失效时允许返回 `isSelected=true/isAvailable=false`;它计入 `selectedCount` 和 `unavailableSelectedCount`,但不参与 `selectedTotalAmount` 与 `availableSelectedCount`。 +- `selectedOnly=true` 按持久化 `isSelected` 过滤,所以仍可返回“已选但失效”条目,不能静默隐藏结算阻断项。分类停用不反向改变仍为 `OnSale` 商品的可结算资格。 +- `checkoutReady=true` 当且仅当 `selectedCount > 0`、`unavailableSelectedCount = 0` 且 `selectedTotalAmount > 0`;本期没有额外最低金额门槛。它只是页面提示,A208/A301 仍重读全部目标。 - 实时单价与库存来自 Catalog 模块;不接受客户端传入的价格或库存覆盖。 #### 缓存、事件或外部依赖 @@ -3543,7 +3645,7 @@ CartListResponse { - 买家购物车 0 条 → `items=[]`,`selectedCount=0`,`selectedTotalAmount=0`。 - 包含已下架商品 → 仍可见,`isAvailable=false`,`selectedTotalAmount` 不计入。 -- 包含失效商品但被选中 → `availableSelectedCount` 仅统计可用条目。 +- 包含失效商品但被选中 → `selectedCount` 计入、`unavailableSelectedCount` 计入、`availableSelectedCount` 不计入且 `checkoutReady=false`;GET 不清除其选择意图。 - 非买家角色调用 → 403 / `AUTH.FORBIDDEN`;列表永远只按当前买家过滤,不存在传入他人用户标识的入口。 - 翻页查询 → 总数与分页元数据稳定,按 `updatedAt desc` 一致排序。 @@ -3620,16 +3722,16 @@ UpdateCartItemQuantityRequest { - 模块 / Tag:Cart - 需求编号:F07、M03-01 - 负责人:朱惠惠 -- 关联数据表:DB041 +- 关联数据表:DB041(`cart_items`) - 当前状态:待交叉评审 -- 用途:买家按稳定请求标识删除本人单条购物车条目;同一请求重放首次结果,新请求删除不存在或不属于本人的条目返回 404。 +- 用途:买家按条目 ID 条件删除本人单条购物车条目;条目已不存在或不属于本人时仍返回相同空结果,避免泄露资源存在性。 - 方法与路径:`DELETE /api/cart/items/{cartItemId}` - operationId:`Cart_RemoveItem` #### 请求 - Route 参数:`cartItemId: uuid` -- Header:`Authorization: Bearer `(必填)、`Idempotency-Key: `(必填) +- Header:`Authorization: Bearer `(必填);不接收 `Idempotency-Key` - Body:无 #### 成功响应 @@ -3641,17 +3743,13 @@ UpdateCartItemQuantityRequest { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| | 400 | `COMMON.INVALID_UUID` | `cartItemId` 不是标准 UUID | -| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失或格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | -| 404 | `CART.ITEM_NOT_FOUND` | 条目不存在或不属于当前买家 | -| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同请求 | #### 业务规则与并发 -- 固定身份、键格式和路由字段校验后先读取持久化幂等结果;同键同请求重放首次 204,同键换目标返回 409。 -- 新请求按 `cart_item_id = :cartItemId AND buyer_id = current_user_id` 过滤;不存在或不属于本人统一形成 404 的确定结果,不泄露真实归属。 -- 删除副作用、确定成功结果和幂等记录在同一事务提交;确定 404 也先保存再返回,瞬态失败不固化。 +- 通过身份和路由字段校验后,按 `cart_item_id = :cartItemId AND buyer_id = current_user_id` 执行一次条件删除;影响 0 行和 1 行都返回 204。 +- 条件删除达到最终状态天然幂等,不建立 DB104 记录,也不通过 404 区分“已删除、不存在、属于他人”。 - 失效条目同样允许删除;删除购物车条目不恢复库存,也不修改收藏、浏览历史等其他模块事实。 #### 缓存、事件或外部依赖 @@ -3661,8 +3759,8 @@ UpdateCartItemQuantityRequest { #### 验证场景 - 删除本人条目 → 204,列表更新。 -- 同一幂等键重复删除同一 `cartItemId` → 重放首次 204。 -- 使用新幂等键删除已删除或他人条目 → 404,不泄露归属。 +- 重复删除同一 `cartItemId` → 204。 +- 删除已不存在或属于他人的条目 → 204,且最多删除当前买家本人一行。 - 未登录调用 → 401 / `AUTH.UNAUTHENTICATED`。 ### A205 批量删除购物车条目 @@ -3673,7 +3771,7 @@ UpdateCartItemQuantityRequest { - 模块 / Tag:Cart - 需求编号:F07、M03-01 - 负责人:朱惠惠 -- 关联数据表:DB041 +- 关联数据表:DB041(`cart_items`)、DB104(`idempotency_records`) - 当前状态:待交叉评审 - 用途:买家按稳定请求标识一次性删除多个本人购物车条目;全部目标先校验,任一不存在或不归属本人时整批拒绝且零删除。 - 方法与路径:`POST /api/cart/items/batch-delete` @@ -3735,9 +3833,9 @@ BatchRemoveCartItemsResponse { - 模块 / Tag:Cart - 需求编号:F07、M03-01 - 负责人:朱惠惠 -- 关联数据表:DB041 +- 关联数据表:DB041(`cart_items`)、DB104(`idempotency_records`) - 当前状态:待交叉评审 -- 用途:买家按稳定请求标识设置购物车条目选中状态;支持全选、全不选、真正反选和显式单选/多选;失效条目始终保持未选中。 +- 用途:买家按稳定请求标识设置购物车条目选中状态;支持全选、全不选、真正反选和显式单选/多选;当前失效条目允许取消选择但不允许从未选变为已选。 - 方法与路径:`PATCH /api/cart/items/selection` - operationId:`Cart_UpdateSelection` @@ -3757,7 +3855,18 @@ UpdateCartItemSelectionRequest { #### 成功响应 - HTTP 状态:`200 OK` -- 响应 Schema:`CartListResponse`(同 A202,按当前选中状态返回完整购物车) +- 响应 Schema:`CartSelectionMutationResponse` + +```text +CartSelectionMutationResponse { + mode: "SelectAll" | "DeselectAll" | "Invert" | "SetExplicit" + affectedCount: integer // 本次实际改变 isSelected 的条目数,可为 0 + selectedIntentCount: integer // 提交后本人 isSelected=true 的持久化意图总数,包含后来失效但尚未被新命令规范化的条目 + selectionChangedAt: datetime // 本次数据库裁决时间 +} +``` + +成功后客户端另行调用 A202 刷新分页购物车与实时 Catalog 读模型;刷新失败只显示“选择已保存,列表刷新失败,可重试”,不得回滚已经提交的选择命令。A206 不返回动态价格、库存、可结算金额或完整购物车,避免纯取消选择被 Catalog 可用性和分页绑架。 #### 失败响应 @@ -3773,10 +3882,11 @@ UpdateCartItemSelectionRequest { #### 业务规则与并发 -- 固定身份、键格式和请求结构校验后先读取持久化幂等结果;`Invert` 等非天然幂等动作重试必须重放首次结果,不能二次翻转。 -- `SelectAll` 把当前买家全部可结算条目设为选中、失效条目设为未选中;`DeselectAll` 把全部本人条目设为未选中;`Invert` 只反转可结算条目的当前值并把失效条目保持未选中。 -- `SetExplicit` 先验证全部 `cartItemIds` 均存在且属于当前买家,任一无效时整次 404 且零修改;当 `isSelected=true` 时,任一目标失效则整次 409 且零修改。 -- 状态变更和确定幂等结果在同一事务提交;确定 404/409 同样保存后返回,瞬态失败不固化。 +- 固定身份、键格式和请求结构校验后先读取持久化幂等结果;`Invert` 等非天然幂等动作重试必须重放首次 `CartSelectionMutationResponse`,不能二次翻转,也不能把首次动态商品读模型当成当前购物车重放。 +- `SelectAll` 把当前买家全部可结算条目设为选中、把当前失效条目规范为未选中;`DeselectAll` 把全部本人条目设为未选中;`Invert` 只反转可结算条目的当前值并把当前失效条目规范为未选中。 +- `SetExplicit` 先验证全部 `cartItemIds` 均存在且属于当前买家,任一无效时整次 404 且零修改;当 `isSelected=true` 时,任一目标失效则整次 409 且零修改;当 `isSelected=false` 时不要求商品可用,允许用户取消“已选但失效”的选择。未在显式集合中的条目保持原值。 +- 被动商品失效不会触发 Cart 写入,A202 因此可能在用户下一次选择命令前返回“已选但失效”。A206 的确定命令按上述规则规范化,不允许实现把 GET 改成隐式写操作。 +- 状态变更、同一数据库裁决时间、提交后选择意图总数和确定幂等结果在同一事务提交;确定 404/409 同样保存后返回,瞬态失败不固化。`DeselectAll` 和 `SetExplicit(isSelected=false)` 不读取 Catalog;其成功不得被 Catalog 故障阻断。 - 选中状态保存在服务端;前端刷新或重新登录后状态保留。 #### 缓存、事件或外部依赖 @@ -3790,6 +3900,8 @@ UpdateCartItemSelectionRequest { - 反选 → 200,所有可用条目取反、失效条目仍 `isSelected=false`;同键重试不再翻转。 - 单选切换某条目 → 200,仅该条目 `isSelected` 变更。 - 尝试选中失效条目 → 409 / `CART.ITEM_UNAVAILABLE`,不修改状态。 +- 显式取消已选但失效条目 → 200,该条目改为未选;不因 Catalog 不可用阻止纯取消动作。 +- A206 成功后 A202 暂时 503 → 选择结果仍已提交;页面提示重试刷新,不重复执行 `Invert`。 - 跨用户或不存在 ID 提交 → 404,整次零修改且不泄露真实归属。 ### A207 清空购物车 @@ -3801,15 +3913,15 @@ UpdateCartItemSelectionRequest { - 模块 / Tag:Cart - 需求编号:F07、M03-01 - 负责人:朱惠惠 -- 关联数据表:DB041 +- 关联数据表:DB041(`cart_items`) - 当前状态:待交叉评审 -- 用途:买家按稳定请求标识一键清空本人购物车全部条目;购物车本来为空仍返回成功空结果。 +- 用途:买家一键清空本人购物车全部条目;购物车本来为空或重复调用仍返回相同空结果。 - 方法与路径:`DELETE /api/cart` - operationId:`Cart_Clear` #### 请求 -- Header:`Authorization: Bearer `(必填)、`Idempotency-Key: `(必填) +- Header:`Authorization: Bearer `(必填);不接收 `Idempotency-Key` - Body:无 #### 成功响应 @@ -3820,15 +3932,13 @@ UpdateCartItemSelectionRequest { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | `Idempotency-Key` 缺失或格式错误 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | -| 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同清空请求指纹 | #### 业务规则与并发 -- 固定身份、键格式和请求结构校验后先读取持久化幂等结果;同键同请求重放首次 204,同键换内容返回 409。 -- 新请求按 `buyer_id = current_user_id` 物理删除全部本人条目;购物车为空也形成成功空结果。删除副作用与确定结果在同一事务提交,瞬态失败不固化。 +- 通过身份校验后按 `buyer_id = current_user_id` 物理删除全部本人条目;影响 0 行或多行都返回 204。 +- 清空后的最终状态天然幂等,不建立 DB104 记录;并发加购与清空以数据库取得写入锁的提交顺序为准,清空不得删除其他买家条目。 - 不影响浏览记录、收藏、消息或默认地址等其他模块数据。 #### 缓存、事件或外部依赖 @@ -3843,21 +3953,22 @@ UpdateCartItemSelectionRequest { ### A208 获取结算预览 -- 请求 Schema:无 +- 请求 Schema:`CheckoutPreviewQuery` - 身份与 Policy:BuyerOnly - 模块 / Tag:Cart - 需求编号:F07、M03-01 - 负责人:朱惠惠 -- 关联数据表:DB041 +- 关联数据表:DB041(`cart_items`)及 Catalog 实时商品、普通库存事实 - 当前状态:待交叉评审 -- 用途:买家进入结算页前查看选中条目总价、可用性与失效原因;服务端再次校验实时价格、库存与归属。 +- 用途:买家进入结算页前查看全部已选条目,或页面明确给出的已选条目子集的总价、可用性与失效原因;服务端再次校验实时价格、库存、选中状态与归属。 - 方法与路径:`GET /api/cart/checkout-preview` - operationId:`Cart_GetCheckoutPreview` #### 请求 -- Query 参数:无;目标固定为服务端保存的当前买家 `isSelected=true` 条目。 +- Query 参数: + - `cartItemId`(可选,可重复):目标购物车条目 UUID。完全不提供时使用当前买家全部 `isSelected=true` 条目;提供时原始参数最多 100 个,服务端按首次出现顺序稳定去重后,只校验和返回该显式子集。 - Header:`Authorization: Bearer `(必填) - Body:无 @@ -3868,9 +3979,13 @@ UpdateCartItemSelectionRequest { ```text CheckoutPreviewResponse { - items: CartItemResponse[] // 当前买家全部已选中条目,含每条可用性与原因 - totalAmount: number? // 仅在全部选中项有效且非空时返回确定总额 - availableForCheckout: boolean // 选中项非空且全部可结算时为 true + targetMode: "AllSelected" | "ExplicitSubset" + cartItemIds: uuid[] // 本次规范化后的完整目标集合,顺序稳定 + items: CartItemResponse[] // 与 cartItemIds 一一对应,含每条实时事实、可用性与原因 + totalAmount: number? // 仅在完整目标集合有效且非空时返回确定预览总额 + availableForCheckout: boolean // 完整目标集合非空且全部可结算时为 true + checkoutRevision: string? // 仅 availableForCheckout=true,64 位小写 SHA-256 hex + evaluatedAt: datetime // 本次数据库权威预览时间 } ``` @@ -3878,15 +3993,21 @@ CheckoutPreviewResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---:|---|---| +| 400 | `COMMON.VALIDATION_FAILED` | 任一 `cartItemId` 不是 UUID,或显式参数超过 100 个 | | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | +| 404 | `CART.ITEM_NOT_FOUND` | 显式子集中任一条目不存在或不属于当前买家;不泄露是哪一种情况 | +| 409 | `CART.ITEM_NOT_SELECTED` | 显式子集中任一本人条目当前未选中;响应不得静默丢弃该条目或改取其他已选条目 | +| 409 | `CART.CHECKOUT_ITEM_LIMIT_EXCEEDED` | 未给显式子集时,当前已选条目超过 100 个;要求买家减少选择后重试 | | 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Catalog 商品状态、实时价格或库存暂时不可用,无法形成完整结算预览 | #### 业务规则与并发 -- 固定按 `isSelected=true AND buyer_id = current_user_id` 读取服务端选中事实,客户端不能传一组 ID 绕过选中状态或让服务端静默取交集。 -- 对全部选中条目重新读取销售状态、实时库存和实时单价;任一条目失效时返回全部目标及问题原因,`availableForCheckout=false`、`totalAmount=null`,不得把可用子集包装成可直接下单结果。 -- 只有选中项非空且全部有效时才由服务端计算 `totalAmount` 并返回 `availableForCheckout=true`;M04 提交订单仍必须再次重读全部事实。 +- 未给显式子集时,固定按 `isSelected=true AND buyer_id=current_user_id` 读取当前全部已选条目;给出显式子集时,必须逐项校验当前买家归属和 `isSelected=true`,不得静默取交集、并入其他已选条目或用不存在/他人条目探测资源。 +- 显式子集只用于固定本次从购物车准备结算的目标,不能绕过 A201/A206 形成未选条目,也不能创建第二套购买入口。本期没有独立“立即购买”能力。 +- 对完整目标集合重新读取销售状态、实时库存和实时单价;任一商品失效时仍返回本次完整目标集合及逐项问题原因,`availableForCheckout=false`、`totalAmount=null/checkoutRevision=null`,不得把可用子集包装成可直接下单结果。无显式子集时同样最多 100 条,超过上限整体 409,不截断。 +- 只有目标非空、全部有效且服务端计算 `totalAmount > 0` 时才返回 `availableForCheckout=true`。服务端按稳定 `cartItemIds` 顺序,对 `buyerId + 每项(cartItemId,productId,cartVersion,quantity,productName,mainImageObjectKey,unitPrice)` 的规范 UTF-8 表示计算 SHA-256,得到 `checkoutRevision`;不纳入预览入口模式或仍足够的剩余库存数字,相同目标内容从“全部已选/显式子集”进入时得到同一 Revision。 +- `checkoutRevision` 不是锁价或权限凭证,客户端也不提交金额;它只证明买家确认过这一版条目、数量、展示内容和单价。A301 必须在下单事务中重读同一集合并按同一算法重算,实质内容不变才继续,变化时零建单并返回 `ORDER.CHECKOUT_CHANGED + latestPreview`。 #### 缓存、事件或外部依赖 @@ -3895,9 +4016,11 @@ CheckoutPreviewResponse { #### 验证场景 -- 选中 2 条可用 + 1 条失效 → `items` 返回全部 3 条并标明原因,`totalAmount=null`、`availableForCheckout=false`。 -- 全部失效 → 返回全部已选条目及原因,`totalAmount=null`、`availableForCheckout=false`。 -- 没有选中条目 → `items=[]`、`totalAmount=null`、`availableForCheckout=false`。 +- 未给显式子集,选中 2 条可用 + 1 条失效 → `items` 返回全部 3 条并标明原因,`totalAmount=null`、`availableForCheckout=false`。 +- 显式给出 1 个已选 `cartItemId` → 只返回该条目,不混入之后新选中的其他商品。 +- 显式子集中有未选条目 → 409 / `CART.ITEM_NOT_SELECTED`;有不存在或他人条目 → 404 / `CART.ITEM_NOT_FOUND`,均不改取其他条目。 +- 全部失效 → 返回完整目标集合及原因,`totalAmount=null`、`availableForCheckout=false`。 +- 未给显式子集且没有选中条目 → `targetMode=AllSelected`、`cartItemIds=[]`、`items=[]`、`totalAmount=null`、`availableForCheckout=false`。 ### A220 商家创建秒杀活动 @@ -3907,7 +4030,7 @@ CheckoutPreviewResponse { - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 -- 关联数据表:DB042、DB043 +- 关联数据表:DB042(`seckill_activities`)、DB104(`idempotency_records`);商品资格通过 Catalog 公开契约读取 - 当前状态:待交叉评审 - 用途:商家创建秒杀草稿;只保存商品、时间、秒杀价、计划秒杀量与单用户限购,不划拨普通库存,也不产生可抢库存。 - 方法与路径:`POST /api/merchant/seckill-activities` @@ -4001,7 +4124,7 @@ SeckillActivityResponse { - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 -- 关联数据表:DB042、DB043 +- 关联数据表:DB042(`seckill_activities`);商品资格通过 Catalog 公开契约读取 - 当前状态:待交叉评审 - 用途:活动创建人在 `Draft` 状态下修改完整活动计划;发布后只允许取消,不再允许编辑。 - 方法与路径:`PATCH /api/merchant/seckill-activities/{activityId}` @@ -4073,7 +4196,7 @@ UpdateSeckillActivityRequest { - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 -- 关联数据表:DB042、DB043 +- 关联数据表:DB022(`products`,普通库存聚合)、DB026(`catalog_inventory_movements`)、DB042(`seckill_activities`)、DB044(`seckill_inventory_movements`)、DB102(`outbox_messages`)、DB104(`idempotency_records`) - 当前状态:待交叉评审 - 用途:活动创建人在开始时间前发布 `Draft`;重新校验后原子划拨普通库存并固定进入 `Published`,由 Worker 到时推进为 `Ongoing`。 - 方法与路径:`POST /api/merchant/seckill-activities/{activityId}/publish` @@ -4115,7 +4238,7 @@ UpdateSeckillActivityRequest { #### 缓存、事件或外部依赖 -- 秒杀活动、状态和库存不进入 Redis/C07。发布划拨减少普通库存后,事务提交后立即失效目标 A103 和唯一固定首页,并在 3 秒后二次失效;缓存失效失败不回滚已提交发布结果。 +- 秒杀活动、状态和库存不进入 Redis/C07。发布划拨减少普通库存的事务同时建立 C07 Immediate 与初始未武装 Delayed;提交可见后前者尽快失效目标 A103 和唯一固定首页,后者由数据库时间武装并等待 3 秒后二次失效;缓存失效失败不回滚已提交发布结果。 #### 验证场景 @@ -4132,7 +4255,7 @@ UpdateSeckillActivityRequest { - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 -- 关联数据表:DB042、DB043 +- 关联数据表:DB042(`seckill_activities`)、DB104(`idempotency_records`) - 当前状态:待交叉评审 - 用途:商家取消 `Draft` / `Published` / `Ongoing` 状态活动;取消后入口立即失效,已存在秒杀订单按既有流程走完;本期按 C01-FR14 保留已分配库存,不回收到普通库存。 - 方法与路径:`POST /api/merchant/seckill-activities/{activityId}/cancel` @@ -4164,13 +4287,15 @@ CancelSeckillActivityRequest { | 401 | `AUTH.UNAUTHENTICATED` | 缺少有效访问令牌 | | 403 | `AUTH.FORBIDDEN` | 当前账号不是商家 | | 404 | `SECKILL.ACTIVITY_NOT_FOUND` | 活动不存在或不是当前商家创建 | -| 409 | `SECKILL.INVALID_STATUS` | 活动已 `Ended` / `Cancelled`,或数据库权威时间已达到 `endAt` | +| 409 | `SECKILL.INVALID_STATUS` | 活动已 `Ended` / `Cancelled`,或已划拨活动在锁后权威时间达到 `endAt`;过期 `Draft` 不适用此限制 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键用于不同取消请求 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 数据库暂时不可用,未形成确定取消结果 | +| 500 | `COMMON.INTERNAL_ERROR` | 未处理的服务端错误 | #### 业务规则与并发 - 完成固定校验后先读取持久化幂等结果;同键同请求重放首次取消结果,新键对 `Ended`/`Cancelled` 请求返回 409。 -- 新请求的条件更新必须同时满足 `status IN ('Draft','Published','Ongoing')` 与 `databaseNow < endAt`;权威时间已达到 `endAt` 时先形成或视为自然 `Ended`,取消返回 `409 SECKILL.INVALID_STATUS`。状态、`cancelReason`、`cancelledAt` 与确定幂等结果在同一事务提交。 +- 新请求锁定活动行后才取得一次 `decisionTime=clock_timestamp()`。`Draft` 不受 `endAt` 限制,无论计划时间是否已过去都允许创建人取消;`Published` / `Ongoing` 在 `decisionTime < endAt` 时允许取消,达到 `endAt` 时先持久化应有 `Ended` 状态并返回 `409 SECKILL.INVALID_STATUS`。状态、`cancelReason`、`cancelledAt`、存储 `result_version` 递增与确定幂等结果在同一事务提交。 - Draft 取消时没有已划拨库存;`Published`/`Ongoing` 取消后,未售的 `remainingStock` 继续隔离留在原活动且不可售,不存在 `frozenCount`,也不回到普通库存。 - 已存在秒杀订单沿用 M04 状态机;C03 超时取消时回补到原秒杀库存通道。 @@ -4181,6 +4306,7 @@ CancelSeckillActivityRequest { #### 验证场景 - `Ongoing` 活动取消 → 200,状态 `Cancelled`,抢购入口立即失效。 +- 计划时间已经过去但从未发布的 `Draft` → 200,状态 `Cancelled`,不存在划拨库存。 - Worker 尚未把状态推进为 `Ended`、但数据库权威时间已达到 `endAt` → 409 / `SECKILL.INVALID_STATUS`,不得误取消自然结束活动。 - 重复取消 → 409 / `SECKILL.INVALID_STATUS`。 - 已取消活动 → 409。 @@ -4193,7 +4319,7 @@ CancelSeckillActivityRequest { - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 -- 关联数据表:DB042、DB043 +- 关联数据表:DB042(`seckill_activities`) - 当前状态:待交叉评审 - 用途:商家分页查询本人维护的秒杀活动,支持按状态、时间窗口和关键词筛选。 - 方法与路径:`GET /api/merchant/seckill-activities` @@ -4234,7 +4360,7 @@ SeckillActivityListResponse { #### 业务规则与并发 -- 列表先按数据库权威时间幂等追赶或计算 `Published → Ongoing → Ended` 的等价有效状态;返回的 `status` 与状态筛选都使用有效状态,Worker 延迟不得让已开始活动仍按 `Published` 筛选,也不得让已结束活动仍显示 `Ongoing`。 +- 列表用同一次数据库权威 `serverTime` 只读计算 `Published → Ongoing → Ended` 的 `effectiveStatus`;不得为了追赶生命周期写库。返回的 `status` 与状态筛选都使用有效状态,Worker 延迟不得让已开始活动仍按 `Published` 筛选,也不得让已结束活动仍显示 `Ongoing`。 - 严格按 `created_by_merchant_user_id = current_user_id` 过滤;创建人是活动操作归属,不代表商品租户隔离。 - 排序默认按 `startAt desc`;相同 `startAt` 时按 `activityId` 稳定排序。 @@ -4256,7 +4382,7 @@ SeckillActivityListResponse { - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 -- 关联数据表:DB042、DB043 +- 关联数据表:DB042(`seckill_activities`);订单统计通过 Ordering 公开契约聚合 DB061/DB062 - 当前状态:待交叉评审 - 用途:商家查看本人秒杀活动详情;包含库存、已售、单用户限购、订单统计与取消原因等内部字段。 - 方法与路径:`GET /api/merchant/seckill-activities/{activityId}` @@ -4299,7 +4425,7 @@ SeckillOrderStatsResponse { #### 业务规则与并发 -- 详情先按数据库权威时间幂等追赶或计算 `Published → Ongoing → Ended` 的等价有效状态;`activity.status` 必须返回有效状态,不能暴露 Worker 延迟形成的旧生命周期值。 +- 详情用一次数据库权威 `serverTime` 只读计算 `Published → Ongoing → Ended` 的 `effectiveStatus`,不得产生状态写入;`activity.status` 必须返回有效状态,不能暴露 Worker 延迟形成的旧生命周期值。 - 严格按 `created_by_merchant_user_id = current_user_id` 过滤;非创建人访问返回 404,避免泄露活动存在性。 - 订单统计通过 Ordering 公开查询契约取得;Seckill 不读取 Ordering 内部订单表,也不维护平行订单事实。 - `activity.cancelReason` 与 `activity.cancelledAt` 仅在 `activity.status=Cancelled` 时非空;详情顶层不重复表达这两个事实。 @@ -4323,7 +4449,7 @@ SeckillOrderStatsResponse { - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 -- 关联数据表:DB042、DB043 +- 关联数据表:DB042(`seckill_activities`);商品公开展示字段通过 Catalog 公开契约取得 - 当前状态:待交叉评审 - 用途:游客和买家查看正在进行或即将开始的秒杀活动;仅返回公开字段。 - 方法与路径:`GET /api/seckill-activities` @@ -4340,6 +4466,7 @@ SeckillOrderStatsResponse { #### 成功响应 - HTTP 状态:`200 OK` +- Response Header:`Cache-Control: no-store`(活动阶段与库存按权威时间动态派生,禁止浏览器、共享代理和 Service Worker 缓存) - 响应 Schema:`PublicSeckillActivityListResponse` ```text @@ -4382,13 +4509,13 @@ PublicSeckillActivityListResponse { #### 业务规则与并发 -- 查询先按数据库权威时间对 `Published → Ongoing → Ended` 做幂等追赶或计算等价有效状态;只返回当前有效状态为 `Published` 且 `databaseNow < startAt`,或有效状态为 `Ongoing` 且 `startAt <= databaseNow < endAt` 的活动。Worker 扫描滞后不得让已结束活动继续公开。 +- 查询用同一次数据库权威 `serverTime` 只读计算 `Published → Ongoing → Ended` 的 `effectiveStatus`,不得产生状态写入;只返回有效状态为 `Published` 且 `serverTime < startAt`,或有效状态为 `Ongoing` 且 `startAt <= serverTime < endAt` 的活动。Worker 扫描滞后不得让已结束活动继续公开。 - 排序默认按 `startAt asc`(即将开始优先),相同 `startAt` 时按 `activityId` 稳定排序。 -- `remainingStock`、`soldCount` 和 `isSoldOut` 从同一已提交库存事实派生;`serverTime` 使用服务端权威 UTC 时间,`resultVersion` 随活动/库存结果单调变化,前端不得用旧结果覆盖新结果。 +- `remainingStock`、`soldCount` 和 `isSoldOut` 从同一已提交库存事实派生;`resultVersion = storedResultVersion * 8 + effectivePhaseCode`,阶段码固定为 `Draft=0、Published=1、Ongoing=2、Ended=3、Cancelled=4`。它同时覆盖真实写入和时间派生阶段,前端只接受版本不小于当前已应用版本的结果。 #### 缓存、事件或外部依赖 -- A226 每次直读 Seckill 自有 PostgreSQL 活动与库存事实,并通过 Catalog 批量公开应用契约取得商品名称与主图;秒杀列表、状态、剩余库存、已售数量和售罄结果不进入 Redis/C07。 +- A226 每次直读 Seckill 自有 PostgreSQL 活动与库存事实,并通过 Catalog 批量公开应用契约取得商品名称与主图;秒杀列表、状态、剩余库存、已售数量和售罄结果不进入 Redis/C07。Nginx/CDN/Service Worker 不缓存动态 JSON。 #### 验证场景 @@ -4404,7 +4531,7 @@ PublicSeckillActivityListResponse { - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 -- 关联数据表:DB042、DB043 +- 关联数据表:DB042(`seckill_activities`);仅有效买家凭据存在时读取 DB043(`seckill_buyer_quotas`),商品字段通过 Catalog 公开契约取得 - 当前状态:待交叉评审 - 用途:游客和买家查看秒杀活动详情;返回公开字段、商品基础信息与抢购入口。 - 方法与路径:`GET /api/seckill-activities/{activityId}` @@ -4419,6 +4546,7 @@ PublicSeckillActivityListResponse { #### 成功响应 - HTTP 状态:`200 OK` +- Response Header:`Cache-Control: private, no-store`(无论本次是否携带有效 Buyer Token 都固定,禁止共享代理和浏览器持久缓存) - 响应 Schema:`PublicSeckillActivityDetailResponse`,继承 A226 的 `PublicSeckillActivityResponse` 全部公开字段,不复用含 `orderStats`、`cancelReason` 或创建人信息的商家详情 Schema。 ```text @@ -4441,13 +4569,13 @@ PublicSeckillActivityDetailResponse extends PublicSeckillActivityResponse { #### 业务规则与并发 -- 查询先按数据库权威时间对 `Published → Ongoing → Ended` 做幂等追赶或计算等价有效状态;只有当前有效状态为 `Published` 且 `databaseNow < startAt`,或为 `Ongoing` 且 `startAt <= databaseNow < endAt` 时返回。已到 `endAt` 即使 Worker 尚未落库 `Ended` 也返回 410;草稿或已取消同样不公开。 +- 查询用同一次数据库权威 `serverTime` 只读计算 `Published → Ongoing → Ended` 的 `effectiveStatus`,不得产生状态写入;只有有效状态为 `Published` 且 `serverTime < startAt`,或为 `Ongoing` 且 `startAt <= serverTime < endAt` 时返回。已到 `endAt` 即使 Worker 尚未落库 `Ended` 也返回 410;草稿或已取消同样不公开。 - 已登录买家提示按 `(activity_id, buyer_id)` 的当前有效占用数量计算,不按订单笔数计算;待支付和已支付订单按购买数量占用,取消成功才按数量释放。 -- `remainingStock`、`soldCount`、`isSoldOut`、`serverTime` 与 `resultVersion` 均来自权威 PostgreSQL 结果,客户端按版本应用更新。 +- `remainingStock`、`soldCount`、`isSoldOut` 与 `serverTime` 均来自同一权威 PostgreSQL 快照;`resultVersion` 使用 A226 的有效版本公式,客户端按版本应用更新。 #### 缓存、事件或外部依赖 -- A227 每次直读 Seckill 自有 PostgreSQL 活动与限购事实,并通过 Catalog 公开应用契约取得商品名称与主图;不缓存活动详情或限购数量,C07 只服务普通商品固定首页和 A103 商品详情。 +- A227 每次直读 Seckill 自有 PostgreSQL 活动与限购事实,并通过 Catalog 公开应用契约取得商品名称与主图;不缓存活动详情或限购数量,C07 只服务普通商品固定首页和 A103 商品详情。固定 `private, no-store` 防止含本人限购字段的响应被代理、其他账号或游客复用,不能改成只依赖 `Vary: Authorization`。 #### 验证场景 @@ -4463,7 +4591,7 @@ PublicSeckillActivityDetailResponse extends PublicSeckillActivityResponse { - 模块 / Tag:Seckill - 需求编号:C01 - 负责人:朱惠惠 -- 关联数据表:DB043;订单事实由 Ordering 的 DB061、DB062 持有 +- 关联数据表:DB001(默认商家门)、DB003(地址版本)、DB042(`seckill_activities`)、DB043(`seckill_buyer_quotas`)、DB044(`seckill_inventory_movements`)、DB061(`orders`)、DB062(`order_items`)、DB063(`order_lifecycle_tasks`)、DB102(`outbox_messages`)、DB104(`idempotency_records`) - 当前状态:待交叉评审 - 用途:买家抢购秒杀商品;服务端以数据库条件更新扣减秒杀库存、创建订单与秒杀订单项快照;事务保证不超卖、不少卖、不产生孤立记录。 - 方法与路径:`POST /api/seckill-orders` @@ -4479,6 +4607,7 @@ PlaceSeckillOrderRequest { activityId: uuid // 必填 quantity: integer // 必填,1 ≤ quantity ≤ perBuyerLimit addressId: uuid // 必填,必须属于当前买家 + addressVersion: integer // 必填,A010/A011 返回且买家本次确认的正整数版本 } ``` @@ -4514,6 +4643,7 @@ PlaceSeckillOrderResponse { | 403 | `AUTH.FORBIDDEN` | 当前账号不是买家 | | 404 | `SECKILL.ACTIVITY_NOT_FOUND` | 活动不存在 | | 404 | `IDENTITY.ADDRESS_NOT_FOUND` | 地址不存在或不属于当前买家 | +| 409 | `IDENTITY.ADDRESS_VERSION_CONFLICT` | 地址仍属于当前买家但版本已变化;零扣库存、零占限购、零建单,刷新地址后重新确认 | | 409 | `SECKILL.NOT_STARTED` | 活动尚未开始 | | 409 | `SECKILL.ALREADY_ENDED` | 活动已结束 | | 409 | `SECKILL.ACTIVITY_CANCELLED` | 活动已取消 | @@ -4522,23 +4652,26 @@ PlaceSeckillOrderResponse { | 409 | `SECKILL.QUANTITY_EXCEEDS_LIMIT` | 单次购买数量超过限购或库存 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一幂等键被用于不同请求内容 | | 429 | `COMMON.RATE_LIMITED` | 触发限流(秒杀入口限流阈值) | -| 503 | `ORDER.DEFAULT_MERCHANT_UNAVAILABLE` | 唯一启用默认商家缺失、重复、禁用或并发失效 | -| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Redis 限流、库存通道或下游服务不可用 | +| 409 | `ORDER.DEFAULT_MERCHANT_UNAVAILABLE` | Identity 已完成权威读取并确定唯一启用默认商家缺失、重复、禁用或并发失效 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Identity、Redis 限流、库存通道或数据库依赖不可用,无法形成确定业务裁决 | | 500 | `COMMON.INTERNAL_ERROR` | 未处理的服务端错误 | #### 业务规则与并发 +- PC Web 从 A227 的“立即抢购”进入独立确认页并复用 A010/A011。唯一默认地址仅预选,无默认地址不自动选第一条;新增地址仍为非默认。买家必须明确确认地址版本与数量后才生成 Key 并调用 A228;登录恢复、新增地址或页面返回都不得自动调用本接口。 - 秒杀下单为高风险操作,必须使用 `Idempotency-Key`;缺失时返回 400 / `COMMON.VALIDATION_FAILED`。 -- 完成认证和固定请求字段校验后,先读取 PostgreSQL 幂等记录:相同 Key + 相同请求指纹直接重放首次结果,不再经过限流、时间、库存或限购校验;相同 Key + 不同指纹立即返回 `409 / IDEMPOTENCY.KEY_REUSED`。只有全新 Key 才进入后续可变业务校验。 -- 全新请求在幂等检查后才执行正式限流,并读取活动、秒杀库存和买家限购占用;随后按数据库权威时间幂等追赶或计算 `Published → Ongoing → Ended` 的等价有效状态,Worker 稍有延迟不得让已开始活动误报未开始,也不得让已结束活动继续成交。A228 不读取 Identity 地址表或默认商家,也不再次用 Catalog 当前上下架状态推翻已发布活动的独立库存资格。地址归属和唯一启用默认商家统一交给 Ordering 订单创建契约校验。 +- 完成认证和固定请求字段校验后,先读取 PostgreSQL 幂等记录;请求指纹固定为规范 UUID 表示的 `activityId + addressId` 与十进制整数 `quantity + addressVersion`,不包含 JWT、Header 顺序、客户端时间或页面库存。相同 Key + 相同指纹直接重放首次结果,不再经过限流、地址、时间、库存或限购校验;相同 Key + 不同指纹立即返回 `409 / IDEMPOTENCY.KEY_REUSED`。只有全新 Key 才进入后续可变业务校验。 +- 网关/Nginx 在请求进入应用前产生的 429 是瞬态入口保护,不可能也不得写 DB104;响应必须带 `Retry-After`,客户端沿用原 Key 重试。全新请求进入应用后才执行正式限流:应用已确定返回的 429 用独立短事务保存为该 Key 的 BusinessFailure,但此时尚未锁活动,响应不携带 `activitySnapshot`。Redis 限流状态未知或写入失败返回 503,不得伪装成正式 429 或写入 DB104。 +- 通过应用限流后进入同一 PostgreSQL 外层事务。A228 编排层不跨模块直读 Identity 表,而是先调用 Ordering/Identity 公开应用契约锁定并复核唯一启用默认商家责任门槛,再锁定本人地址并校验请求 `addressVersion`,得到可信地址快照、买家安全展示名和 `assignedMerchantUserId`;DB001/DB003 行锁及同一 `DbConnection + DbTransaction` 一直保持到秒杀库存、限购、共享订单、Outbox 和幂等结果整体提交。随后才锁定活动和买家配额,取得一次 `decisionTime=clock_timestamp()`,按该时点持久化应有的 `Published → Ongoing → Ended` 状态并递增存储版本,再裁决是否允许成交。Worker 延迟不得让已开始活动误报未开始,也不得让已结束活动继续成交。已发布活动的独立库存资格不再被 Catalog 当前上下架状态反向推翻。 - 同一数据库事务内顺序: - 1. 在同一事务中先按数据库权威时间追赶活动有效状态,再按 `Ongoing + startAt <= databaseNow < endAt + remaining >= quantity` 条件扣减秒杀库存并增加 `soldCount`;影响行数为 0 时不产生部分结果,并按追赶后的状态返回未开始、已结束、已取消或售罄。 - 2. 按 `(activityId, buyerId)` 对当前有效占用数量执行原子条件更新,保证累加后不超过 `perBuyerLimit`;普通聚合查询或 Redis 不能作为限购正确性边界。取消成功按 `orderId` 和原数量最多释放一次。 - 3. 调用 Ordering 统一订单创建契约,由 Ordering 校验 `buyerId + addressId` 并形成地址快照、解析唯一启用默认商家并写入 `assignedMerchantUserId`,随后保存 DB061/DB062 订单与订单项、成交价快照、`Seckill` 来源、`seckillActivityId` 和固定 `paymentDeadline`。Seckill 只提供活动、商品、数量、成交价和库存来源输入;活动 `createdByMerchantUserId` 只决定活动管理权,绝不参与订单分配。 - 4. 秒杀库存、限购占用、共享订单与快照、可靠待发布订单事实和确定幂等结果整体提交;任一步失败全部回滚,Seckill 不建立第二套订单状态机或事件。 + 1. 锁定默认商家 DB001 责任门槛并重检正常状态,再锁定 DB003 本人地址并同时复核 `buyerId + addressId + addressVersion`;默认商家与禁用、地址编辑/删除与下单因此各形成唯一先后结果。地址不存在或越权返回不泄露归属的 404;版本变化返回确定 409,二者都发生在活动扣减前。Identity 只返回可信事实,客户端和活动创建人都不能指定履约商家或地址内容。 + 2. 锁定活动行后取得 `decisionTime`,追赶活动持久状态,再按 `Ongoing + startAt <= decisionTime < endAt + remaining >= quantity` 条件扣减秒杀库存并增加 `soldCount`;影响行数为 0 时不产生部分结果,并按追赶后的状态返回未开始、已结束、已取消或售罄。 + 3. 按 `(activityId, buyerId)` 对当前有效占用数量执行原子条件更新,保证累加后不超过 `perBuyerLimit`;普通聚合查询或 Redis 不能作为限购正确性边界。取消成功按 `orderId` 和原数量最多释放一次。 + 4. 调用 Ordering 统一订单创建契约,使用已锁定并重检的买家、地址与默认商家可信上下文保存 DB061/DB062 订单与订单项、成交价快照、`Seckill` 来源、`seckillActivityId` 和固定 `paymentDeadline`。Seckill 只提供活动、商品、数量、成交价和库存来源输入;活动 `createdByMerchantUserId` 只决定活动管理权,绝不参与订单分配。 + 5. 秒杀库存、限购占用、共享订单与快照、可靠待发布订单事实和确定幂等结果整体提交;任一步失败全部回滚,Seckill 不建立第二套订单状态机或事件。 - 不写入普通商品库存;`products.stock` 不受秒杀下单影响。 -- 成功、未开始、已结束、已取消、售罄、超限、地址无效、默认商家不可用和已正式返回的 429 都是可重放的确定结果,必须与本次 Key 持久绑定后再返回;数据库断连、事务提交未知或依赖中断等未形成确定结果的失败不固化,客户端用原 Key 重试。 -- 成功响应以及上述确定失败的 ProblemDetails `extensions.activitySnapshot` 都返回处理后的 `remainingStock`、`soldCount`、`isSoldOut`、`serverTime`、`resultVersion`,保证同一交互刷新且旧结果不能覆盖新结果。 +- 成功、未开始、已结束、已取消、售罄、超限、地址无效、地址版本冲突、Identity 已确定的默认商家结构性不可用和**应用内**已正式返回的 429 都是可重放的确定结果,必须与本次 Key 持久绑定后再返回;边缘 429、Identity 调用失败、Redis 限流未知、数据库断连、事务提交未知或其他依赖中断等未形成确定结果的失败不固化,客户端用原 Key 重试。 +- 只有已经锁定活动并取得同一 `decisionTime` 后形成的成功、未开始、已结束、已取消、售罄、单次/累计超限结果,才在响应或 ProblemDetails `extensions.activitySnapshot` 返回处理后的 `remainingStock`、`soldCount`、`isSoldOut`、`serverTime`、`resultVersion`。地址无效、默认商家结构性不可用、边缘/应用 429 与依赖失败发生在活动读取之前,不得伪造零库存、旧版本或额外查询得到的活动快照;页面收到这类结果后可调用 A227 重取公开详情。 #### 缓存、事件或外部依赖 @@ -4556,16 +4689,17 @@ PlaceSeckillOrderResponse { - Worker 尚未落库 `Ongoing`、但数据库权威时间已进入活动窗口 → 按有效 `Ongoing` 正常参与库存竞争,不误报未开始。 - 活动发布后普通商品下架不反向改写已划拨秒杀库存资格;A228 仍按活动状态、时间、库存和限购裁决。 - 地址不属于当前买家 → 404 / `IDENTITY.ADDRESS_NOT_FOUND`,不泄露地址存在性。 -- 默认商家配置缺失、重复或禁用 → 503 / `ORDER.DEFAULT_MERCHANT_UNAVAILABLE`,不创建无人负责订单。 +- 地址在确认后被编辑、切换默认状态或发生其他版本变化 → 409 / `IDENTITY.ADDRESS_VERSION_CONFLICT`,活动库存、限购和订单均不变化;刷新 A010/A227 后由买家重新确认并使用新 Key。 +- Identity 权威读取成功且确定默认商家缺失、重复或禁用 → 409 / `ORDER.DEFAULT_MERCHANT_UNAVAILABLE`,不创建无人负责订单;Identity 本身不可用则返回 503 / `COMMON.DEPENDENCY_UNAVAILABLE`,同 Key 可安全重试。 -> 来源:[`interface-wqq.md`](interface/interface-wqq.md)。A301~A308 已统一归入 Ordering,A302/A303 已承接秒杀订单查询;DB061/DB062、跨模块应用契约和状态字段仍待评审。 +> 来源:[`interface-wqq.md`](interface/interface-wqq.md)。A301~A308 已统一归入 Ordering,A302/A303 已承接秒杀订单查询;接口与 DB061/DB062、跨模块应用契约和状态字段已经对齐,待 OpenAPI、实现、测试和交叉评审。 ### A301 提交订单 - **模块 / Tag**:Ordering - **需求编号**:F08 - **负责人**:韦乾强 -- **关联数据表**:DB061(orders)、DB062(order_items) +- **关联数据表**:DB001(默认商家门)、DB003(地址版本)、DB041、DB022、DB026、DB061~DB063、DB102、DB104 - **当前状态**:待交叉评审 - **用途**:买家选择购物车商品和收货地址提交订单,系统原子扣减库存并返回订单号 - **方法与路径**:`POST /api/orders` @@ -4589,7 +4723,9 @@ PlaceSeckillOrderResponse { ```text CreateOrderRequest { addressId: uuid - cartItemIds: uuid[] // 非空,元素唯一 + addressVersion: integer // A010/A011 返回且买家本次确认的正整数版本 + cartItemIds: uuid[] // 1~100 个,元素唯一;必须与 A208 目标一致 + checkoutRevision: string // A208 最近一次有效预览返回的 64 位小写 hex } ``` @@ -4597,12 +4733,16 @@ CreateOrderRequest { ```json { "addressId": "uuid", - "cartItemIds": ["uuid"] + "addressVersion": 3, + "cartItemIds": ["uuid"], + "checkoutRevision": "64-char-lowercase-sha256" } ``` - **校验规则**: - `addressId`:必填,UUID格式,必须属于当前买家 - - `cartItemIds`:必填,非空、元素唯一,每个元素为 UUID;全部条目必须存在、属于当前买家且在服务端当前仍为已选中 + - `addressVersion`:必填,正整数,必须等于锁内 DB003 当前版本 + - `cartItemIds`:必填,1~100 个、元素唯一,每个元素为购物车条目 UUID(不是商品 ID);全部条目必须存在、属于当前买家且在服务端当前仍为已选中 + - `checkoutRevision`:必填,64 位小写十六进制;只接受 A208 对同一规范目标集合签发的最近有效内容版本 - `Idempotency-Key` 只从 Header 读取,Body 不重复传递 #### 成功响应 @@ -4646,38 +4786,70 @@ CreateOrderResponse { | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 403 | AUTH.FORBIDDEN | 当前账号不是买家 | | 404 | IDENTITY.ADDRESS_NOT_FOUND | 地址不存在或不属于当前买家 | +| 409 | IDENTITY.ADDRESS_VERSION_CONFLICT | 地址仍属于当前买家但确认后版本已变化;本次零建单,刷新地址后重新确认 | | 404 | CART.ITEM_NOT_FOUND | 任一购物车条目不存在或不属于当前买家 | -| 409 | CART.ITEM_NOT_SELECTED | 任一请求条目在服务端当前不是已选中状态 | -| 409 | ORDER.STOCK_INSUFFICIENT | 商品库存不足 | -| 409 | ORDER.ITEM_NOT_AVAILABLE | 商品已下架或不可售 | -| 409 | ORDER.TOTAL_MUST_BE_POSITIVE | 服务端按实时成交价重算的订单总额不大于 0 | +| 409 | ORDER.CHECKOUT_CHANGED | 任一仍存在且属于本人的目标条目取消选中,或条目版本、数量、商品名称、主图 Key、单价、销售状态、库存可结算性或正金额资格相对 A208 已变化;ProblemDetails 扩展返回固定 `latestPreview`,本次零建单 | | 409 | IDEMPOTENCY.KEY_REUSED | 同一幂等键被用于不同请求内容 | -| 503 | ORDER.DEFAULT_MERCHANT_UNAVAILABLE | Identity 未能解析唯一且启用的默认商家运营账号 | -| 503 | COMMON.DEPENDENCY_UNAVAILABLE | Identity 地址、Cart 条目、Catalog 商品/库存或数据库依赖暂时不可用,无法安全创建订单 | +| 409 | ORDER.DEFAULT_MERCHANT_UNAVAILABLE | Identity 权威读取成功并确定唯一启用默认商家缺失、重复、禁用或并发失效 | +| 503 | COMMON.DEPENDENCY_UNAVAILABLE | Identity 地址或默认商家、Cart 条目、Catalog 商品/库存或数据库依赖暂时不可用,无法形成确定裁决 | + +`ORDER.CHECKOUT_CHANGED` 是结算内容/资格变化的唯一业务错误,不再同时返回 `ORDER.STOCK_INSUFFICIENT`、`ORDER.ITEM_NOT_AVAILABLE` 或 `ORDER.TOTAL_MUST_BE_POSITIVE`。其 ProblemDetails 固定包含: + +```text +extensions.latestPreview: CheckoutChangedPreview { + targetMode: "ExplicitSubset" + requestedCartItemIds: uuid[] // 与请求规范集合完全一致,按请求顺序 + items: CheckoutChangedItem[] // 与 requestedCartItemIds 一一对应 + totalAmount: number? // 全部仍可结算且总额 > 0 时才非空 + availableForCheckout: boolean + checkoutRevision: string? // availableForCheckout=true 时的新 Revision + evaluatedAt: datetime +} + +CheckoutChangedItem { + cartItemId: uuid + productId: uuid + quantity: integer + isSelected: boolean + isAvailable: boolean + unavailableReason: "NotSelected" | "ProductOffSale" | "OutOfStock" | "QuantityExceedsStock" | null + productName: string + mainImageUrl: string? + unitPrice: number + cartVersion: integer +} +``` + +- 请求中有不存在或不属于本人的条目时固定返回 404 `CART.ITEM_NOT_FOUND`,不返回 `latestPreview`,也不泄露具体是哪一种情况。 +- 地址无效、地址版本冲突、默认商家结构性不可用、幂等键冲突和依赖失败均不属于结算内容变化,不携带 `latestPreview`。 +- `latestPreview.availableForCheckout=true` 时,客户端展示新内容并要求买家重新确认,再用其新 Revision 和**新幂等键**提交;为 false 时保持购物车原样并引导修正问题,不允许提交可用子集或自动取消选择。 #### 业务规则与并发 -1. 完成身份、幂等键格式和固定请求结构校验后,先按 `(buyerId, Idempotency-Key)` 与请求指纹读取 PostgreSQL 订单创建幂等结果,再读取地址、购物车、商品、库存和默认商家等可变事实;同键同请求重放首次完整结果,同键换内容返回 409。 -2. 服务端从购物车重读每项商品 ID、数量、已选中状态和归属事实;任一条目未选中则整单拒绝。从 Catalog 重读 `OnSale`、实时价格与普通库存;不接受客户端传入数量、价格、金额或商家。 -3. 普通库存扣减使用条件更新 `WHERE stock >= quantity`,避免超卖;订单金额由服务端按已确认实时价格计算且必须大于 0,否则整单拒绝。 -4. 本期使用 UUID `orderId` 作为对外订单号,不生成暴露业务量的顺序型 `ORD...` 编号。订单项保存 `orderItemId`、商品 ID、名称、图片、成交单价和数量快照,地址保存完整下单时快照。 -5. 普通订单和秒杀订单都通过 Identity 解析同一唯一启用默认商家并写入 `assignedMerchantUserId`;未配置、重复、禁用或与下单并发失效时整单失败,不创建无人处理订单。 -6. 只有已经形成确定裁决的业务结果才可与幂等键持久绑定并稳定重放,包括成功结果、库存不足、商品不可售、总额不合法等 409 结果,以及默认商家配置确定不可用的 `ORDER.DEFAULT_MERCHANT_UNAVAILABLE`。`COMMON.DEPENDENCY_UNAVAILABLE`、数据库连接中断、事务提交结果未知等未形成确定结果的失败一律不固化;客户端可使用同一键安全重试。 -7. 正式环境的支付截止时间固定为订单 `createdAt + 30 分钟`;演示环境只能通过可追踪配置缩短等待,配置值在订单创建时固化为 `paymentDeadline`,不得追溯修改历史订单或把演示值写成正式规则。 +1. 完成身份、幂等键格式和固定请求结构校验后,先按 `(buyerId, Idempotency-Key)` 与规范 `addressId + addressVersion + cartItemIds + checkoutRevision` 指纹读取 PostgreSQL 订单创建幂等结果,再读取地址、购物车、商品、库存和默认商家等可变事实;同键同请求重放首次完整结果,同键换内容返回 409。 +2. 结算页地址列表复用 A010;唯一默认地址预选并突出展示,没有默认地址时保持未选择且不得自动取第一条。无地址或主动新增时在同一结算上下文调用 A011,成功后保留 `cartItemIds + checkoutRevision`、刷新列表并由买家显式选择。A011 仍固定创建非默认地址且绝不自动调用 A301。 +3. 全局锁序固定为 DB104 订单幂等范围 → Identity 唯一默认商家 DB001 门行 → 本人 DB003 地址行 → 按 ID 排序的 DB041 购物车行 → 按商品 ID 排序的 DB022 Catalog 行 → 订单、流水、任务、Outbox 与幂等结果。Identity 必须加入 Ordering 已开启的同一连接/事务,先重检商家正常,再以 `buyerId + addressId + addressVersion` 锁定地址并返回可信快照;地址不存在/越权返回 404,版本变化返回确定 409,二者都发生在购物车或库存变化前。 +4. 服务端按上述稳定顺序锁定请求中的本人购物车条目和 Catalog 商品/普通库存,从购物车重读商品 ID、数量、版本、已选中状态和归属事实,从 Catalog 重读 `OnSale`、名称、主图 Key、实时价格与普通库存;按 A208 同一规范算法重算 `checkoutRevision`。任一条目未选中、不可结算、库存不足、总额不为正或版本/内容不等时统一返回 `ORDER.CHECKOUT_CHANGED` 及上述最新结构化预览,整单零写入;不接受客户端传入数量、价格、金额或商家。 +5. Revision 一致且锁内库存仍足够后,仍以 `WHERE stock >= quantity` 条件扣减作为数据库防御性不变量;由于同一商品行已被当前事务锁定,正常实现不应在重算与扣减之间产生第二种“库存竞争失败”语义。若条件更新影响 0 行,按不变量异常回滚本次业务副作用、重新读取后返回 `ORDER.CHECKOUT_CHANGED`,并记录可追踪告警,绝不能改成部分建单。Revision 不锁价、不预占库存,也不替代最终条件更新。 +6. 本期使用 UUID `orderId` 作为对外订单号,不生成暴露业务量的顺序型 `ORD...` 编号。订单项保存 `orderItemId`、商品 ID、名称、图片、成交单价和数量快照;地址快照只能来自步骤 3 锁定且版本一致的可信事实,不能静默采用确认后编辑的新内容。 +7. 普通订单和秒杀订单都通过 Identity 解析并锁定同一唯一启用默认商家,重检后写入 `assignedMerchantUserId`;权威读取成功但未配置、重复、禁用或与下单并发失效时返回确定的 409,整单失败且不创建无人处理订单。Identity 无法完成权威读取时返回 503,不能把“未知”固化成商家配置错误。 +8. 只有已经形成确定裁决的业务结果才可与幂等键持久绑定并稳定重放,包括成功结果、`ORDER.CHECKOUT_CHANGED`、`IDENTITY.ADDRESS_VERSION_CONFLICT` 和 Identity 已确定的 `ORDER.DEFAULT_MERCHANT_UNAVAILABLE`。`COMMON.DEPENDENCY_UNAVAILABLE`、数据库连接中断、事务提交结果未知等未形成确定结果的失败一律不固化;客户端可使用同一键安全重试。 +9. 正式环境的支付截止时间固定为订单 `createdAt + 30 分钟`;演示环境只能通过可追踪配置缩短等待,配置值在订单创建时固化为 `paymentDeadline`,不得追溯修改历史订单或把演示值写成正式规则。 #### 缓存、事件或外部依赖 -- 普通库存扣减、订单/订单项、地址快照、默认商家归属、固定 `paymentDeadline`、本次购物车条目清理、可靠订单已创建待发布事实和确定幂等结果在同一 PostgreSQL 事务提交;任一步失败整体回滚,购物车原样保留。 +- Revision 变化的失败响应携带 `latestPreview`,但不改写购物车、库存或订单;买家显式确认新预览后必须使用新 Revision 和新 `Idempotency-Key` 提交。普通库存扣减、订单/订单项、地址快照、默认商家归属、固定 `paymentDeadline`、本次购物车条目清理、可靠订单已创建待发布事实和确定幂等结果在同一 PostgreSQL 事务提交;任一步失败整体回滚,购物车原样保留。 - RabbitMQ 只在事务提交后传输待发布事实;不能用“已发消息”代替来源事务中的可靠记录。 -- 普通库存扣减提交后触发目标 A103 与唯一固定首页立即/3 秒二次失效;缓存失败不回滚订单。秒杀订单扣减独立库存,不触发 C07。 +- 普通库存扣减事务同时建立 C07 Immediate 与初始未武装 Delayed;提交可见后前者尽快失效目标 A103 与唯一固定首页,后者由数据库时间武装并等待 3 秒二次失效;缓存失败不回滚订单。秒杀订单扣减独立库存,不触发 C07。 #### 验证场景 1. 正常提交订单:返回201,订单号 2. 库存不足:返回409,订单未创建 3. 地址无效或任一购物车条目越权:返回404且整单不创建 -4. 任一条目已取消选中,或服务端重算总额不大于 0:返回对应 409,整单不创建 -5. 幂等键重复:返回原订单号,不重复扣库存 +4. 另一标签页修改数量或商品改价后提交旧 Revision:409 + `ORDER.CHECKOUT_CHANGED` 与最新预览;零建单、零扣库存 +5. 任一条目已取消选中,或服务端重算总额不大于 0:返回对应 409,整单不创建 +6. 幂等键重复:返回原订单号,不重复扣库存 --- @@ -4686,9 +4858,9 @@ CreateOrderResponse { - **模块 / Tag**:Ordering - **需求编号**:F09 - **负责人**:韦乾强 -- **关联数据表**:DB061(orders)、DB062(order_items) +- **关联数据表**:DB061 `orders`、DB062 `order_items`;AfterSales DB086、Review DB024 仅经公开批量应用契约读取 - **当前状态**:待交叉评审 -- **用途**:买家分页查询自己的普通或秒杀订单,支持按状态、订单来源和秒杀活动筛选 +- **用途**:买家分页查询自己的普通或秒杀订单,在五种核心状态之外查看履约、售后和评价三个派生摘要 - **方法与路径**:`GET /api/orders` - **operationId**:`Ordering_ListOrders` - **请求Schema**:无 @@ -4723,18 +4895,34 @@ CreateOrderResponse { "items": [ { "orderId": "uuid", - "orderType": "Seckill", - "seckillActivityId": "uuid", - "seckillActivityName": "暑期秒杀", - "status": "PendingPayment", + "orderType": "Normal", + "seckillActivityId": null, + "seckillActivityName": null, + "status": "Paid", "totalAmount": 299.00, "createdAt": "2026-07-24T10:00:00Z", "paymentDeadline": "2026-07-24T10:30:00Z", - "canPay": true, - "availableActions": ["pay", "cancel"], + "fulfillmentSummary": { + "state": "PartiallyRefundedReadyToShip", + "remainingFulfillableQuantity": 1, + "canShip": true + }, + "afterSalesSummary": { + "state": "PartiallyRefunded", + "processingQuantity": 0, + "refundedQuantity": 1 + }, + "reviewSummary": { + "state": "NotApplicable", + "reviewedItemCount": 0, + "reviewableItemCount": 0 + }, + "availableActions": [], "itemSummary": "商品A x1,商品B x2" } ], + "compositionStatus": "Complete", + "unavailableSummaries": [], "page": 1, "pageSize": 10, "total": 25, @@ -4743,6 +4931,39 @@ CreateOrderResponse { } ``` +本接口以及 A303、A305、A306 复用以下只读 Schema;这些字段均由服务端派生,任何命令请求都不得接收客户端回传: + +```text +FulfillmentSummary { + state: "AwaitingPayment" + | "ReadyToShip" + | "BlockedByAfterSales" + | "PartiallyRefundedReadyToShip" + | "FullyRefunded" + | "Shipped" + | "Completed" + | "Cancelled" + remainingFulfillableQuantity: integer + canShip: boolean +} + +AfterSalesSummary { + state: "NotApplicable" | "None" | "Processing" | "PartiallyRefunded" | "FullyRefunded" + processingQuantity: integer + refundedQuantity: integer +} + +ReviewSummary { + state: "NotApplicable" | "Reviewable" | "PartiallyReviewed" | "Reviewed" + reviewedItemCount: integer + reviewableItemCount: integer +} +``` + +`fulfillmentSummary`、`afterSalesSummary`、`reviewSummary` 均为可空字段,但只允许在对应摘要不可安全取得、且响应同时标记降级时为 `null`。正常完整响应不得省略。 + +`remainingFulfillableQuantity` 只表示本期仍可由商家一次性发出的数量:核心状态为 `Paid` 时等于购买总量减已退款总量,其他四种核心状态固定为 0;`Shipped/Completed` 的真实历史发货量由订单项 `shippedQuantity` 表达,不能拿当前未退款量冒充待发量。`ReadyToShip` 仅适用于未退款的 `Paid`,已部分退款但仍可发货必须使用 `PartiallyRefundedReadyToShip`。 + #### 失败响应 | HTTP状态 | 业务错误码 | 触发条件 | @@ -4752,24 +4973,33 @@ CreateOrderResponse { | 400 | `COMMON.INVALID_UUID` | `seckillActivityId` 不是标准 UUID | | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 403 | AUTH.FORBIDDEN | 当前账号不是买家 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Ordering 核心订单、订单项或归属事实无法安全查询 | #### 业务规则与并发 1. 订单按 `createdAt desc, orderId desc` 稳定排序。 2. `itemSummary` 最多展示 3 个商品名称,多的显示“+X件”。 3. A229 已取消;秒杀订单列表由本接口通过 `orderType=Seckill` 或 `seckillActivityId` 查询,不建立第二套订单查询事实。 -4. `canPay` 与 `availableActions` 由服务端同时按 `status=PendingPayment` 和 `databaseNow < paymentDeadline` 派生;已经过期但 C03 尚未提交取消的待支付订单不得返回支付入口。 +4. `availableActions` 只包含订单级动作:`pay`、`cancel`、`confirmReceipt`。`PendingPayment` 在数据库权威时间早于截止时间时返回 `pay/cancel`,到期后只允许显示取消处理中或 `cancel`;`Shipped` 返回 `confirmReceipt`;评价和售后不得放入列表订单级动作。 +5. 三个摘要严格按 M04 第 5.1 节的固定优先级、数量公式和订单项计数派生,不新增核心状态,也不新增摘要筛选条件。`canShip` 只是读取提示,不能代替 A307 写入重检。 +6. Ordering 先读取当前页最多 50 张订单与全部订单项,再分别调用一次 AfterSales `Aggregate` 批量能力和一次 Review 批量评价事实能力;不得逐订单或逐项调用 HTTP,也不得直接读取其他模块内部表。 +7. 批量结果必须覆盖每个请求订单和订单项。少返回 Key、数量为负、处理中数量与已退款数量之和超过购买数量等均按对应摘要不可用处理,禁止截断、补零或返回虚假正常值。 +8. AfterSales 或 Review 摘要失败时仍可返回 `200` 核心订单:`compositionStatus="Degraded"`,`unavailableSummaries` 精确列出 `"AfterSales"` / `"Review"`,对应摘要为 `null`。AfterSales 未知时履约摘要也为 `null`;Review 未知时不返回任何评价结论。 +9. 降级响应必须收紧动作:AfterSales 未知不得开放售后或发货,Review 未知不得显示“未评价”或评价入口。A304、A308 等写接口仍按提交时最新事实重检。 #### 缓存、事件或外部依赖 - 订单列表不进入 C07 或其他业务缓存,每次按当前买家和 PostgreSQL 已提交订单事实查询。 +- Ordering 在任何组合查询前开启短生命周期 `REPEATABLE READ READ ONLY` PostgreSQL 事务,Ordering、AfterSales、Review 的公开读取能力必须复用同一 `DbConnection + DbTransaction`;默认 `READ COMMITTED` 的逐语句快照不满足“一次响应同一快照”。共享事务不授权跨模块访问 DbContext、仓储或内部表。 #### 验证场景 -1. 正常查询:返回订单列表 +1. 正常查询:返回订单列表及三个摘要,覆盖正常待发货、售后阻断、部分退款待发、全部退款、待评价、部分已评和全部已评 2. 分页参数非法:返回400 3. 状态筛选非法:返回 400 字段级错误,不按全部状态查询 4. 无订单:返回空列表 +5. AfterSales 或 Review 批量查询失败/缺 Key:返回 200 降级,不伪造正常摘要或相关动作 +6. 当前页 50 张订单:AfterSales 与 Review 各只有一次批量读取,不出现 N+1 --- @@ -4778,7 +5008,7 @@ CreateOrderResponse { - **模块 / Tag**:Ordering - **需求编号**:F09 - **负责人**:韦乾强 -- **关联数据表**:DB061(orders)、DB062(order_items) +- **关联数据表**:DB061 `orders`、DB062 `order_items`;Payment DB085、AfterSales DB086、Review DB024 仅经公开应用契约读取 - **当前状态**:待交叉评审 - **用途**:买家查看单个订单的完整详情 - **方法与路径**:`GET /api/orders/{orderId}` @@ -4807,28 +5037,45 @@ CreateOrderResponse { "message": "ok", "data": { "orderId": "uuid", - "orderType": "Seckill", - "seckill": { - "activityId": "uuid", - "activityName": "暑期秒杀", - "originalUnitPrice": 399.00, - "seckillUnitPrice": 199.00 - }, - "status": "PendingPayment", - "totalAmount": 199.00, + "orderType": "Normal", + "seckill": null, + "status": "Completed", + "totalAmount": 299.00, "createdAt": "2026-07-24T10:00:00Z", "paymentDeadline": "2026-07-24T10:30:00Z", - "canPay": true, - "payment": null, + "payment": { + "paymentId": "uuid", + "source": "Wallet", + "amount": 299.00, + "paidAt": "2026-07-24T10:05:00Z" + }, "cancellation": null, - "completion": null, + "completion": { + "completedAt": "2026-07-25T10:05:00Z", + "completedBy": "BuyerConfirmed" + }, + "fulfillmentSummary": { + "state": "Completed", + "remainingFulfillableQuantity": 0, + "canShip": false + }, + "afterSalesSummary": { + "state": "None", + "processingQuantity": 0, + "refundedQuantity": 0 + }, + "reviewSummary": { + "state": "Reviewable", + "reviewedItemCount": 0, + "reviewableItemCount": 1 + }, "addressSnapshot": { - "receiverName": "张三", + "recipientName": "张三", "phone": "138****8888", "province": "广东省", "city": "深圳市", "district": "南山区", - "detailAddress": "科技园路1号" + "detail": "科技园路1号" }, "items": [ { @@ -4836,21 +5083,33 @@ CreateOrderResponse { "productId": "uuid", "productName": "商品A", "imageUrl": "https://...", - "unitPrice": 199.00, + "unitPrice": 299.00, "quantity": 1, - "shippedQuantity": 0, + "shippedQuantity": 1, "afterSales": { + "state": "None", "processingQuantity": 0, "refundedQuantity": 0, - "remainingApplicableQuantity": 1 + "remainingEligibleQuantity": 1, + "eligible": true, + "reason": null, + "availableTypes": ["RefundOnly", "ReturnAndRefund"], + "deadlineAt": "2026-08-01T10:05:00Z" }, - "subtotal": 199.00 + "reviewState": "Reviewable", + "availableActions": ["requestAfterSales", "review"], + "subtotal": 299.00 } ], "statusHistory": [ - {"status": "PendingPayment", "time": "2026-07-24T10:00:00Z"} + {"status": "PendingPayment", "time": "2026-07-24T10:00:00Z"}, + {"status": "Paid", "time": "2026-07-24T10:05:00Z"}, + {"status": "Shipped", "time": "2026-07-24T11:00:00Z"}, + {"status": "Completed", "time": "2026-07-25T10:05:00Z"} ], - "availableActions": ["pay", "cancel"] + "availableActions": [], + "compositionStatus": "Complete", + "unavailableSummaries": [] } } ``` @@ -4862,7 +5121,7 @@ CreateOrderResponse { | 400 | `COMMON.INVALID_UUID` | `orderId` 不是标准 UUID | | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 404 | ORDER.NOT_FOUND | 订单不存在或不属于当前买家 | -| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Payment 或 AfterSales 已提交摘要暂时不可用,无法返回完整订单详情 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Ordering 核心详情,或当前订单状态所必需的已提交 Payment 成功事实无法安全取得 | #### 业务规则与并发 @@ -4870,19 +5129,26 @@ CreateOrderResponse { 2. 普通订单 `orderType=Normal` 且 `seckill=null`;秒杀订单返回活动 ID、活动名称、原价和秒杀价快照,承接已取消的 A230。 3. 支付成功时 `payment` 返回最小摘要:`paymentId`、`source`(`Wallet`/`SimulatedChannel`)、`amount`、`paidAt`;没有确定支付事实时为 `null`,不能仅按订单状态猜测。 4. `cancellation` 在已取消时返回 `cancelledAt` 与 `reason`(`BuyerRequested`/`PaymentExpired`);`completion` 在已完成时返回唯一 `completedAt` 与 `completedBy`(`BuyerConfirmed`/`AutoCompleted`)。 -5. 每个订单项返回购买数量、实际已发货数量,以及处理中售后数量、已退款数量和剩余可申请数量;售后摘要由 M10 公开契约派生,不修改订单核心状态。 -6. `canPay` 与 `availableActions` 按最新订单状态、`paymentDeadline`、评价资格和售后资格派生;过期但尚未被 C03 取消的 `PendingPayment` 订单也必须 `canPay=false`。 -7. `statusHistory` 覆盖创建、支付、发货、完成或取消的真实已提交时间点,不生成计划时间。 +5. 顶层必须返回 A302 已定义的三个订单级摘要。每个订单项的 `afterSales` 固定包含状态、处理中数量、已退款数量、剩余可申请数量、资格、原因、类型和截止时间;`remainingEligibleQuantity = quantity - processingQuantity - refundedQuantity`。 +6. 订单项 `afterSales.state` 使用 `NotApplicable/None/Processing/PartiallyRefunded/FullyRefunded`;不可申请原因至少使用 `OrderStatusNotEligible`、`AfterSalesWindowExpired`、`NoRemainingQuantity`。资格为真时 `reason=null`,可申请类型只允许 `RefundOnly/ReturnAndRefund`。 +7. 订单项 `reviewState` 只允许 `NotApplicable/Reviewable/Reviewed`。评价或售后动作必须放在订单项 `availableActions` 并绑定当前 `orderItemId`;顶层 `availableActions` 只允许 `pay/cancel/confirmReceipt`。 +8. 过期但 C03 尚未取消成功的 `PendingPayment` 不返回 `pay`;`Shipped` 才返回 `confirmReceipt`。A412/A142 提交时仍完整重检,查询资格不是授权承诺。 +9. AfterSales 或 Review 摘要不可用时返回 `200 + compositionStatus="Degraded"`:对应订单级摘要和订单项字段为 `null`,`unavailableSummaries` 列出维度,并移除 `requestAfterSales` / `review`。AfterSales 未知时履约摘要也为 `null`。 +10. 批量结果缺少任何订单项、数量为负、处理中加已退款超过购买数量,或 `Completed` 订单没有订单项时按摘要降级并记录可追踪错误,不得归零、截断或伪造资格。 +11. `statusHistory` 覆盖创建、支付、发货、完成或取消的真实已提交时间点,不生成计划时间。 #### 缓存、事件或外部依赖 -- 订单详情不缓存;支付与售后摘要通过公开应用契约读取已提交事实,不直接访问模块内部表。 +- 订单详情不缓存;Payment、AfterSales、Review 通过公开应用契约读取已提交事实,不直接访问模块内部表。AfterSales 使用一次 `ItemDetail` 批量读取,Review 一次覆盖本详情全部订单项,不循环调用 A143。 #### 验证场景 1. 正常查询:返回完整订单详情 2. 订单不存在:返回404 3. 跨用户访问:返回404,不泄露订单存在性 +4. 多订单项中一部分已评价:订单级 `PartiallyReviewed`,每项 `reviewState` 与动作正确 +5. AfterSales/Review 故障或缺 Key:核心详情 200 降级,未知维度为 `null` 且相关动作关闭 +6. 处理中、部分退款、全部退款数量与 `remainingEligibleQuantity` 按固定公式一致 --- @@ -4891,7 +5157,7 @@ CreateOrderResponse { - **模块 / Tag**:Ordering - **需求编号**:F09 - **负责人**:韦乾强 -- **关联数据表**:DB061(orders)、DB062(order_items) +- **关联数据表**:DB061 `orders`、DB062 `order_items`、DB063 `order_lifecycle_tasks`、原普通库存 DB022/DB026,或秒杀库存与限购 DB042/DB043/DB044、DB102 `outbox_messages` - **当前状态**:待交叉评审 - **用途**:买家取消自己待支付的订单,触发库存回补 - **方法与路径**:`POST /api/orders/{orderId}/cancel` @@ -4947,7 +5213,7 @@ CreateOrderResponse { #### 缓存、事件或外部依赖 - 来源事务保存可靠待发布取消事实,事务提交后才由 RabbitMQ 传输;库存回补根据订单快照的原库存通道调用 Catalog 或 Seckill 公开应用契约,不依赖客户端输入。 -- 普通库存回补提交后立即/3 秒二次失效目标 A103 与固定首页;秒杀原活动回补不改变普通库存,不触发 C07。缓存失败不回滚取消。 +- 普通库存回补事务同时建立 C07 Immediate 与初始未武装 Delayed;提交可见后前者尽快失效目标 A103 与固定首页,后者由数据库时间武装并等待 3 秒二次失效;秒杀原活动回补不改变普通库存,不触发 C07。缓存失败不回滚取消。 #### 验证场景 @@ -4963,9 +5229,9 @@ CreateOrderResponse { - **模块 / Tag**:Ordering - **需求编号**:F12 - **负责人**:韦乾强 -- **关联数据表**:DB061(orders)、DB062(order_items) +- **关联数据表**:DB061 `orders`、DB062 `order_items`;AfterSales DB086 仅经公开批量应用契约读取 - **当前状态**:待交叉评审 -- **用途**:商家分页查询分配给当前运营账号的订单,支持按状态筛选 +- **用途**:商家分页查询分配给当前运营账号的订单,按五种核心状态筛选,并查看统一履约/售后摘要 - **方法与路径**:`GET /api/merchant/orders` - **operationId**:`Ordering_ListMerchantOrders` - **请求Schema**:无 @@ -5005,9 +5271,21 @@ CreateOrderResponse { "shippedAt": null, "completedAt": null, "cancelledAt": null, - "itemCount": 2 + "itemCount": 2, + "fulfillmentSummary": { + "state": "ReadyToShip", + "remainingFulfillableQuantity": 2, + "canShip": true + }, + "afterSalesSummary": { + "state": "None", + "processingQuantity": 0, + "refundedQuantity": 0 + } } ], + "compositionStatus": "Complete", + "unavailableSummaries": [], "page": 1, "pageSize": 10, "total": 15, @@ -5021,24 +5299,31 @@ CreateOrderResponse { | HTTP状态 | 业务错误码 | 触发条件 | |---|---|---| | 400 | ORDER.INVALID_PAGE_PARAM | 分页参数非法 | +| 400 | `COMMON.VALIDATION_FAILED` | `status` 不是五种受控订单状态 | | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 403 | AUTH.FORBIDDEN | 当前账号不是商家 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Ordering 核心订单、订单项或归属事实无法安全查询 | #### 业务规则与并发 1. 只返回 `assignedMerchantUserId = currentUserId` 的订单;本项目不按商户租户拆分商品或结算。 2. 排序固定为 `createdAt desc, orderId desc`;所有筛选都追加相同稳定次序。 3. 列表只返回脱敏的最小买家摘要,不返回收货地址、完整手机号或其他私人资料;履约所需地址只在 A306 当前授权详情中返回。 -4. `Paid` 只表示订单可以进入履约复核;是否实际可发货仍须结合 M10 处理中售后、已退款数量和剩余可履约数量判断。 +4. 每个列表项复用 A302 定义的 `FulfillmentSummary`、`AfterSalesSummary`;`Paid` 只表示进入履约复核,只有 `fulfillmentSummary.canShip=true` 才能提示当前可发货。 +5. 状态筛选仍只有五种核心订单状态,不按派生摘要新增接口筛选。列表不提供快捷发货命令;商家进入 A306 后再确认。 +6. 当前页最多 50 张订单,只调用一次 AfterSales `Aggregate` 批量能力;每个请求订单和订单项都必须有结果,缺 Key 不能按零售后处理。 +7. AfterSales 不可用或不变量破坏时返回 `200 + compositionStatus="Degraded"`,`unavailableSummaries=["AfterSales"]`,两个摘要均为 `null`;不得返回 `canShip=true` 或“无售后”。 #### 缓存、事件或外部依赖 -无 +- 不缓存。Ordering 与 AfterSales 通过同一个短生命周期 `REPEATABLE READ READ ONLY` PostgreSQL 事务、同一 `DbConnection + DbTransaction` 和公开应用契约组合,不直接访问 AfterSales 内部表。 #### 验证场景 1. 正常查询:返回订单列表 2. 无订单:返回空列表 +3. 同为 `Paid` 的正常待发、售后阻断、部分退款待发和全部退款订单返回不同摘要 +4. AfterSales 故障或缺 Key:返回核心列表的明确降级结果,不误显示可发货 --- @@ -5047,7 +5332,7 @@ CreateOrderResponse { - **模块 / Tag**:Ordering - **需求编号**:F12 - **负责人**:韦乾强 -- **关联数据表**:DB061(orders)、DB062(order_items) +- **关联数据表**:DB061 `orders`、DB062 `order_items`;Payment DB085、AfterSales DB086 仅经公开应用契约读取 - **当前状态**:待交叉评审 - **用途**:商家查看分配给当前运营账号的订单详情 - **方法与路径**:`GET /api/merchant/orders/{orderId}` @@ -5086,13 +5371,23 @@ CreateOrderResponse { "amount": 299.00, "paidAt": "2026-07-24T10:05:00Z" }, + "fulfillmentSummary": { + "state": "ReadyToShip", + "remainingFulfillableQuantity": 1, + "canShip": true + }, + "afterSalesSummary": { + "state": "None", + "processingQuantity": 0, + "refundedQuantity": 0 + }, "addressSnapshot": { - "receiverName": "张三", + "recipientName": "张三", "phone": "13800008888", "province": "广东省", "city": "深圳市", "district": "南山区", - "detailAddress": "科技园路1号" + "detail": "科技园路1号" }, "items": [ { @@ -5109,14 +5404,13 @@ CreateOrderResponse { "subtotal": 199.00 } ], - "canShip": true, - "hasBlockingAfterSales": false, - "blockReason": null, "statusHistory": [ { "status": "PendingPayment", "time": "2026-07-24T10:00:00Z" }, { "status": "Paid", "time": "2026-07-24T10:05:00Z" } ], - "availableActions": ["ship"] + "availableActions": ["ship"], + "compositionStatus": "Complete", + "unavailableSummaries": [] } } ``` @@ -5128,18 +5422,21 @@ CreateOrderResponse { | 400 | `COMMON.INVALID_UUID` | `orderId` 不是标准 UUID | | 401 | AUTH.UNAUTHENTICATED | 缺少有效访问令牌 | | 404 | ORDER.NOT_FOUND | 订单不存在或未分配给当前 `assignedMerchantUserId` | -| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | AfterSales 履约阻断或退款数量快照暂时不可用,无法返回完整详情 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Ordering 核心详情,或当前订单状态所必需的已提交 Payment 成功事实无法安全取得 | #### 业务规则与并发 1. 只返回分配给当前运营账号的整单及其订单项,不把一个订单拆成多商户子订单;不属于当前商家与不存在统一 404。 -2. 每个订单项返回购买数量、处理中售后数量、已退款数量、剩余可履约数量与实际已发货数量;这些数量来自同一 M10 履约快照,服务端保证口径不互相重叠。 -3. 使用明确 `canShip`、`hasBlockingAfterSales`、`blockReason` 派生发货入口;存在处理中售后或无剩余可履约数量时不返回 `ship`。 -4. 返回最小支付摘要和完整已提交状态时间线;地址详情只向当前 `assignedMerchantUserId` 返回实际履约所需快照,列表接口不得返回。 +2. 订单级复用 A302 定义的 `FulfillmentSummary`、`AfterSalesSummary`。每个订单项返回购买数量、处理中售后数量、已退款数量、剩余可履约数量与实际已发货数量;前三个 M10 数量来自同一次 `ItemDetail` 批量快照。 +3. 订单项 `remainingFulfillableQuantity` 只在核心状态为 `Paid` 时等于 `quantity - refundedQuantity`;`PendingPayment/Cancelled/Shipped/Completed` 固定为 0。处理中数量不从 `Paid` 的最终剩余可履约数量扣除,但任一处理中数量会让订单级 `BlockedByAfterSales` 且 `canShip=false`;历史实际发货量始终读取 `shippedQuantity`。 +4. 只有 `fulfillmentSummary.canShip=true` 时顶层 `availableActions` 才包含 `ship`。不再返回重复且易漂移的顶层 `canShip`、`hasBlockingAfterSales` 或自由文本 `blockReason`。 +5. `shippedQuantity` 是发货时冻结的历史事实;后续退款不得递减或重算。未发货时为 0,发货后与当时服务端确定的实际数量一致。 +6. AfterSales 不可用、缺 Key、数量为负或处理中加已退款超过购买数量时,返回 `200 + compositionStatus="Degraded"`:两个订单级摘要以及每项 `processingAfterSalesQuantity/refundedQuantity/remainingFulfillableQuantity` 为 `null`,`shippedQuantity` 仍返回 Ordering 历史事实,`availableActions` 为空。 +7. 返回最小支付摘要和完整已提交状态时间线;地址详情只向当前 `assignedMerchantUserId` 返回实际履约所需快照,列表接口不得返回。 #### 缓存、事件或外部依赖 -- 通过 AfterSales 公开应用契约查询当前订单的履约阻断状态和各订单项累计已退款数量;不直接读取售后内部表。 +- 通过 AfterSales `ItemDetail` 批量公开应用契约一次查询当前订单全部订单项的履约阻断、已退款与资格事实;结果必须覆盖全部 Key,不直接读取售后内部表。 #### 验证场景 @@ -5147,6 +5444,8 @@ CreateOrderResponse { 2. 订单不存在:返回404 3. 跨商家访问:返回404 4. 存在处理中售后或部分退款:返回可理解的履约状态和准确剩余数量,不错误展示发货入口 +5. 全部退款:`FullyRefunded + canShip=false`,不返回 `ship` +6. AfterSales 故障/缺 Key/数量不变量破坏:核心详情 200 降级,派生字段为 `null` 且不返回 `ship` --- @@ -5155,7 +5454,7 @@ CreateOrderResponse { - **模块 / Tag**:Ordering - **需求编号**:F12 - **负责人**:韦乾强 -- **关联数据表**:DB061(orders)、DB062(order_items) +- **关联数据表**:DB061~DB063、DB102 `outbox_messages`、DB104 `idempotency_records`;AfterSales 履约事实只经公开应用契约读取 - **当前状态**:待交叉评审 - **用途**:商家对已支付订单执行发货操作 - **方法与路径**:`POST /api/merchant/orders/{orderId}/ship` @@ -5249,7 +5548,7 @@ CreateOrderResponse { - **模块 / Tag**:Ordering - **需求编号**:F09 - **负责人**:韦乾强 -- **关联数据表**:DB061(orders) +- **关联数据表**:DB061 `orders`、DB102 `outbox_messages` - **当前状态**:待交叉评审 - **用途**:买家确认已收到商品,将订单状态从 `Shipped` 变更为 `Completed` - **方法与路径**:`POST /api/orders/{orderId}/confirm-receipt` @@ -5315,20 +5614,21 @@ CreateOrderResponse { --- -> 来源:[`interface-zhy.md`](interface/interface-zhy.md)。A418 已取消并入 A414,A431 已取消且退款改用 Payment 应用契约,A434 已补齐退货信息;资金、售后状态和对账字段仍待数据库与 OpenAPI 评审。 +> 来源:[`interface-zhy.md`](interface/interface-zhy.md)。A418 已取消并入 A414,A431 已取消且退款改用 Payment 应用契约,A434 已补齐退货信息;资金、售后状态、退款恢复和对账字段已反查统一数据库设计,待 OpenAPI、实现与交叉评审。 ### A401 查询钱包余额 - **模块 / Tag**:Payment - **需求编号**:M05-01-FR01 - **负责人**:张海洋 -- **关联数据表**:DB081(待评审)— `wallets` +- **关联数据表**:DB081(`wallet_accounts`) - **当前状态**:待交叉评审 - **用途**:查询当前买家钱包余额 - **方法与路径**:`GET /api/payment/wallet` - **operationId**:`Payment_GetWalletBalance` - **请求 Schema**:(无) - **响应 Schema**:`WalletBalanceResponse` +- **字段约定**:`balance: decimal`、`currency: "CNY"`、`updatedAt: datetime?`;钱包记录尚不存在时返回 `balance=0`、`updatedAt=null`,GET 不得为补时间戳创建钱包。 - **身份与 Policy**:JWT Bearer + `BuyerOnly` - **资源归属**:当前 buyerId 钱包 - **幂等要求**:GET 天然幂等 @@ -5396,7 +5696,7 @@ CreateOrderResponse { - **模块 / Tag**:Payment - **需求编号**:M05-01-FR02 / FR03 - **负责人**:张海洋 -- **关联数据表**:DB082(待评审)— `wallet_topups`、DB083(待评审)— `wallet_ledgers` +- **关联数据表**:DB081 `wallet_accounts`、DB082 `wallet_topups`、DB083 `wallet_transactions`、DB096 `financial_posting_sequences`、DB104 `idempotency_records` - **当前状态**:待交叉评审 - **用途**:买家向本人钱包充值(使用模拟支付通道) - **方法与路径**:`POST /api/payment/wallet/topups` @@ -5457,10 +5757,11 @@ CreateOrderResponse { #### 业务规则与并发 - 钱包余额增加、充值记录、钱包流水和首次幂等结果在同一事务提交(按 PAY-R06)。 -- 完成身份、幂等键格式和固定请求结构校验后先查询 PostgreSQL 幂等结果;命中同指纹直接重放首次完整结果,同键换金额返回 409 +- 完成身份、幂等键格式和固定请求结构校验后先查询 PostgreSQL 幂等结果;命中同指纹直接重放首次完整结果,同键换金额返回 409。 - 本期不建立 `Pending` / `Failed` / `Succeeded` 充值状态机;失败请求不生成充值记录。金额非法等确定失败可持久化后重放,数据库未知失败不固化。 - 单笔上限 10000.00 元(业务规则 PAY-R16,zhy 7-23 提交 db840e4 强调) -- 充值成功后才更新余额;不为重试创建多条 `wallet_ledgers` +- 充值成功后才更新余额;不为重试创建多条 `wallet_transactions` +- 新请求锁定当前买家钱包并完成余额前后值准备后,最后取得 DB096 财务水位写锁并立即生成唯一 `finalTime`;DB082 `creditedAt`、DB083 `createdAt`、DB096 `lastPostedAt` 和响应时间均复用该值。生成后不得再取得新的共享业务锁或调用外部服务。 #### 缓存、事件或外部依赖 @@ -5483,7 +5784,7 @@ CreateOrderResponse { - **模块 / Tag**:Payment - **需求编号**:M05-01-FR04 - **负责人**:张海洋 -- **关联数据表**:DB082(待评审)— `wallet_topups` +- **关联数据表**:DB082(`wallet_topups`) - **当前状态**:待交叉评审 - **用途**:分页查询当前买家充值记录 - **方法与路径**:`GET /api/payment/wallet/topups` @@ -5498,16 +5799,16 @@ CreateOrderResponse { - **Route 参数**:(无) - **Query 参数**: - - `createdFrom`(可选):ISO 8601 UTC,包含 - - `createdTo`(可选):ISO 8601 UTC,不包含 + - `creditedFrom`(可选):ISO 8601 UTC,包含 + - `creditedTo`(可选):ISO 8601 UTC,不包含 - `page`(默认 `1`) - `pageSize`(默认 `10`,1-100) - - `sortBy`(白名单:`createdAt`,默认 `createdAt desc`) + - `sortBy`(白名单:`creditedAt`,默认 `creditedAt desc`) - `sortOrder`(`asc` / `desc`) - **Header**:`Authorization: Bearer ` - **Body**:(无) - **校验规则**: - - `createdFrom ≤ createdTo`(否则 400) + - `creditedFrom ≤ creditedTo`(否则 400) - `pageSize` ∈ [1, 100] #### 成功响应 @@ -5548,7 +5849,7 @@ CreateOrderResponse { #### 业务规则与并发 - 仅返回当前 buyerId 已成功到账的充值事实;本期没有 `Pending` / `Failed` 充值状态机或对应查询筛选 -- 列表按 `createdAt desc, topupId desc` 稳定排序,避免翻页重复 +- 列表按 `creditedAt desc, topupId desc` 稳定排序,避免翻页重复 #### 缓存、事件或外部依赖 @@ -5560,7 +5861,7 @@ CreateOrderResponse { - 正常:返回当前用户充值记录 - 异常:page=0 → 400 + `COMMON.VALIDATION_FAILED` -- 异常:createdFrom > createdTo → 400 + `COMMON.VALIDATION_FAILED` +- 异常:creditedFrom > creditedTo → 400 + `COMMON.VALIDATION_FAILED` --- @@ -5569,7 +5870,7 @@ CreateOrderResponse { - **模块 / Tag**:Payment - **需求编号**:M05-01-FR05 - **负责人**:张海洋 -- **关联数据表**:Payment 不拥有订单表;订单金额、归属和状态通过 Ordering 公开应用契约读取 +- **关联数据表**:DB061(`orders`)、DB081(`wallet_accounts`)、DB085(`payments`);Payment 通过 Ordering 公开应用契约取得订单事实,不直接访问 Ordering 内部仓储 - **当前状态**:待交叉评审 - **用途**:进入支付前的订单金额、应付、钱包余额、可用渠道聚合查询 - **方法与路径**:`GET /api/payment/checkout/{orderId}` @@ -5610,6 +5911,7 @@ CreateOrderResponse { "availableChannels": ["Wallet"], "paymentDeadline": "2026-07-23T09:00:00Z", "serverTime": "2026-07-23T08:35:00Z", + "deadlineExpired": false, "canPay": true, "nonPayableReason": null, "paymentSummary": null @@ -5633,7 +5935,7 @@ CreateOrderResponse { - 不修改订单或钱包状态,纯查询 - 余额、订单金额、应付以服务端实时值(按 PAY-R01) -- `canPay` 必须同时满足订单属于本人、`status=PendingPayment` 且 `serverTime < paymentDeadline`;到期但 C03 尚未取消时也返回 `canPay=false` 并请求 M04/C03 取消通道。 +- `canPay` 必须同时满足订单属于本人、`status=PendingPayment` 且 `serverTime < paymentDeadline`;`deadlineExpired = serverTime >= paymentDeadline`。到期但 C03 尚未取消时仍只返回 `200`、`canPay=false`、`deadlineExpired=true` 和稳定 `nonPayableReason="PaymentDeadlineExpired"`,GET 不得触发取消、回补、任务创建或任何状态写入。 - `shortfallAmount = max(payableAmount - walletBalance, 0)`;`nonPayableReason` 只使用稳定枚举或 `null`,页面不得从自然语言推断流程。 - 订单已支付 → 返回 `200`、`canPay=false`、`orderStatus=Paid` 和真实 `paymentSummary`;已取消订单返回 409。买家可选渠道本期只有 `Wallet`,C08 不增加前台通道入口。 @@ -5649,6 +5951,7 @@ CreateOrderResponse { - 正常:订单本人 + `PendingPayment` + 余额充足 → 返回 `shortfallAmount=0` - 异常:他人订单 → 404 + `RESOURCE.NOT_FOUND` - 正常:订单已支付 → 200,`canPay=false`,不再显示支付按钮 +- 边界:订单仍是 `PendingPayment` 但已到截止时间 → 200,`canPay=false`、`deadlineExpired=true`;随后由 A405、A421、A304 或 C03 的命令路径竞争统一过期取消 --- @@ -5657,7 +5960,7 @@ CreateOrderResponse { - **模块 / Tag**:Payment - **需求编号**:M05-01-FR05~FR09 - **负责人**:张海洋 -- **关联数据表**:DB081(待评审)— `wallets`、DB083(待评审)— `wallet_ledgers`、DB085(待评审)— `payments`;订单状态通过 Ordering 公开应用契约协作 +- **关联数据表**:DB061(`orders`)、DB081(`wallet_accounts`)、DB083(`wallet_transactions`)、DB085(`payments`)、DB096(`financial_posting_sequences`)、DB102(`outbox_messages`)、DB104(`idempotency_records`) - **当前状态**:待交叉评审 - **用途**:从买家钱包扣款并完成订单支付 - **方法与路径**:`POST /api/payment/orders/{orderId}/pay` @@ -5682,7 +5985,8 @@ CreateOrderResponse { - **校验规则**: - `Idempotency-Key` 必填 - `expectedAmount` 选填,只用于识别页面旧值;最终扣款金额和币种始终来自 Ordering 持久化应付事实 - - 新请求必须同时满足订单为 `PendingPayment` 且 `databaseNow < paymentDeadline`;到期返回 `PAYMENT.DEADLINE_EXPIRED` 并触发同一 M04/C03 过期取消通道 + - 页面时间、请求到达时间和事务预检查不授权支付。新请求在取得订单、既有成功支付、本人钱包与财务提交水位等全部可能阻塞的共享事实后,只读取一次数据库 `finalTime`;必须同时满足订单为 `PendingPayment` 且 `finalTime < paymentDeadline` + - `finalTime >= paymentDeadline` 返回 `PAYMENT.DEADLINE_EXPIRED`;先持久化该请求的无副作用确定结果,提交后再尽力触发 M04/C03 统一过期取消 - 订单归属当前 buyerId #### 成功响应 @@ -5740,12 +6044,15 @@ PaymentResultResponse { #### 业务规则与并发 -- 完成身份、幂等键格式和固定请求结构校验后先查 PostgreSQL 幂等结果,再读取订单、余额和截止时间;同键同指纹重放首次确定结果,同键换内容返回 409。 +- 完成身份、幂等键格式和固定请求结构校验后,先取得 `(buyerId, A405, Idempotency-Key)` 唯一处理资格并查询 PostgreSQL 幂等结果;同键同指纹重放首次确定结果,同键换内容返回 409。 - 新请求以 Ordering 应付金额与币种为扣款事实,`expectedAmount` 只做可选旧页面冲突检查。余额不足、已取消、已过期等确定业务失败与幂等键持久绑定后返回;基础设施或提交结果未知不固化。 -- 钱包条件扣减 + 钱包流水 + `source=Wallet` 的支付记录 + 订单 `Paid` + 可靠支付成功待发布事实 + 幂等结果 **同一事务**(按 PAY-R06) -- 与买家取消、C03 超时取消和 C08 模拟通道回调同时通过 `WHERE status='PendingPayment' AND databaseNow < paymentDeadline` 竞争,唯一胜出(按 PAY-R05) +- 全新请求先取得订单级事务 advisory lock `payment-result:{orderId}`,再按固定顺序取得:幂等处理资格 → DB061 目标订单 → 查询同订单 DB085 既有成功支付 → DB081 本人钱包 → DB096 财务提交水位 `FOR UPDATE`。A406 与 A421 使用同一 advisory 命名空间同步结果观察;DB096 是最后一个可能阻塞的共享锁,取得后立即且仅调用一次 `clock_timestamp()` 形成 `finalTime`。 +- 取得 `finalTime` 后不得获取任何新的可能阻塞锁、不得调用外部服务,只能重新校验已锁事实:订单归属、状态、金额、币种、唯一成功来源、钱包余额和 `finalTime < paymentDeadline`。等待钱包或 DB096 期间跨过截止点时必须转为 `PAYMENT.DEADLINE_EXPIRED`,不得扣款、创建支付或递增资金副作用。 +- 钱包条件扣减 + DB083 钱包流水 + `source=Wallet` 的 DB085 支付记录 + 订单 `Paid` + DB096 财务序号 + 可靠支付成功待发布事实 + 幂等结果 **同一事务**。订单 `paidAt`、支付 `paidAt`、钱包流水时间和 DB096 `lastPostedAt` 均等于同一 `finalTime`。 +- 与买家取消、C03 超时取消和 C08 模拟通道回调竞争同一 `PendingPayment` 事实;成功资格固定为 `finalTime < paymentDeadline`,唯一胜出。 - 余额不得为负(条件更新 + CHECK 约束)(按 PAY-R02) - 同一订单已经由 Wallet 或 C08 `SimulatedChannel` 支付成功时,无论请求使用原 Key 还是新 Key,均返回既有 `200 PaymentResultResponse`,不重复扣款、写流水或发布事件;C08 支付结果的 `walletBalanceAfter` 为 `null`。 +- 到期拒绝与 DB104 在当前支付事务先提交;随后调用 Ordering 统一过期取消。取消使用独立锁后 `cancelDecisionTime` 重检,失败由 DB063/C03 继续收敛,不回写 A405 的 `finalTime` 或幂等结果。 #### 缓存、事件或外部依赖 @@ -5761,6 +6068,7 @@ PaymentResultResponse { - 重复:订单已支付 → 200,返回既有支付结果,不重复扣款 - 异常:订单已取消 → 409 + `PAYMENT.ORDER_NOT_PAYABLE` - 异常:订单已到 `paymentDeadline` → 409 + `PAYMENT.DEADLINE_EXPIRED`,进入同一过期取消通道 +- 并发:先阻塞 DB081 或 DB096,待截止时间跨过后再释放 → 409 + `PAYMENT.DEADLINE_EXPIRED`;无钱包扣款、支付记录或财务序号副作用 - 异常:金额不一致 → 409 + `PAYMENT.AMOUNT_MISMATCH` - 并发:与 C03 同时操作 → 唯一胜出,败方 409 + `PAYMENT.ORDER_NOT_PAYABLE` @@ -5771,7 +6079,7 @@ PaymentResultResponse { - **模块 / Tag**:Payment - **需求编号**:M05-01-FR09 - **负责人**:张海洋 -- **关联数据表**:DB085(待评审)— `payments` +- **关联数据表**:DB061 `orders`、DB085 `payments` - **当前状态**:待交叉评审 - **用途**:查询指定订单的支付结果与支付记录 - **方法与路径**:`GET /api/payment/orders/{orderId}` @@ -5801,11 +6109,15 @@ PaymentResultResponse { OrderPaymentLookupResponse { orderId: uuid orderStatus: "PendingPayment" | "Paid" | "Shipped" | "Completed" | "Cancelled" - paymentResult: PaymentResultResponse? // 尚无已确定支付事实时为 null + resultState: "Succeeded" | "NotPaid" | "Confirming" | "ExpiredOrCancelled" | "Indeterminate" + paymentResult: PaymentResultResponse? // 仅 Succeeded 非空 + serverTime: datetime + paymentDeadline: datetime + retryAfterSeconds: integer? // 仅 Confirming 使用,固定 1 } ``` -- **示例**:没有已确定支付事实时返回 `200` 与 `"paymentResult": null`;有记录时返回本人已确认的支付结果 +- **示例**:同一订单支付写事务仍在运行时返回 `Confirming + paymentResult=null + retryAfterSeconds=1`;锁后确认仍可支付且无成功事实时返回 `NotPaid`;有唯一成功事实时返回 `Succeeded`。 #### 失败响应 @@ -5820,7 +6132,9 @@ OrderPaymentLookupResponse { #### 业务规则与并发 -- 只返回当前订单已经确定的 Wallet 或 `SimulatedChannel` 支付事实;没有记录时明确返回 `paymentResult=null`,不得根据订单是否 `PendingPayment`、`Paid` 或 `Cancelled` 猜测。 +- A405、A421 在任何支付副作用或幂等处理资格前先取得同一订单级事务 advisory lock `payment-result:{orderId}`。A406 完成本人归属校验后尝试取得同一锁:未取得表示已有支付裁决正在运行,立即返回 `Confirming`,不能把当前 `paymentResult=null` 当作失败;取得后再锁定并读取订单与唯一 DB085,直到响应事务结束。 +- 锁后结果矩阵固定:存在唯一成功支付且订单处于 `Paid/Shipped/Completed` 已支付谱系时为 `Succeeded`;订单为 `PendingPayment`、尚未到截止且无支付时为 `NotPaid`;订单为 `Cancelled`,或仍为 `PendingPayment` 但数据库时间已到截止且无支付时为 `ExpiredOrCancelled`;订单/支付组合违反已支付谱系不变量时为 `Indeterminate` 并触发告警,客户端不得再次付款。 +- `Confirming` 只表示已有裁决未完成,客户端等待 `retryAfterSeconds` 后重查或使用原 `Idempotency-Key` 重试 A405;`NotPaid` 只是本次同步点的确定未支付快照,不阻止之后由用户发起新支付。任何不确定场景都不得生成新 Key 盲目重付。 - C08 迟到成功回调只形成 `Difference` 来源,不伪造成功支付记录;因此已取消且无确定支付事实的订单仍返回 `paymentResult=null`。 - 订单归属与 `orderStatus` 通过 4.2 的 Ordering→Payment 公开应用契约取得;Payment 不直接读取 Ordering 内部订单表。 @@ -5833,7 +6147,10 @@ OrderPaymentLookupResponse { #### 验证场景 - 正常:订单已支付 → 返回支付结果 -- 正常:订单没有确定支付事实 → 200 + `paymentResult=null` +- 正常:订单可支付且没有确定支付事实 → 200 + `NotPaid` +- 并发:A405/A421 正在裁决 → 200 + `Confirming`,随后重查得到确定状态 +- 边界:已取消或已过截止但 C03 尚未取消 → 200 + `ExpiredOrCancelled` +- 异常:已支付谱系与支付事实不一致 → 200 + `Indeterminate`,不得引导重复支付 - 异常:他人订单 → 404 + `RESOURCE.NOT_FOUND` --- @@ -5843,7 +6160,7 @@ OrderPaymentLookupResponse { - **模块 / Tag**:Payment - **需求编号**:M05-01-FR07 - **负责人**:张海洋 -- **关联数据表**:DB085(待评审)— `payments` +- **关联数据表**:DB085 `payments` - **当前状态**:待交叉评审 - **用途**:分页查询当前买家支付记录 - **方法与路径**:`GET /api/payments` @@ -5929,7 +6246,7 @@ OrderPaymentLookupResponse { - **模块 / Tag**:Payment - **需求编号**:M05-01-FR07 - **负责人**:张海洋 -- **关联数据表**:DB085(待评审)— `payments` +- **关联数据表**:DB085 `payments` - **当前状态**:待交叉评审 - **用途**:查询单笔支付详情 - **方法与路径**:`GET /api/payments/{paymentId}` @@ -6003,7 +6320,7 @@ OrderPaymentLookupResponse { - **模块 / Tag**:AfterSales - **需求编号**:M10-FR01 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests`;订单项归属、状态与实付快照通过 Ordering 公开应用契约查询 +- **关联数据表**:DB061 `orders`、DB062 `order_items`、DB085 `payments`、DB086 `after_sales_requests`;Ordering 与 Payment 事实通过公开应用契约查询 - **当前状态**:待交叉评审 - **用途**:预检指定订单项是否可申请售后 - **方法与路径**:`GET /api/after-sales/eligibility` @@ -6088,7 +6405,7 @@ OrderPaymentLookupResponse { - **模块 / Tag**:AfterSales - **需求编号**:M10-FR02 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests` +- **关联数据表**:DB001(责任商家并发门)、DB061 `orders`、DB062 `order_items`、DB085 `payments`、DB086 `after_sales_requests`、DB087 `after_sales_status_histories`、DB102 `outbox_messages`、DB104 `idempotency_records` - **当前状态**:待交叉评审 - **用途**:买家提交退款/退货申请 - **方法与路径**:`POST /api/after-sales/requests` @@ -6170,7 +6487,8 @@ OrderPaymentLookupResponse { - 申请数量不得超过剩余可售后数量(防重复申请)。 - 状态写入 `PendingReview`(M10 状态机)。 - 同一订单项可按剩余数量分次申请;仅处理中和已退款数量占用额度,不因存在另一笔 `PendingReview` 就整项禁止申请。 -- A412 必须与 A307 共用 Ordering 提供的订单级变更契约:在同一 PostgreSQL 事务中锁定目标 `orders` 行、读取最新履约状态并保持到售后申请写入提交,禁止查询后另开事务插入。申请先提交时占用数量进入履约快照;发货先提交时,本请求按已发货后的类型和库存规则重新校验。 +- A412 不得照搬 A307 的“订单先锁”顺序。固定执行:取得 DB104 售后幂等范围并重查结果 → 无锁预读订单不可变的 `assignedMerchantUserId` 仅用于定位门行 → 锁定该商家的 DB001 责任门并重检正常状态 → 通过 Ordering 公开能力锁定 DB061 订单并再次核对归属与 `assignedMerchantUserId` → 锁定/聚合 DB086 既有申请和目标 DB062 订单项 → 写申请、时间线、Outbox 与幂等结果。DB001/DB061/DB086 锁保持到同一 PostgreSQL 事务提交;预读不得作为授权或最终归属事实。 +- A307 仍按其发货契约锁订单后读取售后履约快照。两条路径通过上述固定顺序形成唯一先后结果:申请先提交时占用数量进入履约快照;发货先提交时,本请求按已发货后的类型和库存规则重新校验。任何实现都禁止“先查后改”或另开事务插入。 - 申请、数量占用、服务端计算金额、首条状态时间线、订单指定商家通知可靠事实和幂等成功结果必须在同一事务形成;任一写入失败则整体不生效。 - 已完成业务校验后能够确定的 404/409 结果也按 1.12.1 保存并重放;鉴权失败、请求格式错误和未知内部错误不保存为业务幂等结果。 @@ -6193,7 +6511,7 @@ OrderPaymentLookupResponse { - **模块 / Tag**:AfterSales - **需求编号**:M10-FR03 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests` +- **关联数据表**:DB086 `after_sales_requests` - **当前状态**:待交叉评审 - **用途**:买家本人或商家按范围分页查询售后申请 - **方法与路径**:`GET /api/after-sales/requests` @@ -6282,7 +6600,7 @@ OrderPaymentLookupResponse { - **模块 / Tag**:AfterSales - **需求编号**:M10-FR04 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_status_histories` +- **关联数据表**:DB086(`after_sales_requests`)、DB087(`after_sales_status_histories`)、DB088(`refund_operations`)、DB092(`after_sales_return_shipments`)、DB093(`refund_attempts`) - **当前状态**:待交叉评审 - **用途**:查询单条售后申请的详细信息与状态时间线 - **方法与路径**:`GET /api/after-sales/requests/{requestId}` @@ -6321,7 +6639,7 @@ OrderPaymentLookupResponse { "primaryImageUrl": "https://cdn.example.test/products/p1/main.jpg", "paidUnitPrice": 100.00, "purchasedQuantity": 2, - "orderType": "Ordinary" + "orderType": "Normal" }, "type": "RefundOnly", "quantity": 1, @@ -6351,7 +6669,11 @@ OrderPaymentLookupResponse { - `orderItemSnapshot` 固定返回下单时的商品名称、主图、实付单价、购买数量和订单类型;秒杀订单还返回 `seckillActivityId`,但不暴露内部库存实现字段。 - `audit` 在已审核后返回 `decision`、`note`、`actorDisplayName`、`auditedAt`,不暴露内部账号 ID。 - `returnInfo` 在买家已寄回后返回 `carrier`、`trackingNumber`、`shippedAt`、`note`。 -- `refundOperation` 在退款操作建立后返回 `refundOperationId`、`amount`、`status`(`Processing` / `Succeeded` / `DefiniteFailure`)、`startedAt`、`completedAt`、`failedAt` 和可安全展示的 `failureCode`;结果未知时保持 `Processing`,不得返回钱包账户内部流水、异常堆栈或第三方原始报文。 +- `refundOperation` 在退款操作建立后返回: + - `refundOperationId`、`amount`、`status`(`Processing` / `Succeeded` / `DefiniteFailure`)、`attemptCount`、`startedAt`、`completedAt`、`failedAt`; + - `recoveryState`:`None` / `Executing` / `VerifyingUnknown` / `AutomaticRetryScheduled` / `MerchantRetryRequired` / `OperatorAttentionRequired`; + - `nextActionAt`、`manualRetryAvailableAt` 与可安全展示的 `lastFailureCode`(不适用时为 `null`)。 +- 结果未知时 `status=Processing + recoveryState=VerifyingUnknown`;自动重试排队时为 `Processing + AutomaticRetryScheduled`。买家只能看到安全状态和预计下一处理时间;商家仅在 `MerchantRetryRequired` 且冷却已结束时得到 `actions.canRetryRefund=true`。不得返回 Worker 实例、租约 Token、内部异常、钱包流水实现或第三方原始报文。 - `timeline` 返回申请创建、撤销、审核、寄回、确认收货、退款中、退款成功或确定失败等完整领域状态变化;A418 不再提供重复时间线接口。 - `actions` 只按当前身份、账号状态、资源归属和当前已提交状态给出页面提示,执行动作时仍须由对应命令接口重新校验。 @@ -6367,10 +6689,10 @@ OrderPaymentLookupResponse { #### 业务规则与并发 -- 时间线读取模块内的 `after_sales_status_histories`(DB087 待数据库设计确认),不读取通用操作审计。 +- 时间线读取已确认的 DB087 `after_sales_status_histories`,不读取通用操作审计。 - 详情必须一次返回订单项快照、服务端金额、买家申请内容、审核结果、退货说明、同一退款操作的当前结果和完整状态时间线,不要求客户端拼接已取消的 A418/A432/A433。 - 买家仅可读取本人申请;商家仅可读取订单 `assignedMerchantUserId` 等于当前账号的申请。申请不存在和不在授权范围统一返回 404,禁止泄露资源是否存在。 -- `Refunding`、`RefundFailed` 都是可查询真实状态;结果未知时 `refundOperation.status=Processing`,不得伪造成失败或省略。 +- `Refunding`、`RefundFailed` 都是可查询真实状态。`Unknown` 和自动重试期间申请保持 `Refunding`;只有 `MerchantRetryRequired` / `OperatorAttentionRequired` 进入 `RefundFailed`。接口不得把未知结果、一次自动失败或系统关注问题一律伪造成可由商家重试的失败。 - 不返回内部审计字段、商家内部 ID、支付签名、钱包余额变更实现细节或可被用于越权的数据。 #### 缓存、事件或外部依赖 @@ -6391,7 +6713,7 @@ OrderPaymentLookupResponse { - **模块 / Tag**:AfterSales - **需求编号**:M10-FR10 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests` +- **关联数据表**:DB086 `after_sales_requests`、DB087 `after_sales_status_histories`、DB104 `idempotency_records` - **当前状态**:待交叉评审 - **用途**:买家撤销本人仍处 `PendingReview` 状态的申请 - **方法与路径**:`POST /api/after-sales/requests/{requestId}/cancel` @@ -6464,7 +6786,7 @@ OrderPaymentLookupResponse { - **模块 / Tag**:AfterSales - **需求编号**:M10-FR05 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_status_histories` +- **关联数据表**:DB086~DB088、DB093、DB102、DB104;退款成功时另用原普通库存 DB022/DB026,或秒杀库存与限购 DB042/DB043/DB044,以及 DB081、DB083、DB085、DB096 - **当前状态**:待交叉评审 - **用途**:商家同意或拒绝售后申请 - **方法与路径**:`POST /api/after-sales/requests/{requestId}/audit` @@ -6522,9 +6844,9 @@ OrderPaymentLookupResponse { - 审核结果由申请类型决定,客户端不能通过布尔字段选择是否退款: - `Reject`:原子进入 `Rejected`、释放申请数量、记录审核意见和买家通知可靠事实。 - `Approve + ReturnAndRefund`:原子进入 `PendingReturn`、记录审核意见和买家通知可靠事实,等待 A434 和 A417,不在审核时退款或回补库存。 - - `Approve + RefundOnly`:原子记录同意意见、建立或关联该申请唯一退款操作并进入 `Refunding`,随后由退款执行器推进;HTTP 返回最新已提交状态,不要求在一次请求内伪造为 `Refunded`。 + - `Approve + RefundOnly`:通过 RefundOrchestrator 原子记录同意意见、建立该申请唯一 DB088、创建 `attemptNumber=1/Initial/Executing` DB093 并进入 `Refunding`,随后执行已持久化尝试;HTTP 返回最新已提交状态,不要求在一次请求内伪造为 `Refunded`。四项任一失败整体回滚,禁止留下 `Refunding + attemptCount=0`。 - `RefundOnly` 若 Ordering 快照表明订单仍为 `Paid` 且未发货,退款成功结果还必须按普通/秒杀原通道回补库存;对 `Shipped` 或 `Completed` 订单只退款、不回补库存。 -- 退款结果未知时保持 `Refunding` 并核实同一尝试;只有得到确定失败且确认余额、库存均未增加时才进入 `RefundFailed`。已经提交审核事实后,不用 503 隐藏或回滚当前状态。 +- 退款结果未知时保持 `Refunding` 并核实同一尝试。得到确定失败且确认钱包、支付累计、库存、售后终态均无本次成功副作用后,仍须按服务端固定处置分流:`AutomaticRetry` 保持 `Refunding` 并进入系统退避;只有 `MerchantRetryRequired` / `OperatorAttentionRequired` 进入 `RefundFailed`。已经提交审核事实后,不用 503 隐藏或回滚当前状态。 - 每份申请最多建立一个稳定退款操作;相同审核重放不得创建第二笔退款或第二次库存回补。 - 退款成功时,钱包入账、必要库存回补、`Refunded`、时间线和买家通知可靠事实必须形成完整原子结果;任一步失败不得留下部分资金或库存结果。 - 售后状态历史记录审核人、审核意见和状态变化;这是领域时间线,不是未选择的通用后台操作日志。 @@ -6539,7 +6861,7 @@ OrderPaymentLookupResponse { - 正常:商家 Approve → 退货退款进入 `PendingReturn`;仅退款可靠进入 `Refunding` 并返回当前状态 - 正常:商家 Reject → 状态进入 `Rejected` -- 正常:退款执行已完成或得到确定失败 → 同一调用重放可返回 `Refunded` 或 `RefundFailed` +- 正常:退款执行成功返回 `Refunded`;确定失败按处置返回 `Refunding/AutomaticRetryScheduled` 或 `RefundFailed` - 异常:退款结果未知 → 保持并返回 `Refunding`,不创建第二笔退款 - 异常:买家角色调用 → 403 + `AUTH.FORBIDDEN` - 异常:他人商家申请 → 404 + `RESOURCE.NOT_FOUND` @@ -6552,7 +6874,7 @@ OrderPaymentLookupResponse { - **模块 / Tag**:AfterSales - **需求编号**:M10-FR11 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_status_histories` +- **关联数据表**:DB086~DB088、DB092、DB093、DB102、DB104;退款成功时另用原普通库存 DB022/DB026,或秒杀库存与限购 DB042/DB043/DB044,以及 DB081、DB083、DB085、DB096 - **当前状态**:待交叉评审 - **用途**:订单指定商家确认已收到整笔退货,可靠进入退款流程 - **方法与路径**:`POST /api/after-sales/requests/{requestId}/confirm-receipt` @@ -6608,9 +6930,9 @@ OrderPaymentLookupResponse { - 完成固定字段校验后先查询幂等结果;相同 Key 同请求即使申请已进入 `Refunding` / `Refunded` / `RefundFailed`,也返回同一退款操作的当前结果,不重复确认或退款。 - 仅在收货与 `Refunding` 状态尚未可靠提交、且必需依赖无法安全确认时返回 503;状态已提交后依赖暂时不可用仍返回 200 与同一退款操作当前结果。 - 状态条件更新同时校验关联订单 `assigned_merchant_user_id = currentUserId`。 -- 首次确认时,收货事实、该申请唯一退款操作和 `PendingReceipt → Refunding` 状态时间线必须原子提交;退款失败也不得回到 `PendingReturn` 或抹去已确认收货事实。 +- 首次确认时,RefundOrchestrator 必须原子提交收货事实、该申请唯一 DB088、`attemptNumber=1/Initial/Executing` DB093 和 `PendingReceipt → Refunding` 状态时间线;退款失败也不得回到 `PendingReturn` 或抹去已确认收货事实。任一写入失败整体回滚,禁止留下没有首个可恢复尝试的 `Refunding`。 - 退款执行器根据 Ordering 快照的 `orderType` 与 `seckillActivityId`,在退款成功原子结果中按原通道回补整笔申请数量;本期不支持部分收货或部分退款。 -- 退款结果未知时保持 `Refunding` 并核实原尝试;确定失败且余额、库存均未增加时进入 `RefundFailed`;全部成功时资金入账、原通道库存回补、`Refunded`、时间线和买家通知可靠事实形成完整结果。 +- 退款结果未知时保持 `Refunding` 并核实原尝试;确定失败且确认全部参与事实无本次成功副作用后,再按固定处置分流:`AutomaticRetry` 保持 `Refunding`,`MerchantRetryRequired` / `OperatorAttentionRequired` 进入 `RefundFailed`;全部成功时资金入账、原通道库存回补、`Refunded`、时间线和买家通知可靠事实形成完整结果。 #### 缓存、事件或外部依赖 @@ -6633,7 +6955,7 @@ OrderPaymentLookupResponse { - **模块 / Tag**:AfterSales - **需求编号**:M10-FR07 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB088(待评审)— `refunds` +- **关联数据表**:DB086(`after_sales_requests`)、DB087(`after_sales_status_histories`)、DB088(`refund_operations`)、DB093(`refund_attempts`)、DB102(`outbox_messages`)、DB104(`idempotency_records`);成功结果还协作 DB081/DB083/DB085/DB096,以及原普通库存 DB022/DB026,或秒杀库存与限购 DB042/DB043/DB044 - **当前状态**:待交叉评审 - **用途**:订单指定商家对状态为 `RefundFailed` 的申请安全重试原退款操作 - **方法与路径**:`POST /api/after-sales/requests/{requestId}/retry-refund` @@ -6657,7 +6979,8 @@ OrderPaymentLookupResponse { ``` - **校验规则**: - 申请关联订单分配给当前商家账号 - - 首次开始该次人工重试时申请状态必须为 `RefundFailed`;已经为 `Refunding` 或 `Refunded` 时返回同一退款操作当前结果 + - 首次开始该次人工重试时申请必须为 `RefundFailed`,DB088 为确定失败,最新 DB093 恢复处置为 `MerchantRetryRequired`,不存在 `Executing` / `Unknown` 尝试,且数据库时间已经达到 `manualRetryAvailableAt` + - 已为 `Refunding` 或 `Refunded` 时返回同一退款操作当前结果;自动重试仍排队、冷却未结束或 `OperatorAttentionRequired` 时不得启动人工重试 - `Idempotency-Key` 必填 #### 成功响应 @@ -6675,7 +6998,10 @@ OrderPaymentLookupResponse { | 401 | `AUTH.UNAUTHENTICATED` | 缺少 JWT | | 403 | `AUTH.FORBIDDEN` | 角色非 Merchant | | 404 | `RESOURCE.NOT_FOUND` | 申请不存在或不在授权范围 | -| 409 | `AFTER_SALES.INVALID_STATUS` | 状态既非可重试的 `RefundFailed`,也不是同一退款操作可返回的 `Refunding` / `Refunded` | +| 409 | `AFTER_SALES.INVALID_STATUS` | 状态既非可判断恢复处置的 `RefundFailed`,也不是同一退款操作可返回的 `Refunding` / `Refunded` | +| 409 | `AFTER_SALES.RETRY_NOT_AVAILABLE` | 原尝试仍为 `Executing/Unknown` 或自动重试仍排队;响应返回安全的 `recoveryState` / `nextActionAt` | +| 409 | `AFTER_SALES.RETRY_COOLDOWN` | 尚未达到 `manualRetryAvailableAt`;响应返回该时间 | +| 409 | `AFTER_SALES.RETRY_NOT_ALLOWED` | 恢复处置为 `OperatorAttentionRequired`,本期不允许商家反复触发 | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | | 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | 新重试状态提交前必需的 Ordering、Payment 或原库存通道依赖暂时不可用 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | @@ -6685,23 +7011,27 @@ OrderPaymentLookupResponse { - 完成固定字段校验后先查询幂等结果;命中同 Key 同请求时返回原调用结果或同一退款操作的当前确定状态,不因状态已经变化而重新报错。 - 仅在新的重试尚未可靠提交、且必需依赖无法安全确认时返回 503;已经进入 `Refunding` 后返回 200 与同一退款操作当前结果,不因依赖瞬态中断发起第二次退款。 - 状态条件更新同时校验关联订单 `assigned_merchant_user_id = currentUserId`。 -- 首次人工重试只能把 `RefundFailed → Refunding`,并继续使用原 `refundOperationId`、金额、收款人、申请数量和库存通道;不得重新审核或创建第二笔业务退款。 +- 首次人工重试先按标识只读解析关联关系,再由 RefundOrchestrator 在一个事务内按 DB061 订单 → DB086 申请 → DB088 退款操作 → 最新 DB093 尝试锁定并重检;A419 只提交 `ManualRetry` 触发意图,Payment 负责把旧处置改为 `Consumed`、创建唯一 `manual_retry` DB093 并递增 DB088 `attemptCount`,AfterSales 负责申请 `RefundFailed → Refunding` 与 DB087 `refund_retried`,M00 保存 DB104 幂等结果。继续使用原 `refundOperationId`、金额、收款人、申请数量和库存通道;不得由 AfterSales 直写 DB088/DB093、重新审核、反向先锁退款操作或创建第二笔业务退款。 - 商家人工重试与系统恢复任务竞争时,最多一个执行器取得同一退款操作的执行权,其他调用返回 `Refunding` 或已确定结果。 - 若发现旧尝试结果未知,保持 `Refunding` 并核实原尝试,不立即开始另一笔无法去重的退款。 -- 成功时只形成一次钱包入账、必要库存回补、`Refunded`、时间线和买家通知可靠事实;得到确定失败且确认没有部分资金/库存结果时才重新进入 `RefundFailed`。 +- 状态已经可靠进入 `Refunding` 后,即使本次执行变成 Unknown,也返回 `200 + Refunding/VerifyingUnknown`,不得返回 503 诱导另一笔退款。 +- 成功时只形成一次钱包入账、必要库存回补、`Refunded`、时间线和买家通知可靠事实。确定失败仍按固定恢复处置分流:有自动额度时保持 `Refunding`;`MerchantRetryRequired` / `OperatorAttentionRequired` 才重新进入 `RefundFailed`。 - 系统恢复不使用 Merchant JWT 或 A419,而通过 AfterSales 内部应用能力按相同约束推进。 #### 缓存、事件或外部依赖 - 缓存:不缓存 -- 事件:重试动作本身不发布集成事件;最终只按结果可靠形成 `RefundCompletedIntegrationEvent` 或 `RefundFailedIntegrationEvent`,两者都只通知申请买家。 +- 事件:重试动作本身不发布集成事件;最终按结果形成 `RefundCompletedIntegrationEvent` 或 `RefundFailedIntegrationEvent`。失败事件始终通知申请买家;仅 `MerchantRetryRequired` 额外派生订单指定商家的 `RefundRetryRequired` 消息,Unknown/自动重试/运维告警不生成额外 M09 消息。 - 外部依赖:PostgreSQL + Ordering 售后快照 + Payment 退款应用契约 + Catalog/Seckill 库存回补应用契约 #### 验证场景 -- 正常:商家对 `RefundFailed` 重试 → 唯一进入 `Refunding`,返回当前状态 +- 正常:商家对 `MerchantRetryRequired` 且冷却结束的 `RefundFailed` 重试 → 唯一进入 `Refunding`,返回当前状态 +- 边界:自动重试排队或 Unknown → 409 `AFTER_SALES.RETRY_NOT_AVAILABLE`,不创建人工尝试 +- 边界:冷却未结束 → 409 `AFTER_SALES.RETRY_COOLDOWN` +- 边界:`OperatorAttentionRequired` → 409 `AFTER_SALES.RETRY_NOT_ALLOWED` - 正常:原退款已成功 → 重放当前 `Refunded`,不重复入账或回补 -- 正常:再次得到确定失败 → 返回可查询的 `RefundFailed` +- 正常:再次得到确定失败 → 按固定处置返回 `Refunding/AutomaticRetryScheduled` 或可查询的 `RefundFailed` - 异常:原尝试结果未知 → 返回 `Refunding` 并继续核实 - 异常:状态既非首次可重试的 `RefundFailed`,也非可返回当前结果的 `Refunding` / `Refunded` → 409 + `AFTER_SALES.INVALID_STATUS` @@ -6712,7 +7042,7 @@ OrderPaymentLookupResponse { - **模块 / Tag**:Payment - **需求编号**:C08-FR01 / FR02 / FR03 / FR04 / FR05 - **负责人**:张海洋 -- **关联数据表**:DB089(待评审)— `payment_callbacks`、DB085(待评审)— `payments`;订单状态通过 Ordering 公开应用契约协作 +- **关联数据表**:DB061(`orders`)、DB084(`payment_channel_attempts`)、DB085(`payments`)、DB089(`payment_callbacks`)、DB096(`financial_posting_sequences`)、DB102(`outbox_messages`);到期回调提交后由独立 Ordering 过期取消命令处理 DB063,并按订单快照回补原普通库存 DB022/DB026,或秒杀库存与限购 DB042/DB043/DB044 - **当前状态**:待交叉评审 - **用途**:接收受控模拟支付通道回调,完成来源鉴别、回调幂等、支付流水聚合、订单竞争和确定结果回执 - **方法与路径**:`POST /api/payment/callbacks` @@ -6727,7 +7057,7 @@ OrderPaymentLookupResponse { - **Route 参数**:(无) - **Query 参数**:(无) -- **Header**:`X-Callback-Key-Id: `、`X-Callback-Timestamp: `、`X-Callback-Signature: `(均必填)、`Content-Type: application/json` +- **Header**:`X-Callback-Key-Id: `、`X-Callback-Timestamp: `、`X-Callback-Signature: `(均必填)、`Content-Type: application/json`;本期只接受无 `Content-Encoding` 的 UTF-8 JSON - **Body**: ```json { @@ -6741,17 +7071,22 @@ OrderPaymentLookupResponse { } ``` - **校验规则**: - - `callbackId` 必填,UUID;同一标识只能绑定一份规范请求内容和一份首次确定结果 - - `paymentSerialNumber` 必填,1~100 字符;标识一次模拟通道支付尝试,不是第二个回调幂等键 + - V1 Body 是封闭对象,属性名区分大小写,必须且只能出现一次 `callbackId`、`paymentSerialNumber`、`orderId`、`result`、`occurredAt`、`amount`、`currency` 七个非空标量属性;重复/未知/缺失属性、数组、嵌套对象、未定义 `null`、注释、尾随内容、非 UTF-8、BOM 或压缩请求均在落库前返回 `COMMON.VALIDATION_FAILED` + - `callbackId` 必填,UUID;同一标识只能绑定一份规范业务请求内容和一份首次确定结果 + - `paymentSerialNumber` 必填,1~100 个 Unicode 字符,必须已经去除首尾空白且不含控制字符;标识一次模拟通道支付尝试,不是第二个回调幂等键 - `result` 枚举:`Success` / `Failed` - - `orderId` 必填,UUID;`amount` 必填且 > 0,最多两位小数;`currency` 固定 `CNY` - - `occurredAt` 必填,ISO 8601 UTC,只用于追踪和对账,是否可支付以服务端处理时间和订单 `paymentDeadline` 为准 - - `X-Callback-Timestamp` 使用 Unix 秒,与服务端时间允许偏差固定为 300 秒 - - `keyId` 仅允许命中当前密钥或轮换宽限期内的上一把密钥;未知、过期密钥与错误签名统一返回无细节的签名错误 - - `bodyHash = lowercase-hex(SHA256(raw UTF-8 body))` - - `canonical = keyId + "\n" + timestamp + "\n" + bodyHash` - - `signature = Base64(HMAC-SHA256(secret, UTF8(canonical)))`,使用常量时间比较 - - 验签必须基于未被 JSON 反序列化改写的原始请求字节;成功后再做固定字段校验和回调幂等检查 + - `orderId` 必填,UUID;`amount` 必填,是 `0.01~9999999999999999.99` 的非指数 JSON 十进制数且最多两位小数;`currency` 固定 `CNY` + - `occurredAt` 必填,ISO 8601 且显式为零偏移 UTC,只用于追踪和对账;是否可支付以取得全部可能阻塞共享事实后的数据库 `finalTime` 与订单 `paymentDeadline` 为准 + - 验签开始先从 PostgreSQL 取得一次 `databaseNow=clock_timestamp()`;`X-Callback-Timestamp` 使用 Unix 秒,与该 `databaseNow` 的允许偏差固定为正负 300 秒 + - 三个签名 Header 均必须恰好一个值且不允许首尾空白、逗号合并或重复:`X-Callback-Key-Id` 是 1~64 个 ASCII 字符并匹配 `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`;`X-Callback-Timestamp` 是 10~19 位 ASCII 十进制正整数、无符号和前导零;`X-Callback-Signature` 必须是标准 Base64 的 44 个 ASCII 字符,严格解码后恰好 32 字节,拒绝 Base64URL、内部空白和非规范填充。格式非法与验签失败统一返回无细节签名错误 + - `X-Callback-Key-Id` 在协议上就是显式 `keyVersion`,只能精确命中当前版本或紧邻上一版本,服务端不得试验多把密钥。两个可接受版本都必须满足 `activatedAt <= callbackTimestamp`;上一版本还必须额外满足 `callbackTimestamp <= retiredAt + 10 minutes` 且验签时数据库时间 `databaseNow <= retiredAt + 10 minutes`。更老版本、未知版本、未激活版本、宽限已过、时间偏差超限和签名错误统一返回无细节的签名错误 + - HMAC 只覆盖**原始请求实体字节**:`bodyHash = lowercase-hex(SHA256(rawBodyBytes))` + - `signatureCanonical = keyVersion + "\n" + timestamp + "\n" + bodyHash` + - `signature = Base64(HMAC-SHA256(secret, UTF8(signatureCanonical)))`,解码与比较均采用固定时长策略 + - 原始请求实体上限固定为 16 KiB(16384 字节),按收到的未解压字节计数。本期禁止 `Content-Encoding`;`Content-Length > 16384` 时在读取 Body 前返回 413,缺少 `Content-Length` 或采用分块传输时最多读取 16385 字节作为超限哨兵并立即停止。Nginx 该 location、Kestrel 端点限制与应用有界读取必须使用同一 16384 字节上限;不得先整包无界缓冲、写临时文件、记录原文或进入 HMAC/JSON/数据库后才拒绝 + - 服务端只在上限内保留一份原始实体字节并完成时间窗、密钥版本和 HMAC 校验,再以拒绝重复属性的解析器处理 JSON;不得先反序列化、重新编码、排序字段或格式化数字后验签 + - 验签通过后生成独立的业务指纹。`businessCanonicalJson` 固定按 `callbackId,paymentSerialNumber,orderId,result,occurredAt,amount,currency` 顺序输出:UUID 为小写 `D` 格式,普通字符串为 Unicode NFC,枚举保持本契约精确大小写,UTC 时间统一为 `.NET "O"` 的 `Z` 形式,金额统一为无指数的两位小数,币种固定 `CNY`;输出 UTF-8、无空白 + - `requestFingerprint = lowercase-hex(SHA256(UTF8(businessCanonicalJson)))`。字段顺序、JSON 空白、UUID 大小写、UTC 的等价零偏移写法及 `199` / `199.0` / `199.00` 等合法等价序列化可以拥有不同 raw-body HMAC,但在分别验签成功后必须得到相同业务指纹;任何业务值变化必须得到不同指纹 #### 成功响应 @@ -6777,27 +7112,31 @@ OrderPaymentLookupResponse { | HTTP 状态 | 业务错误码 | 触发条件 | |---|---|---| -| 400 | `COMMON.VALIDATION_FAILED` | 字段错误 | +| 400 | `COMMON.VALIDATION_FAILED` | 封闭 JSON、编码、重复/未知属性或字段值错误 | | 401 | `PAYMENT.CALLBACK_INVALID_SIGNATURE` | 签名验证失败 | +| 413 | `COMMON.PAYLOAD_TOO_LARGE` | 原始请求实体超过 16384 字节;未进入 HMAC、JSON 或业务落库 | | 409 | `PAYMENT.CALLBACK_ID_REUSED` | 同一 `callbackId` 被用于不同规范请求内容 | -| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | Ordering 权威订单状态、应付金额或支付截止时间暂时不可用,未形成确定回调裁决 | +| 503 | `COMMON.DEPENDENCY_UNAVAILABLE` | PostgreSQL 权威验签时间、Ordering 订单状态、应付金额或支付截止时间暂时不可用,未形成确定验签/回调裁决 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 来源与固定字段校验通过后,先按 `callbackId + 规范请求指纹` 查询首次确定结果:同标识同内容重放原 HTTP 状态与响应;同标识不同内容拒绝并记录安全冲突。 +- 原始字节验签和封闭字段校验通过后,先按 `callbackId + requestFingerprint` 查询首次确定结果:同一 `callbackId` 的等价 JSON 序列化或使用仍有效新密钥版本重新签名时重放原 HTTP 状态与响应;业务规范值不同则拒绝并记录安全冲突。DB089 只保存首次合法投递的业务指纹、`bodyHash/keyVersion/callbackTimestamp` 审计值,不因等价重放覆盖,也不保存 HMAC 密钥或完整签名。 - `paymentSerialNumber` 第一次出现时绑定订单、金额和币种;同一流水允许不同 `callbackId` 表达先失败后成功、先成功后失败或重复成功等乱序信号。后续若改变不可变绑定则形成 `Difference` 来源,不在幂等门口静默丢弃。 -- 服务端取得新回调唯一处理资格后,读取支付流水聚合、权威订单状态、应付金额、`paymentDeadline` 和已有成功来源,并按固定矩阵裁决: - - `PendingPayment`、服务端当前时间早于 `paymentDeadline`、金额与币种一致且 `result=Success`:`ProcessedSuccess`。 +- 全新回调先按声明订单 ID 取得订单级事务 advisory lock `payment-result:{orderId}`,再按固定串行和锁顺序取得:`callbackId` 事务级处理资格并重查 DB089 → 独立命名空间的 `paymentSerialNumber` 事务级处理资格 → 声明订单存在时锁 DB061 订单 → 锁定或创建 DB084 通道尝试并重检首次绑定 → 查询/锁定同订单 DB085 成功支付事实 → DB096 财务提交水位 `FOR UPDATE`。该订单级锁与 A405/A406 共用,流水处理资格仍必须覆盖 DB084 尚不存在的首次创建竞争。 +- DB096 是最后一个可能阻塞的共享锁;取得后立即且仅调用一次 `clock_timestamp()` 形成 `finalTime`。此后不得再取得新的可能阻塞锁或调用外部服务,只能按全部已锁事实执行四终态矩阵。订单不存在时跳过订单锁,但仍须在流水处理资格下创建/锁定尝试,再取得 DB096 和 `finalTime`。 +- 服务端按固定矩阵裁决: + - `PendingPayment`、`finalTime < paymentDeadline`、金额与币种一致且 `result=Success`:`ProcessedSuccess`。 - 同样可支付但 `result=Failed`:`ProcessedFailure`,订单保持待支付。 - - `PendingPayment` 但服务端当前时间已经达到 `paymentDeadline`,且 `result=Success`:`Difference`,不创建成功支付,并触发 M04/C03 统一过期取消。 - - `PendingPayment` 但服务端当前时间已经达到 `paymentDeadline`,且 `result=Failed`:`ProcessedFailure`,不创建成功支付,并触发 M04/C03 统一过期取消。 + - `PendingPayment` 但 `finalTime >= paymentDeadline`,且 `result=Success`:`Difference` / `expired_success`,不创建成功支付;先提交回调终态,再触发 M04/C03 统一过期取消。 + - `PendingPayment` 但 `finalTime >= paymentDeadline`,且 `result=Failed`:`ProcessedFailure` / `failure_recorded_after_deadline`,不创建成功支付;先提交回调终态,再触发 M04/C03 统一过期取消。 - 已支付后收到失败、已取消或其他不可支付终态收到失败、同一流水在已成功后重复成功:`Ignored`。 - 订单缺失、金额/币种/绑定不符、到期或已取消后收到成功、已有其他成功支付来源后收到成功:`Difference`。 -- `ProcessedSuccess` 必须原子提交 `source=SimulatedChannel` 的支付事实、订单 `PendingPayment → Paid`、回调终态和可靠支付成功事实,绝不扣减小金库。 -- `ProcessedFailure`、`Ignored` 和 `Difference` 也必须先保存完整回调终态与首次回执再返回。`Difference` 只保存完整差异来源,不在回调事务中提前创建 A424 差异条目。 +- 每个合法四终态都在 DB096 分配一次 `postingSequence`,并令 DB089 `processedAt`、DB084 `lastProcessedAt`、DB096 `lastPostedAt/updatedAt` 等于同一 `finalTime`。只有 `ProcessedSuccess` 创建 DB085 成功支付并推进 DB061 `Paid`,其 `paidAt` 同样等于 `finalTime`;绝不扣减小金库。 +- `ProcessedFailure`、`Ignored` 和 `Difference` 也必须先保存完整回调终态、首次回执和财务提交序号再返回。`Difference` 只保存完整差异来源,不在回调事务中提前创建 A424 差异条目。 +- 到期回调采用两个可独立重试的提交边界:第一阶段按上述顺序取得事实和 `finalTime`,提交 DB089 完整终态、回执、财务水位和可靠“需要过期裁决”事实;第二阶段由受信任 Ordering 过期取消命令锁定订单并取得新的 `cancelDecisionTime`,仅当仍为 `PendingPayment` 且 `cancelDecisionTime >= paymentDeadline` 时原子取消、回补原库存通道、完成 DB063 并发布取消事实。第二阶段失败不得回滚或改写第一阶段 `finalTime` 与回调终态,由 C03 使用同一订单责任持续重试;其他支付或取消先提交时按当前终态幂等收敛。 - 合法业务冲突统一返回 `200 + Difference`;签名错误、固定字段非法和基础设施故障不占用四种业务终态。 -- 模拟成功回调、A405 小金库支付、A304 买家取消和 C03 超时取消共同条件竞争 `PendingPayment` 与 `paymentDeadline`,最多一个形成合法订单终态。 +- 模拟成功回调、A405 小金库支付、A304 买家取消和 C03 超时取消竞争同一订单事实,最多一个形成合法订单终态。A421/A405 的支付资格均以各自最后共享锁后的 `finalTime` 为准;取消使用独立 `cancelDecisionTime`。请求到达、验签、取得订单锁或回调 `occurredAt` 都不能取得业务优先权。 #### 缓存、事件或外部依赖 @@ -6811,10 +7150,16 @@ OrderPaymentLookupResponse { - 正常:同一流水先失败后成功 → 分别返回 `ProcessedFailure`、`ProcessedSuccess` - 正常:同一流水先成功后失败 → 后续失败返回 `Ignored`,订单不回退 - 重复:相同 `callbackId` → 返回首次结果,不重复处理 +- 重复:同一 `callbackId` 使用不同字段顺序、空白或等价金额写法,且各自 raw-body HMAC 均正确 → 重放同一首次结果;任一业务值变化 → 409 - 异常:签名错误 → 401 + `PAYMENT.CALLBACK_INVALID_SIGNATURE` +- 边界:`Content-Length` 或分块 Body 超过 16384 字节 → 413,读取有界停止且 DB084/DB089 均无新增;恰好 16384 字节仍按正常验签与字段校验处理 +- 边界:签名 Header 重复、含空白、Key ID 超过 64 字符、Timestamp 非规范十进制,或签名不是严格 44 字符/32 字节标准 Base64 → 401,且不试验其他密钥 +- 边界:金额为 0、负数、超过 `9999999999999999.99`、三位小数或指数形式 → 400,不进入回调幂等或支付裁决 +- 边界:上一密钥退役后 10 分钟内且满足固定 300 秒时间窗 → 可按显式旧 `keyVersion` 验签;宽限结束或尝试更老版本 → 401,且不猜测其他密钥 - 边界:金额不一致或订单缺失 → 200 + `Difference`,不创建成功支付 - 边界:已取消、已到期或已由其他来源支付后收到 Success → 200 + `Difference`,订单终态不变 - 边界:到期订单收到 Failed → 200 + `ProcessedFailure`,同时进入同一过期取消通道且不再开放支付 +- 并发:先阻塞通道尝试或 DB096,待截止时间跨过后再释放 → Success 返回 `Difference/expired_success`,无成功支付;第一阶段终态不被后续取消结果改写 --- @@ -6823,7 +7168,7 @@ OrderPaymentLookupResponse { - **模块 / Tag**:Payment - **需求编号**:C08-FR06 - **负责人**:张海洋 -- **关联数据表**:DB090(待评审)— `reconciliation_batches` +- **关联数据表**:DB090 `reconciliation_batches` - **当前状态**:待交叉评审 - **用途**:分页查询每日对账批次 - **方法与路径**:`GET /api/admin/reconciliation/batches` @@ -6863,12 +7208,13 @@ OrderPaymentLookupResponse { "businessDate": "2026-07-22", "rangeFrom": "2026-07-22T00:00:00Z", "rangeTo": "2026-07-23T00:00:00Z", - "watermarkAt": "2026-07-23T00:00:05Z", + "watermarkSequence": 120045, + "watermarkAt": "2026-07-23T00:05:01Z", "totalCount": 100, "matchedCount": 98, "differenceCount": 2, "status": "HasDifferences", - "createdAt": "2026-07-23T01:00:00Z", + "createdAt": "2026-07-23T00:05:02Z", "resolvedAt": null } ], @@ -6893,8 +7239,8 @@ OrderPaymentLookupResponse { - 仅管理员访问(按 C08 业务规则:“对账数据仅向管理员开放”)。 - 批次提交时直接进入 `Matched` 或 `HasDifferences`;最后一个差异闭环后才进入 `Resolved`,不存在 `Pending` 批次。 -- `businessDate` 是按服务端成功提交时间归属的上一完整 UTC 业务日;`rangeFrom` / `rangeTo` 是半开区间,`watermarkAt` 是该批次一致读取水位。 -- 计数单位是“比较单元”:同一业务问题及同一比较规则只计一次,并满足 `totalCount = matchedCount + differenceCount`。 +- `businessDate` 可以是任一已经结束、尚未生成权威批次的完整 UTC 业务日;正常日更通常处理上一日,停机恢复时按日期升序补齐更早缺失日。`rangeFrom` / `rangeTo` 是半开区间,`watermarkSequence + watermarkAt` 共同冻结该批次一致读取水位。 +- 计数单位是唯一财务 `anchorPostingSequence`:`totalCount` 为本日去重锚点数,`matchedCount` 为零规则命中的锚点数,`differenceCount` 为至少命中一条规则的锚点数,始终满足 `totalCount = matchedCount + differenceCount`。同一锚点命中多条规则时 differenceCount 仍只加 1。 - 默认排序 `businessDate desc, batchId desc`。 #### 缓存、事件或外部依赖 @@ -6915,7 +7261,7 @@ OrderPaymentLookupResponse { - **模块 / Tag**:Payment - **需求编号**:C08-FR06 - **负责人**:张海洋 -- **关联数据表**:DB090(待评审)— `reconciliation_batches`、DB091(待评审)— `reconciliation_differences` +- **关联数据表**:DB090 `reconciliation_batches`、DB091 `reconciliation_differences` - **当前状态**:待交叉评审 - **用途**:查询单批对账详情 - **方法与路径**:`GET /api/admin/reconciliation/batches/{batchId}` @@ -6949,7 +7295,8 @@ OrderPaymentLookupResponse { "businessDate": "2026-07-22", "rangeFrom": "2026-07-22T00:00:00Z", "rangeTo": "2026-07-23T00:00:00Z", - "watermarkAt": "2026-07-23T00:00:05Z", + "watermarkSequence": 120045, + "watermarkAt": "2026-07-23T00:05:01Z", "totalCount": 100, "matchedCount": 98, "differenceCount": 2, @@ -6962,7 +7309,7 @@ OrderPaymentLookupResponse { "paymentUnits": 80, "refundUnits": 20 }, - "createdAt": "2026-07-23T01:00:00Z", + "createdAt": "2026-07-23T00:05:02Z", "resolvedAt": null } } @@ -6980,8 +7327,8 @@ OrderPaymentLookupResponse { #### 业务规则与并发 -- 详情返回与 A422 相同的日期、半开范围、水位、比较单元计数、状态和时间,并增加 `differenceCountsByType` 与支付/退款比较单元汇总。 -- `totalCount = matchedCount + differenceCount`;同一业务问题与同一比较规则即使同时来自回调 `Difference` 和横向比对,也只归入一个差异单元并保留多份证据。 +- 详情返回与 A422 相同的日期、半开范围、水位、锚点计数、状态和时间,并增加 `differenceCountsByType` 与支付/退款锚点汇总。`paymentUnits` 统计 Payment/Callback 锚点,`refundUnits` 统计 RefundOperation 锚点,满足 `totalCount = paymentUnits + refundUnits`。 +- `differenceCountsByType` 按 DB091 差异行分组,不按锚点去重,因此各类型数量之和可以大于 `differenceCount`。同一 `(batchId,anchorPostingSequence,comparisonRuleCode,comparisonRuleVersion)` 即使同时来自回调 Difference 与横向扫描,也只保留一行并累积全部证据;同一锚点命中不同规则必须分别返回。 #### 缓存、事件或外部依赖 @@ -7001,7 +7348,7 @@ OrderPaymentLookupResponse { - **模块 / Tag**:Payment - **需求编号**:C08-FR07 / FR08 - **负责人**:张海洋 -- **关联数据表**:DB091(待评审)— `reconciliation_differences` +- **关联数据表**:DB091(`reconciliation_differences`)、DB094(`reconciliation_evidence`) - **当前状态**:待交叉评审 - **用途**:分页查询某批次的所有差异 - **方法与路径**:`GET /api/admin/reconciliation/batches/{batchId}/differences` @@ -7040,17 +7387,24 @@ OrderPaymentLookupResponse { "differenceId": "...", "batchId": "...", "type": "LateSuccessCallback", - "subjectType": "Order", - "subjectId": "3f0ed9a9-...", + "anchorKind": "Callback", + "anchorId": "5a8e...", + "anchorPostingSequence": 120041, + "subjectType": "Callback", + "subjectId": "5a8e...", + "comparisonRuleCode": "C08.CALLBACK_LATE_SUCCESS", + "comparisonRuleVersion": 1, "orderId": "3f0ed9a9-...", "paymentId": "8d2e9d11-...", "refundOperationId": null, "callbackId": "5a8e...", "status": "Pending", + "version": 1, "currentAssigneeUserId": null, "claimExpiresAt": null, + "claimIsExpired": false, "evidenceCount": 2, - "createdAt": "2026-07-23T01:00:00Z", + "createdAt": "2026-07-23T00:05:02Z", "resolvedAt": null } ], @@ -7074,22 +7428,26 @@ OrderPaymentLookupResponse { #### 业务规则与并发 -- 差异类型至少识别: - - `PaymentSucceededOrderNotUpdated`:支付成功但订单未更新 - - `OrderPaidPaymentMissing`:订单已支付但缺支付记录或流水 - - `MultipleSuccessfulPaymentSources`:同一订单出现多个成功支付来源 - - `LateSuccessCallback`:已取消订单收到迟到成功回调 - - `CallbackBindingMismatch`:同一支付流水后续回调改变订单、金额或币种绑定 - - `ProcessedSuccessPaymentMissing`:回调为 `ProcessedSuccess`,但缺成功支付事实 - - `RefundedOperationMissing`:售后已 `Refunded` 但缺成功退款操作 - - `DuplicateRefundOperation`:同一售后申请存在多个成功退款操作 - - `RefundSucceededAfterSalesNotUpdated`:退款操作成功但售后未进入 `Refunded` - - `RefundSucceededWalletCreditMissing`:售后退款成功但小金库未入账 - - `DuplicateWalletCredit`:同一退款发生重复入账 - - `RefundAmountMismatch`:退款记录、钱包流水或入账金额不一致 +- 差异类型、锚点、主体和规则版本是封闭矩阵: + +| `type` | `anchorKind / anchorId / sequence` | `subjectType / subjectId` | `comparisonRuleCode / version` | +|---|---|---|---| +| `PaymentSucceededOrderNotUpdated` | Payment / paymentId / payment sequence | Order / orderId | `C08.PAYMENT_ORDER_LINEAGE / 1` | +| `OrderPaidPaymentMissing` | Payment / orderId / order payment sequence | Order / orderId | `C08.ORDER_PAYMENT_PRESENCE / 1` | +| `MultipleSuccessfulPaymentSources` | Payment / laterPaymentId / later sequence | Payment / laterPaymentId | `C08.ORDER_SINGLE_SUCCESS_SOURCE / 1` | +| `LateSuccessCallback` | Callback / callbackId / callback sequence | Callback / callbackId | `C08.CALLBACK_LATE_SUCCESS / 1` | +| `CallbackBindingMismatch` | Callback / callbackId / callback sequence | Callback / callbackId | `C08.CALLBACK_BINDING_IMMUTABLE / 1` | +| `ProcessedSuccessPaymentMissing` | Callback / callbackId / callback sequence | Callback / callbackId | `C08.CALLBACK_SUCCESS_PAYMENT_PRESENCE / 1` | +| `RefundedOperationMissing` | RefundOperation / requestId / after-sales refund sequence | AfterSalesRequest / requestId | `C08.AFTER_SALES_REFUND_OPERATION_PRESENCE / 1` | +| `DuplicateRefundOperation` | RefundOperation / laterRefundOperationId / later sequence | RefundOperation / laterRefundOperationId | `C08.REFUND_OPERATION_SINGLE_SUCCESS / 1` | +| `RefundSucceededAfterSalesNotUpdated` | RefundOperation / refundOperationId / refund sequence | AfterSalesRequest / requestId | `C08.REFUND_AFTER_SALES_TERMINAL / 1` | +| `RefundSucceededWalletCreditMissing` | RefundOperation / refundOperationId / refund sequence | RefundOperation / refundOperationId | `C08.REFUND_WALLET_CREDIT_PRESENCE / 1` | +| `DuplicateWalletCredit` | RefundOperation / refundOperationId / later wallet sequence | WalletTransaction / laterWalletTransactionId | `C08.REFUND_WALLET_SINGLE_CREDIT / 1` | +| `RefundAmountMismatch` | RefundOperation / refundOperationId / refund sequence | RefundOperation / refundOperationId | `C08.REFUND_AMOUNT_CONSISTENCY / 1` | + + `anchorKind` 只允许 `Payment/Callback/RefundOperation`;同一序号同时存在多个合法事实时按 `RefundOperation > Payment > Callback` 选择。缺失支付/退款操作时按表用 `orderId/requestId` 作为非空锚点 ID,不能使用随机 ID。 - 状态管理(按 C08-FR08):`Pending` / `InProgress` / `Resolved` -- 摘要固定返回差异主体 `subjectType + subjectId`、关联 `orderId` / `paymentId` / `refundOperationId` / `callbackId`、当前领取人、领取有效期、证据数量和创建/解决时间;不在列表中回传原始签名或敏感渠道报文。 -- 同一回调差异来源和每日横向比对指向同一业务对象及比较规则时,合并为一个差异并累积证据引用。 +- 摘要固定返回锚点三字段、主体、规则码/版本、关联 `orderId` / `paymentId` / `refundOperationId` / `callbackId`、当前 `version`、领取人、领取有效期/派生过期标记、证据数量和创建/解决时间;`version` 可直接作为 A425 `expectedVersion`,但执行时仍须重新校验。不在列表中回传原始签名或敏感渠道报文。 - 默认排序固定为 `createdAt desc, differenceId desc`。 #### 缓存、事件或外部依赖 @@ -7110,13 +7468,13 @@ OrderPaymentLookupResponse { - **模块 / Tag**:Payment - **需求编号**:C08-FR08 - **负责人**:张海洋 -- **关联数据表**:DB091(待评审)— `reconciliation_differences` +- **关联数据表**:DB090(`reconciliation_batches`)、DB091(`reconciliation_differences`)、DB094(`reconciliation_evidence`)、DB095(`reconciliation_actions`)、DB104(`idempotency_records`) - **当前状态**:待交叉评审 - **用途**:管理员领取、释放、接管或闭环对账差异 - **方法与路径**:`POST /api/admin/reconciliation/differences/{differenceId}/process` - **operationId**:`Payment_ProcessReconciliationDifference` - **请求 Schema**:`ProcessDifferenceRequest` -- **响应 Schema**:`ReconciliationDifferenceDetailResponse` +- **响应 Schema**:`ProcessReconciliationDifferenceResponse` - **身份与 Policy**:JWT Bearer + `AdminOnly` - **资源归属**:N/A(管理员) - **幂等要求**:**必须支持 `Idempotency-Key`**(按 1.12.1 状态副作用) @@ -7130,50 +7488,54 @@ OrderPaymentLookupResponse { ```json { "action": "Resolve", + "expectedVersion": 7, "resolutionType": "CorrectedByControlledAction", - "resolutionNote": "已通过订单模块受控动作完成纠正并复核一致", - "evidenceRefs": ["evidence://ordering/action/9c10..."], - "controlledActionRef": "ordering-action:9c10..." + "resolutionNote": "退款恢复已完成,售后、退款操作和钱包入账重新核对一致", + "evidenceRefs": [ + "payment:refund-operation:4f9c...", + "payment:wallet-transaction:7241..." + ], + "controlledActionRef": "payment:refund-recovery:8b8b9c4e..." } ``` - **校验规则**: - `differenceId` 必填 + - `expectedVersion` 必填,正整数;必须等于 DB091 当前版本,所有动作都使用它防止旧页面覆盖新领取或解决结果 - `action` 枚举:`Claim` / `Release` / `Takeover` / `Resolve` - `Claim`:当前状态必须为 `Pending` - - `Release`:当前状态必须为 `InProgress` 且当前管理员仍是有效领取人;`resolutionNote` 作为释放原因必填 + - `Release`:当前状态必须为 `InProgress`、当前管理员仍是领取人,且数据库时间严格早于 `claimExpiresAt`;`resolutionNote` 作为释放原因必填 - `Takeover`:当前状态必须为 `InProgress`,且当前无领取人,或原领取人被禁用、主动释放、领取已过期;必须填写接管原因 - - `Resolve`:当前状态必须为 `InProgress` 且当前管理员仍是有效领取人 + - `Resolve`:当前状态必须为 `InProgress`、当前管理员仍是领取人,且数据库时间严格早于 `claimExpiresAt` - `resolutionType` 仅 `Resolve` 必填,枚举:`CorrectedByControlledAction` / `ConfirmedNoBusinessImpact` - `resolutionNote` 在 `Release` / `Takeover` / `Resolve` 时必填,1~1000 字 - `evidenceRefs` 在 `Resolve` 时至少 1 项;单项 1~500 字,最多 20 项 - - `CorrectedByControlledAction` 必须填写 `controlledActionRef`,引用事实所属模块已经成功提交的受控动作 + - `CorrectedByControlledAction` 必须填写 `controlledActionRef`,引用事实所属模块已经成功提交的受控动作;`ConfirmedNoBusinessImpact` 时该字段必须为 `null` - `Idempotency-Key` 必填 + - 请求采用封闭字段模型,未知字段一律返回 `COMMON.VALIDATION_FAILED`,不能静默忽略。各动作允许字段矩阵固定如下: + +| action | 必填业务字段 | 允许为空/省略 | 禁止携带 | +|---|---|---|---| +| `Claim` | `action`、`expectedVersion` | 无 | `resolutionNote`、`resolutionType`、`evidenceRefs`、`controlledActionRef` | +| `Release` | `action`、`expectedVersion`、`resolutionNote` | 无 | `resolutionType`、`evidenceRefs`、`controlledActionRef` | +| `Takeover` | `action`、`expectedVersion`、`resolutionNote` | 无 | `resolutionType`、`evidenceRefs`、`controlledActionRef` | +| `Resolve + CorrectedByControlledAction` | `action`、`expectedVersion`、`resolutionType`、`resolutionNote`、非空 `evidenceRefs`、`controlledActionRef` | 无 | 无 | +| `Resolve + ConfirmedNoBusinessImpact` | `action`、`expectedVersion`、`resolutionType`、`resolutionNote`、非空 `evidenceRefs` | `controlledActionRef` 只能省略或为 `null` | 非空 `controlledActionRef` | #### 成功响应 - **HTTP 状态**:`200 OK` -- **响应 Schema**:`ReconciliationDifferenceDetailResponse` -- **示例**: -```json -{ - "code": "success", - "message": "ok", - "data": { - "differenceId": "...", - "batchId": "...", - "type": "LateSuccessCallback", - "status": "Resolved", - "resolutionType": "CorrectedByControlledAction", - "resolutionNote": "已通过订单模块受控动作完成纠正并复核一致", - "currentAssigneeUserId": "admin-uuid", - "evidenceRefs": ["evidence://ordering/action/9c10..."], - "verificationResult": "Matched", - "resolvedAt": "2026-07-23T03:00:00Z", - "resolvedBy": "admin-uuid" - } +- **响应 Schema**: + +```text +ProcessReconciliationDifferenceResponse { + difference: ReconciliationDifferenceDetailResponse // 动作提交后的完整当前详情,结构与 A426 完全相同 + batchStatus: "HasDifferences" | "Resolved" // 同一事务提交后的批次状态 } ``` +- `Claim`、`Release`、`Takeover` 和复核失败后通常返回 `batchStatus=HasDifferences`;`Resolve` 关闭批次最后一条未解决差异时返回 `Resolved`,否则仍为 `HasDifferences`。 +- `difference` 不允许使用旧的扁平 `currentAssigneeUserId/resolutionType/verificationResult` 结构;领取、处置和复核分别固定放入 A426 定义的 `claim/resolution/verification`,全过程放入 `timeline`。 + #### 失败响应 | HTTP 状态 | 业务错误码 | 触发条件 | @@ -7185,20 +7547,25 @@ OrderPaymentLookupResponse { | 404 | `PAYMENT.RECONCILIATION_DIFFERENCE_NOT_FOUND` | 差异不存在 | | 409 | `PAYMENT.RECONCILIATION_INVALID_STATUS` | 当前状态不允许该动作 | | 409 | `PAYMENT.RECONCILIATION_CLAIM_CONFLICT` | 领取人或领取有效期已变化 | +| 409 | `PAYMENT.RECONCILIATION_VERSION_CONFLICT` | `expectedVersion` 与当前差异版本不一致;响应返回最新版本、状态和领取摘要 | | 409 | `PAYMENT.RECONCILIATION_STILL_MISMATCHED` | 重跑原比较规则后仍不一致,差异保持 `InProgress` | | 409 | `IDEMPOTENCY.KEY_REUSED` | 同一 Key 不同请求内容 | | 500 | `COMMON.INTERNAL_ERROR` | 未知服务端错误 | #### 业务规则与并发 -- 完成固定字段校验后先查询幂等结果;同 Key 同请求重放首次结果,同 Key 不同请求返回 `IDEMPOTENCY.KEY_REUSED`,再读取当前状态和领取权。 -- `Claim` 使用条件更新唯一推进 `Pending → InProgress` 并记录领取人、领取时间和服务端配置的有效期;并发领取只有一人成功。 +- 完成封闭字段与动作矩阵校验后先查询幂等结果;唯一范围为 `(adminUserId, A425, Idempotency-Key)`。请求指纹只覆盖该动作**允许出现**的规范字段:共同部分为 `differenceId + action + expectedVersion`;Release/Takeover 追加去首尾空白后的 `resolutionNote`;Resolve 再追加 `resolutionType + resolutionNote + 按规范字符串去重后字节升序排列的 evidenceRefs + 规范化 controlledActionRef/null`。禁止字段在计算指纹前就拒绝,省略与允许的 `null` 只按上表归一。同 Key 同指纹重放首次结果,同 Key 换内容返回 `IDEMPOTENCY.KEY_REUSED`,再读取当前版本、状态和领取权。 +- `Claim` 使用条件更新唯一推进 `Pending → InProgress`。成功时只取一次数据库权威时间作为 `claimedAt`,并固定写入 `claimExpiresAt = claimedAt + 30 minutes`;并发领取只有一人成功。本期没有 `Renew` 动作、心跳续期或后台延长入口,打开页面、查询详情、保存草稿和部署配置均不得改变 30 分钟。 - `Release` 保持差异为 `InProgress`,只把 `currentAssigneeUserId`、`claimedAt`、`claimExpiresAt` 清空并记录释放原因与时间;不得把差异退回 `Pending` 或清空既有领取历史。 -- `Takeover` 在 `InProgress` 上写入新的当前领取人和有效期,并保留释放、失效和转交历史;任何时刻只有当前有效领取人可提交 `Resolve`。 -- C08 不直接修改订单、支付、退款或钱包表。确需纠正时,管理员先调用事实所属模块的受控业务动作,再在 `Resolve` 中引用其已提交结果。 -- `Resolve` 必须重新读取权威事实并重跑生成该差异的同一比较规则;仍不一致时返回 `PAYMENT.RECONCILIATION_STILL_MISMATCHED` 并保持 `InProgress`,不能只凭文字说明关闭。 -- 仅“所属模块已纠正且复核一致”或“按固定规则确认无未决资金/订单影响”可进入 `Resolved`。 -- 差异关闭、处置类型、说明、证据、处理人、复核结果、状态时间线,以及关闭最后一条差异时批次 `HasDifferences → Resolved`,必须在同一事务提交。 +- `Takeover` 在 `InProgress` 上使用一次数据库权威时间写入新的当前领取人、`claimedAt` 和固定 `claimExpiresAt = claimedAt + 30 minutes`,并保留释放、失效和转交历史;接管也不可续期。任何时刻只有当前领取人、领取未过期且 `expectedVersion` 匹配时可提交 `Resolve`。 +- 到达 `claimExpiresAt` 后差异仍为 `InProgress`,但原领取权立即失效;原处理人调用 `Release` 或 `Resolve` 必须返回 `409 PAYMENT.RECONCILIATION_CLAIM_CONFLICT`,不得以请求已经在编辑或网络在途为由延长。页面须先把尚未提交的说明、证据和受控动作引用保存在本地草稿,刷新 A426 取得最新 `version/claim`,再使用新的 `Idempotency-Key`、最新 `expectedVersion` 和显式 `Takeover` 重新领取;若其他管理员已经接管,只能保留或复制本地草稿,不得覆盖其领取或处置。 +- C08 不直接修改订单、支付、退款、钱包或库存表,本接口也不提供 `Correct`。确需纠正时,合法主体先通过事实所属模块既有受控动作完成;所属模块事务成功后签发稳定 `controlledActionRef`,当前领取管理员再用 `Resolve` 引用。 +- 服务端不能依赖 `controlledActionRef` 的字符串格式判断真实性,必须通过所属模块只读公开能力验证:引用真实存在且成功提交;模块与差异类型相符;订单/支付/售后/退款/钱包主体、买家、金额、币种和稳定操作身份与当前差异一致;动作发生时间能够解释差异。失败、处理中、Unknown、已回滚、日志文本、截图链接和手写编号都不是合法受控动作。 +- `Resolve` 必须重新读取权威事实并按 DB091 保存的 `comparisonRuleCode + comparisonRuleVersion` 重跑原比较规则;仍不一致时返回 `PAYMENT.RECONCILIATION_STILL_MISMATCHED`、保持 `InProgress`、写入 DB095 `verification_failed`,并返回最新 `version` 与 `remainingMismatchReason`。 +- 仅“所属模块已纠正且复核一致”或“按 C08 流程 8.1 对应差异类型的固定规则确认无未决业务影响”可进入 `Resolved`。十二类差异的允许/禁止矩阵是规范边界;没有合法纠正动作且不能证明无影响时必须保持 `InProgress`。 +- 所属模块纠正事务与 A425 闭环事务是两个独立事务,不能伪造成跨模块大事务。A425 事务只锁定 DB091/DB090,校验版本、当前领取和幂等,验证引用、重跑比较规则,并原子更新 DB094/DB095/DB091/DB104 及必要的 DB090;不得更新业务模块事实。 +- `Resolve` 请求中的每个 `evidenceRefs` 元素都写成一条 DB094 `phase=resolution` 证据,`sourceType=controlled_action_ref` 或 `resolution_reference`,`sourceId` 保存完整 1~500 字引用并用规范内容哈希去重;不得把数组拼接进一列或只保存第一项。 +- 差异关闭、处置类型、说明、证据、处理人、复核结果、状态时间线,以及关闭最后一条差异时批次 `HasDifferences → Resolved`,必须在同一事务提交。进入 `Resolved` 后清空 `currentAssigneeUserId/claimedAt/claimExpiresAt`,最终处理人只由 `resolvedByUserId`/响应 `resolvedBy` 表达。 - 可确定的领取冲突、状态冲突和复核仍不一致结果也保存为可重放幂等结果;提交结果未知不固化为业务失败。 #### 缓存、事件或外部依赖 @@ -7210,8 +7577,13 @@ OrderPaymentLookupResponse { #### 验证场景 - 正常:管理员 Claim → 唯一领取并进入 `InProgress` +- 正常:Claim/Takeover 返回的 `claimExpiresAt` 精确等于本次 `claimedAt + 30 minutes`,持续查询和编辑不会续期 - 正常:当前领取人 Resolve 且复核一致 → 差异进入 `Resolved`,必要时同事务关闭批次 - 异常:仍不一致 → 409 + `PAYMENT.RECONCILIATION_STILL_MISMATCHED`,保持 `InProgress` +- 异常:旧页面携带过期 `expectedVersion` → 409 + `PAYMENT.RECONCILIATION_VERSION_CONFLICT`,返回最新版本且不覆盖当前领取/解决结果 +- 异常:原领取人在固定期限过后提交 Resolve → 409 + `PAYMENT.RECONCILIATION_CLAIM_CONFLICT`;本地保留草稿、刷新 A426 后才可显式 Takeover +- 异常:受控动作引用不存在、未成功或主体/金额不匹配 → 409 + `PAYMENT.RECONCILIATION_STILL_MISMATCHED`,保持 `InProgress` +- 边界:`duplicate_wallet_credit` 在本期没有已确认冲正动作且存在真实重复入账 → 保持 `InProgress`,不得自动扣余额或删除流水 - 异常:状态已为 `Resolved` → 409 + `PAYMENT.RECONCILIATION_INVALID_STATUS` - 异常:买家调用 → 403 + `AUTH.FORBIDDEN` @@ -7222,7 +7594,7 @@ OrderPaymentLookupResponse { - **模块 / Tag**:Payment - **需求编号**:C08-FR07 / FR08 - **负责人**:张海洋 -- **关联数据表**:DB091(待评审)— `reconciliation_differences` 及其来源、证据和状态时间线 +- **关联数据表**:DB091(`reconciliation_differences`)、DB094(`reconciliation_evidence`)、DB095(`reconciliation_actions`) - **当前状态**:待交叉评审 - **用途**:管理员查看单条差异的比较规则、权威事实、全部来源证据、领取与处置时间线 - **方法与路径**:`GET /api/admin/reconciliation/differences/{differenceId}` @@ -7245,6 +7617,82 @@ OrderPaymentLookupResponse { - **HTTP 状态**:`200 OK` - **响应 Schema**:`ReconciliationDifferenceDetailResponse` +- **固定结构**: + +```text +ReconciliationDifferenceDetailResponse { + differenceId: uuid + batchId: uuid + type: string + status: "Pending" | "InProgress" | "Resolved" + version: integer + anchorKind: "Payment" | "Callback" | "RefundOperation" + anchorId: uuid + anchorPostingSequence: integer + subjectType: "Order" | "Payment" | "Callback" | "AfterSalesRequest" | "RefundOperation" | "WalletTransaction" + subjectId: uuid + comparisonRuleCode: string + comparisonRuleVersion: integer + comparisonRuleText: string + expectedFacts: object + actualFacts: object + references: { + orderId: uuid? + paymentId: uuid? + refundOperationId: uuid? + callbackIds: uuid[] + } + evidenceRefs: ReconciliationEvidenceResponse[] { + sourceType: string + sourceId: string + observedAt: datetime + } + claim: { + currentAssigneeUserId: uuid + claimedAt: datetime + claimExpiresAt: datetime + isExpired: boolean + }? + resolution: { + resolutionType: "CorrectedByControlledAction" | "ConfirmedNoBusinessImpact" + resolutionNote: string + controlledActionRef: string? + evidenceRefs: string[] + resolvedByUserId: uuid + resolvedAt: datetime + }? + verification: { + lastVerifiedAt: datetime? + result: "Matched" | "StillMismatched" | null + remainingMismatchReason: string? + } + timeline: ReconciliationTimelineItemResponse[] + serverTime: datetime + createdAt: datetime + resolvedAt: datetime? +} +``` + +- `claim` 在当前仍保存领取人时非空,包括已经到期但尚未 Release/Takeover 的领取;`isExpired` 由本次响应的数据库 `serverTime >= claimExpiresAt` 派生,不是持久化授权。Release 或最终 Resolved 后为 `null`,Takeover 后替换为新领取;全部历史仍保留在 `timeline`。 +- `resolution` 仅在 `status=Resolved` 时非空,且其 `resolvedAt` 必须等于顶层 `resolvedAt`;`ConfirmedNoBusinessImpact` 的 `controlledActionRef=null`。 +- `comparisonRuleCode + comparisonRuleVersion` 是生成与复核事实源;`comparisonRuleText` 是生成时保存的安全展示文案,不参与规则选择、幂等或复核。`expectedFacts` 与 `actualFacts` 必须采用下面固定键集合,不能用自由文本、当前读模型或未知扩展键替代。 + +版本 1 的十二类 JSON 规则与 C08 流程 7.1 是同一事实源;键必须全部出现,缺失值使用 JSON `null`,不得省略。金额使用两位小数 CNY 字符串,UUID 使用小写 `D` 格式,时间使用 UTC,ID 数组按 `postingSequence`、ID 升序。规则升级必须新增版本,不得改写已经生成的差异: + +| `type` / `comparisonRuleCode` / version | `expectedFacts` 固定键 | `actualFacts` 固定键 | +|---|---|---| +| `PaymentSucceededOrderNotUpdated` / `C08.PAYMENT_ORDER_LINEAGE` / 1 | `paymentId,orderId,success,amount,currency,paymentPostingSequence,allowedOrderStatuses` | `orderStatus,paidAt,paymentPostingSequence,orderAmount,orderCurrency` | +| `OrderPaidPaymentMissing` / `C08.ORDER_PAYMENT_PRESENCE` / 1 | `orderId,orderPaidLineage,paymentPostingSequence,successfulPaymentCount,amount,currency` | `orderStatus,paidAt,paymentPostingSequence,successfulPaymentCount,paymentIds,paymentPostingSequences` | +| `MultipleSuccessfulPaymentSources` / `C08.ORDER_SINGLE_SUCCESS_SOURCE` / 1 | `orderId,successfulPaymentCount,authoritativePaymentId,amount,currency` | `successfulPaymentCount,paymentIds,sourceTypes,postingSequences,amounts,currencies` | +| `LateSuccessCallback` / `C08.CALLBACK_LATE_SUCCESS` / 1 | `acceptedSuccess,paymentCreated,walletDelta,successNotificationCreated` | `callbackResult,callbackDisposition,orderId,orderStatus,paymentDeadline,finalTime,paymentCreated,walletDelta,successNotificationCreated` | +| `CallbackBindingMismatch` / `C08.CALLBACK_BINDING_IMMUTABLE` / 1 | `channelTransactionNo,boundOrderId,boundAmount,boundCurrency` | `callbackId,claimedOrderId,claimedAmount,claimedCurrency,mismatchedFields` | +| `ProcessedSuccessPaymentMissing` / `C08.CALLBACK_SUCCESS_PAYMENT_PRESENCE` / 1 | `callbackDisposition,successfulPaymentCount,orderPaidLineage,orderId,amount,currency` | `callbackDisposition,successfulPaymentCount,paymentIds,orderStatus,paidAt,paymentPostingSequence` | +| `RefundedOperationMissing` / `C08.AFTER_SALES_REFUND_OPERATION_PRESENCE` / 1 | `afterSalesRequestId,afterSalesStatus,successfulRefundOperationCount,approvedRefundAmount,currency,refundPostingSequence` | `afterSalesStatus,closedAt,refundPostingSequence,successfulRefundOperationCount,refundOperationIds` | +| `DuplicateRefundOperation` / `C08.REFUND_OPERATION_SINGLE_SUCCESS` / 1 | `afterSalesRequestId,successfulRefundOperationCount,authoritativeRefundOperationId,amount,currency` | `successfulRefundOperationCount,refundOperationIds,postingSequences,amounts,currencies` | +| `RefundSucceededAfterSalesNotUpdated` / `C08.REFUND_AFTER_SALES_TERMINAL` / 1 | `refundOperationId,refundOperationStatus,afterSalesStatus,amount,currency,refundPostingSequence` | `refundOperationStatus,completedAt,afterSalesStatus,closedAt,refundPostingSequence` | +| `RefundSucceededWalletCreditMissing` / `C08.REFUND_WALLET_CREDIT_PRESENCE` / 1 | `refundOperationId,buyerId,walletCreditCount,walletCreditAmount,currency,refundPostingSequence` | `walletCreditCount,walletTransactionIds,walletCreditAmounts,walletPostingSequences` | +| `DuplicateWalletCredit` / `C08.REFUND_WALLET_SINGLE_CREDIT` / 1 | `refundOperationId,buyerId,walletCreditCount,walletCreditAmount,currency` | `walletCreditCount,walletTransactionIds,walletCreditAmounts,walletPostingSequences` | +| `RefundAmountMismatch` / `C08.REFUND_AMOUNT_CONSISTENCY` / 1 | `afterSalesRequestId,refundOperationId,approvedRefundAmount,refundOperationAmount,walletCreditAmount,paymentRefundedTotalDelta,currency` | `approvedRefundAmount,refundOperationAmount,walletCreditAmount,paymentRefundedTotalDelta,currencies,walletCreditCount` | - **示例**: ```json { @@ -7255,17 +7703,31 @@ OrderPaymentLookupResponse { "batchId": "f12a...", "type": "LateSuccessCallback", "status": "InProgress", - "subjectType": "Order", - "subjectId": "3f0ed9a9-...", - "comparisonRule": "订单取消后不得存在新的成功支付来源", + "version": 7, + "anchorKind": "Callback", + "anchorId": "5a8e...", + "anchorPostingSequence": 120041, + "subjectType": "Callback", + "subjectId": "5a8e...", + "comparisonRuleCode": "C08.CALLBACK_LATE_SUCCESS", + "comparisonRuleVersion": 1, + "comparisonRuleText": "截止后成功回调不得创建支付、钱包变化或成功通知", "expectedFacts": { - "orderStatus": "Cancelled", - "successfulPaymentCount": 0 + "acceptedSuccess": false, + "paymentCreated": false, + "walletDelta": "0.00", + "successNotificationCreated": false }, "actualFacts": { + "callbackResult": "Success", + "callbackDisposition": "expired_success", + "orderId": "3f0ed9a9-...", "orderStatus": "Cancelled", - "successfulPaymentCount": 0, - "lateSuccessCallbackCount": 1 + "paymentDeadline": "2026-07-23T00:30:00Z", + "finalTime": "2026-07-23T00:40:00Z", + "paymentCreated": false, + "walletDelta": "0.00", + "successNotificationCreated": false }, "references": { "orderId": "3f0ed9a9-...", @@ -7283,7 +7745,8 @@ OrderPaymentLookupResponse { "claim": { "currentAssigneeUserId": "admin-uuid", "claimedAt": "2026-07-23T02:00:00Z", - "claimExpiresAt": "2026-07-23T02:30:00Z" + "claimExpiresAt": "2026-07-23T02:30:00Z", + "isExpired": false }, "resolution": null, "verification": { @@ -7292,19 +7755,20 @@ OrderPaymentLookupResponse { "remainingMismatchReason": null }, "timeline": [ - { "action": "Created", "at": "2026-07-23T01:00:00Z", "actorType": "System" }, + { "action": "Created", "at": "2026-07-23T00:05:02Z", "actorType": "System" }, { "action": "Claimed", "at": "2026-07-23T02:00:00Z", "actorType": "Admin" } ], - "createdAt": "2026-07-23T01:00:00Z", + "serverTime": "2026-07-23T02:05:00Z", + "createdAt": "2026-07-23T00:05:02Z", "resolvedAt": null } } ``` -- `expectedFacts` / `actualFacts` 返回生成该差异时使用的固定比较口径与安全业务摘要,不回传支付签名、密钥、钱包敏感字段或异常堆栈。 +- `anchorKind/anchorId/anchorPostingSequence`、`subjectType/subjectId` 与 A424 封闭矩阵逐项一致;六种 `subjectType` 和三种 `anchorKind` 之外的值不得序列化。`expectedFacts` / `actualFacts` 返回生成该差异时使用的固定比较口径与安全业务摘要,不回传支付签名、密钥、钱包敏感字段或异常堆栈。 - `references` 包含适用的 `orderId`、`paymentId`、`refundOperationId`、`callbackIds`;不存在的关联项返回 `null` 或空数组,不伪造标识。 - `evidenceRefs` 返回全部发现来源。回调 `Difference` 和横向比对命中同一业务对象与比较规则时保留多份证据,但仍是一条差异。 -- `claim` 返回当前领取人、领取时间和有效期;`resolution` 在已关闭后返回处置类型、说明、证据引用、受控动作引用、处理人和处理时间。 +- `version` 是 A425 所需的当前并发版本;任何领取、释放、接管、复核失败或解决写入都会递增。`claim` 返回当前领取人、领取时间、固定 30 分钟有效期和本次读取时的派生过期标记;过期领取仍展示但不再授予 Release/Resolve 权限,客户端应保留本地草稿、刷新后显式 Takeover。`resolution` 在已关闭后返回处置类型、说明、证据引用、受控动作引用、处理人和处理时间。 - `verification` 返回最后复核时间、`Matched` / `StillMismatched` 结果和仍不一致原因;`timeline` 保留创建、领取、释放、接管、复核失败和解决全过程。 #### 失败响应 @@ -7320,7 +7784,7 @@ OrderPaymentLookupResponse { #### 业务规则与并发 - 只读取已提交的批次、差异、来源、证据、领取和处置事实,不在 GET 中自动领取、修复或改变状态。 -- 当前领取人失效或过期仍如实返回,是否可接管由 A425 在执行时重新校验。 +- 当前领取人失效或过期仍在 `claim` 中如实返回并令 `isExpired=true`,是否可接管由 A425 使用数据库权威时间与最新版本重新校验;GET 不自动释放、续期或接管。 - 管理员只能通过 A425 引用所属模块已完成的受控动作并重跑比较规则,不能在详情接口直接修改资金或订单。 #### 缓存、事件或外部依赖 @@ -7333,6 +7797,7 @@ OrderPaymentLookupResponse { - 正常:管理员查看差异 → 返回比较事实、全部证据、领取信息和完整时间线 - 正常:差异已解决 → 返回处置类型、证据、处理人与最终复核结果 +- 边界:领取已超过 30 分钟但无人接管 → 仍返回原 `claim`、`isExpired=true` 和最新 `version`,不在 GET 中清空或续期 - 异常:差异不存在 → 404 + `PAYMENT.RECONCILIATION_DIFFERENCE_NOT_FOUND` - 异常:非管理员调用 → 403 + `AUTH.FORBIDDEN` @@ -7343,7 +7808,7 @@ OrderPaymentLookupResponse { - **模块 / Tag**:AfterSales - **需求编号**:M10-FR11 - **负责人**:张海洋 -- **关联数据表**:DB086(待评审)— `after_sales_requests`、DB087(待评审)— `after_sales_status_histories`;退货信息作为申请从属数据由数据库设计确认是否单独建表 +- **关联数据表**:DB086 `after_sales_requests`、DB087 `after_sales_status_histories`、DB092 `after_sales_return_shipments`、DB102 `outbox_messages`、DB104 `idempotency_records` - **当前状态**:待交叉评审 - **用途**:买家提交退货的物流单号与快递公司(用于 `ReturnAndRefund` 类型的申请) - **方法与路径**:`POST /api/after-sales/requests/{requestId}/return-info` @@ -7454,14 +7919,14 @@ OrderPaymentLookupResponse { --- -> 来源:[`interface-lhc.md`](interface/interface-lhc.md)。HTTP 主体能力已覆盖;Messaging 集成事件和 SignalR 契约已登记,仍待 DB101~DB120、来源模块评审与真实 OpenAPI。 +> 来源:[`interface-lhc.md`](interface/interface-lhc.md)。HTTP 主体能力已覆盖;Messaging 集成事件、SignalR 契约及 DB101~DB107 映射已登记,仍待来源模块评审与真实 OpenAPI。 ### A501 查询本人消息列表 - 模块 / Tag:Messaging - 需求编号:X03-FR03、X03-FR10、X03-FR11 - 负责人:罗皓晨 -- 关联数据表:待 `docs/02-设计文档/database/database-lhc.md` 确认 +- 关联数据表:DB101 `messages` - 当前状态:待交叉评审 - 用途:按创建时间倒序分页查询当前用户自己的消息。 - 方法与路径:`GET /api/messages` @@ -7564,7 +8029,7 @@ OrderPaymentLookupResponse { - 模块 / Tag:Messaging - 需求编号:X03-FR04、X03-FR11 - 负责人:罗皓晨 -- 关联数据表:待 `docs/02-设计文档/database/database-lhc.md` 确认 +- 关联数据表:DB101 `messages` - 当前状态:待交叉评审 - 用途:查询当前用户拥有的一条完整站内消息。 - 方法与路径:`GET /api/messages/{messageId}` @@ -7645,7 +8110,7 @@ OrderPaymentLookupResponse { - 模块 / Tag:Messaging - 需求编号:X03-FR05、C06-FR05 - 负责人:罗皓晨 -- 关联数据表:待 `docs/02-设计文档/database/database-lhc.md` 确认 +- 关联数据表:DB101 `messages` - 当前状态:待交叉评审 - 用途:为消息入口角标、首次连接和断线重连补偿提供当前未读总数。 - 方法与路径:`GET /api/messages/unread-count` @@ -7709,7 +8174,7 @@ OrderPaymentLookupResponse { - 模块 / Tag:Messaging - 需求编号:X03-FR06 - 负责人:罗皓晨 -- 关联数据表:待 `docs/02-设计文档/database/database-lhc.md` 确认 +- 关联数据表:DB101 `messages` - 当前状态:待交叉评审 - 用途:幂等地记录当前用户一条消息的首次已读时间。 - 方法与路径:`POST /api/messages/{messageId}/read` @@ -7778,7 +8243,7 @@ OrderPaymentLookupResponse { - 模块 / Tag:Messaging - 需求编号:X03-FR07 - 负责人:罗皓晨 -- 关联数据表:待 `docs/02-设计文档/database/database-lhc.md` 确认 +- 关联数据表:DB101 `messages` - 当前状态:待交叉评审 - 用途:将操作开始时当前用户已经存在的未读消息批量标记为已读。 - 方法与路径:`POST /api/messages/read-all` @@ -7801,6 +8266,8 @@ OrderPaymentLookupResponse { - HTTP 状态:`200 OK` - 响应 Schema:`MarkAllMessagesReadResponse` +- `highWatermark`:操作开始时本人已提交历史消息的最大 `serverSequence`;从未有历史消息时固定为 `0`。 +- `readAt`:本次确实更新至少一条消息时使用的同一数据库 UTC 时间;`markedCount=0` 时固定为 `null`,不得返回历史批次时间或伪造当前时间。 - 示例: ```json @@ -7826,9 +8293,10 @@ OrderPaymentLookupResponse { #### 业务规则与并发 -- 更新条件必须包含当前认证用户、`isRead = false` 和 `serverSequence <= highWatermark`。 +- 更新条件必须包含当前认证用户、`read_at IS NULL` 和 `serverSequence <= highWatermark`;`isRead` 只是响应中由 `read_at IS NOT NULL` 派生的字段,不是数据库更新条件。 - `serverSequence` 是消息落库时由 PostgreSQL 产生的稳定单调序列;`highWatermark` 是本次动作开始时当前用户可见的已提交消息上界。不能用 `createdAt` 或 `readAt` 划定范围。 -- `readAt` 只表示本次批量写入的服务端时间,不承担消息范围截止语义。 +- 从未有历史消息时返回 `highWatermark=0, markedCount=0, readAt=null`;有历史但本次没有未读消息时返回现有最大高水位,同时仍为 `markedCount=0, readAt=null`。 +- 只有条件更新命中一条及以上消息时才取得一个数据库时间并写入全部命中行,响应返回同一 `readAt`;零更新不得写库,也不得返回上一次批量时间。 - 高水位捕获后到达的新消息保持未读;即使新消息与本批消息拥有相同 `createdAt`,也不能被误标为已读。 - `markedCount` 是本次首次变为已读的记录数,不是用户历史消息总数。 @@ -7839,7 +8307,7 @@ OrderPaymentLookupResponse { #### 验证场景 -- 验证存在多条未读、没有未读、重复调用、操作期间并发到达新消息,以及相同 `createdAt` 但序列高于高水位的消息。 +- 验证存在多条未读、从未有历史、已有历史但没有未读、重复调用、操作期间并发到达新消息,以及相同 `createdAt` 但序列高于高水位的消息;两种零更新场景都必须 `readAt=null`。 - 使用两个用户确认只更新当前用户数据。 ### A506 API 存活检查 @@ -7944,6 +8412,13 @@ OrderPaymentLookupResponse { "migrationVersion": "healthy", "postgres": "healthy" }, + "authenticationConfiguration": { + "digestAlgorithm": "SHA-256", + "expectedDigest": "1111111111111111111111111111111111111111111111111111111111111111", + "httpDigest": "1111111111111111111111111111111111111111111111111111111111111111", + "hubDigest": "1111111111111111111111111111111111111111111111111111111111111111", + "matchesExpected": true + }, "capabilities": { "catalogCache": "available", "protectedAuthentication": "available", @@ -7954,6 +8429,16 @@ OrderPaymentLookupResponse { } ``` +`authenticationConfiguration` 是固定对象: + +| 字段 | 类型 | 可空 | 规则 | +|---|---|---:|---| +| `digestAlgorithm` | string | 否 | 固定 `SHA-256` | +| `expectedDigest` | string | 是 | Compose 注入的预期 64 位小写十六进制摘要;缺失或格式错误时返回 `null` 并令实例 NotReady | +| `httpDigest` | string | 是 | 本实例 HTTP Bearer 实际生效选项摘要;无法安全计算时为 `null` 并令实例 NotReady | +| `hubDigest` | string | 是 | 本实例 SignalR Hub 实际生效选项摘要;无法安全计算时为 `null` 并令实例 NotReady | +| `matchesExpected` | boolean | 否 | 仅三份非空摘要完全相同时为 `true` | + #### 失败响应 | HTTP 状态 | 响应 | 触发条件 | @@ -7970,10 +8455,17 @@ OrderPaymentLookupResponse { "version": "commit-sha-or-version-tag", "checkedAt": "2026-07-24T02:46:00Z", "globalGates": { - "secureConfiguration": "healthy", + "secureConfiguration": "unhealthy", "runtimeVersion": "healthy", "migrationVersion": "healthy", - "postgres": "unhealthy" + "postgres": "healthy" + }, + "authenticationConfiguration": { + "digestAlgorithm": "SHA-256", + "expectedDigest": "1111111111111111111111111111111111111111111111111111111111111111", + "httpDigest": "2222222222222222222222222222222222222222222222222222222222222222", + "hubDigest": "1111111111111111111111111111111111111111111111111111111111111111", + "matchesExpected": false }, "capabilities": { "catalogCache": "fallback", @@ -7988,6 +8480,10 @@ OrderPaymentLookupResponse { #### 业务规则与并发 - 全局 `Ready` 固定且只由四项门槛决定:安全配置完整、运行版本兼容、Migration 版本等于部署目标、PostgreSQL 可用。任一失败都返回 `503`,Nginx 不再向该实例分发新流量。 +- HTTP Bearer 与 SignalR Hub 分别从各自**实际生效选项**按以下精确顺序生成源对象:`issuer,audience,keyFingerprint,accessTokenLifetimeSeconds,clockSkewSeconds,tokenVersionValidationRule`。字符串必须为 Unicode NFC 且不带首尾空白,整数使用十进制,`clockSkewSeconds` 必须为 `0`;按该顺序输出固定属性名、无空白 UTF-8 JSON,再计算 SHA-256 小写 64 位十六进制摘要。 +- `keyFingerprint` 只能来自密钥管理提供的非秘密 KeyId 或验签公钥指纹;摘要源、健康响应和日志都不得包含签名 Secret、私钥、对称密钥哈希、连接字符串或原始配置值,也不得临时散列弱口令来冒充指纹。 +- Compose 必须向两个 API 实例提供同一个非秘密 `expectedDigest`。每个实例分别计算实际 `httpDigest` 与 `hubDigest`;预期摘要缺失/格式错误、HTTP 或 Hub 摘要不等于预期值、两个本地摘要不同,或 `ClockSkew != 0`,均令 `secureConfiguration=unhealthy`、`matchesExpected=false` 并返回 `503 notReady`。只有三份摘要均相同,该实例才可承接受保护 HTTP 与 Hub 流量;部署门还必须确认两个 API 返回的三份摘要完全相同后才开放双实例业务流量。 +- `authenticationConfiguration` 五个字段在 `200/503` 都必须返回;摘要是脱敏配置一致性证据,不是认证能力是否可用的替代。签名 Secret 缺失或不可读取即使摘要字段可计算,也仍属于安全配置失败。 - Redis、RabbitMQ、SeaweedFS 不改变全局 `200/503`,而是分别改变 `capabilities`: - Redis 不可用:`catalogCache=fallback`、`protectedAuthentication=failClosed`、`realTimeMessaging=disabled`。 - RabbitMQ 不可用:`outboxDelivery=paused`;业务事务已提交的 Outbox 事实保留。 @@ -8004,6 +8500,7 @@ OrderPaymentLookupResponse { #### 验证场景 - 四项固定全局门槛全部通过时返回 `200`;其中任一失败时返回 `503`。 +- 两个 API 实例的 `expectedDigest/httpDigest/hubDigest` 三者各自相等且跨实例一致时才可开放双实例业务流量;任一实例改变 Issuer、Audience、签名材料版本、Token 有效期、ClockSkew 或 tokenVersion 规则后必须独立返回 503。 - Redis 不可用但四项门槛正常时仍返回 `200`,同时准确返回缓存回退、受保护鉴权失败关闭和实时禁用。 - Redis 恢复但撤销事实尚未安全重建时,缓存可以恢复,受保护鉴权与实时能力继续失败关闭。 - RabbitMQ 或 SeaweedFS 不可用时仍按全局门槛返回状态,并分别报告 Outbox 暂停或对象写入禁用。 @@ -8035,7 +8532,7 @@ OrderPaymentLookupResponse { | 提供模块 | 使用方 | 公开能力 | 最小输入与输出 | 事实所有权 | |---|---|---|---|---| -| Identity | Ordering | 校验本人地址并返回地址快照 | `buyerId + addressId -> AddressSnapshot` | Identity 拥有地址;Ordering 只保存下单快照 | +| Identity | Ordering | 校验本人地址版本并返回地址快照 | `buyerId + addressId + addressVersion -> AddressSnapshot`;同一事务锁定本人 DB003 地址行,只有 `version` 精确匹配才返回,已删除、非本人或版本变化均不得生成可信快照 | Identity 拥有地址;Ordering 只保存通过版本复核的下单快照;版本冲突由调用方返回最新结算信息并要求用户确认 | | Identity | Ordering | 解析并校验订单处理商家 | 普通订单与秒杀订单都必须解析唯一且启用的默认商家运营账号;活动创建人只拥有活动管理权 | Identity 拥有账号与默认标记;Ordering 保存 `assignedMerchantUserId` 快照 | | Identity | Review | 返回当前买家的安全展示名 | `buyerId -> maskedUsername`,只使用自动用户名的脱敏快照 | Identity 拥有自动用户名;Review 只在评价创建时保存脱敏展示快照 | | Identity | Messaging | 解析并校验事件派生接收身份 | `userId + expectedRole -> exists + actualRole + accountStatus`;账号禁用但身份和归属仍有效时返回可保存,不要求账号启用 | Identity 拥有账号身份与角色;Messaging 用于整事件校验和按角色生成文案,不跨模块读账号表 | @@ -8046,64 +8543,86 @@ OrderPaymentLookupResponse { | Catalog | Seckill | 返回活动维护与公开展示所需的统一经营目录商品快照 | `productId[] -> exists + name + mainImageUrl + salesStatus + currentPrice + ordinaryAvailableStock`;A220/A221 按单商品校验 `OnSale` 与计划量,A226/A227 批量取得公开展示字段 | Catalog 拥有商品与普通库存事实;所有正常商家共享同一经营目录,Seckill 不按商家隔离商品 | | Catalog | Seckill | 发布活动时原子划转普通库存到秒杀配额 | 商品、活动、数量、幂等键 -> 划转结果 | Catalog 扣减普通库存;Seckill 拥有已划转配额 | | Cart | Ordering | 读取本人已选条目并在下单成功后清理 | `buyerId + cartItemIds -> CheckoutItems` | Cart 拥有购物车 | -| Ordering | Payment(M05) | 查询本人支付快照并按状态条件推进已支付 | `buyerId + orderId -> amount + currency + assignedMerchantUserId + orderStatus + paymentDeadline`;Wallet 成功与订单 `PendingPayment → Paid` 形成同一确定结果 | Ordering 拥有订单状态、截止时间与处理商家归属;Payment 拥有 Wallet 资金事实 | -| Ordering | C08 回调处理 | 在受信回调上下文中条件推进订单已支付 | `orderId + paymentId + SimulatedChannel + amount + currency -> committed Paid / current status + paymentDeadline`;Ordering 在同一条件推进中自行取得数据库权威时间,不接收或复用调用方预读时间;无需买家 JWT,仍校验金额、截止时间、既有成功来源并与取消条件竞争 | Ordering 拥有订单状态;Payment/C08 拥有回调与支付聚合,双方在受控共享事务中只形成一个成功来源 | +| Ordering | Payment(M05) | 锁定本人应付订单并在同一事务条件推进已支付 | `buyerId + orderId -> 已锁 amount + currency + assignedMerchantUserId + orderStatus + paymentDeadline`;Payment 在继续取得既有成功支付、钱包和最后的 DB096 锁后形成唯一 `finalTime`,再在已持有订单锁下把 `finalTime` 交给 Ordering 校验并推进 `PendingPayment → Paid`,不得重新取锁 | Ordering 拥有订单状态、截止时间与处理商家归属;Payment 拥有 Wallet 资金事实和 `finalTime`;两者加入同一事务 | +| Ordering | C08 回调处理 | 在受信回调事务锁定订单并条件推进已支付 | `orderId -> 已锁 amount + currency + orderStatus + paymentDeadline`;C08 继续锁通道尝试、既有成功支付和最后的 DB096 后形成唯一 `finalTime`,只在已持有锁下请求 Ordering 推进;无需买家 JWT,仍校验金额、截止时间和成功来源 | Ordering 拥有订单状态;Payment/C08 拥有回调、支付聚合与 `finalTime`;双方在受控共享事务中只形成一个成功来源 | | Payment | Ordering、M05、C08 | 查询订单已有成功支付来源 | `orderId -> existingPaymentSummary? { paymentId, source, amount, paidAt }` | Payment 拥有 `Wallet` / `SimulatedChannel` 成功支付事实;同一订单最多一个成功来源 | | Ordering | AfterSales | 在共享事务中锁定订单履约变更并返回售后校验快照 | `orderId + buyerId + orderItemId -> locked order status + 实付 + orderType + seckillActivityId + assignedMerchantUserId` | Ordering 拥有订单行和履约状态;行锁保持到调用方事务提交 | -| AfterSales | Ordering | 查询发货阻断与剩余可履约数量 | `orderId -> hasBlockingRequest + item[{orderItemId, refundedQuantity}]` | AfterSales 拥有申请状态和已退款数量;Ordering 计算并固化实际发货数量 | +| AfterSales | Ordering(A302/A303/A305/A306) | 批量查询订单售后聚合与订单项资格 | `projection=Aggregate/ItemDetail + orderSnapshots[最多50张]{orderId,buyerId,status,completedAt,items[{orderItemId,quantity,shippedQuantity}]}` → `evaluatedAt + orders[{orderId,processingQuantity,refundedQuantity,items?[]}]`;每个请求 Key 必须返回,缺 Key 整维度失败 | AfterSales 拥有申请、资格与退款数量;Ordering 只组合派生摘要,不直读 DB086,不持久化摘要 | | Ordering | M06-02 / A307 | 原子执行责任商家发货 | `merchantUserId + orderId + idempotencyKey + note -> Shipped + shippedAt + shippedItems[]`;内部先锁定订单并读取 AfterSales 履约快照,同键重放首次确定结果 | Ordering 拥有归属校验、`Paid → Shipped`、实际发货数量、幂等结果和可靠发货事实;M06-02 不直写订单表 | | Ordering | Review | 校验评价资格并返回订单项快照 | `buyerId + orderItemId -> Completed + product/order snapshot` | Ordering 拥有订单完成与订单项归属事实 | +| Review | Ordering(A302/A303) | 批量查询订单项唯一评价存在性 | `buyerId + orderItemId[] -> [{orderItemId, reviewed}]`;一次覆盖当前页或详情全部订单项,每个请求 ID 必须返回,失败/缺 Key 不得按未评价处理 | Review 拥有 DB024 唯一评价事实;Ordering 结合自身 `Completed` 状态派生摘要,不直读 Review 表、不循环调用 A143 | | Ordering | Catalog | 判断商品是否存在历史订单关联 | `productId -> hasHistoricalOrders` | Ordering 拥有历史订单关联;Catalog 据此保护删除 | -| Ordering | Seckill | 在秒杀库存条件扣减成功后创建共享订单事实 | `activity/product/buyer/addressId/quantity/seckillPrice/库存来源 -> 订单结果`;Ordering 校验地址并解析唯一启用默认商家 | Ordering 是唯一订单事实来源;Seckill 不读取 Identity 地址或默认商家事实 | +| Ordering / Identity | Seckill(A228 外层编排) | 在活动与配额锁之前准备可信结算和责任上下文 | `buyerId + addressId + addressVersion -> AddressSnapshot + buyerDisplayName + assignedMerchantUserId + 已锁默认商家责任门`;复用 A228 的同一 `DbConnection + DbTransaction`,锁定 DB001 买家、DB003 地址并复核地址版本,默认商家 DB001 行锁保持到最终提交 | Identity 拥有地址与账号;Ordering 定义共享下单上下文;Seckill 不直读 Identity 表,客户端和活动创建人不能指定商家;地址版本变化必须终止本次创建并返回最新确认信息 | +| Ordering | Seckill(A228 外层编排) | 在活动/限购条件更新成功后,用既有可信上下文创建共享订单事实 | `trustedCheckoutContext(含 addressId + addressVersion + AddressSnapshot) + activity/product/quantity/seckillPrice/库存来源 + decisionTime -> 订单结果`;不得重新解析地址、接受旧版本、放开默认商家锁或另开事务 | Ordering 是唯一订单事实来源;Seckill 只拥有活动库存/限购并加入同一事务,不建立平行订单 | | Ordering | Seckill | 查询活动对应共享订单统计 | `seckillActivityId -> totalOrders + paidOrders + cancelledOrders + totalSoldAmount`;由 Seckill 先完成活动创建人授权,Ordering 只按活动来源汇总,不返回买家或订单私人明细 | Ordering 拥有共享订单与状态事实;Seckill 只在 A225 展示经营汇总,不维护平行订单统计事实 | | Seckill | Ordering、C03 | 待支付订单取消时幂等回补原活动库存并释放限购占用 | 活动/订单项/买家/数量/取消原因/稳定操作标识 -> `remainingStock + soldCount + buyerOccupiedQuantity` | Seckill 拥有活动库存与买家当前有效限购占用;只在订单首次取消完整结果中按原数量释放 | | Seckill | AfterSales | 已支付退款时按 M10 矩阵幂等回补原活动库存 | 活动/订单项/买家/退款数量/稳定退款操作标识 -> `remainingStock + soldCount`;不释放买家既有购买限购额度 | Seckill 拥有活动库存;已支付购买量仍计入买家活动限购,防止退款后再次突破上限 | -| Payment | AfterSales | 执行或核实同一退款操作 | `ExecuteRefundCommand -> RefundExecutionResult`,稳定 `refundOperationId`,结果为 `Succeeded` / `DefiniteFailure` / `Unknown` | Payment 拥有钱包、退款和资金流水;AfterSales 拥有申请状态 | -| Ordering、Payment、AfterSales | C08 对账 Worker | 按固定 UTC 范围和一致水位读取已提交比较事实 | `businessDate + rangeFrom + rangeTo + watermarkAt -> 订单支付终态、成功支付与回调聚合、售后终态、退款操作和钱包入账比较单元` | 各来源模块拥有原事实;C08 只保存批次、差异、证据和处置时间线,不跨模块直接读表 | +| RefundOrchestrator(应用编排) | A416/A417/A419、退款恢复 Worker | 开始、执行、核实或恢复同一退款操作 | 受信触发意图 → 锁内派生 `RefundSettlementContext` → 已持久化 `refundOperationId/refundAttemptId/executionToken/attemptKind` → `RefundExecutionResult`;结果为 `Succeeded/DefiniteFailure/Unknown` | 编排器不拥有表;Ordering 拥有订单快照,AfterSales 拥有资格/状态/回补策略,Payment 独占 DB088/DB093、钱包和资金流水,Catalog/Seckill 独占库存;`Verify` 只核实原尝试 | +| Ordering、Payment、AfterSales | C08 对账 Worker | 按固定 UTC 范围和一致财务水位读取已提交比较事实 | `businessDate + rangeFrom + rangeTo + watermarkSequence + watermarkAt -> 当日财务锚点 + 其稳定引用的水位内历史佐证`;锚点须同时满足日期区间和水位,历史佐证不重复计数;模拟通道 as-of 状态由 DB089 截止 `rangeTo/watermarkSequence` 重建,不读取 DB084 可变当前 `last*` 聚合 | 各来源模块拥有原事实;C08 只保存批次、差异、证据和处置时间线,不跨模块直接读表 | | Ordering、Payment、AfterSales | Messaging | 发布已提交业务事实与归属快照 | 标准事件 Envelope + 稳定 `eventId` + 业务归属字段 + 最小资源快照;不接收 `recipients[]` | 来源模块拥有业务事实与归属;Messaging 按 4.3.6 固定矩阵派生接收人并持久化 | 本期采用单店 B2C,不建设商户租户、拆单、结算或商品归属模型。Identity 的默认商家标记最多一个,并由启动配置/种子数据保证存在一个启用账号;默认商家不能通过 A016 直接禁用,非默认商家存在固定阻断责任时也拒绝禁用,本期不自动重新分配。普通订单和秒杀订单都使用唯一启用的默认商家账号;活动 `createdByMerchantUserId` 只用于活动管理授权。Ordering 持久化 `assignedMerchantUserId`,商家订单、售后和消息必须精确校验该账号,不得向全部 Merchant 角色广播。 非默认商家禁用与新责任受理必须形成唯一先后结果:新订单、售后责任或未结束活动先成立则禁用被阻断;禁用先成立则 Ordering、AfterSales、Seckill 后续责任受理必须拒绝该账号。责任依赖不可用时不能把未知当作无责任;本期不改派既有业务,也不把商品引用或维护记录当作阻断条件。 -发货与提交售后不得采用“先查后改”。A307 与 A412 都必须先通过 Ordering 应用契约在当前共享事务中锁定同一 `orders` 行并复核最新状态,锁保持到各自业务写入提交;A307 随后读取 AfterSales 履约快照。发货先提交时售后按已发货规则重算,售后先提交时处理中申请阻断发货、已退款数量从实际发货数量中扣除。该协作不新增 HTTP 接口或订单核心状态。 +发货与提交售后不得采用“先查后改”,但两者锁序不同。A307 先通过 Ordering 在当前共享事务锁定 DB061 订单,再读取 AfterSales 履约快照。A412 固定为 DB104 幂等范围 → 无锁预读订单不可变责任商家 ID → DB001 责任商家门 → DB061 订单并重检归属/责任商家 → DB086 申请事实;全部锁保持到各自业务写入提交。发货先提交时售后按已发货规则重算,售后先提交时处理中申请阻断发货、已退款数量从实际发货数量中扣除。该协作不新增 HTTP 接口或订单核心状态。 -普通库存、秒杀配额、限购额度、订单、退款和售后均在同一 PostgreSQL 部署内,但各模块只通过上述公开应用契约写自己拥有的表;Redis 不参与唯一正确性。需要跨模块原子提交的用例由应用层编排受控共享事务,不允许调用方直接取得其他模块 DbContext 或仓储。 +订单列表和详情的扩展摘要是批量只读组合,不是新的业务状态。Ordering 在读取前开启短生命周期 `REPEATABLE READ READ ONLY` 事务,取得本页/详情的权威订单快照后,以同一 `DbConnection + DbTransaction` 调用 AfterSales 和 Review 公开应用能力;成功调用必须全量返回请求 Key。默认 `READ COMMITTED` 不能冒充同一事务快照。摘要故障只允许明确降级并关闭依赖动作,不允许跨模块直读表、逐项调用 HTTP 或把缺失结果补成零值。 + +A405 与 A421 的支付写事务由 Payment 编排统一锁顺序。Ordering 的公开能力先锁定目标订单并返回应付快照;Payment 继续取得该路径其余共享事实,并把 DB096 作为最后一个可能阻塞的共享锁。取得 DB096 后只生成一次 `finalTime`,后续 Ordering 状态推进必须复用该值且不得再取新锁或调用外部服务;这保证截止裁决时间与成功入账时间完全一致。 + +普通库存、秒杀配额、限购额度、订单、退款和售后均在同一 PostgreSQL 部署内,但各模块只通过上述公开应用契约写自己拥有的表;Redis 不参与唯一正确性。需要跨模块原子提交的用例由应用层编排受控共享事务,不允许调用方直接取得其他模块 DbContext 或仓储。退款用例只允许 `RefundOrchestrator` 作为外层事务协调者,避免 AfterSales 与 Payment 同时成为主责。 #### 4.2.1 A431 历史取消编号的替代退款应用契约 > A431 是已取消的 HTTP 历史追踪编号,不再分配路径或 `operationId`。A432/A433 也已取消,退款摘要、结果和时间线统一由 A414 返回。以下内部应用契约不使用 Axxx。 -- **模块 / Tag**:Payment(应用服务层) +- **模块 / Tag**:RefundOrchestrator(应用编排层)+ Payment(资金能力) - **需求编号**:M10-FR07 - **负责人**:张海洋 -- **关联数据表**:DB088(待评审)— `refunds`、DB081(待评审)— `wallets`、DB083(待评审)— `wallet_ledgers` +- **关联数据表**:DB081(`wallet_accounts`)、DB083(`wallet_transactions`)、DB088(`refund_operations`)、DB093(`refund_attempts`)、DB096(`financial_posting_sequences`)、DB102(`outbox_messages`) - **当前状态**:已设计(内部契约) -- **用途**:由 AfterSales 对同一稳定退款操作执行、核实或恢复,将服务端确定金额至多一次退回申请买家小金库 +- **用途**:由唯一应用编排器从锁定的 Ordering/AfterSales 事实派生可信结算上下文,协调 Payment、Catalog/Seckill 和 AfterSales 对同一稳定退款操作执行、核实或恢复 - **调用方式**:进程内应用服务调用(**非 HTTP**) -- **应用服务签名**:`IRefundService.ExecuteAsync(ExecuteRefundCommand command, CancellationToken cancellationToken) → RefundExecutionResult` -- **命令 Schema**:`ExecuteRefundCommand`(公开应用能力) +- **应用服务签名**: + - `IRefundOrchestrator.StartAsync(StartRefundCommand command, CancellationToken cancellationToken) → RefundExecutionResult` + - `IRefundOrchestrator.VerifyAsync(VerifyRefundCommand command, CancellationToken cancellationToken) → RefundExecutionResult` + - Payment 内部参与能力:`IRefundService.BeginAttemptAsync/ApplyWalletSettlementAsync/CompleteAttemptAsync`,只能由编排器在现有事务中调用 +- **命令 Schema**:`StartRefundCommand` / `ExecuteRefundAttemptCommand` / `VerifyRefundCommand`(进程内公开应用能力) - **结果 Schema**:`RefundExecutionResult` - **身份与 Policy**:内部模块信任(无 Policy) -- **资源归属**:AfterSales 先校验申请状态与归属;Payment 再按本模块持有的原支付事实校验买家、币种和可退款上限 +- **资源归属**:RefundOrchestrator 先按固定锁序调用 Ordering 与 AfterSales 校验申请、订单和责任商家;Payment 再按原支付事实校验买家、币种和可退款上限 - **幂等要求**:以稳定 `refundOperationId` 作为业务身份;人工重试和系统恢复始终复用,不为每次尝试创建新业务退款 ##### 命令输入 -- **退款操作**:`refundOperationId`(UUID,首次进入 `Refunding` 时由 M10 建立) -- **目标申请**:`afterSalesRequestId`(UUID) -- **原支付**:`originalPaymentId`(UUID) -- **收款买家**:`buyerId`(UUID) -- **退款金额**:`amount`(decimal,由 M10 根据订单项实付快照确定) -- **币种**:`currency`(固定 `CNY`) +`StartRefundCommand` 只表达受信触发意图: + +- `afterSalesRequestId`:UUID; +- `triggerKind`:`InitialApproval / InitialReceipt / ManualRetry / AutomaticRetry`; +- `expectedRequestVersion`:锁内重检的申请版本; +- `expectedPreviousAttemptId`:后继尝试必填,首次尝试为空; +- `actorUserId`:A416/A417/A419 为已校验责任商家,Worker 为空。 + +RefundOrchestrator 在固定锁序内派生不可由调用方覆盖的 `RefundSettlementContext`: + +- `refundOperationId/refundAttemptId/executionToken/attemptKind`; +- `orderId/orderItemId/productId/originalPaymentId/buyerId`; +- `quantity/amount/currency`; +- `stockReturnPolicy/inventorySource/seckillActivityId`。 + +首次进入 `Refunding` 的事务必须同时创建 DB088 和 `attemptNumber=1/Initial/Executing` DB093;后继尝试由 Payment 根据触发类型、旧处置和 `expectedPreviousAttemptId` 创建。`ExecuteRefundAttemptCommand` 必须携带上述已持久化尝试身份和完整可信上下文;Payment 不接受 HTTP/商家/Worker 自报金额、数量、库存来源或回补决定。 + +`VerifyRefundCommand` 只用于核实一个已经存在的旧尝试,必须包含 `refundOperationId`、`afterSalesRequestId`、`refundAttemptId` 和该尝试首次执行时固定的 `executionToken`。它不接受新金额、买家、库存通道或新的业务退款身份。 ##### 校验规则 -- AfterSales 调用前必须已经原子建立同一 `refundOperationId` 并把申请推进到 `Refunding`;Payment 不接受客户端身份、金额或库存通道。 +- A416/A417 首次触发时,RefundOrchestrator 必须在同一事务原子建立 `Refunding + DB088 + initial DB093`;A419/Worker 只提交触发意图,AfterSales 不直接写 DB088/DB093。 - `refundOperationId` 必须稳定绑定 `afterSalesRequestId`、`originalPaymentId`、`buyerId`、`amount` 和 `currency`;任一绑定改变都拒绝且不覆盖原操作。 - `originalPaymentId`、`buyerId`、币种和可退款上限必须与 Payment 持有的原支付事实一致。 - 同一操作已成功时直接返回首次成功结果;已知确定失败时只允许同一操作开启受控重试;旧尝试结果未知时先核实,不开启第二笔退款。 +- `VerifyAsync` 只能读取并核实指定旧尝试,绝不能创建 DB088、新 DB093 或新的钱包/库存副作用;`refundAttemptId + executionToken` 不匹配时安全拒绝。 ##### 结果输出 @@ -8111,19 +8630,23 @@ OrderPaymentLookupResponse { - `refundOperationId`:UUID - `afterSalesRequestId`:UUID - `originalPaymentId`:UUID - - `buyerId`:string + - `buyerId`:UUID - `amount`:decimal - `currency`:string - `status`:`Succeeded` / `DefiniteFailure` / `Unknown` - `walletBalanceAfter`:decimal?(仅 `Succeeded`) - `completedAt`:UTC 时间?(仅 `Succeeded`) - `failureCode`:string?(仅 `DefiniteFailure`,只返回安全稳定码) + - `recoveryDisposition`:`AutomaticRetry` / `MerchantRetryRequired` / `OperatorAttentionRequired`?(仅 `DefiniteFailure`) ##### 业务规则与并发 -- 每个 `refundOperationId` 任一时刻最多一个执行器;A416、A417、A419 和系统恢复并发时,其他调用读取 `Unknown`/当前状态或首次确定结果。 -- `Succeeded` 是完整原子结果:退款操作成功、买家钱包只入账一次、钱包流水、必要的 Catalog 普通库存或 Seckill 原活动库存回补、M10 `Refunded`、状态时间线和买家退款成功通知可靠事实必须一起提交。 -- `DefiniteFailure` 只有在已确认钱包与库存均无副作用时才能形成;M10 随后以完整失败结果进入 `RefundFailed`、记录时间线和只通知买家的失败事实。不得描述为“同一事务整体回滚后仍在该事务保存失败状态”。 +- 每个 `refundOperationId` 任一时刻最多一个执行器;A416、A417、A419 和系统恢复并发时,RefundOrchestrator 按 DB061 → DB086 → DB088 → 最新 DB093 → DB085 → DB081 → 原库存聚合 → DB096 锁定,其他调用读取 `Unknown`/当前状态或首次确定结果。 +- `Initial`、`AutomaticRetry`、`ManualRetry` 三类新 DB093 都在取得本次数据库 `startedAt` 时写 `executionLeaseExpiresAt=startedAt+60 秒`。执行者每 20 秒只可用 `refundAttemptId + executionToken + Executing` 条件续租到“数据库当前时间 + 60 秒”,且永远不得超过不可变 `startedAt+5 分钟`;续租影响 0 行立即停止并重读。到达租约或 5 分钟上限仍无确定结果时,恢复 Worker 以同一行条件竞争原子推进为 `Unknown + VerifyOriginal`,旧执行器之后的完成写入必须影响 0 行。 +- 原退款执行器写入确定结果时必须条件匹配 `refundAttemptId + executionToken + Executing`。Worker 对执行租约过期行的接管与该完成更新竞争同一 DB093 行:接管必须原子改为 `Unknown + VerifyOriginal` 并写入恢复围栏,任一方条件更新影响 0 行后重读当前事实;迟到执行器不得覆盖已经开始的核实。 +- 执行租约与恢复租约是两个独立阶段:前者只保护某次真实退款执行,Token 为 `executionToken`;后者只保护 Unknown 核实或 AutomaticRetry 调度,Token 为 `recoveryLeaseToken`。二者不得共用列、Token、续租时钟或把执行租约续期冒充恢复责任。 +- `Succeeded` 是完整原子结果:退款操作成功、买家钱包只入账一次、钱包流水、必要的 Catalog 普通库存或 Seckill 原活动库存回补、M10 `Refunded`、状态时间线和买家退款成功通知可靠事实必须一起提交。取得订单、申请、退款操作、原支付、钱包和原库存聚合后,DB096 必须是最后一个可能阻塞的共享业务锁;随后生成唯一 `finalTime`,退款入账、库存回补、售后/退款完成时间和财务水位均复用该值。 +- `DefiniteFailure` 只有在已确认钱包、库存、售后终态均无本次成功副作用时才能形成;`failureCode + recoveryDisposition` 由服务端固定映射,调用方和商家不能指定。`AutomaticRetry` 使申请保持 `Refunding`;后两类才使申请进入 `RefundFailed`。不得描述为“同一事务整体回滚后仍在该事务保存失败状态”。 - `Unknown` 表示执行结果尚不能确认。申请保持 `Refunding`,继续核实原尝试;不得把超时直接当作失败或立即开始第二笔退款。 - 订单核心履约状态不因退款改写;库存是否回补以及原通道由 M10 固定矩阵决定。 - 成功退款操作、M10 `Refunded` 和钱包入账必须纳入 C08 每日三方对账;`RefundFailed` 与未知 `Refunding` 不伪造成功记录。 @@ -8132,15 +8655,16 @@ OrderPaymentLookupResponse { ##### 缓存、事件或外部依赖 - 缓存:退款操作、尝试和确定结果都保存在 PostgreSQL,不依赖 Redis。 -- 事件:成功可靠形成一次只通知申请买家的 `RefundCompletedIntegrationEvent`;确定失败由 AfterSales 可靠形成只通知申请买家的 `RefundFailedIntegrationEvent`;未知结果不发成功或失败消息。 +- 事件:成功可靠形成一次只通知申请买家的 `RefundCompletedIntegrationEvent`。`MerchantRetryRequired` 由 AfterSales 形成同时面向买家状态与订单指定商家行动入口的 `RefundFailedIntegrationEvent`;`OperatorAttentionRequired` 只通知买家并走运维告警;Unknown 和单次自动失败不发业务消息。 - 外部依赖:PostgreSQL;AfterSales 仅通过本公开应用契约调用 ##### 验证场景 - 正常:同一退款操作完成 → 一次钱包入账、必要库存回补和 `Refunded` - 重复:成功操作再次执行 → 返回首次 `Succeeded`,不重复入账或回补 -- 异常:得到确定失败 → 返回 `DefiniteFailure`,确认无部分资金/库存结果后进入 `RefundFailed` +- 异常:得到确定失败 → 返回 `DefiniteFailure + recoveryDisposition`;自动重试保持 `Refunding`,另两类进入 `RefundFailed` - 异常:提交或外部结果未知 → 返回 `Unknown`,保持 `Refunding` 并核实原尝试 +- 恢复:`VerifyAsync` 使用原 attemptId/token 发现成功、确认无副作用或仍未知 → 分别收敛原尝试,且不创建第二笔退款 - 并发:人工与系统同时重试 → 一个执行器推进,其余返回当前结果 ##### 调用方 @@ -8170,7 +8694,8 @@ OrderPaymentLookupResponse { | `AfterSalesPendingReturn` | 退货申请审核通过,提醒买家寄回商品 | | `AfterSalesReturnSubmitted` | 买家已提交寄回信息,提醒指定商家处理 | | `RefundSucceeded` | 售后退款成功并已退回小金库 | -| `RefundFailed` | 售后退款得到确定失败结果,买家可在售后详情查看;买家不能发起重试 | +| `RefundFailed` | 售后退款进入最终失败状态,买家可在售后详情查看;买家不能发起重试 | +| `RefundRetryRequired` | 退款失败且确需订单指定商家处理;进入商家售后详情后重新校验 A419 资格 | 后续新增消息类型属于兼容性扩展。客户端必须对未知值使用“业务通知”兜底展示,不能因此白屏。 @@ -8216,46 +8741,66 @@ OrderPaymentLookupResponse { #### 4.3.6 业务模块到 Messaging 的集成事件 -Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封向 Messaging 提交已经发生且已提交的业务事实: +Ordering、Payment 和 AfterSales 只使用 `MessagingSourceEventV1` 封闭联合提交已经发生且已经落库的业务事实。V1 Envelope 必须且只能出现下列八个顶层属性;缺失、重复、`null`、未知属性或数组均是确定契约错误: -| 字段 | 类型 | 必需 | 说明 | -|---|---|---:|---| -| `eventId` | UUID | 是 | 整个已提交来源事件的稳定标识,也是 Inbox 第一去重键;不是最终消息 ID | -| `type` | string | 是 | 下表白名单值 | -| `schemaVersion` | string | 是 | 当前固定 `v1` | -| `occurredAt` | UTC 时间 | 是 | 业务事实发生时间 | -| `aggregateId` | UUID | 是 | 订单、支付或售后申请 ID | -| `correlationId` | string | 是 | 跨请求与消息链路追踪标识,与当前 Trace 关联但不暴露内部实现 | -| `ownership` | object | 是 | 固定接收矩阵所需的 `buyerId`、`assignedMerchantUserId` 等业务归属;来源不直接给接收人数组 | -| `data` | object | 是 | 仅包含生成标题、摘要、正文与安全跳转所需的最小业务快照 | - -事件登记与消息映射: - -| 来源模块 | `type` | Routing Key | 精确接收账号来源 | `MessageType` | `data` 最小字段 | 默认跳转 | -|---|---|---|---|---|---|---| -| Ordering | `OrderCreatedIntegrationEvent` | `ordering.order.created.v1` | 订单 `buyerId` | `OrderCreated` | `orderId`、`totalAmount` | `OrderDetail` | -| Ordering | `OrderCancelledIntegrationEvent` | `ordering.order.cancelled.v1` | 订单 `buyerId` | `OrderCancelled` | `orderId`、`cancelReason` | `OrderDetail` | -| Payment | `OrderPaidIntegrationEvent` | `payment.order.paid.v1` | 订单 `buyerId`、`assignedMerchantUserId` | `PaymentSucceeded` | `orderId`、`paymentId`、`amount` | 买家 `PaymentDetail`;商家 `OrderDetail` | -| Ordering | `OrderShippedIntegrationEvent` | `ordering.order.shipped.v1` | 订单 `buyerId` | `OrderShipped` | `orderId`、`shippedAt` | `OrderDetail` | -| Ordering | `OrderCompletedIntegrationEvent` | `ordering.order.completed.v1` | 订单 `buyerId` | `OrderCompleted` | `orderId`、`completedAt`、`completedBy` | `OrderDetail` | -| AfterSales | `AfterSalesApplicationSubmittedIntegrationEvent` | `after-sales.request.submitted.v1` | 订单 `assignedMerchantUserId` | `AfterSalesSubmitted` | `requestId`、`orderId`、`type` | `AfterSalesDetail` | -| AfterSales | `AfterSalesApplicationAuditedIntegrationEvent` | `after-sales.request.audited.v1` | 申请 `buyerId` | `AfterSalesReviewed` 或 `AfterSalesPendingReturn` | `requestId`、`decision`、`status` | `AfterSalesDetail` | -| AfterSales | `AfterSalesReturnInfoSubmittedIntegrationEvent` | `after-sales.return-info.submitted.v1` | 订单 `assignedMerchantUserId` | `AfterSalesReturnSubmitted` | `requestId`、`status` | `AfterSalesDetail` | -| Payment | `RefundCompletedIntegrationEvent` | `payment.refund.completed.v1` | 申请 `buyerId` | `RefundSucceeded` | `requestId`、`refundOperationId`、`amount` | `AfterSalesDetail` | -| AfterSales | `RefundFailedIntegrationEvent` | `after-sales.refund.failed.v1` | 申请 `buyerId` | `RefundFailed` | `requestId`、`failureCode` | `AfterSalesDetail` | - -传输与幂等规则: - -- 事件发布到 `eshop.events` Exchange;Routing Key 使用上表固定值,新增事件仍遵守 `...v1`。 -- 来源模块在业务事务中写 Outbox;Worker 发布 RabbitMQ;Messaging 在保存消息的同一事务中写 Inbox。 -- 来源模块只提交已经发生的事实、稳定 `eventId` 和真实业务归属,不得自由指定 `recipients[]`。Messaging 必须按上表从 `ownership` 整事件派生全部必需接收人。 -- Messaging 先整体校验事件白名单、字段、归属、接收账号与角色;任一必需接收人缺失、角色错误或归属不符时,整事件零消息、记录 `traceId` 和安全原因并告警,不得先保存部分接收人的消息。 -- 每个接收人的持久化消息由 Messaging 单独生成 `messageId`;`eventId` 与 `messageId` 不得复用。Inbox 处理结果、全部接收人消息和 `(eventId, recipientUserId, MessageType)` 唯一结果必须在同一事务提交。 -- 同一 `eventId` 重复投递返回整事件既有结果,不重复生成消息、增加未读数或再次触发同一通知。 -- 已禁用但身份与业务归属仍有效的账号仍是合法接收人,消息照常保存;禁用只阻断消息查询、已读操作和实时推送,不删除历史。 -- `data` 不包含完整手机号、地址、支付凭证、JWT、密码或内部前端路由;消息文案由 Messaging 按每项接收人的 `role` 选择模板。 -- 支付失败、回调 `Ignored`、回调 `Difference`、对账差异发现和管理员对账处置是明确的“无消息事实”;未知或非法事件类型必须拒绝并告警,不能静默当作正常无消息。 -- 消息保存成功后才触发 SignalR;RabbitMQ 或实时推送失败不回滚已经提交的来源业务事实或 M09 消息。 +| 字段 | 精确类型与规则 | +|---|---| +| `eventId` | UUID;来源事务生成的稳定事件标识,也是 DB103 第一去重键,不得复用最终 `messageId` | +| `type` | 只允许下表 10 个精确值 | +| `schemaVersion` | 精确字符串 `v1` | +| `occurredAt` | 已提交业务事实发生时间,UTC | +| `aggregateId` | UUID;必须等于下表指定的 `data` 标识 | +| `correlationId` | 1~64 字符,必须匹配 `^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$`;不得含 Token、手机号、地址或控制字符 | +| `ownership` | 对应 `type` 的精确对象;不得出现 `recipients` 或自由路由 | +| `data` | 对应 `type` 的精确业务快照对象 | + +| `type` / Routing Key | `aggregateId` | `ownership` 精确属性 | `data` 精确属性与约束 | 原子消息结果 | +|---|---|---|---|---| +| `OrderCreatedIntegrationEvent` / `ordering.order.created.v1` | `orderId` | `buyerId` | `orderId,totalAmount,currency="CNY"` | 买家 `OrderCreated` | +| `OrderCancelledIntegrationEvent` / `ordering.order.cancelled.v1` | `orderId` | `buyerId` | `orderId,cancelReason`,原因仅 `BuyerRequested/PaymentExpired` | 买家 `OrderCancelled` | +| `OrderPaidIntegrationEvent` / `payment.order.paid.v1` | `paymentId` | `buyerId,assignedMerchantUserId` | `orderId,paymentId,amount,currency="CNY"` | 买家与指定商家各一条 `PaymentSucceeded` | +| `OrderShippedIntegrationEvent` / `ordering.order.shipped.v1` | `orderId` | `buyerId` | `orderId,shippedAt` | 买家 `OrderShipped` | +| `OrderCompletedIntegrationEvent` / `ordering.order.completed.v1` | `orderId` | `buyerId` | `orderId,completedAt,completedBy`,来源仅 `BuyerConfirmed/AutoCompleted` | 买家 `OrderCompleted` | +| `AfterSalesApplicationSubmittedIntegrationEvent` / `after-sales.request.submitted.v1` | `requestId` | `buyerId,assignedMerchantUserId` | `requestId,orderId,requestType`,类型仅 `RefundOnly/ReturnAndRefund` | 指定商家 `AfterSalesSubmitted` | +| `AfterSalesApplicationAuditedIntegrationEvent` / `after-sales.request.audited.v1` | `requestId` | `buyerId,assignedMerchantUserId` | `requestId,orderId,decision,status` | 申请买家收到审核结果 | +| `AfterSalesReturnInfoSubmittedIntegrationEvent` / `after-sales.return-info.submitted.v1` | `requestId` | `buyerId,assignedMerchantUserId` | `requestId,orderId,status="PendingReceipt"` | 指定商家 `AfterSalesReturnSubmitted` | +| `RefundCompletedIntegrationEvent` / `payment.refund.completed.v1` | `refundOperationId` | `buyerId,assignedMerchantUserId` | `requestId,refundOperationId,amount,currency="CNY"` | 买家 `RefundSucceeded` | +| `RefundFailedIntegrationEvent` / `after-sales.refund.failed.v1` | `refundOperationId` | `buyerId,assignedMerchantUserId` | `requestId,refundOperationId,failureCode,recoveryDisposition`,以及只在下述处分允许时出现的 `manualRetryAvailableAt` | 买家始终 `RefundFailed`,商家是否收到行动消息由处分决定 | + +封闭子变体和通用校验: + +- 审核只允许 `Reject + Rejected → AfterSalesReviewed`、`Approve + PendingReturn → AfterSalesPendingReturn`、`Approve + Refunding → AfterSalesReviewed` 三种组合。 +- `MerchantRetryRequired` 必须带 `manualRetryAvailableAt`,同一消息事务生成买家 `RefundFailed` 与指定商家 `RefundRetryRequired`;这是不可拆分的整事件结果。 +- `OperatorAttentionRequired` 禁止出现 `manualRetryAvailableAt`,但**仍必须生成买家 `RefundFailed`**;不生成商家行动消息,也不创建管理员消息,运维告警走监控系统。 +- UUID 归属与聚合标识必须一致;金额是 `0.01~9999999999999999.99` 的非指数 JSON 十进制数、最多两位,币种仅 `CNY`;时间必须显式为 UTC;枚举区分大小写。RefundFailed 的 `failureCode` 必须匹配 `^[A-Z][A-Z0-9_.]{0,63}$`,禁止控制字符、外部异常正文、堆栈、账号标识和其他敏感信息。 +- Routing Key 必须与 `type` 同行精确匹配。未知组合、任一必需接收人缺失、角色错误或权威归属重检失败时,整事件零消息,不能部分提交或猜接收人。 + +Worker 在查询或写入 DB103 前,对完整 Envelope 生成规范 JSON:对象属性按 UTF-8 字节序升序,UUID 为小写 `D`,时间为 `.NET "O"` UTC,金额为两位小数无指数,普通字符串先做 Unicode NFC,不输出空白或未定义 `null`;对规范 UTF-8 字节执行 SHA-256,保存 64 位小写十六进制 `payloadHash`。 + +- `(consumerName,eventId)` 不存在时执行完整校验;相同 `eventId + payloadHash` 重投返回既有 Processed/Rejected 结果;相同 `eventId` 但哈希不同是 Critical 契约/安全冲突,保留既有 DB103 与消息,当前投递零消息、告警并死信,不得自动换 ID 掩盖。 +- 两个首投竞争 DB103 唯一键时,败方必须回读已提交行并执行相同哈希判断,不能把唯一冲突直接当成功或 500。 +- 新事件的字段、Routing Key、枚举、金额或权威归属出现确定契约错误时,只原子写一条 `Rejected` DB103 与脱敏原因,零 DB101、零实时提示,提交后告警并死信;Identity/业务归属暂时不可确认或消息事务失败时整体回滚并让 Broker 重投,不得把瞬态依赖错误固化为 Rejected。 +- 来源模块在自身业务事务写 DB102 Outbox,Publisher 至少一次投递。`Mall.Worker` 是来源事件消费者;API 不消费来源事件,也不在 HTTP/Hub 请求中补消息。 +- Worker 对一个合法来源事件必须在**同一 PostgreSQL 事务**提交一条 DB103 结果、全部接收人的 DB101 消息,以及每条 DB101 对应的一条 DB102 `MessagingRealtimeHintRequestedV1`;事务失败时三者全部不存在。`(eventId,recipientUserId,MessageType)` 保持唯一。 + +`MessagingRealtimeHintRequestedV1` 也是封闭 V1 对象,必须且只能出现下列字段: + +| 字段 | 精确规则 | +|---|---| +| `eventId` | 独立 UUID;不得复用来源业务 `eventId` 或 `messageId` | +| `schemaVersion` | 精确字符串 `v1` | +| `messageId` / `recipientUserId` | 已提交 DB101 消息与接收人 UUID,统一小写 `D`;`messageId` 同时是 Outbox 责任和客户端轻提示去重键 | +| `recipientRole` | 只允许 `Buyer` / `Merchant`,必须与 DB101 接收角色和目标连接角色一致 | +| `messageType` | 只允许 `OrderCreated`、`OrderCancelled`、`PaymentSucceeded`、`OrderShipped`、`OrderCompleted`、`AfterSalesSubmitted`、`AfterSalesReviewed`、`AfterSalesPendingReturn`、`AfterSalesReturnSubmitted`、`RefundSucceeded`、`RefundFailed`、`RefundRetryRequired` | +| `title` / `summary` | 与 DB101 完全相同的不可变纯文本快照;先做 Unicode NFC、去除首尾 Unicode 空白,分别为 1~100 / 1~200 个 Unicode 标量,禁止任何 Unicode General_Category 为 `Cc` 或 `Cf` 的字符 | +| `relatedResourceType` / `relatedResourceId` | 必须同空或同非空;类型只允许 `Order` / `Payment` / `AfterSales`,ID 为 DB101 同一小写 `D` UUID | +| `actionTarget` / `actionResourceId` | 必须同空或同非空;目标只允许 `OrderDetail` / `PaymentDetail` / `AfterSalesDetail`,ID 为 DB101 同一小写 `D` UUID;不是 URL、客户端路由或自由字符串 | +| `createdAt` | DB101 消息创建 UTC 时间 | +| `expiresAt` | 精确等于 `createdAt + 60 seconds` | + +四个可空字段 `relatedResourceType,relatedResourceId,actionTarget,actionResourceId` 必须始终出现;无值时显式输出 JSON `null`,不得省略、把动作嵌套为对象或改成 URL。实时提示禁止 `body`、完整订单/支付/售后对象、地址、手机号、JWT、密码、密钥、支付凭证、异常文本、URL、内部前端路由、连接信息和任何未知属性。Outbox Publisher 将其至少一次发布到 RabbitMQ 共享提示队列;任一 `Mall.Api` 入口消费者取得后把同一封闭提示发布到版本化 Redis 频道 `eshop:{environment}:signalr:message-hints:v1`。Redis 发布成功才确认当前 RabbitMQ 投递;发布失败且尚未到 `expiresAt` 时重投,达到截止时间后确认丢弃。每个 API 实例的 `RealtimeFanoutHostedService` 各订阅并接收一次频道命令,只对本实例连接登记执行复核和发送。RabbitMQ、入口消费者和 Redis Pub/Sub 允许重复,客户端只按 `messageId` 展示一次并使用 A501/A503 恢复权威消息和未读数;Redis 不保留离线实例遗漏的历史命令。 + +支付失败、回调 `Ignored/Difference`、对账发现/处置、退款 Unknown、单次自动失败、自动重试排队和纯运维告警均不额外产生 M09 消息。已禁用但业务归属有效的账号仍保存历史消息,只阻断查询、已读和实时推送。 #### 4.3.7 Hub 连接 @@ -8263,58 +8808,77 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 |---|---| | Hub 路径 | `/hubs/messaging` | | 鉴权 | `BuyerOnly / MerchantOnly`(满足其中任一) | -| 身份来源 | 服务端认证上下文中的用户 ID 和角色 | +| 身份来源 | 认证主体中恰好一个可解析为 UUID 的 `sub`;`IUserIdProvider` 固定返回小写 `D` 格式 | | 客户端订阅参数 | 无,不接受客户端传入任意 `userId`、角色或组名 | | 传输 | 仅 WebSockets,客户端固定 `skipNegotiation = true` | -| 多实例 | 使用 Redis Backplane | +| 多实例 | 使用版本化 Redis 频道 `eshop:{environment}:signalr:message-hints:v1` 分发轻提示命令;不保存在线或消息事实 | | 事实来源 | PostgreSQL 中的 M09 消息 | 浏览器在 WebSocket 握手限制下可通过 SignalR `accessTokenFactory` 传递令牌。服务端只允许在 `/hubs/messaging` 握手路径读取受控的 `access_token` Query,并必须在 Nginx、ASP.NET Core、Serilog 和 Trace 中完整脱敏;集成、演示和发布环境只使用 HTTPS/WSS。Nginx 负责 WebSocket Upgrade;本期不启用 SSE、长轮询或会话亲和,WebSocket 不可用时只回退 A501/A503 HTTP 补查。 +- `sub` 缺失、重复、格式非法,或角色不是 Buyer/Merchant 时握手直接拒绝;Query、Header、Hub 参数中的客户端 `userId` 均不能覆盖身份。 +- Hub 固定设置 `CloseOnAuthenticationExpiration=true` 且 JWT `ClockSkew=0`;到达 `exp` 时立即关闭,不允许默认时钟宽限。 +- 每个 API 实例只维护本实例的连接登记:`connectionId -> userId,jti,tokenVersion,expiresAt,role,AbortHandle`,并建立 `userId`、`jti` 反向索引;连接关闭即清理,内存登记不是跨实例在线事实。 +- 每个 API 实例运行一个 `RealtimeFanoutHostedService` 订阅同一版本化频道;收到命令后只筛选、复核本实例登记的连接,并逐一调用 `IHubContext.Clients.Client(connectionId)`。入口消费者不得直接调用 `Clients.User(userId)` 把未经过远端实例逐连接复核的载荷交给标准 Backplane,也不得发送不属于本实例登记的连接;本落点不要求自定义 `HubLifetimeManager`。 +- 每次准备推送前必须复核签名有效期、DB004 `jti` 撤销、DB001 账号状态与 `tokenVersion`;空闲连接也至少每 30 秒复核一次。事实不匹配或无法安全确认时先 `Abort`,不得未知放行。 +- A003 仅在数据库 `decisionTime < JWT exp` 且首次提交 DB004 撤销的同一事务写 `IdentityRealtimeCredentialInvalidatedV1` 的 `TokenRevoked` 变体,精确字段为 `eventId,type="TokenRevoked",userId,jti,invalidatedAt,schemaVersion="v1"`;自然到期结果不建立该事件。A006、A016 及全部旧凭证失效动作在递增 DB001 `tokenVersion` 的同一事务写 `AccountCredentialsInvalidated` 变体,精确字段为 `eventId,type="AccountCredentialsInvalidated",userId,currentTokenVersion,accountStatus,invalidatedAt,schemaVersion="v1"`,其中账号状态仅 `Normal/Disabled`。 +- 安全失效 Outbox 发布到广播 Exchange;每个运行中 API 实例声明独立、排他、自动删除队列。`TokenRevoked` 只 Abort 同一 `jti`;账号级变体关闭 `tokenVersion < currentTokenVersion` 的连接,`Disabled` 关闭该账号全部连接。一个实例消费不能替代其他实例,广播只缩短延迟,不能代替数据库权威复核。 - 非主动断线固定按“立即、2 秒、5 秒、10 秒”进行四次重连;四次均失败后暂停,只有浏览器恢复在线或用户手动重试才开始新一轮。 - 初次连接和每次重连成功后都调用 A503 校正权威未读数,并按需调用 A501/A502 补查;服务端不承诺重放断线期间的实时事件。 -- 用户主动退出、JWT 到期、手机号修改、账号禁用或全部旧凭证失效时关闭既有连接;撤销或账号状态无法安全确认时同样关闭,不能维持“未知但放行”的连接。 -- 服务端断开时清理本实例连接状态;单实例内存连接表不作为跨实例唯一在线事实,目标用户的跨实例连接由 SignalR + Redis Backplane 协作触达。 +- 已认证且页面可见时,WebSocket 正常仍每 60 秒调用 A503 校正跨标签页已读变化;四次重连失败后每 15 秒补查 A503,消息中心当前可见时再按当前分页条件补查 A501。WebSocket 恢复后立即补查一次并回到 60 秒周期。 +- 页面隐藏时完成当前在途请求即暂停定时补查;`visibilitychange` 回到可见、浏览器 `online`、WebSocket 恢复或用户进入消息中心时立即补查。相同账号、端点和查询条件最多一个在途请求;期间多个触发合并为一次尾随刷新。HTTP 瞬态失败按 5 秒、15 秒、30 秒、60 秒封顶退避,成功后重置。 +- 主动退出、401/403、JWT 到期、手机号修改、账号禁用或页面卸载时,停止连接与补查、取消可取消在途请求并清理本地实时状态;旧凭证不得继续后台轮询。广播丢失时,`CloseOnAuthenticationExpiration`、推送前复核与最长 30 秒周期复核仍必须失败关闭。 #### 4.3.8 服务端事件 `MessageCreated` -服务端向目标认证用户的全部在线连接推送 `MessageCreated`。载荷 Schema 为 `MessageCreatedPayload`: +服务端只消费尚未过期的 `MessagingRealtimeHintRequestedV1`,逐连接复核通过后向目标认证用户的全部有效在线连接推送 `MessageCreated`。`MessageCreatedPayload` 与内部提示采用同一封闭字段和值,不能增加、删除或改名: | 字段 | 类型 | 可空 | 说明 | |---|---|---:|---| -| `messageId` | UUID | 否 | 已持久化消息 ID,也是客户端去重键 | -| `type` | `MessageType` | 否 | 消息类型 | -| `title` | string | 否 | 标题 | -| `summary` | string | 否 | 摘要 | +| `eventId` | UUID | 否 | 本条实时提示事件 ID;不作为展示去重键 | +| `schemaVersion` | string | 否 | 固定 `v1` | +| `messageId` | UUID | 否 | 已持久化消息 ID,也是客户端唯一轻提示去重键 | +| `recipientUserId` | UUID | 否 | 必须等于当前连接的小写 `D` 用户 ID | +| `recipientRole` | string | 否 | `Buyer` / `Merchant`,必须等于当前连接角色 | +| `messageType` | `MessageType` | 否 | 4.3.1 的 12 种精确值 | +| `title` | string | 否 | 已持久化标题;NFC + 去首尾 Unicode 空白后 1~100 个 Unicode 标量,禁止 `Cc/Cf` | +| `summary` | string | 否 | 已持久化摘要;NFC + 去首尾 Unicode 空白后 1~200 个 Unicode 标量,禁止 `Cc/Cf` | | `relatedResourceType` | `RelatedResourceType` | 是 | 关联业务类型 | -| `relatedResourceId` | UUID | 是 | 关联业务 ID | -| `action` | `MessageAction` | 是 | 安全跳转描述 | +| `relatedResourceId` | UUID | 是 | 与类型同空同非空的关联业务 ID | +| `actionTarget` | string | 是 | 与 `actionResourceId` 同空同非空;仅 `OrderDetail` / `PaymentDetail` / `AfterSalesDetail` | +| `actionResourceId` | UUID | 是 | 与 `actionTarget` 同空同非空的安全操作目标资源 ID,不是 URL | | `createdAt` | UTC 时间 | 否 | 服务端消息创建时间 | +| `expiresAt` | UTC 时间 | 否 | 固定等于 `createdAt + 60 秒` | 示例: ```json { + "eventId": "3a627725-74fb-4635-9163-cb197f42b49c", + "schemaVersion": "v1", "messageId": "6aa23ac8-8cf7-4cb5-8387-69f463df4b77", - "type": "OrderShipped", + "recipientUserId": "2e4a56ff-2453-4eb1-8b42-37f775666bcb", + "recipientRole": "Buyer", + "messageType": "OrderShipped", "title": "订单已发货", "summary": "你的订单已由商家发出,可进入订单详情查看。", "relatedResourceType": "Order", "relatedResourceId": "93c8d8e1-bdac-4a13-8ba2-0c21c327b81e", - "action": { - "target": "OrderDetail", - "resourceId": "93c8d8e1-bdac-4a13-8ba2-0c21c327b81e" - }, - "createdAt": "2026-07-24T02:30:00Z" + "actionTarget": "OrderDetail", + "actionResourceId": "93c8d8e1-bdac-4a13-8ba2-0c21c327b81e", + "createdAt": "2026-07-24T02:30:00Z", + "expiresAt": "2026-07-24T02:31:00Z" } ``` 规则: - 只有消息数据库事务成功提交后才能推送。 -- 推送前重新确认目标连接身份仍有效;账号已禁用、Token 已失效或认证事实无法安全确认时关闭连接且不推送。 +- 载荷必须逐项等于 4.3.6 的封闭提示;缺失必填字段、重复/未知属性、接收人或角色不匹配、成对字段只空一项、文本未满足 NFC/长度/`Cc/Cf` 规则,均不得发布。 +- API 轻量消费者不得直写 DB101/DB103;提示过期、重复或未送达均不改变消息事实,超过 `createdAt + 60 秒` 后不得继续实时重试。 +- Redis 频道只传播分发命令;每个存活 API 的 `RealtimeFanoutHostedService` 各接收一次,按本地登记逐 `connectionId` 复核后调用 `IHubContext.Clients.Client(connectionId)`。账号已禁用、Token 已失效或认证事实无法安全确认时关闭连接且不推送;禁止入口直接使用 `Clients.User(userId)` 绕过远端逐连接复核,也不要求自定义 `HubLifetimeManager`。 - 向目标用户全部有效在线连接发送;客户端按 `messageId` 去重轻提示后必须调用 A503 校正角标,不得执行“本地未读数 + 1”。 -- Redis Backplane 或发送失败时记录 `messageId`、实例标识和 `traceId`;不回滚业务事务或 M09 消息,也不把消息重新标记为未生成。 +- 入口发布 Redis 失败且提示未过期时让 RabbitMQ 重投,过期后 ACK 丢弃;实例本地发送失败记录 `messageId`、实例标识和 `traceId`,不回滚业务事务或 M09 消息,也不把消息重新标记为未生成。Redis Pub/Sub 断线期间遗漏由 A501/A503 补查,不增加自建重放仓库。 - 重连后不重放历史 `MessageCreated`;由 A501/A503 补偿。客户端不得仅凭推送载荷修改订单、支付或售后最终状态。 - 本期不提供客户端调用的聊天、广播、已送达回执、任意加组或按用户订阅 Hub 方法。 @@ -8327,17 +8891,22 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 - 只有 A102 的固定首页形态进入缓存:无筛选、仅 `OnSale`、按 `createdAt desc, productId desc` 取前 12 条;普通库存为 0 的商品仍返回并标记售罄。其他分类、关键词、普通列表和组合筛选全部直读 PostgreSQL。 - A103 只缓存商品自身公开字段,不含评价、评分、收藏、购物车、商家管理字段或任何身份化数据;M07 变化不触发 C07。 - 正常值 TTL 固定 60 秒;固定首页空结果和 A103 不存在/不可公开的短空值 TTL 固定 10 秒。 -- 同一 Key 只允许一个跨实例填充者。其他请求最多等待 500 毫秒后重读;仍未命中则直查 PostgreSQL 并返回,不继续争抢填充资格。 -- 取得填充资格后有效回填窗口最多 2 秒;超过窗口的查询结果仍可按接口返回,但不得再写入缓存。 +- 同一 Key 的填充锁固定 `SET NX PX 3000`,Token 由密码学安全随机源生成且熵不少于 128 bit,锁不续期。其他请求最多等待 500 毫秒后重读;仍未命中则直查 PostgreSQL 并返回,不继续争抢或回填。 +- 只有取得填充锁后才开启新的短 `READ COMMITTED` 只读事务查询 PostgreSQL,不能复用锁前查询或长事务快照。有效回填窗口从取得锁起最多 2 秒。 +- 回填必须用 Lua 比较锁值仍等于本人 Token,并在同一脚本原子 SET 序列化值与 60/10 秒 TTL;超过窗口、锁丢失、Token 不符或脚本结果未知时只返回数据库结果,不得回填。释放同样用 compare-and-delete Lua,不能用普通 DEL 误删后继锁。 - Redis 不可用、缓存损坏或读写失败时回退 PostgreSQL,并记录命中、未命中、耗时、错误与降级指标;Redis 不保存交易事实。 +- A102/A103 全部 JSON 响应固定 `Cache-Control: no-store`,Nginx/CDN/Service Worker 不得形成第二份 JSON 缓存;不可变版本化媒体 URL 可以单独长期公共缓存。 -#### 4.4.2 提交后失效 +#### 4.4.2 提交可见后的双阶段失效 -- 所属业务事实成功提交后立即删除受影响 Key,并在提交后第 3 秒执行同一组二次删除;失效失败不回滚已经提交的商品、订单或售后事务。 +- 所属业务来源事务取得一次仅供审计的 `invalidationBaseTime`,计算稳定 `operationId`,并与业务事实一起原子插入两条独立 DB102。Immediate 使用 `schedule_mode='fixed'`,创建时 `available_at=next_attempt_at=invalidationBaseTime`;Delayed 使用 `schedule_mode='after_commit_delay'`、`delay_seconds=3`,创建时 `armed_at/available_at/next_attempt_at` 全空。任一业务事实或 Outbox 插入失败时整笔来源事务回滚;Redis 删除失败则不回滚已经提交的业务事实。 +- 两条 `CatalogCacheInvalidationRequestedV1` 使用不同 eventId,去重键固定 `cache:{operationId}:immediate` / `cache:{operationId}:delayed`;payload 含 `operationId,phase,invalidationBaseTime,delayAfterCommitSeconds,productIds,invalidateHomepage,schemaVersion=v1`,不携带预提交绝对投递时间字段。Immediate 的 `delayAfterCommitSeconds=0`,Delayed 固定为 `3`;`productIds` 去重后按 UUID 字节升序,1~100 个;除 `phase/delayAfterCommitSeconds` 外两阶段内容一致,未知字段或版本拒绝并告警。`invalidationBaseTime` 只表达业务发生时间,不参与 Delayed 的到期计算。 +- Outbox 调度器在独立短事务中按 `created_at,eventId` 扫描已提交可见、仍为 Pending 且未武装的 Delayed,使用 `FOR UPDATE SKIP LOCKED` 互斥,以同一数据库 `clock_timestamp()` 原子写 `armed_at`、`available_at=next_attempt_at=armed_at+3 秒`。武装事务失败或进程崩溃会回滚并由后续轮次重扫;武装提交后实例崩溃则由普通到期扫描接管同一 eventId。Delayed 最早不早于来源事务提交可见后 3 秒,调度观察延迟只会使其更晚。 +- Immediate/Delayed 消费者分别从配置规则派生 Redis Key 并执行幂等 DEL;Key 不存在也是成功。只有删除结果明确成功后才写本消费者 DB103 Processed 并 ACK;失败或结果未知时不写 Inbox、不 ACK,只重投同一阶段 eventId。Delayed 必须由来源事务创建,不由 Immediate 或调度器派生;Immediate 的投递、成功、失败和重试均不得创建、武装、取消或修改 Delayed。 - 精确失效来源包括:商品名称、价格、普通库存、描述、分类展示或商品分类关系、图片新增/删除/排序/主图、上架、下架、删除、普通订单扣减、主动或超时取消回补、C01 发布划拨普通库存、M10 普通库存售后回补。 - 新建草稿只清理同标识详情短空值;其未上架前不进入固定首页。分类展示或关系变化只失效受影响商品及确实受影响的固定首页。 - C01 活动内部抢购、秒杀订单取消、M10 回补原活动库存,以及 M07 评价、评价图和评分变化均不触发 C07。 -- 提交前旧查询最迟可能在提交后第 2 秒回填;二次删除通常清除该旧值。两次删除均失败时,正常旧值兜底上限为提交后 62 秒,旧空值为提交后 12 秒。 +- 提交前旧查询最迟可能在提交可见后第 2 秒回填;独立 Delayed 在提交可见后武装并等待 3 秒,正常情况下会在该迟到回填之后清除旧值。即使武装或两阶段删除持续失败,正常旧值兜底上限仍为提交可见后 62 秒,旧空值为 12 秒。 - F08 下单、取消和 M10 售后退款始终重读 PostgreSQL,不等待缓存一致,也不使用缓存结果作为价格、状态或库存条件。 ### 4.5 Worker 内部契约 @@ -8355,9 +8924,9 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 ##### 幂等与恢复 -- 多实例 Worker 使用小批量扫描与 `FOR UPDATE SKIP LOCKED`(或等价条件更新)避免重复处理;重复扫描已经到达目标状态的活动无副作用。 +- 默认每 5 秒扫描、每批最多 100 条;多实例 Worker 使用 `FOR UPDATE SKIP LOCKED` 与条件更新避免重复处理。DB107 使用 `jobName=seckill.activity_lifecycle`、`runKey=UTC 五秒窗口`,60 秒租约、每 20 秒续租;重复扫描已经到达目标状态的活动无副作用。 - 进程重启后继续以数据库时间字段扫描,不依赖内存定时器保存唯一任务事实。 -- 单条失败记录 `activityId`、目标状态与 `traceId` 后重试,不阻塞同批次其他活动。 +- 单条失败记录 `activityId`、目标状态与 `traceId`,按 5 秒、30 秒、2 分钟、10 分钟、之后每小时重试,不阻塞同批次其他活动。瞬态失败不能 DeadLetter;后续窗口仍重扫全部已到期活动。 ##### 验证场景 @@ -8369,6 +8938,8 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 > **说明**:C03订单超时自动取消由Worker后台任务执行,不对外提供HTTP API。接口设计记录其与外部系统的交互关系。 +- **关联数据表**:DB061/DB062 订单与订单项、DB063 生命周期责任、DB102 可靠取消事实;普通订单按快照操作 DB022 库存聚合与 DB026 流水,秒杀订单按快照操作 DB042 活动、DB043 买家限购占用与 DB044 流水。 + ##### 业务规则 1. 正式环境固定以 `createdAt + 30 分钟` 生成 `paymentDeadline`。演示环境只能通过明确、可追踪的配置缩短等待;当时生效值随订单固化,后续配置变化不追溯修改历史订单,演示值也不得替代正式规则或影响生产环境。 @@ -8380,10 +8951,11 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 ##### 任务触发 -- `Mall.Worker` 按稳定顺序领取有界批次的到期待支付订单;批量大小、扫描间隔和单轮重试次数是后续可观测配置,不在业务契约中写死。 +- `Mall.Worker` 使用 `OrderLifecycle:ScanIntervalSeconds`、`OrderLifecycle:BatchSize`、`OrderLifecycle:MaxBatchesPerRun`,默认 5 秒、100 条、10 批,允许范围分别为 1~60 秒、1~500 条、1~100 批;越界或多实例配置摘要不一致时该能力 NotReady。每轮按 `nextAttemptAt asc, dueAt asc, taskId asc` 稳定顺序领取 DB063;达到单轮批数上限主动让出,下一窗口继续处理积压。 +- 单批通过短事务和 `FOR UPDATE SKIP LOCKED` 领取后逐笔独立执行业务事务;单笔失败写入本责任的退避结果并继续同批,数据库连接级失败停止本轮。领取租约 60 秒、每 20 秒续租,完成与重试更新必须匹配本轮 `leaseToken`;吞吐配置不能改变围栏和持续收敛语义。 - 多实例可重复发现候选,但最终由 M04 条件竞争保证一个取消完整结果;进程内集合或单机锁不作为唯一正确性保障。 - 每次领取和执行必须持久保留独立运维任务记录,最少包含 `orderId`、`attemptCount`、`lastAttemptAt`、`lastOutcome`(本次结果)、`executionStatus`(`PendingRetry` / `Finalized`)、可空 `finalOutcome` 与 `traceId`;只有 `executionStatus=Finalized` 时才写入 `finalOutcome`(`Cancelled` / `AlreadyResolvedByCompetitor`)。该记录只用于恢复、业务追踪和告警,不成为订单业务状态。 -- 暂时失败时更新 `lastOutcome` 并保持 `executionStatus=PendingRetry`,按退避策略进入后续重试;订单保持“已过期但尚待取消”的 `PendingPayment`,仍不可支付。首次完整取消或确认订单已由竞争方推进后改为 `Finalized` 并写入确定 `finalOutcome`,后续重扫复用该结果而不重复回补或通知。 +- 暂时失败或租约过期时更新 `lastOutcome=RetryableFailure` 并保持 `executionStatus=PendingRetry`,退避固定为 5 秒、30 秒、2 分钟、10 分钟、30 分钟,此后每 1 小时持续重试;订单保持“已过期但尚待取消”的 `PendingPayment`,仍不可支付。第 5 次失败 Warning,第 20 次及以后每 24 小时聚合 Critical;不能因次数耗尽把逐笔责任 DeadLetter。首次完整取消或确认订单已由竞争方推进后改为 `Finalized` 并写入确定 `finalOutcome`,后续重扫复用该结果而不重复回补或通知。 - Worker 重启后重新扫描 PostgreSQL 共享事实,不依赖内存定时器或未持久化队列保存唯一到期责任。 ##### 事件发布 @@ -8415,6 +8987,7 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 2. 复用 Ordering 的完成订单应用能力,使用 `WHERE status = 'Shipped'` 条件更新为 `Completed`,并记录 `completedAt`、`completedBy = AutoCompleted`;买家主动确认的来源固定为 `BuyerConfirmed`。 3. 成功后只写一次 `OrderCompletedIntegrationEvent` Outbox;与买家主动确认并发时仅一个条件更新成功,失败方读取并返回当前终态,不重复发布事件。 4. 订单事实和发货时间均来自 PostgreSQL;Worker 重启后继续扫描,不依赖进程内定时器保存唯一任务事实。 +5. 自动完成与 C03 共用 DB063 参数:每 5 秒扫描、每批最多 100 条、60 秒租约/20 秒续租,瞬态失败按 5 秒、30 秒、2 分钟、10 分钟、30 分钟、之后每小时持续重试;只有 Completed 或竞争方已合法推进才 Finalized。 ##### 验证场景 @@ -8429,12 +9002,17 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 ##### 业务规则 -1. 按业务结果的服务端成功提交时间生成上一完整 UTC 自然日批次,范围左闭右开,并固定一致读取 `watermarkAt`;不使用客户端时间、回调发生时间或接收时间归属。 -2. 同一 `businessDate + rangeFrom + rangeTo` 最多一个有效批次;`watermarkAt` 是该批次首次生成时冻结的内容而非唯一键维度。重复执行返回既有批次,任务中断时批次与全部差异必须同时不存在或同时完整。 -3. 支付比较覆盖 Wallet 与 SimulatedChannel 成功支付、Ordering `Paid`、四种回调终态及成功来源唯一性;退款比较覆盖 M10 `Refunded`、成功退款操作与买家钱包入账三方事实。 -4. 回调 `Difference` 是差异来源而非预建管理员条目;同一业务对象与同一比较规则被多个来源发现时只形成一个差异单元,并保留全部证据引用。 -5. 批次与全部差异在一个原子结果中生成;无差异直接为 `Matched`,有差异为 `HasDifferences`,不存在 `Pending` 批次。 -6. 本任务不自动修复订单、支付、退款或钱包,也不发送管理员站内消息;管理员通过 A422~A426 查询、领取、引用受控动作并复核闭环。 +1. 调度触发点固定为每日 `00:05:00Z`。Worker 正常运行时在该时刻触发,并每分钟执行一次同口径漏跑检查;进程启动后立即执行一次补查。三种入口只能发现同一日期责任,不能生成不同任务身份或改变业务口径。 +2. `latestEligibleBusinessDate` 只取已经到达次日 `00:05:00Z` 触发点的最近完整日:当前 UTC 时刻不早于 `00:05:00Z` 时取前一日,早于该时刻时取前两日。当前 UTC 日以及已经结束但尚未到次日 `00:05:00Z` 的业务日都不得提前生成。 +3. 每个日期范围固定为 `[businessDate 00:00:00Z, businessDate + 1 day 00:00:00Z)`;DB107 固定使用 `jobName=PaymentDailyReconciliation`、`runKey=YYYY-MM-DD`,其中 `runKey` 就是该 UTC `businessDate`,不得附加实例号、重试次数、触发来源或当前时间。 +4. 计算 `coverageStart = min(latestEligibleBusinessDate, earliestPostingDate(若存在), earliestExistingBatchDate(若存在))`;不存在历史事实或批次时取 `latestEligibleBusinessDate`。在 `[coverageStart, latestEligibleBusinessDate]` 中选择最早缺失日并按日期升序逐日处理;不得只补昨天、合并多日或跳过中间空日/失败日。 +5. 同一 `businessDate + rangeFrom + rangeTo` 最多一个有效批次;已存在 `Matched` / `HasDifferences` / `Resolved` 均为该日权威结果,不重新生成、覆盖或因规则升级重开。某日失败时停止本轮,已成功前序日不回滚;同一 DB107 责任保持 `RetryWait` 并按 1 分钟、5 分钟、15 分钟、1 小时封顶持续重试和告警,绝不进入 `DeadLettered`,成功前不处理后续日期。 +6. 任务取得当日执行资格后,先等待已经分配序号的在途财务事务提交或回滚,再在短事务读取 DB096 已提交 `watermarkSequence + watermarkAt`;随后开启新的 `REPEATABLE READ READ ONLY` 快照。不得先建立旧快照再等待水位,也不得在扫描中移动水位或锁定/修改来源业务表。 +7. 只有 `postingTime ∈ [rangeFrom,rangeTo)` 且 `postingSequence <= watermarkSequence` 的财务结果可成为本日锚点;稳定引用的更早事实只作佐证。唯一 `anchorPostingSequence` 是批次计数单位,同序号事实按 `RefundOperation > Payment > Callback` 选择锚点种类;缺失事实使用矩阵指定的权威来源 ID,不得使用空 ID 或随机 ID。 +8. `totalCount` 为本日去重锚点数,`matchedCount` 为零条规则命中的锚点数,`differenceCount` 为至少命中一条规则的锚点数,固定满足 `totalCount = matchedCount + differenceCount`。`paymentUnits` 统计 Payment/Callback 锚点,`refundUnits` 统计 RefundOperation 锚点,固定满足 `totalCount = paymentUnits + refundUnits`。`differenceCountsByType` 按差异行计数,可因同锚点命中多规则而大于 `differenceCount`。 +9. 检测只使用 A424/A426 固定的十二类 `comparisonRuleCode + comparisonRuleVersion`、锚点、主体与 JSON 键矩阵。同一 `(batchId,anchorPostingSequence,comparisonRuleCode,comparisonRuleVersion)` 最多一条 DB091,回调 `Difference` 与横向扫描重复命中时合并证据,不合并不同规则。模拟通道历史按不可变 DB089 与固定水位重建,不用 DB084 当前 `last*` 字段改写历史。 +10. 每日的 DB090、全部 DB091、检测阶段 DB094 和 DB107 成功结果在一个事务提交;任务中断时该日批次与全部差异必须同时不存在或同时完整。无交易日也生成 `totalCount=matchedCount=differenceCount=0` 的 `Matched` 空批次。 +11. 本任务不自动修复订单、支付、退款、钱包或库存,也不发送管理员站内消息;管理员通过 A422~A426 查询、领取、引用所属模块受控动作并复核闭环。 ##### 验证场景 @@ -8442,6 +9020,55 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 - “支付成功但订单未更新”、重复成功来源、迟到成功回调、退款与流水不一致均生成可由 A424~A426 查询和 A425 闭环的差异。 - 回调来源与横向比对命中同一问题时只生成一个差异并保留全部证据。 - Worker 中途失败后重试得到完整批次,不留下半批次或重复差异。 +- 停机三日后恢复时按日期升序分别生成三个批次;中间日期失败后不生成后续日期,下次从该日期继续。 +- 无交易的完整 UTC 日生成空 `Matched` 批次;当前未结束 UTC 日不生成。 +- Worker 在 `00:05Z` 前启动或执行分钟补查时不提前生成最近结束日;到达 `00:05Z` 后三种触发入口竞争同一 `runKey=YYYY-MM-DD`。 +- 同一锚点命中两条规则时 `differenceCount` 只加一、两种 `differenceCountsByType` 各加一,且 `totalCount = paymentUnits + refundUnits`。 + +#### 4.5.5 M10 退款恢复 + +> 本任务由 `Mall.Worker` 执行,不调用 A419 HTTP、不伪造 Merchant JWT,也不占用 Axxx 编号。逐笔恢复责任保存在 DB093;DB107 只记录每轮调度运行。 + +##### 领取与调度 + +1. 默认每 5 秒扫描一次、单批最多 50 条。所有 Initial/AutomaticRetry/ManualRetry 执行租约固定首次 60 秒、每 20 秒按 `refundAttemptId + executionToken + Executing` 续租、绝对上限 `startedAt+5 分钟`。领取第一短事务只对 DB093 使用 `FOR UPDATE SKIP LOCKED`,原子写入/接管恢复租约和必要的 `Executing → Unknown` 后立即提交,绝不等待 DB061/DB086/DB088;第二业务事务才按父子锁序重锁并以 Token 重检。候选包括执行租约已过期或已到绝对上限的 `Executing`、到期的 `Unknown/verify_original`、到期的 `DefiniteFailure/automatic_retry`。 +2. 恢复租约默认 60 秒,每 20 秒续租;DB093 保存不可变的本轮 `recoveryLeaseAcquiredAt`,租约不得延长到首次取得后 5 分钟之外。领取时生成不可猜测的 `recoveryLeaseToken`;接管不同 Token 的过期租约递增连续接管计数,正常完成一轮后归零,连续 3 次触发告警。所有完成更新必须以该 Token 为条件,租约丢失后旧执行器丢弃执行结果并重读,不能覆盖接管者。 +3. 过期 `Executing` 必须锁定同一 DB093 行并以原 `status=Executing + executionToken + executionLeaseExpiresAt<=databaseTime`(或 `startedAt+5 分钟<=databaseTime`)原子转为 `Unknown + VerifyOriginal`、写入恢复围栏,不得直接创建新尝试;若条件更新未命中则重读先提交的执行结果。Unknown 调用 `IRefundService.VerifyAsync`,携带原 `refundAttemptId + executionToken`;核实能力绝不能创建新业务退款,迟到原执行器也不能覆盖该状态。执行租约和本节 60/20/5 分钟恢复租约使用不同列与 Token,不能相互续期。 +4. 自动重试只为同一 `refundOperationId` 创建下一 DB093,绝不创建新 DB088。商家 A419 与 Worker 都先只读解析关联 ID,再固定按 DB061 订单 → DB086 申请 → DB088 退款操作 → 最新 DB093 尝试锁定并重检恢复租约,只有一个执行器能创建下一尝试;候选领取对 DB093 的短租约不改变该业务锁顺序。 +5. Worker 重启后从 DB093 `nextActionAt` 和租约事实恢复,不依赖内存队列。DB107 固定 `jobName=after_sales.refund_recovery`,`runKey=UTC 调度窗口`,只记录扫描数、成功数、Unknown 数、自动重试数和升级数;单轮失败不能清除逐笔退款责任。 + +##### 恢复处置 + +- Unknown 核实退避为 10 秒、30 秒、2 分钟、10 分钟、30 分钟、60 分钟;第 6 次仍 Unknown 后标记需关注,转为每小时安全核实,始终保持 `Refunding`。 +- 瞬态 `DefiniteFailure + AutomaticRetry` 最多自动重试 3 次,初始执行不计入,退避为 30 秒、2 分钟、10 分钟;耗尽后进入 `RefundFailed + MerchantRetryRequired`。 +- `MerchantRetryRequired` 使用处置决策的同一 `finalTime` 写 `manualRetryAvailableAt=finalTime+60 秒`,通知买家和订单指定商家;A419 仍在执行时按数据库时间重检。`OperatorAttentionRequired` 通知买家并产生运维告警,不生成商家行动消息。 +- 核实发现完整原子成功事实时,同一尝试收敛 `Succeeded`,申请收敛 `Refunded`;确认所有参与事实均无副作用时,同一尝试收敛 `DefiniteFailure` 并按服务端恢复处置继续;仍无法判断时保持 Unknown。 + +##### 告警与验证 + +- Unknown 持续 15 分钟 Warning、持续 1 小时 Critical;三次自动重试耗尽 Warning;`OperatorAttentionRequired` 立即 Critical;同一操作连续三次租约接管 Warning;到期积压超过 100 条或最老到期超过 10 分钟 Critical。 +- 多实例同时扫描同一候选只允许一个租约生效;Worker 崩溃后新实例可接管,旧围栏 Token 不能提交。 +- Unknown 核实不创建新尝试;三次自动重试封顶;A419 与自动重试竞争只创建一个下一尝试;成功后所有恢复重放都返回首次结果,不重复钱包入账或库存回补。 +- 运维告警不进入 M09,不新增管理员消息中心;Unknown 和单次自动失败不生成业务失败消息。 + +#### 4.5.6 Outbox 投递 + +> 每条可靠事件的逐笔责任保存在 DB102,DB106 保存每次真实 Broker 调用或结果未知的尝试;RabbitMQ 只负责传输,不能覆盖 PostgreSQL 事实。 + +1. 默认每 1 秒先执行武装扫描,再执行发布扫描。武装扫描按 `created_at,eventId` 领取最多 100 条 `Pending + after_commit_delay + armed_at=null` 的已提交可见 DB102,使用 `FOR UPDATE SKIP LOCKED`,以数据库 `clock_timestamp()` 原子写 `armed_at` 与 `available_at=next_attempt_at=armed_at+delay_seconds`;该短事务失败即整体回滚,下一轮重扫,不建立 DB106。发布扫描只按 `next_attempt_at<=databaseTime,eventId` 领取已到期且已具备 `available_at` 的最多 100 条;领取短事务把 DB102 改为 Publishing、`attemptCount+1`,写 30 秒租约并插入同编号 DB106 Executing。长调用每 10 秒续租,Publisher Confirm 最长等待 5 秒。 +2. Confirm 后同一事务写 DB106 Confirmed 与 DB102 Published;Confirm 前明确失败写 DB106 Failed,DB102 回 Pending。退避固定为 1 秒、5 秒、30 秒、2 分钟、10 分钟,之后每小时持续重试。 +3. Publishing 租约到期表示结果 Unknown;接管者先将旧 DB106 Executing 改为 Unknown,再重投同一 `eventId`。Broker 已收但数据库未标记时允许重复投递,消费者必须以 DB103 Inbox 幂等,不允许来源模块生成新 eventId“补发”。 +4. 网络、Broker 不可用、限流、Confirm 超时和消费者离线都不是 DeadLetter 条件;第 5 次 Warning,第 20 次及以后每 24 小时聚合 Critical。只有事件类型、路由或 Schema 的确定契约错误才 DeadLetter 并立即 Critical,修复后通过受控运维动作恢复同一 eventId。 +5. 未武装 Delayed 没有 `next_attempt_at`,不得进入发布领取、Publishing、DB106 或重试退避;武装提交后调度字段不可重算。Immediate 的任何状态变化都不得触碰相同 `operationId` 的 Delayed,长事务超过 3 秒也不能让 Delayed 在提交可见时立即到期。 + +#### 4.5.7 上传过期与对象清理 + +1. A127 的 DB023 上传预留固定 15 分钟;`catalog.expired_product_upload_cleanup` 每 1 分钟扫描、每批 100 条到期预留,先为原图/缩略图 UPSERT DB105,再移除预留。迟到上传不能 Attached,并须触发同 Key 再核验。 +2. A122 的 DB104 Processing 租约默认 60 秒、每 20 秒续租,工作期限 24 小时;`catalog.product_create_recovery` 每 10 秒扫描、每批 50 条。期限内只允许按不可变工作清单续传或在租约到期后换发 fencing token 接管;超过工作期限后禁止继续上传,改为为全部已知 Key 登记 DB105。只有全部 Key 已由当前代清理责任确认不存在或删除后,才可在同一幂等 advisory lock 下移除 Processing,让原幂等键重新开始。 +3. A141 的 DB104/DB025 首次工作期限固定 24 小时;`review.expired_image_upload_cleanup` 每 1 分钟扫描、每批 100 条 DB025 到期 Uploading/Pending,按 `uploadExpiresAt,imageId` 锁定订单项与图片,先以 DB105 `EnsureActive` 登记对象责任,再移除 DB025 和到期 A141 DB104。Attached 或未到期图片绝不处理;旧上传者失去 Token 后不能完成。 +4. `storage.object_cleanup` 每 10 秒扫描、每批 100 条 DB105;租约 60 秒、每 20 秒续租,单次对象调用超时 10 秒。登记使用 objectKey 级事务 advisory lock并显式区分:重复扫描用 `EnsureActive`,活动责任不换代;在先前登记后又确认迟到 PUT 或最后引用解除时必须用 `Supersede`,无论当前状态都 `generation+1`、清空旧结果与 lease、重置 Pending。领取、续租与完成同时匹配 `generation + leaseToken`,上一代迟到执行者不能覆盖新一代。瞬态失败按 10 秒、30 秒、2 分钟、10 分钟、30 分钟、之后每小时持续重试;确定非法 Key/类型才 DeadLetter。 +5. 删除前重查 DB023 当前/归档、DB025 Attached/未过期暂存、DB062 历史订单快照及有效 A122/A141 DB104 工作清单;查询未知时停止删除。NotFound 和 `objectStillReferenced` 都只是本代结果,新外部事实必须 Supersede。任何清理结果都不得反向修改已提交商品、评价或订单。 +6. `storage.orphan_inventory_reconciliation` 每日 02:00 UTC 执行,启动时补跑已到触发点的缺失当日运行;按 500 个对象分页列举三个受控前缀,只对年龄超过 25 小时且上述所有数据库引用均不存在的 Key 登记 DB105,不直接删除。对象列表或数据库核查失败按任务退避,不得把未知当孤儿。 ### 4.6 C10 运行协作契约 @@ -8474,17 +9101,17 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 ### 5.1 当前成熟度 -本次已把六份最新个人原稿综合到唯一主接口文档,并按已确认流程闭合有效接口的请求、响应、错误、鉴权、幂等、并发和依赖边界。当前状态为 **完整定义,待交叉评审,尚未冻结**:本文件可作为后续数据库设计与 OpenAPI 的输入,但 DBxxx、真实 OpenAPI、跨模块实现签名和交叉评审证据仍未完成。 +本次以已确认需求和业务流程派生唯一主接口契约,并仅使用六份个人原稿核对成员贡献范围与遗漏;旧原稿不参与反向拼接业务语义,也不能覆盖本文件。有效接口的请求、响应、错误、鉴权、幂等、并发和依赖边界已经闭合,99 个活动 HTTP 契约及非 HTTP 协作已经逐项反查统一数据库设计。当前状态为 **完整定义,数据库设计已确认,待 OpenAPI、实现与交叉评审,尚未冻结**:DBxxx 映射已经完成,但真实 OpenAPI、跨模块实现签名、契约测试和交叉评审证据仍未完成。 -| 负责人 | 追踪编号 | 有效 HTTP | 本次已闭合 | 仍需确认 | +| 负责人 | 追踪编号 | 有效 HTTP | 本次已闭合 | 仍需完成 | |---|---:|---:|---|---| -| 唐宇昊 | 25 | 22 | A005/A009/A023 取消、A024/A025、身份边界、令牌撤销、账号状态幂等 | DB001~DB006、OpenAPI | -| 顾欣月 | 23 | 22 | A115 分类删除、A144 取消、图片原子边界、公开评价字段与用户名脱敏快照、C04 索引边界 | DB021~DB025、OpenAPI | -| 朱惠惠 | 19 | 17 | A229/A230 取消、商家活动路径、库存/限购边界、生命周期 Worker | DB041~DB044、公开应用契约签名、OpenAPI | -| 韦乾强 | 8 | 8 | A308、Ordering 命名、秒杀查询复用、取消与自动完成 Worker | DB061/DB062 完整字段、公开应用契约签名、OpenAPI | -| 张海洋 | 27 | 23 | A418/A431/A432/A433 取消、A426/A434、唯一退款操作、回调四终态与差异闭环 | DB081~DB091、退款事务编排、OpenAPI | -| 罗皓晨 | 7 | 7 | 买家/商家授权、事件映射、SignalR、健康检查 | DB101~DB120、来源模块评审、OpenAPI | -| **合计** | **109** | **99** | **10 个历史取消编号已隔离** | **尚不能宣称冻结或已实现** | +| 唐宇昊 | 25 | 22 | A005/A009/A023 取消、A024/A025、身份边界、令牌撤销、账号状态幂等及 DB001~DB007 映射 | OpenAPI、实现、契约测试与交叉评审 | +| 顾欣月 | 23 | 22 | A115 分类删除、A144 取消、图片历史保留、公开评价字段、C04 索引边界及 DB021~DB027 映射 | OpenAPI、实现、图片/搜索测试与交叉评审 | +| 朱惠惠 | 19 | 17 | A229/A230 取消、购物车选择语义、活动库存/限购、生命周期 Worker 及 DB041~DB044 映射 | 公开应用签名、OpenAPI、实现、并发测试与交叉评审 | +| 韦乾强 | 8 | 8 | A308、Ordering 命名、组合读模型、取消/自动完成 Worker 及 DB061~DB063 映射 | 公开应用签名、OpenAPI、实现、状态竞争测试与交叉评审 | +| 张海洋 | 27 | 23 | A418/A431/A432/A433 取消、A426/A434、唯一退款操作、恢复处置、跨日对账及 DB081~DB096 映射 | 公开应用签名、OpenAPI、实现、资金/恢复测试与交叉评审 | +| 罗皓晨 | 7 | 7 | 买家/商家授权、事件映射、SignalR、健康检查及 DB101~DB107 映射 | 来源模块评审、OpenAPI、实现、可靠性测试与交叉评审 | +| **合计** | **109** | **99** | **10 个历史取消编号已隔离,44 张表已完成契约反查** | **尚不能宣称冻结、实现或验证** | ### 5.2 已确认的综合决策 @@ -8499,14 +9126,14 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 9. 本期为单店 B2C;订单以 `assignedMerchantUserId` 指定处理账号,不建设多商户商品归属、拆单或结算模型。 10. C01 活动推进、C03 超时取消、M04-04 自动完成和 C08 每日对账均由 `Mall.Worker` 扫描 PostgreSQL 事实并幂等执行。 -### 5.3 冻结前必须完成 +### 5.3 当前完成项与冻结条件 -1. 六名负责人完成个人数据库设计并汇总到《数据库设计》,逐项反查本文件中的 DBxxx、字段、约束、索引和事务边界。 +1. 已完成:统一《数据库设计》已从流程和接口派生 44 张表,并逐项反查本文件的 DBxxx、字段、约束、索引、事务、恢复和 Worker 责任;尚未创建真实实体或 Migration。 2. 将 99 个活动 HTTP 契约落成真实 OpenAPI,校验路径、方法、`operationId`、Schema 引用、状态码和安全方案均唯一有效。 3. 把 4.2 的模块间应用边界落实为公开 Contracts/Application 接口,不允许跨模块直接读写内部表。 -4. Ordering、Payment、AfterSales 负责人确认 4.3 的事件字段、接收账号和触发时机;Messaging 完成 Inbox 去重与断线补偿设计。 -5. 为 4.5 的四类 Worker 按流程边界设计扫描索引、可观测配置、重试与多实例互斥策略,并登记对应 DBxxx。 +4. Ordering、Payment、AfterSales 负责人交叉确认 4.3 的事件字段、接收账号和触发时机;Messaging 的 Inbox 去重与断线补偿设计已完成,仍待实现和测试。 +5. 把 4.5 的全部 Worker 契约落实为实现与配置,验证扫描索引、可观测性、重试、围栏租约和多实例互斥策略均与 DB063/DB093/DB102/DB105/DB107 等责任表一致。 6. 对订单、秒杀、支付、回调、退款、售后和全部已读并发场景建立契约测试或验收用例。 7. 每个负责人至少由一名其他成员完成交叉评审;确认后的接口才可把状态从“部分定义/待交叉评审”改为“已确认”。 -在以上条件完成前,可以按已稳定的编号和路径继续数据库及 OpenAPI 设计,但不得宣称接口文档已经冻结、接口已经实现或通过验收。 +在以上条件完成前,可以按已稳定的编号、路径和统一数据库设计继续 OpenAPI 与实现,但不得宣称接口文档已经冻结、接口已经实现或通过验收。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241.md" index 52308f1..14416f6 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241.md" @@ -60,15 +60,17 @@ - 不可变流水只保存 `created_at` 或业务发生时间,不伪造 `updated_at`。 - `created_at` 默认 `CURRENT_TIMESTAMP`;应用更新可变记录时必须同时写 `updated_at = clock_timestamp()`。 - 通用 `version` 创建时使用各表初值;每次成功改变该聚合的任一可变业务事实,必须在同一 `UPDATE` 中执行 `version = version + 1`。纯读取、校验失败、条件更新未命中和幂等重放不递增;钱包 version=0 初态及活动 `result_version` 等特殊初值按各表更严格规则执行。 -- 订单支付、活动抢购、超时取消等请求可能等待行锁;必须在取得目标行锁后调用一次 `clock_timestamp()` 形成 `decision_time`,后续同一事务复用该值。不得用锁等待前取得的应用时间或事务开始时的旧时间越过截止边界。 +- 订单支付、活动抢购、超时取消、售后资格判断等请求可能等待共享事实锁;必须在取得本路径全部可能阻塞且会影响裁决的共享事实后,调用一次 `clock_timestamp()` 形成唯一裁决时间,后续同一事务复用该值。不得用锁等待前取得的应用时间或事务开始时的旧时间越过截止边界,也不得在裁决后再取得会改变业务结论的新共享锁。 +- 资金原子结果采用更严格规则:DB096 必须是本路径最后一个可能阻塞的共享业务锁;取得后立即生成唯一 `final_time`。`final_time` 同时承担截止裁决时间、财务 `posting_time` 和该原子结果涉及各表的业务时间,生成后不得再调用外部服务或取得新的共享业务锁。 - 数据库默认值不能代替接口要求的服务端计算。例如 `payment_deadline`、`auto_complete_at`、图片过期时间必须由用例按已追踪配置显式写入。 ### 2.2 金额、数量与快照 - 所有金额最多两位小数;单价、总额和余额使用 `numeric(18,2)`。 - 所有业务数量使用 `integer`,并以 Check 保证非负或正数。 +- 商品描述与评价正文都保存规范化纯文本:应用把 `CRLF/CR` 统一为 `LF`、去除首尾 Unicode 空白,并按 Unicode 标量值计数;数据库 `char_length` Check 作为 2000/500 上限与评价非空的最终保护。任何层都不得把这些列解释为 HTML。 - 订单保存地址、商品名称、主图对象 Key、成交单价、原价、活动名称和买家安全展示名快照;历史展示不得回查当前商品或地址覆盖快照。 -- 对象 URL 不作为长期事实。数据库保存 `object_key`、`media_type`、尺寸和顺序,API 通过对象存储适配器生成受控 URL。 +- 对象 URL 不作为长期事实。数据库保存 `object_key`、`media_type`、尺寸和顺序;DB023 Attached/Detached 商品图与 DB025 Attached 评价图由环境配置的只读媒体源按不可变 Key 派生稳定公开 URL,不携带短效签名或上传凭据。DB025 Uploading/Pending 不是公开内容,媒体源必须按数据库状态拒绝。 - 对象 Key 遵守命名规范 `//.`,并用互斥资源前缀冻结命名空间:商品原图 `products/{productId}/...`、缩略图 `product-thumbnails/{productId}/...`、评价图 `reviews/{orderItemId}/...`。Key 创建后不可改、不复用,Check 校验前缀与所属资源 ID,避免不同列或对象类型碰撞。 ### 2.3 外键与删除 @@ -96,8 +98,8 @@ - 支付:Payment 钱包/支付 + Ordering 状态 + Outbox + Idempotency; - 取消:Ordering + 原 Catalog/Seckill 库存通道 + Outbox; - 退款成功:Payment 钱包/退款 + AfterSales + 原库存通道 + Outbox; -- 消息消费:Inbox + 同一事件全部接收人消息。 -- C07 失效:Catalog/库存来源事务 + Immediate Outbox;消费者首次 DEL 后以 Inbox + Delayed Outbox 原子登记第二阶段责任。 +- 消息消费:Inbox + 同一事件全部接收人消息 + 每条消息一条 60 秒实时轻提示 Outbox。 +- C07 失效:Catalog/库存来源事务在提交业务事实时,同时写入彼此独立的 Immediate 与初始未武装 Delayed Outbox;Immediate 固定立即调度,Delayed 只能在提交可见后由 Outbox 调度器以数据库时间武装并等待 3 秒。两个消费者分别完成本阶段 DEL 与 Inbox,不允许由 Immediate 消费者或调度器派生 Delayed。 共享事务只解决原子性,不改变数据所有权。 @@ -122,7 +124,7 @@ | DB007 | `browsing_history_settings` | Engagement | 浏览记录开关 | 已确认 | | DB021 | `categories` | Catalog | 两级分类及启停状态 | 已确认 | | DB022 | `products` | Catalog | 商品、价格、普通库存与三态 | 已确认 | -| DB023 | `product_images` | Catalog | 商品图片上传、主图和排序 | 已确认 | +| DB023 | `product_images` | Catalog | 商品图片上传、主图、排序与历史订单媒体保留 | 已确认 | | DB024 | `reviews` | Review | 订单项唯一评价与买家展示快照 | 已确认 | | DB025 | `review_images` | Review | 评价前暂存及评价图片 | 已确认 | | DB026 | `catalog_inventory_movements` | Catalog | 普通库存不可变变动链与回补去重 | 已确认 | @@ -146,7 +148,7 @@ | DB090 | `reconciliation_batches` | Payment | 每日一致水位对账批次 | 已确认 | | DB091 | `reconciliation_differences` | Payment | 唯一比较单元与当前处置状态 | 已确认 | | DB092 | `after_sales_return_shipments` | AfterSales | 一次性退货物流事实 | 已确认 | -| DB093 | `refund_attempts` | Payment | 同一退款操作的多次执行尝试 | 已确认 | +| DB093 | `refund_attempts` | Payment | 同一退款操作的执行、核实、调度、租约与恢复处置 | 已确认 | | DB094 | `reconciliation_evidence` | Payment | 差异多来源证据 | 已确认 | | DB095 | `reconciliation_actions` | Payment | 领取、释放、接管、复核和解决时间线 | 已确认 | | DB096 | `financial_posting_sequences` | Payment | 严格财务提交水位 | 已确认 | @@ -203,6 +205,8 @@ 关系与删除:被业务表引用后永久保留,所有外键 `RESTRICT`。A016 对默认商家由 Check 最终保护;非默认商家责任阻断由各模块公开能力在同一事务复核。 +商家责任并发门固定使用目标商家的 DB001 行,不再由各模块自行发明锁:A016、A220、A222、A301、A228、A412 都在取得各自 DB104 幂等范围后,以 `SELECT ... FOR UPDATE` 锁定目标 Merchant 行并复核 `role='merchant' AND status='normal'`,再按稳定顺序锁活动、订单、售后或库存事实。A016 持有同一行锁后查询固定责任清单;新责任提交在前则必被看见,禁用提交在前则新责任受理必须失败。A412 可先无锁读取订单的不可变 `assigned_merchant_user_id` 以定位门行,但锁定门行后必须再锁订单并重检归属,不能把预读结果作为授权事实。 + ### 4.2 DB002 `user_status_histories` 用途:保存 A016/A017 的领域状态变化,不与通用技术日志混用。 @@ -253,6 +257,8 @@ - `ix_addresses_buyer_id_created_at(buyer_id, created_at DESC, id DESC)`。 - A011 固定创建非默认;A014 锁定买家账号行后先清旧默认再设新默认,同一事务提交。 - A013 允许物理删除;删除默认地址不自动选择其他地址。订单只保留 `source_address_id` 弱引用和完整地址快照,不对地址建立订单外键。 +- `version >= 1`。A011 创建为 1;A012 规范化字段实际变化、A014 目标成为默认或原默认被取消时,对每条发生变化的地址分别 `version+1`。无实际变化的幂等结果不递增。 +- A301/A228 通过 Identity 公开能力加入 Ordering/Seckill 已开启的共享事务,锁定 `buyer_id=:buyerId AND id=:addressId` 的 DB003 行并要求 `version=:addressVersion`,再从该行形成订单地址快照。地址缺失/越权与版本冲突都必须在库存副作用前裁决;不得以先查后改或当前新地址内容覆盖买家确认版本。 ### 4.4 DB004 `revoked_access_tokens` @@ -266,7 +272,7 @@ | `token_version` | `bigint` | 否 | JWT 内版本 | | `reason` | `varchar(32)` | 否 | `logout/security_invalidation` | | `revoked_at` | `timestamptz` | 否 | 首次撤销时间 | -| `expires_at` | `timestamptz` | 否 | 不早于 JWT 自然过期 | +| `expires_at` | `timestamptz` | 否 | 固定等于 JWT `exp` 对应 UTC 时间 | 约束与索引: @@ -275,8 +281,9 @@ - Check `expires_at > revoked_at`、`token_version >= 1`、原因白名单。 - `ix_revoked_access_tokens_expires_at(expires_at)` 用于过期清理。 - `ix_revoked_access_tokens_user_id_expires_at(user_id, expires_at)` 用于安全重建。 -- A003 使用 `INSERT ... ON CONFLICT (jti) DO NOTHING`,重复退出返回首次 `revoked_at`。 +- A003 尚无既有记录时,在同一事务只读取一次 `decision_time=clock_timestamp()`:仅 `decision_time < JWT exp` 时以该时间执行 `INSERT ... ON CONFLICT (jti) DO NOTHING` 并原子建立安全失效 DB102;并发冲突读取既有记录并返回首次 `revoked_at`。若 `decision_time >= exp`,令牌已自然失效,固定不插入 DB004/DB102,不伪造较早 `revoked_at`,也不触发本表 Check 失败。 - 账号级全部旧凭证失效以 `users.token_version` 为权威事实,不批量展开每个未知 JTI。 +- JWT 验证固定 `ClockSkew=0`,因此撤销事实只需覆盖到同一 `exp`;清理器只可按数据库时间删除 `expires_at <= clock_timestamp()` 的记录,不额外猜测应用实例时钟。两个 API 与 Hub 的认证配置摘要不一致时实例 NotReady,不能靠延长 DB004 留存掩盖配置漂移。 ### 4.5 DB005 `favorites` @@ -368,7 +375,7 @@ | `category_id` | `uuid` | 否 | — | 当前分类 | | `name` | `varchar(100)` | 否 | — | 商品名 | | `normalized_name` | `varchar(100)` | 否 | — | 搜索规范化名称 | -| `description` | `varchar(2000)` | 是 | — | 受控描述 | +| `description` | `varchar(2000)` | 是 | — | 规范化纯文本描述;空白输入存空值 | | `price` | `numeric(18,2)` | 否 | — | 当前普通售价 | | `stock` | `integer` | 否 | — | 普通库存,不含秒杀已划拨库存 | | `status` | `varchar(16)` | 否 | `'draft'` | `draft/on_sale/off_sale` | @@ -380,10 +387,10 @@ 约束: - PK `pk_products`;FK `category_id → categories.id ON DELETE RESTRICT`。 -- `ck_products_price(price >= 0)`、`ck_products_stock(stock >= 0)`、状态白名单、名称非空、`version >= 1`。 +- `ck_products_price(price >= 0)`、`ck_products_on_sale_price(status <> 'on_sale' OR price > 0)`、`ck_products_stock(stock >= 0)`、`ck_products_description(description IS NULL OR char_length(description) BETWEEN 1 AND 2000)`、状态白名单、名称非空、`version >= 1`。零价只允许作为 `draft/off_sale` 的编辑占位,不进入公开销售与正金额订单链路。 - `status='on_sale'` 时 `on_sale_at IS NOT NULL`;草稿可为空;下架保留最近上架时间。 - A123 修改库存时必须写 DB026;订单、发布秒杀、取消和退款也只通过库存公开能力修改 `stock`。 -- A125 必须在锁定商品行后确认商品至少一张已完成主图、当前分类有效、价格和库存合法;真实 `draft/off_sale → on_sale` 时更新 `on_sale_at`,重复上架不改时间。 +- A125 必须在锁定商品行后确认商品至少一张已完成主图、当前分类有效、`price > 0` 且库存非负;真实 `draft/off_sale → on_sale` 时更新 `on_sale_at`,重复上架不改时间。 索引: @@ -411,38 +418,52 @@ | `byte_size` | `bigint` | 是 | — | 文件大小 | | `width` | `integer` | 是 | — | 像素宽 | | `height` | `integer` | 是 | — | 像素高 | -| `status` | `varchar(16)` | 否 | `'uploading'` | `uploading/attached` | +| `status` | `varchar(16)` | 否 | `'uploading'` | `uploading/attached/detached` | | `sort_order` | `smallint` | 是 | — | 已关联图片 1~8 | | `is_primary` | `boolean` | 否 | `false` | 主图 | | `alt_text` | `varchar(100)` | 是 | — | 替代文本 | | `upload_expires_at` | `timestamptz` | 是 | — | 上传预留过期时间 | | `attached_at` | `timestamptz` | 是 | — | 关联完成时间 | +| `detached_at` | `timestamptz` | 是 | — | 从当前商品图库移除时间;历史订单仍可引用 | | `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 创建时间 | | `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 更新时间 | 约束: - PK `pk_product_images`;FK `product_id → products.id ON DELETE RESTRICT`。 -- Unique `ux_product_images_object_key(object_key)`、`ux_product_images_thumbnail_object_key(thumbnail_object_key)`。 +- Unique `ux_product_images_object_key(object_key)`、`ux_product_images_thumbnail_object_key(thumbnail_object_key)`;另建 Unique `ux_product_images_object_key_product_id(object_key,product_id)`,供订单图片快照同时证明“对象存在且属于该商品”。 - Check 原图 Key 匹配 `products/{product_id}/...`、缩略图匹配 `product-thumbnails/{product_id}/...` 且二者不等;互斥前缀与列内唯一共同保证全局不复用。 - `ux_product_images_product_id_sort_order(product_id, sort_order) WHERE status='attached'`。 - `ux_product_images_product_id_primary(product_id) WHERE status='attached' AND is_primary`。 -- `uploading`:原图与缩略图 Key 已预生成并持久化;媒体元数据、排序、主图、关联时间为空/false,`upload_expires_at` 非空。 -- `attached`:媒体元数据、`sort_order`、`attached_at` 非空,过期时间为空;类型、5 MB、400~4096 像素和顺序范围满足接口。 +- `uploading`:原图与缩略图 Key 已预生成并持久化;媒体元数据、排序、主图、关联/移除时间为空或 false,`upload_expires_at` 非空。 +- `attached`:媒体元数据、`sort_order`、`attached_at` 非空,`upload_expires_at/detached_at` 为空;类型、5 MB、400~4096 像素和顺序范围满足接口。 +- `detached`:保留媒体元数据、`attached_at`、原图 Key 与缩略图 Key,`detached_at` 非空;`sort_order/upload_expires_at` 为空且 `is_primary=false`。该状态不进入当前图库、主图、公开详情或后台排序查询,只承担历史订单媒体保留。 +- 时间顺序满足 `attached_at >= created_at`,非空 `detached_at >= attached_at`。 索引: - `ix_product_images_product_id_sort_order(product_id, sort_order, id) WHERE status='attached'`。 - `ix_product_images_upload_expires_at(upload_expires_at) WHERE status='uploading'`。 +- `ix_product_images_detached_at(detached_at, id) WHERE status='detached'` 只服务历史引用核查与受控清理,不用于当前图库。 上传协议: -1. A127 先锁商品行并把“已关联 + 未过期上传预留”控制在 8 条内,预生成原图和缩略图全部不可变 Key,提交一条 `uploading` 预留; +1. A127 先锁商品行并把“已关联 + 未过期上传预留”控制在 8 条内,预生成原图和缩略图全部不可变 Key,提交一条 `uploading` 预留;`upload_expires_at` 固定取该短事务 `reservation_time + interval '15 minutes'`,本期不续期; 2. 写对象与缩略图; -3. 新事务再次锁商品,补齐元数据并切换 `attached`、主图和排序; +3. 新事务再次锁商品,只有 `status='uploading' AND upload_expires_at > decision_time` 才可补齐元数据并切换 `attached`、主图和排序; 4. 尚未写入任何对象时才可直接删除失败预留;任一对象已经写入但业务关联未提交时,必须先在数据库事务中为每个已写对象登记 DB105,再删除预留。数据库不可用时保留原预留,不得丢弃最后一份持久化 Key 清单。 -A122 使用 DB104 的持久化工作清单预生成全部图片 ID、原图 Key 和缩略图 Key,对象成功后在一个数据库事务直接创建 `attached` 图片、Draft 商品和完成幂等结果。任一部分对象已写而商品事务未完成时同样先登记 DB105,或保留 Processing 工作清单等待恢复;A128 删除关联时先在同一事务登记 DB105,再物理删除图片行,删除主图时按 `sort_order,id` 提升下一张。 +A122 使用 DB104 的持久化工作清单预生成全部图片 ID、原图 Key 和缩略图 Key,对象成功后在一个数据库事务直接创建 `attached` 图片、Draft 商品和完成幂等结果。任一部分对象已写而商品事务未完成时同样先登记 DB105,或保留 Processing 工作清单等待恢复。 + +过期上传收敛固定由 `catalog.expired_product_upload_cleanup` Worker 每 1 分钟扫描一次、每批最多 100 条,使用 `FOR UPDATE SKIP LOCKED` 领取 `status='uploading' AND upload_expires_at <= decision_time` 的行。领取事务为原图和缩略图分别 UPSERT DB105 后才可删除预留;对象清理 Worker 删除前继续核查 DB023/DB062 引用。迟到上传者不能把已过期/已删除预留切换为 Attached,并须按相同 Key 重新 UPSERT 清理责任;数据库不可用时预留保持不变,下一轮继续扫描。 + +A128 与普通下单、秒杀下单都按商品 ID 升序锁定同一 DB022 商品行,避免“图片移除”和“订单快照建立”先查后改: + +1. A128 锁商品与目标图片后,先检查 DB062 是否已经引用该 `object_key`。 +2. 已有任一历史订单项引用时,只执行 `attached → detached`,清空当前排序/主图并写 `detached_at`;原图、缩略图、DB023 行均保留,A303/A306 继续由快照 Key 生成受控 URL。 +3. 尚无历史引用时,在同一数据库事务分别为原图和缩略图登记 DB105,再物理删除 DB023;对象只能在事务提交后异步删除。 +4. 目标图片原顺序记为 `k`。必须先把目标切为 `detached` 或删除以释放唯一序号 `k`,再在同一商品行锁事务内按当前 `sort_order ASC,id ASC` 逐行执行 `k+1→k、k+2→k+1…`;单商品最多 8 张,逐行更新可避免非可延迟部分唯一索引发生临时冲突。目标原为主图时,压缩完成后把当前最小 `sort_order,id` 的剩余 Attached 图片设为唯一主图;不存在剩余图片时商品保持无主图,后续上架完整性校验负责阻断。 +5. DB062 的外键是最终竞争保护;任何引用检查后的新订单若先获得商品锁并建立快照,A128 必须转入保留分支,不得删除对象。 ### 5.4 DB024 `reviews` @@ -454,7 +475,7 @@ A122 使用 DB104 的持久化工作清单预生成全部图片 ID、原图 Key | `buyer_id` | `uuid` | 否 | 评价买家 | | `product_id` | `uuid` | 否 | 被评商品 | | `rating` | `smallint` | 否 | 1~5 | -| `content` | `varchar(500)` | 否 | 评价正文 | +| `content` | `varchar(500)` | 否 | 规范化纯文本评价正文 | | `buyer_display_name_snapshot` | `varchar(50)` | 否 | 已脱敏展示名 | | `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | @@ -464,7 +485,7 @@ A122 使用 DB104 的持久化工作清单预生成全部图片 ID、原图 Key - 复合 FK `(order_id,buyer_id) → orders(id,buyer_id)`、`(order_item_id,order_id,product_id) → order_items(id,order_id,product_id)`;用户、商品删除关系均 `RESTRICT`。订单、买家、订单项和商品引用因此不能被合法但互不归属的 ID 拼接。 - Unique `ux_reviews_order_item_id(order_item_id)`。 - Unique `ux_reviews_id_buyer_order_item(id,buyer_id,order_id,order_item_id)` 供评价图片复合归属。 -- Check `rating BETWEEN 1 AND 5`,正文去空白后 1~500 字。 +- Check `rating BETWEEN 1 AND 5`、`char_length(content) BETWEEN 1 AND 500`,并由应用保证正文已完成统一换行与首尾 Unicode 空白规范化。 - `ix_reviews_product_id_created_at(product_id, created_at DESC, id DESC) INCLUDE (rating)` 同时服务 A140 分页与实时评分汇总。 - `ix_reviews_buyer_id_created_at(buyer_id, created_at DESC, id DESC)` 用于归属验证与审计。 @@ -478,6 +499,7 @@ A122 使用 DB104 的持久化工作清单预生成全部图片 ID、原图 Key | `buyer_id` | `uuid` | 否 | — | 上传买家 | | `order_id` | `uuid` | 否 | — | 由订单项派生的订单 | | `order_item_id` | `uuid` | 否 | — | 上传资格绑定 | +| `idempotency_record_id` | `uuid` | 否 | — | A141 持久工作与重放事实 | | `review_id` | `uuid` | 是 | — | 正式评价 | | `object_key` | `varchar(500)` | 否 | — | 对象 Key | | `media_type` | `varchar(32)` | 是 | — | 图片 MIME | @@ -486,26 +508,34 @@ A122 使用 DB104 的持久化工作清单预生成全部图片 ID、原图 Key | `height` | `integer` | 是 | — | 像素高 | | `status` | `varchar(16)` | 否 | `'uploading'` | `uploading/pending/attached` | | `sort_order` | `smallint` | 是 | — | 正式评价内 1~6 | -| `upload_expires_at` | `timestamptz` | 否 | — | 未关联图片过期时间 | +| `upload_expires_at` | `timestamptz` | 是 | — | 未关联图片过期时间;Attached 后清空 | | `attached_at` | `timestamptz` | 是 | — | 正式关联时间 | | `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 创建时间 | | `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 更新时间 | 约束与索引: -- PK `pk_review_images`;复合 FK `(order_id,buyer_id) → orders(id,buyer_id)`、`(order_item_id,order_id) → order_items(id,order_id)` 均 `RESTRICT`;可空复合 FK `(review_id,buyer_id,order_id,order_item_id) → reviews(id,buyer_id,order_id,order_item_id)`,保证暂存和正式关联阶段的图片、评价、买家、订单与订单项始终一致。`order_id` 由服务端从已锁定订单项派生,不接受客户端自由组合。 -- Unique `ux_review_images_object_key(object_key)`。 +- PK `pk_review_images`;复合 FK `(order_id,buyer_id) → orders(id,buyer_id)`、`(order_item_id,order_id) → order_items(id,order_id)` 均 `RESTRICT`;FK `idempotency_record_id → idempotency_records.id ON DELETE RESTRICT`;可空复合 FK `(review_id,buyer_id,order_id,order_item_id) → reviews(id,buyer_id,order_id,order_item_id)`,保证暂存和正式关联阶段的图片、评价、买家、订单与订单项始终一致。`order_id` 由服务端从已锁定订单项派生,不接受客户端自由组合。 +- Unique `ux_review_images_object_key(object_key)`、`ux_review_images_idempotency_record_id(idempotency_record_id)`。 - Check `object_key` 匹配 `reviews/{order_item_id}/...`;与商品/缩略图互斥命名空间保证全局不复用。 - `ux_review_images_review_id_sort_order(review_id, sort_order) WHERE status='attached'`。 - 状态组合 Check: - - `uploading/pending` 时 `review_id/sort_order/attached_at` 为空; - - `attached` 时三者非空; + - `uploading/pending` 时 `review_id/sort_order/attached_at` 为空且 `upload_expires_at` 非空; + - `attached` 时 `review_id/sort_order/attached_at` 非空且 `upload_expires_at` 为空; - 媒体元数据在 `pending/attached` 时完整,符合 5 MB、200~4096 像素和类型白名单。 - `ix_review_images_buyer_order_item_pending(buyer_id, order_item_id, upload_expires_at) WHERE status IN ('uploading','pending')`。 - `ix_review_images_review_id_sort_order(review_id, sort_order, id) WHERE status='attached'`。 - `ix_review_images_upload_expires_at(upload_expires_at) WHERE status <> 'attached'`。 -A141 按 `(buyer_id,order_item_id)` 取得事务 advisory lock,连同未过期上传预留计数,保证并发第 7 张失败。上传完成进入 `pending`,默认可引用期 **24 小时**;A142 在同一锁下重检资格、图片归属和未过期状态,创建评价并把最多 6 张图片切换为 `attached`。过期图片不能再引用,由 Worker 登记 DB105 后清理。 +A141 使用 DB104 持久工作清单与 DB025 三阶段上传: + +1. 完成文件 SHA-256 和媒体基础校验后,取得 A141 幂等 advisory lock,再按 `(buyer_id,order_item_id)` 取得事务 advisory lock;重检评价资格及“未过期 Uploading/Pending < 6”,预生成 `image_id/object_key`。DB104 保存 `buyer_id/order_item_id/file_hash/image_id/object_key`、60 秒 lease Token 和 `work_expires_at=reservation_time+24 hours`,同事务插入 DB025 Uploading,`upload_expires_at=work_expires_at`。 +2. 只向清单中的不可变 Key 写对象;每 20 秒匹配 DB104 Processing + lease Token 续租,单次对象调用最长 2 分钟。租约到期后只可对同一清单换发 Token 接管,不能生成第二个图片 ID/Key。 +3. 完成事务同时匹配 DB104 当前 Token 与 DB025 Uploading、重检 `upload_expires_at > decision_time`,补齐媒体元数据、切为 Pending,并把 DB104 改为 Completed 保存首次 `imageId` 响应。响应丢失后同键同文件稳定重放,不重复计数;同键换内容拒绝。 + +续租不延长 24 小时可引用期限。A142 在同一 `(buyer_id,order_item_id)` 锁下重检资格、图片归属和未过期 Pending 状态,创建评价并把最多 6 张图片切换为 Attached,同时清空 `upload_expires_at`。Uploading/Pending 不进入公开媒体源;只有 Attached 可由 A140/A142 返回公开 URL。 + +`review.expired_image_upload_cleanup` 每 1 分钟扫描、每批最多 100 条 `status IN ('uploading','pending') AND upload_expires_at <= decision_time`。它按稳定 `upload_expires_at,id` 顺序取得同一订单项 advisory lock与行锁,复核未 Attached 后,以 DB105 `EnsureActive` 登记对象清理并移除 DB025 预留;关联的过期 A141 DB104 在同一幂等锁下解除,旧执行者因 Token/行不存在不能完成。若失去 Token 的执行者稍后确认 PUT 成功,必须以 `Supersede` 登记新代 DB105 责任。对象存储前 24 小时内的 DB025/DB104 事实和每日孤儿盘点共同保证崩溃恢复。 ### 5.6 DB026 `catalog_inventory_movements` @@ -517,6 +547,7 @@ A141 按 `(buyer_id,order_item_id)` 取得事务 advisory lock,连同未过期 | `product_id` | `uuid` | 否 | 商品 | | `movement_type` | `varchar(32)` | 否 | 变动类型 | | `operation_id` | `uuid` | 否 | 跨重试稳定操作 ID | +| `refund_operation_id` | `uuid` | 是 | 仅售后回补使用的退款操作强引用 | | `order_id` | `uuid` | 是 | 订单引用 | | `order_item_id` | `uuid` | 是 | 订单项引用 | | `quantity_delta` | `integer` | 否 | 正数增加、负数减少 | @@ -527,7 +558,7 @@ A141 按 `(buyer_id,order_item_id)` 取得事务 advisory lock,连同未过期 约束: -- PK `pk_catalog_inventory_movements`;FK `product_id → products.id ON DELETE CASCADE`,属于 Catalog 商品聚合内部历史;只有商品已通过所有跨模块引用检查后才可能触发。 +- PK `pk_catalog_inventory_movements`;FK `product_id → products.id ON DELETE CASCADE`,属于 Catalog 商品聚合内部历史;只有商品已通过所有跨模块引用检查后才可能触发。可空 FK `refund_operation_id → refund_operations.id ON DELETE RESTRICT` 在初始 Migration 后段追加。 - Unique `ux_catalog_inventory_movements_type_operation_id(movement_type, operation_id)`。 - Check `quantity_delta <> 0`、`stock_before >= 0`、`stock_after >= 0`、`stock_after = stock_before + quantity_delta`。 - 类型:`product_created/product_adjusted/seckill_allocated/order_debited/order_cancel_returned/after_sales_returned`。 @@ -538,12 +569,14 @@ A141 按 `(buyer_id,order_item_id)` 取得事务 advisory lock,连同未过期 - `seckill_allocated/order_debited` 的 `quantity_delta < 0`; - `order_cancel_returned/after_sales_returned` 的 `quantity_delta > 0`; - 订单相关类型要求订单/订单项非空。 +- `after_sales_returned` 必须满足 `refund_operation_id IS NOT NULL AND refund_operation_id = operation_id`;其他类型必须 `refund_operation_id IS NULL`。因此通用操作 ID 的唯一保护不能伪造并不存在的售后退款来源。 - 订单相关行使用复合 FK `(order_item_id,order_id,product_id) → order_items(id,order_id,product_id) ON DELETE RESTRICT`;写入能力必须从已锁定订单项派生三者,禁止调用方分别传入可能不一致的引用。因表创建顺序产生的跨模块 FK 在初始 Migration 后段追加。 索引: - `ix_catalog_inventory_movements_product_id_created_at(product_id, created_at, id)`。 - `ix_catalog_inventory_movements_order_item_id(order_item_id) WHERE order_item_id IS NOT NULL`。 +- `ix_catalog_inventory_movements_refund_operation_id(refund_operation_id) WHERE refund_operation_id IS NOT NULL`。 所有库存修改先锁商品或使用 `WHERE stock >= :quantity` 条件更新,取得前后值后同事务插入流水。A122 创建正库存商品时也写入 `product_created`,使审计链从 0 开始。取消/退款先尝试插入稳定 `operation_id` 流水;唯一冲突表示已处理,不能再次增加库存。 @@ -604,7 +637,9 @@ A141 按 `(buyer_id,order_item_id)` 取得事务 advisory lock,连同未过期 - 新条目默认选中;重复加购锁定既有行并累加,不能生成平行条目。 - 不保存价格、库存、商品状态、`is_available` 或失败原因;每次查询、选择和结算从 Catalog 取得当前事实。 -- GET 不产生写操作。已选商品后来失效时可返回 `isSelected=true/isAvailable=false`,但不计入可结算金额;A206 处理选择动作时必须把失效条目保持未选中。 +- GET 不产生写操作。`is_selected` 保存买家的选择意图,商品当前是否可结算由 Catalog 实时事实单独派生,因此四种 `isSelected × isAvailable` 组合都可能出现。 +- 已选商品后来失效时仍返回 `isSelected=true/isAvailable=false`,不计入可结算金额,并使 `checkoutReady=false`;服务端不得在 GET 中静默取消选择,也不得在 A208/A301 中跳过该条目继续下单。 +- A206 `SetExplicit(false)` 允许取消任意本人条目,不要求商品可用;`SetExplicit(true)` 对失效条目返回确定冲突。`SelectAll/Invert` 对操作时失效条目统一写 `false`,只选择当时可用条目。商品恢复后,如果期间没有选择命令覆盖,原持久化选择意图重新成为可结算选择。 - A301 成功时精确删除请求中的已选条目;订单失败则购物车原样保留。 ### 6.2 DB042 `seckill_activities` @@ -670,7 +705,7 @@ draft / published / ongoing → cancelled - Draft 无论计划时间是否已经过去都允许创建人取消,避免“过期草稿既不自动结束又无法取消”的死状态。 - Published/Ongoing 取消时若锁后数据库时间已达到 `end_at`,先按自然结束处理并返回状态冲突。 - 发布后商品、活动名称、价格、计划量、限购和时间全部不可编辑。 -- 每次已提交的活动内容修改、发布、生命周期推进、取消、抢购扣减、待支付取消回补或售后回补都必须在同一条件更新中执行 `result_version = result_version + 1`;只读时间推导不改版本。任何写入路径不得绕过该规则,A226/A227/A228 以此拒绝旧结果覆盖新快照。 +- 每次已提交的活动内容修改、发布、生命周期推进、取消、抢购扣减、待支付取消回补或售后回补都必须在同一条件更新中执行 `result_version = result_version + 1`;只读时间推导不改存储列。对外 `resultVersion` 固定计算为 `result_version * 8 + effective_phase_code`,阶段码为 `draft=0、published=1、ongoing=2、ended=3、cancelled=4`。这样同一存储版本内由时间推导的阶段仍严格递增,任一后续真实写入的最小版本也大于此前所有阶段码;A226/A227/A228 必须返回该有效版本,不能直接暴露存储列。 - 已结束/取消活动的剩余库存仍属于原活动,不回普通库存,也不重新开放抢购;历史订单取消/退款仍可回补该封闭库存。 ### 6.3 DB043 `seckill_buyer_quotas` @@ -702,6 +737,7 @@ draft / published / ongoing → cancelled | `activity_id` | `uuid` | 否 | 活动 | | `movement_type` | `varchar(32)` | 否 | 变动类型 | | `operation_id` | `uuid` | 否 | 稳定业务操作 | +| `refund_operation_id` | `uuid` | 是 | 仅售后回补使用的退款操作强引用 | | `order_id` | `uuid` | 是 | 订单 | | `order_item_id` | `uuid` | 是 | 订单项 | | `buyer_id` | `uuid` | 是 | 买家 | @@ -716,7 +752,7 @@ draft / published / ongoing → cancelled 约束与索引: -- PK `pk_seckill_inventory_movements`;FK `activity_id → seckill_activities`、可空 `buyer_id → users` 均 `ON DELETE RESTRICT`。 +- PK `pk_seckill_inventory_movements`;FK `activity_id → seckill_activities`、可空 `buyer_id → users` 均 `ON DELETE RESTRICT`;可空 FK `refund_operation_id → refund_operations` 在初始 Migration 后段追加并使用 `ON DELETE RESTRICT`。 - 订单相关行使用复合 FK `(order_item_id,order_id,activity_id) → order_items(id,order_id,seckill_activity_id)` 与 `(order_id,buyer_id) → orders(id,buyer_id)`,均 `ON DELETE RESTRICT`;跨模块 FK 在初始 Migration 后段追加。 - Unique `ux_seckill_inventory_movements_type_operation_id(movement_type, operation_id)`。 - 类型:`catalog_allocated/order_debited/order_cancel_returned/after_sales_returned`。 @@ -728,10 +764,11 @@ draft / published / ongoing → cancelled - `quota_after = quota_before - quantity_delta`; - `sold_count_after = sold_count_before - quantity_delta`。 因此抢购扣库存时占用/已售增加,待支付取消时二者减少。 -- `after_sales_returned` 的 `operation_id=refund_operation_id`,要求订单、订单项、买家和已售前后值非空,且 `sold_count_after = sold_count_before - quantity_delta`;它只增加活动库存并减少已售量,不修改限购,配额字段为空。 +- `after_sales_returned` 要求 `refund_operation_id IS NOT NULL AND operation_id=refund_operation_id`,并要求订单、订单项、买家和已售前后值非空,且 `sold_count_after = sold_count_before - quantity_delta`;它只增加活动库存并减少已售量,不修改限购,配额字段为空。其他类型必须 `refund_operation_id IS NULL`。 - 订单相关引用必须从已锁定订单项、订单和买家事实派生,不接受调用方自由组合。 - `ix_seckill_inventory_movements_activity_created_at(activity_id, created_at, id)`。 - `ix_seckill_inventory_movements_order_item_id(order_item_id) WHERE order_item_id IS NOT NULL`。 +- `ix_seckill_inventory_movements_refund_operation_id(refund_operation_id) WHERE refund_operation_id IS NOT NULL`。 发布活动时,DB026 `seckill_allocated` 负流水和本表 `catalog_allocated` 正流水使用同一 `operation_id=activity_id`,与活动划拨在一个事务提交。 @@ -828,11 +865,11 @@ draft / published / ongoing → cancelled 约束与索引: -- PK `pk_order_items`;FK `order_id → orders`、`product_id → products`、可空 `seckill_activity_id → seckill_activities`,均 `RESTRICT`。 +- PK `pk_order_items`;FK `order_id → orders`、`product_id → products`、可空 `seckill_activity_id → seckill_activities`,均 `RESTRICT`;复合 FK `(product_image_object_key_snapshot,product_id) → product_images(object_key,product_id) ON DELETE RESTRICT`,禁止把商品 A 与商品 B 的合法图片 Key 拼成订单快照。 - Unique `ux_order_items_order_id_product_id(order_id, product_id)`。 - Unique `ux_order_items_id_order_id(id,order_id)` 供上传资格绑定。 - Unique `ux_order_items_identity(id,order_id,product_id)`、`ux_order_items_seckill_identity(id,order_id,seckill_activity_id)` 供库存流水复合归属。 -- Unique `ux_order_items_after_sales_snapshot(id,order_id,product_id,unit_price,quantity,inventory_source)`,供售后不可改写成交单价、购买量和库存来源。 +- Unique `ux_order_items_after_sales_snapshot(id,order_id,product_id,product_name_snapshot,product_image_object_key_snapshot,unit_price,quantity,inventory_source)`,供售后不可改写商品名、历史图片、成交单价、购买量和库存来源。 - 可空复合 FK `(seckill_activity_id,product_id) → seckill_activities(id,product_id)`,保证秒杀订单项商品就是活动商品。 - Check: - `unit_price >= 0`、`original_unit_price >= 0`; @@ -843,11 +880,26 @@ draft / published / ongoing → cancelled - `ix_order_items_order_id(order_id, id)`。 - `ix_order_items_product_id(product_id)`。 - `ix_order_items_seckill_activity_id(seckill_activity_id) WHERE seckill_activity_id IS NOT NULL`。 +- `ix_order_items_product_image_object_key_snapshot(product_image_object_key_snapshot)`,供 A128 在持有商品/图片锁期间按历史强引用定点判断,禁止全表扫描 DB062 后再决定是否删除对象。 -库存回补始终读取 `inventory_source + seckill_activity_id + quantity` 快照。售后处理中/已退款数量由 AfterSales 提供,不在订单项冗余维护;A307 有任何非终态售后时整单阻断,否则 `shipped_quantity = quantity - refunded_quantity`。 +库存回补始终读取 `inventory_source + seckill_activity_id + quantity` 快照。售后处理中/已退款数量由 AfterSales 提供,不在订单项冗余维护;A307 有任何非终态售后时整单阻断,并在发货迁移瞬间按当时已退款量写入 `shipped_quantity = quantity - refunded_quantity_at_ship`。该字段随后冻结为真实历史发货快照;`Shipped/Completed` 之后发生的退款只由 AfterSales、退款操作和库存流水表达,不回写或递减已发货量。 + +`product_image_object_key_snapshot` 是历史媒体强引用,不只是自由文本快照。创建订单时只能引用当时主图对应的 DB023 `attached` 行;订单提交后即使商家从当前图库移除该图片,DB023 只转为 `detached`,原图与缩略图对象继续保留。接口从对象 Key 派生本期稳定公开读 URL,订单仍不固化域名或 URL。 订单头与明细属于跨行不变量:Normal 订单全部明细必须为 `catalog` 且活动为空;Seckill 订单必须恰有一条明细,明细为 `seckill`,并与订单头保存同一 `seckill_activity_id`。Ordering 在同一创建事务中生成全部明细并在提交前校验,数据库集成测试必须构造非法组合证明不能经公开写入能力落库;该规则不能伪装成单行 Check。 +#### 订单组合读模型 + +A302/A303/A305/A306 的履约、售后与评价摘要都是请求时派生的只读组合,不增加订单状态列,也不持久化易漂移的 `can_ship`、售后汇总或评价汇总: + +- Ordering 在读取任何组合事实前开启同一短生命周期 `REPEATABLE READ READ ONLY` 事务,以同一 `DbConnection + DbTransaction` 读取 DB061/DB062 并调用 AfterSales、Review 公开批量能力,再按本页最多 50 张订单形成去重的 `order_id/order_item_id` 集合;默认 `READ COMMITTED` 的逐语句快照不满足同一响应的一致性要求。 +- AfterSales 通过一次批量公开能力读取 DB086,按订单项计算 `processing_quantity = SUM(quantity WHERE status IN ('pending_review','pending_return','pending_receipt','refunding','refund_failed'))`、`refunded_quantity = SUM(quantity WHERE status='refunded')`;每个请求 Key 必须返回,不能把缺 Key 当零。 +- Review 通过一次批量公开能力读取 DB024,以 `order_item_id` 唯一评价是否存在派生 `reviewed_item_count`;订单为 Completed 时,`reviewable_item_count = order_item_count - reviewed_item_count`,其他核心状态为 0。 +- 只有核心状态为 `Paid` 时,`remaining_fulfillable_quantity = SUM(order_items.quantity) - SUM(refunded_quantity)`;`PendingPayment/Cancelled/Shipped/Completed` 固定为 0。处理中数量不从 `Paid` 的最终可履约量扣除,但任一处理中数量使 `FulfillmentSummary=BlockedByAfterSales` 且 `canShip=false`;`Shipped/Completed` 的历史真实发货量只读冻结的 `shipped_quantity`。 +- `Paid` 且处理中数量为 0、剩余可履约量大于 0 时,`refunded_quantity > 0` 映射为 `PartiallyRefundedReadyToShip`,否则才是 `ReadyToShip`;剩余量为 0 时为 `FullyRefunded`。`Shipped/Completed/Cancelled/PendingPayment` 分别按核心状态优先映射,不能被摘要反向改写。 +- DB086 数量为负、处理中加已退款超过购买量、批量响应缺 Key或依赖不可用时,组合状态必须标记 `Degraded`,相关摘要返回空且所有依赖动作失败关闭;不得回退为“无售后”“未评价”或 `canShip=true`。 +- 多个模块虽共用同一 PostgreSQL 部署,应用代码仍只能调用公开 Application/Contracts 能力;禁止跨模块直接查询 DbContext、逐订单发 HTTP 或形成 N+1。 + ### 6.7 DB063 `order_lifecycle_tasks` 用途:记录 C03 超时取消与发货后自动完成的尝试、退避和最终结果;订单行仍是唯一业务状态。 @@ -888,19 +940,21 @@ draft / published / ongoing → cancelled Worker 同时扫描订单部分索引,以权威订单时间为准 `INSERT ... ON CONFLICT DO UPDATE` 修复缺失或错误调度时间;执行时再次锁订单并按订单字段重检,绝不能仅凭任务 `due_at` 提前改变业务状态。租约和 `SKIP LOCKED` 只减少重复工作,最终正确性仍由订单状态条件和库存流水唯一约束保证。 +执行参数使用 `OrderLifecycle:ScanIntervalSeconds`、`OrderLifecycle:BatchSize`、`OrderLifecycle:MaxBatchesPerRun`,默认 5 秒、100 条、10 批,允许范围 1~60 秒、1~500 条、1~100 批;越界或多实例配置摘要不一致时该 Worker 能力 NotReady。每轮按 `next_attempt_at,due_at,id` 升序领取,单批短事务提交租约后逐笔独立执行业务事务,单笔失败不阻断同批;达到单轮批数上限即让出,数据库连接级失败停止本轮。领取 DB063 时生成 60 秒 `lease_token`,长事务每 20 秒续租,所有完成/重试更新必须匹配 Token。领取即原子 `attempt_count+1` 并写 `last_attempt_at`;显式瞬态失败或过期租约按 5 秒、30 秒、2 分钟、10 分钟、30 分钟退避,此后每 1 小时持续重试。DB063 不设“重试耗尽”终态:只有业务取消/完成已提交,或订单已被支付、主动取消、买家确认等竞争方合法推进时才 Finalized;第 5 次失败 Warning,第 20 次及以后每 24 小时聚合 Critical。吞吐配置不得改变持续收敛、固定退避阶梯、Token 围栏和 PostgreSQL 权威时间语义。 + ### 6.8 核心事务与锁顺序 #### A301 普通下单 -1. 取得 DB104 幂等范围锁并重查确定结果。 -2. 通过 Identity 锁定唯一启用默认商家责任门槛,读取买家安全展示名与地址快照。 +1. 取得 `(buyer_id,idempotency_key)` 的 DB104 幂等范围锁并重查确定结果;请求哈希固定绑定规范化 `address_id + address_version + 按 UUID 字节升序的 cart_item_ids + checkout_revision`。 +2. 通过 Identity 锁定唯一启用默认商家 DB001 责任门槛并重检 Normal,再锁定本人 DB003 地址行并要求 `version=:address_version`,读取买家安全展示名与可信地址快照。地址缺失/越权返回安全 404,版本变化返回确定 `IDENTITY.ADDRESS_VERSION_CONFLICT`,两者都发生在购物车和库存副作用前。 3. 按 ID 稳定顺序锁定请求中的本人、已选购物车条目。 -4. 按商品 ID 稳定顺序锁定并重检 Catalog 普通库存;锁定全部资格事实后取得一次 `order_created_at=clock_timestamp()`,显式写订单 `created_at` 并由它计算固化 `payment_deadline`,再预生成订单/订单项 ID,写 DB061/DB062。 -5. 按相同顺序条件扣减普通库存并写引用已存在订单项的 DB026,再写 DB063 支付过期任务、OrderCreated 事件与 C07 Immediate 失效事件。 +4. 按商品 ID 稳定顺序锁定并重检 DB022 Catalog 普通库存,并按 A208 同一规范对 `buyer_id + 每项(cart_item_id,product_id,cart.version,quantity,product.name,main_image_object_key,unit_price)` 重算 SHA-256 `checkout_revision`。与请求不等时不写任何业务表,返回最新预览并要求新 Key 再确认;相等后取得一次 `order_created_at=clock_timestamp()`,显式写订单 `created_at` 并由它计算固化 `payment_deadline`,再预生成订单/订单项 ID,写 DB061/DB062。 +5. 按相同顺序条件扣减普通库存并写引用已存在订单项的 DB026,再写 DB063 支付过期任务、OrderCreated 事件与 C07 Immediate/Delayed 两条独立失效责任。 6. 精确删除本次购物车条目并核对行数。 7. 写 DB104 成功结果,一次提交。 -任一步失败整体回滚。库存不足、商品不可售、默认商家确定缺失等确定失败使用 Savepoint 回滚业务副作用后保存 DB104;连接中断和提交未知不保存结果。 +Revision 不入库、不锁价、不预占库存,只进入 A301 请求指纹与确定失败响应。购物车条目仍归属本人但取消选中,或锁内数量、版本、名称、主图 Key、价格、销售状态、库存可结算性和正金额资格任一变化,统一返回 `ORDER.CHECKOUT_CHANGED + latestPreview`;不存在/他人条目固定 404 且不返回预览。地址版本冲突单独返回 `IDENTITY.ADDRESS_VERSION_CONFLICT` 且不携带购物车预览。接口不再为同一原因并列 `ORDER.STOCK_INSUFFICIENT/ORDER.ITEM_NOT_AVAILABLE/ORDER.TOTAL_MUST_BE_POSITIVE`。任一步失败整体回滚。CheckoutChanged、地址版本冲突与默认商家确定缺失等确定失败使用 Savepoint 回滚业务副作用后保存 DB104;连接中断和提交未知不保存结果。 #### A222 秒杀发布 @@ -908,13 +962,13 @@ Worker 同时扫描订单部分索引,以权威订单时间为准 `INSERT ... 2. 重检 Draft、商品 OnSale、计划量、当前普通库存与时间。 3. Catalog 条件扣减普通库存,写 DB026; 4. 固化原价,令 `allocated_quantity=planned_quantity`、`remaining_stock=allocated_quantity`、`sold_count=0`,写发布时间和 `published`;DB044 首条 `catalog_allocated` 的数量和前后库存与这些值完全一致; -5. 为普通库存变化写 DB102 C07 Immediate 失效事件; +5. 为普通库存变化同时写 DB102 C07 Immediate/Delayed 两条独立失效责任; 6. 完成 DB104 后提交。发布后不再编辑。 #### A228 秒杀下单 -1. 在应用准入限流前先识别格式有效的幂等键;边缘/Nginx 429 是瞬态结果,不承诺数据库重放,只有进入应用幂等范围后的确定 429 才可保存。 -2. 取得 DB104 幂等锁、默认商家责任门槛、活动行和买家配额行。 +1. 在应用准入限流前先识别格式有效的幂等键;请求哈希固定绑定规范化 `activity_id + quantity + address_id + address_version`。边缘/Nginx 429 是瞬态结果,不承诺数据库重放;只有进入应用幂等范围后的确定 429 才在独立短事务保存,且因尚未锁活动而不保存 `activity_snapshot`。 +2. 通过应用限流后开启同一外层事务,依次取得 DB104 幂等锁、唯一启用默认商家 DB001 责任门槛,再锁定本人 DB003 地址并校验 `buyer_id + address_id + address_version`,取得可信地址快照后才锁活动行和买家配额行;商家/地址锁与同一连接/事务保持到共享订单、Outbox 和幂等结果提交。地址版本冲突是库存扣减前的确定拒绝并保存 DB104,Identity 或数据库结果未知则不固化。 3. 锁后取得 `decision_time`,按时间推导最新有效状态;只有 Ongoing 且 `start_at <= decision_time < end_at` 可继续。 4. 条件扣减活动库存、增加占用;以锁后 `decision_time` 同时作为显式 `order_created_at` 并计算支付截止,预生成订单/订单项 ID,由 Ordering 写共享订单、订单项、支付过期任务和 Outbox;A228 不读购物车。 5. 写引用已存在订单项的 DB044 抢购流水。 @@ -926,21 +980,32 @@ Worker 同时扫描订单部分索引,以权威订单时间为准 `INSERT ... 2. 按订单项 ID 稳定顺序处理: - Catalog:插入 DB026 唯一回补流水并增加普通库存; - Seckill:插入 DB044 唯一回补流水,增加原活动库存、减少 `sold_count`;待支付取消再减少 DB043 占用。 -3. 写订单取消字段、完成 DB063 任务、写 OrderCancelled Outbox;Catalog 回补同时写 C07 Immediate 失效事件。 +3. 写订单取消字段、完成 DB063 任务、写 OrderCancelled Outbox;Catalog 回补同时写 C07 Immediate/Delayed 两条独立失效责任。 4. 同一事务提交;任一项失败整单回滚。 #### 发货与售后竞争 -锁顺序固定为: +A307 发货锁序固定为: ```text -orders -→ AfterSales 目标事实 -→ order_items -→ 本模块结果 / Outbox / Idempotency +DB061 orders +→ DB086 AfterSales 履约快照 +→ DB062 order_items +→ DB063 / Outbox / DB104 ``` -A307 与 A412 都先通过 Ordering 公开能力锁同一订单行并保持至提交。售后先提交时非终态申请阻断整单发货;发货先提交时 A412 按最新已发货规则重算资格。 +A412 提交售后锁序固定为: + +```text +DB104 售后幂等范围 +→ 无锁预读 DB061 不可变 assigned_merchant_user_id(只定位门行) +→ DB001 责任商家门并重检 Normal +→ DB061 orders 并重检归属与 assigned_merchant_user_id +→ DB086 既有申请 / DB062 order_items +→ DB087 / Outbox / DB104 +``` + +两条路径都保持所获锁到共享事务提交,但不得把 A307 的订单先锁顺序套到 A412。售后先提交时非终态申请阻断整单发货;发货先提交时 A412 按最新已发货规则重算资格。 A307 在锁内按订单项重算已退款数量,要求不存在任何非终态售后且 `SUM(order_item.quantity - refunded_quantity) > 0`;全部数量已退款时必须拒绝,不能把零数量订单推进为 Shipped。成功发货时每项 `shipped_quantity` 精确写为 `quantity - refunded_quantity`,所有订单项与订单 `paid → shipped`、任务、Outbox 和 DB104 在同一事务提交。 @@ -1055,10 +1120,11 @@ A401 没有钱包行时返回逻辑零余额且不写库。首次入账可先在 - PK `pk_payment_channel_attempts`。 - Unique `ux_payment_channel_attempts_serial(payment_serial_number)`。 - Unique `ux_payment_channel_attempts_binding(id,bound_order_id,bound_amount,bound_currency)` 供成功支付复合绑定。 -- Check 金额正数、币种 `CNY`、`version >= 1`、时间顺序。 +- Check `bound_amount BETWEEN 0.01 AND 9999999999999999.99`、币种 `CNY`、`version >= 1`、时间顺序。 - `ix_payment_channel_attempts_bound_order_id(bound_order_id,last_processed_at,id)` 服务按订单聚合回调与对账;该索引不把不存在订单误认为合法引用。 - `bound_order_id` 不建硬 FK,因为“流水绑定不存在订单”正是必须保存的差异证据。 - A421 按流水号锁定本行;后续回调改变订单/金额/币种时不覆盖绑定,而把回调裁决为 Difference。 +- DB084 只保存不可变首次绑定和当前运行聚合;`first_success_callback_id/last_callback_id/last_processed_at/version` 都不能作为历史日批次的 as-of 事实。C08 必须从不可变 DB089 截止范围重建当时回调序列。 ### 7.5 DB085 `payments` @@ -1164,7 +1230,7 @@ refund_failed → refunding - PK `pk_after_sales_requests`;复合 FK: - `(order_id,buyer_id) → orders(id,buyer_id)`; - - `(order_item_id,order_id,product_id,unit_price_snapshot,purchased_quantity_snapshot,inventory_source) → order_items(id,order_id,product_id,unit_price,quantity,inventory_source)`; + - `(order_item_id,order_id,product_id,product_name_snapshot,product_image_object_key_snapshot,unit_price_snapshot,purchased_quantity_snapshot,inventory_source) → order_items(id,order_id,product_id,product_name_snapshot,product_image_object_key_snapshot,unit_price,quantity,inventory_source)`; - `(order_id,order_type_snapshot) → orders(id,order_type)`; - 可空 `(order_id,order_completed_at) → orders(id,completed_at)`; - `(original_payment_id,order_id,buyer_id,assigned_merchant_user_id) → payments(id,order_id,buyer_id,assigned_merchant_user_id)`; @@ -1196,7 +1262,7 @@ refund_failed → refunding | `refunded` | 与进入退款时一致 | 全空 | 与 `refunding` 一致 | 必填 | 必填 | - `audited_by_user_id`、`receipt_confirmed_by_user_id` 非空时必须等于 `assigned_merchant_user_id`。 -- `pending_return/pending_receipt` 只允许 `return_and_refund`;`refunding/refund_failed/refunded` 必须存在同一申请唯一 DB088。DB092/DB088 的“必须存在”属于共享事务跨表不变量,不能伪装成单行 Check,必须用同事务写入与数据库集成测试证明。 +- `pending_return/pending_receipt` 只允许 `return_and_refund`;`refunding/refund_failed/refunded` 必须存在同一申请唯一 DB088。首次进入 `refunding` 的提交还必须同时存在该 DB088 的唯一 `attempt_number=1/attempt_kind='initial'` DB093,且 DB088 `attempt_count=1`;禁止提交 `refunding + attempt_count=0`。DB092/DB088/首个 DB093 的“必须存在”属于共享事务跨表不变量,不能伪装成单行 Check,必须由 RefundOrchestrator 通过同事务公开能力写入并用数据库集成测试证明。 索引: @@ -1206,7 +1272,6 @@ refund_failed → refunding | `ix_after_sales_requests_merchant_status_created_at` | `assigned_merchant_user_id, status, created_at DESC, id DESC` | A413 商家 | | `ix_after_sales_requests_order_item_status` | `order_item_id, status` | 数量汇总 | | `ix_after_sales_requests_order_non_terminal` | `order_id, order_item_id WHERE status IN ('pending_review','pending_return','pending_receipt','refunding','refund_failed')` | A307 发货阻断 | -| `ix_after_sales_requests_refund_recovery` | `updated_at, id WHERE status IN ('refunding','refund_failed')` | 退款恢复 | | `ix_after_sales_requests_original_payment_id` | `original_payment_id` | 金额上限与对账 | | `ux_after_sales_requests_refund_posting_sequence` | `UNIQUE refund_posting_sequence WHERE refund_posting_sequence IS NOT NULL` | C08 水位唯一 | | `ix_after_sales_requests_product_id` | `product_id` | 商品删除保护 | @@ -1272,15 +1337,12 @@ A412 必须在取得订单锁后调用一次 `decision_time=clock_timestamp()` | `amount` | `numeric(18,2)` | 否 | 不可变退款额 | | `currency` | `char(3)` | 否 | `CNY` | | `status` | `varchar(24)` | 否 | 三态,默认 `processing` | -| `attempt_count` | `integer` | 否 | 已建立尝试数,默认 `0` | +| `attempt_count` | `integer` | 否 | 已建立尝试数;随首个尝试创建,默认 `1` | | `last_failure_code` | `varchar(100)` | 是 | 确定失败码 | | `posting_sequence` | `bigint` | 是 | 成功财务水位 | | `started_at` | `timestamptz` | 否 | 开始时间 | | `completed_at` | `timestamptz` | 是 | 成功时间 | -| `failed_at` | `timestamptz` | 是 | 最近确定失败 | -| `next_recovery_at` | `timestamptz` | 是 | 未知结果下次核实时间 | -| `last_verified_at` | `timestamptz` | 是 | 最近核实时间 | -| `verification_count` | `integer` | 否 | 核实次数,初始 0 | +| `failed_at` | `timestamptz` | 是 | 进入需人工处理的确定失败时间 | | `updated_at` | `timestamptz` | 否 | 最近变化 | | `version` | `bigint` | 否 | 并发版本,默认 `1` | @@ -1290,14 +1352,12 @@ A412 必须在取得订单锁后调用一次 `decision_time=clock_timestamp()` - Unique `ux_refund_operations_after_sales_request_id(after_sales_request_id)`,每申请一个业务退款。 - 复合 FK `(after_sales_request_id,original_payment_id,buyer_id,amount,currency) → after_sales_requests(id,original_payment_id,buyer_id,calculated_amount,currency) ON DELETE RESTRICT`。 - Unique `ux_refund_operations_wallet_source_identity(id,buyer_id,amount,currency,posting_sequence)` 供钱包流水复合来源 FK。 -- Check 金额正数、币种、尝试数和状态: - - `verification_count >= 0`; - - `processing` 无成功/确定失败终态时间,`next_recovery_at` 非空; - - `succeeded` 有 `posting_sequence/completed_at`,失败与恢复字段为空; - - `definite_failure` 有 `last_failure_code/failed_at`,成功水位/完成时间/恢复时间为空。 -- `ix_refund_operations_recovery(status,next_recovery_at,id) WHERE status='processing'` 服务 Unknown 核实与崩溃恢复。 +- Check 金额正数、币种、`attempt_count >= 1`、`version >= 1`,并冻结三态字段组合: + - `processing`:成功水位、完成时间、最终失败码和失败时间全空;它覆盖首次执行、Unknown 核实中和 AutomaticRetry 排队/执行中; + - `succeeded`:`posting_sequence/completed_at` 非空,最终失败字段为空; + - `definite_failure`:`last_failure_code/failed_at` 非空,成功水位/完成时间为空;只表示最新恢复处置已经是 `merchant_retry_required` 或 `operator_attention_required`,不能把单次自动失败或 Unknown 写成该聚合状态。 - Unique `ux_refund_operations_posting_sequence(posting_sequence) WHERE posting_sequence IS NOT NULL` 服务 C08。 -- `definite_failure → processing` 只复用同一 ID、金额、买家和支付;`succeeded` 永久终态。 +- 调度、核实计数、执行/恢复租约和下一动作全部属于 DB093,不在 DB088 复制。`definite_failure → processing` 只允许 A419 对 `merchant_retry_required` 开启同一退款操作的人工尝试;`operator_attention_required` 不允许商家重试,`succeeded` 永久终态。 ### 7.9 DB089 `payment_callbacks` @@ -1306,13 +1366,14 @@ A412 必须在取得订单锁后调用一次 `decision_time=clock_timestamp()` | `id` | `uuid` | 否 | 直接使用 callbackId | | `server_sequence` | `bigint` | 否 | Identity,内部稳定顺序 | | `channel_attempt_id` | `uuid` | 否 | 规范流水绑定 | -| `request_fingerprint` | `char(64)` | 否 | 规范请求 SHA-256 hex | +| `raw_body_hash` | `char(64)` | 否 | 首次合法投递原始 UTF-8 Body 的 SHA-256 hex,用于 HMAC 证据 | +| `request_fingerprint` | `char(64)` | 否 | 规范业务请求 SHA-256 hex;JSON 等价表达得到同值 | | `claimed_order_id` | `uuid` | 否 | 回调声明订单;不建 FK | | `callback_result` | `varchar(16)` | 否 | `success/failed` | | `amount` | `numeric(18,2)` | 否 | 声明金额 | | `currency` | `char(3)` | 否 | 声明币种 | | `occurred_at` | `timestamptz` | 否 | 渠道发生时间,仅追踪 | -| `callback_key_id` | `varchar(100)` | 否 | 验签 Key 标识 | +| `callback_key_version` | `varchar(64)` | 否 | 首次合法投递从 `X-Callback-Key-Id` 取得的显式 `keyVersion` | | `callback_timestamp` | `timestamptz` | 否 | 签名时间 | | `status` | `varchar(32)` | 否 | 四种确定终态 | | `disposition_code` | `varchar(100)` | 否 | 稳定裁决原因 | @@ -1331,7 +1392,7 @@ A412 必须在取得订单锁后调用一次 `decision_time=clock_timestamp()` - PK `pk_payment_callbacks`;Unique `ux_payment_callbacks_server_sequence(server_sequence)`,列定义 `GENERATED ALWAYS AS IDENTITY`。 - FK `channel_attempt_id → payment_channel_attempts`;可空复合 FK `(existing_payment_id,claimed_order_id) → payments(id,order_id)`,保证既有成功来源属于声明订单;`processed_success` 使用复合 FK `(created_payment_id,claimed_order_id,channel_attempt_id,amount,currency) → payments(id,order_id,channel_attempt_id,amount,currency)`,保证回调创建的支付与声明订单、流水、金额、币种一致,均 `ON DELETE RESTRICT`。 - 状态只允许 `processed_success/processed_failure/ignored/difference`;不持久化 Processing。 -- Check 指纹格式、金额、币种、HTTP 范围、posting sequence、`response_body` 为 JSON object;两个支付引用不得同时非空。字段矩阵: +- Check `raw_body_hash/request_fingerprint` 均为 64 位小写十六进制、`amount BETWEEN 0.01 AND 9999999999999999.99`、币种、HTTP 范围、posting sequence、`callback_key_version` 匹配 `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`、`response_body` 为 JSON object;两个支付引用不得同时非空。字段矩阵: - `processed_success` 必须且只能有 `created_payment_id`; - `processed_failure` 两个支付引用均空; - `ignored/difference` 的 `created_payment_id` 为空,`existing_payment_id` 按裁决码决定; @@ -1346,18 +1407,29 @@ A412 必须在取得订单锁后调用一次 `decision_time=clock_timestamp()` - 同时命中多个 Difference 原因时按 `order_not_found → serial_binding_mismatch → amount_mismatch → currency_mismatch → other_payment_source_succeeded → cancelled_success → expired_success` 选择主裁决码;全部命中事实进入安全响应和后续对账证据,不能因单一主码丢失。 - `ix_payment_callbacks_attempt_sequence(channel_attempt_id,server_sequence)`。 - `ix_payment_callbacks_order_processed_at(claimed_order_id,processed_at,id)`。 +- `ix_payment_callbacks_reconciliation_as_of(processed_at,posting_sequence,channel_attempt_id,server_sequence)`,供 C08 在固定 `range_to + watermark_sequence` 下重建历史流水聚合。 - `ix_payment_callbacks_status_posting_sequence(status,posting_sequence)`。 - Unique `ux_payment_callbacks_posting_sequence(posting_sequence)`。 - `ix_payment_callbacks_difference(posting_sequence,id) WHERE status='difference'`。 -同 callbackId 先用 advisory lock,再查 PK:同指纹精确重放 `response_http_status/response_body`,不同指纹返回冲突。签名失败、字段格式错误和基础设施未知不写业务回调表。 +HMAC、严格 JSON 与幂等指纹固定按以下顺序执行: + +1. Nginx 回调 location、Kestrel 端点和应用读取把原始请求实体统一限制为 16384 字节;`Content-Length` 预检和分块/无长度请求的第 16385 字节哨兵都必须立即返回 413,不进入 HMAC、JSON 或数据库。上限内只保留一份未经反序列化改写的原始 UTF-8 Body 字节,计算 `rawBodyHash = lowercase-hex(SHA256(rawBodyBytes))`。`X-Callback-Key-Id` 的值在协议上就是 `keyVersion`,签名串固定为 `keyVersion + "\n" + unixTimestamp + "\n" + rawBodyHash`;使用该版本对应 Secret 计算 HMAC-SHA256,严格 Base64 解码请求签名后以常量时间比较。DB089 只保存哈希和版本标识,绝不保存 Secret、原始 Body 或原始签名。 +2. 本次验签先从 PostgreSQL 取得一次 `databaseNow=clock_timestamp()`;`X-Callback-Timestamp` 与 `databaseNow` 偏差固定不得超过正负 300 秒。Key Ring 只接受请求显式指定的当前版本或紧邻上一版本,不得依次试验多把密钥:两种版本都必须满足 `activatedAt <= callbackTimestamp`;上一版本还必须同时满足 `callbackTimestamp <= retiredAt + 10 minutes` 与 `databaseNow <= retiredAt + 10 minutes`。更老版本、未知版本、未激活版本、已过固定 10 分钟退役宽限版本和错误签名统一返回不泄露细节的签名失败。所有 API 实例的当前/上一 `keyVersion`、`activatedAt/retiredAt` 与配置摘要必须一致,不一致的实例 NotReady。 +3. HMAC 通过后使用严格 JSON 读取器解析。顶层只能且必须出现一次 `callbackId,paymentSerialNumber,orderId,result,occurredAt,amount,currency`;属性名区分大小写,重复属性、未知属性、缺失属性、数组、嵌套对象及未定义 `null` 全部拒绝,不能让反序列化器采用“最后一个同名属性获胜”。 +4. 规范业务请求属性顺序固定为 `callbackId,paymentSerialNumber,orderId,result,occurredAt,amount,currency`:UUID 转小写 D;流水字符串限 1~100 个 Unicode 字符、禁止控制字符,做 Unicode NFC、保留大小写并拒绝首尾空白;`result` 只用 `Success/Failed`;`occurredAt` 输入必须显式零偏移,解析为同一瞬时后转 UTC `.NET "O"` 的 `Z` 形式;金额限定 `0.01~9999999999999999.99`,按 invariant `0.00` 两位小数且无指数;币种固定 `CNY`。按固定顺序输出无 BOM、无额外空白的 UTF-8 JSON,再计算 `request_fingerprint`。 +5. JSON 属性顺序、属性间空白、UUID 字母大小写、合法等价零偏移 UTC 表达以及 `199/199.0/199.00` 等价金额表达不改变 `request_fingerprint`;它们仍会因原始字节不同得到不同 `raw_body_hash`,但业务幂等视为同一请求。同一 callbackId 首次保存的 `raw_body_hash/callback_key_version/callback_timestamp` 不因等价重放而覆盖。 + +同 callbackId 先用 advisory lock,再查 PK:同 `request_fingerprint` 精确重放首次 `response_http_status/response_body`,不同指纹返回冲突。使用新 Key 对同一规范业务请求重签仍可重放首次结果;旧 Key 超过 10 分钟宽限则在查询 DB089 前拒绝。签名失败、严格 JSON/固定字段错误和基础设施未知不写业务回调表。 + +C08 每日比较只把 `posting_time ∈ [range_from,range_to)` 且 `posting_sequence <= watermark_sequence` 的支付、回调或退款财务结果作为本日计数锚点。锚点通过稳定 ID 引用的更早订单、支付、回调、售后、退款和钱包事实可作为水位内佐证,但不得再次计数;`range_to` 之后提交的任何事实不得影响旧日结论。订单支付一致性以 DB061 的已支付谱系为准:`paid/shipped/completed` 且 `paid_at/payment_posting_sequence` 非空都必须有唯一 DB085,成功 DB085 也只允许对应这三态;`pending_payment/cancelled` 与成功支付不相容。 ### 7.10 DB090 `reconciliation_batches` | 字段 | 类型 | 空 | 说明 | |---|---|---:|---| | `id` | `uuid` | 否 | 批次 ID | -| `business_date` | `date` | 否 | 前一完整 UTC 业务日 | +| `business_date` | `date` | 否 | 任一已经结束且尚未生成批次的完整 UTC 业务日 | | `range_from` | `timestamptz` | 否 | 半开区间起 | | `range_to` | `timestamptz` | 否 | 半开区间止 | | `watermark_at` | `timestamptz` | 否 | 展示水位时间 | @@ -1375,13 +1447,20 @@ A412 必须在取得订单锁后调用一次 `decision_time=clock_timestamp()` - PK `pk_reconciliation_batches`。 - Unique `ux_reconciliation_batches_business_date(business_date)`;同一 UTC 日只有一个权威批次,重复任务返回既有批次。 -- Check `range_from = business_date::timestamp AT TIME ZONE 'UTC'`、`range_to = range_from + interval '1 day'`、`watermark_at >= range_to`、`watermark_sequence >= 0`、所有计数非负;因此批次只能覆盖上一完整 UTC 自然日,不能用同一 `business_date` 抢占任意时间范围: +- Check `range_from = business_date::timestamp AT TIME ZONE 'UTC'`、`range_to = range_from + interval '1 day'`、`watermark_at >= range_to`、`watermark_sequence >= 0`、所有计数非负;同一 `business_date` 不能抢占任意时间范围: - `total_count = payment_unit_count + refund_unit_count`; - `total_count = matched_count + difference_count`; - Matched 要求差异 0、解决时间空; - HasDifferences 要求差异 >0、解决时间空; - Resolved 要求差异 >0、解决时间非空。 - `ix_reconciliation_batches_status_business_date(status,business_date DESC,id DESC)`。 +- `total_count` 是本日去重 `anchor_posting_sequence` 数;Payment/Callback 锚点进入 `payment_unit_count`,RefundOperation 锚点进入 `refund_unit_count`。`difference_count` 是至少有一条 DB091 的不同锚点数,不是差异行数;同一锚点命中多条规则只计 1。`differenceCountsByType` 由 DB091 行分组,因此各类型之和可以大于 `difference_count`。 +- 服务端只允许已经到达固定次日 `00:05Z` 触发点的 `business_date`。当前 UTC 时间不早于 `00:05Z` 时,`latest_eligible_business_date=当前日期-1 日`;早于 `00:05Z` 时取 `当前日期-2 日`。该时间条件不能伪装成 PostgreSQL Check,必须由 Worker 输入校验和集成测试保证。 +- 先计算 `coverage_start = LEAST(latest_eligible_business_date, earliest_posting_date(若存在), earliest_existing_batch_date(若存在))`,不存在的候选不参与,财务事实和批次都不存在时取 `latest_eligible_business_date`;在 `[coverage_start,latest_eligible_business_date]` 中找最早缺失日并按日期升序补齐。无交易、停机多日或刚结束但尚未到触发点的日期都不得导致跳日或提前建批。 +- 每个日期拥有独立 DB107 `run_key=YYYY-MM-DD` 责任、独立 DB096 截止水位和独立生成事务。任务取得执行资格后,先等待已经分配财务序号的在途事务提交或回滚,再读取 DB096 已提交 `watermark_sequence` 与同一数据库时钟 `watermark_at`,随后开启新的 `REPEATABLE READ` 快照;不能先建立旧快照后等待水位,也不能扫描中途移动水位。 +- 对账生成事务对 DB061/DB083/DB085~DB089 等来源业务事实只做不带 `FOR UPDATE` 的读取,仅写 DB090~DB095 与 DB107。锚点必须同时满足 `posting_time ∈ [range_from,range_to)` 和 `posting_sequence <= watermark_sequence`;水位后的当前值、迟到纠正和后来状态不得改写旧批次快照。 +- 某日失败立即停止本轮,已经成功的前序日期不回滚,下次仍从首个缺失日继续。不得合并多日、跳过失败日或只处理昨天。 +- `total_count=0/difference_count=0` 的完整空日仍写 `matched`,用于区分“已执行但无交易”与“任务未执行”。任一已存在的 Matched、HasDifferences 或 Resolved 批次都是该日不可覆盖的权威快照;比较规则升级也不得重开或静默改写历史批次。 ### 7.11 DB091 `reconciliation_differences` @@ -1389,6 +1468,9 @@ A412 必须在取得订单锁后调用一次 `decision_time=clock_timestamp()` |---|---|---:|---| | `id` | `uuid` | 否 | 差异 ID | | `batch_id` | `uuid` | 否 | 批次 | +| `anchor_kind` | `varchar(32)` | 否 | `payment/callback/refund_operation` | +| `anchor_id` | `uuid` | 否 | 本次锚点稳定 ID;缺失事实按固定矩阵使用订单/售后申请 ID | +| `anchor_posting_sequence` | `bigint` | 否 | 批次内唯一财务比较锚点序号 | | `difference_type` | `varchar(64)` | 否 | 已确认 12 类之一 | | `status` | `varchar(24)` | 否 | `pending/in_progress/resolved` | | `subject_type` | `varchar(32)` | 否 | 比较主体类型 | @@ -1434,19 +1516,41 @@ A412 必须在取得订单锁后调用一次 `decision_time=clock_timestamp()` - `duplicate_wallet_credit`; - `refund_amount_mismatch`。 - 业务对象引用故意不统一建立 FK,因为“对象缺失”本身可能是差异;已存在引用由生成/解决规则校验。 -- `expected_facts/actual_facts` 必须为 JSON object,`comparison_rule_version >= 1`、`version >= 1`。 -- Unique `ux_reconciliation_differences_unit(batch_id,subject_type,subject_id,comparison_rule_code)`,同一比较单元累积证据而不重复计数。 +- `anchor_kind` 只允许 `payment/callback/refund_operation`;`anchor_posting_sequence > 0`。同一序号存在多个原子事实时按 `refund_operation > payment > callback` 选锚点类别;缺失支付时 `anchor_kind=payment,anchor_id=order_id`,缺失退款操作时 `anchor_kind=refund_operation,anchor_id=after_sales_request_id`,不得生成空或随机锚点。 +- `subject_type` 只允许 `order/payment/callback/after_sales_request/refund_operation/wallet_transaction`,并与下方固定矩阵一致。 +- `expected_facts/actual_facts` 必须为 JSON object,且只含对应规则版本的固定键;`comparison_rule_version >= 1`、`version >= 1`。 +- Unique `ux_reconciliation_differences_anchor_rule(batch_id,anchor_posting_sequence,comparison_rule_code,comparison_rule_version)`,同一锚点同一规则累积证据而不重复建行;同一锚点命中不同规则必须分别建行。 - 状态组合: - Pending 无领取/解决; - InProgress 可有有效领取人,也允许 Release 后领取字段全空; - Resolved 解决字段、处理人、解决时间和 `verification_result='matched'` 非空,当前领取三字段必须清空;领取历史保存在 DB095。 -- 领取三字段全空或全非空;领取有效期晚于领取时间。 +- 领取三字段全空或全非空;`claim_expires_at = claimed_at + interval '30 minutes'`,固定不可续期。到期不自动把差异改回 Pending,也不清空或隐藏三项当前领取字段;A426 必须继续返回原领取人的非空 `claim` 并按本次数据库 `serverTime` 派生 `isExpired=true`,只是原领取权立即失效。只有 Release、Takeover 或 Resolve 的成功事务才按各自动作改写领取字段,全部历史另存 DB095;新的 Takeover 使用新时间写新的固定 30 分钟,不得沿用或延长旧到期时间。 - `resolution_type` 只允许 `corrected_by_controlled_action/confirmed_no_business_impact`;前者必须有受控动作引用,后者必须为空。 - 复核失败保持 `in_progress`,固定 `verification_result='still_mismatched'` 且 `remaining_mismatch_reason` 非空;复核成功进入 Resolved 后该原因必须为空。 +固定规则矩阵如下。金额在 JSON 中使用两位小数 CNY 字符串,UUID 使用小写 D,时间使用 UTC;缺失值保留键并写 JSON `null`,不得省略或加入自由键: + +| `difference_type` | 锚点 / 主体 | `comparison_rule_code/version` | `expected_facts` 固定键 | `actual_facts` 固定键 | +|---|---|---|---|---| +| `payment_succeeded_order_not_updated` | Payment/paymentId/payment.sequence;Order/orderId | `C08.PAYMENT_ORDER_LINEAGE/1` | `paymentId,orderId,success,amount,currency,paymentPostingSequence,allowedOrderStatuses` | `orderStatus,paidAt,paymentPostingSequence,orderAmount,orderCurrency` | +| `order_paid_payment_missing` | Payment/orderId/order.sequence;Order/orderId | `C08.ORDER_PAYMENT_PRESENCE/1` | `orderId,orderPaidLineage,paymentPostingSequence,successfulPaymentCount,amount,currency` | `orderStatus,paidAt,paymentPostingSequence,successfulPaymentCount,paymentIds,paymentPostingSequences` | +| `multiple_successful_payment_sources` | Payment/laterPaymentId/laterPayment.sequence;Payment/laterPaymentId | `C08.ORDER_SINGLE_SUCCESS_SOURCE/1` | `orderId,successfulPaymentCount,authoritativePaymentId,amount,currency` | `successfulPaymentCount,paymentIds,sourceTypes,postingSequences,amounts,currencies` | +| `late_success_callback` | Callback/callbackId/callback.sequence;Callback/callbackId | `C08.CALLBACK_LATE_SUCCESS/1` | `acceptedSuccess,paymentCreated,walletDelta,successNotificationCreated` | `callbackResult,callbackDisposition,orderId,orderStatus,paymentDeadline,finalTime,paymentCreated,walletDelta,successNotificationCreated` | +| `callback_binding_mismatch` | Callback/callbackId/callback.sequence;Callback/callbackId | `C08.CALLBACK_BINDING_IMMUTABLE/1` | `channelTransactionNo,boundOrderId,boundAmount,boundCurrency` | `callbackId,claimedOrderId,claimedAmount,claimedCurrency,mismatchedFields` | +| `processed_success_payment_missing` | Callback/callbackId/callback.sequence;Callback/callbackId | `C08.CALLBACK_SUCCESS_PAYMENT_PRESENCE/1` | `callbackDisposition,successfulPaymentCount,orderPaidLineage,orderId,amount,currency` | `callbackDisposition,successfulPaymentCount,paymentIds,orderStatus,paidAt,paymentPostingSequence` | +| `refunded_operation_missing` | RefundOperation/afterSalesRequestId/afterSales.sequence;AfterSalesRequest/afterSalesRequestId | `C08.AFTER_SALES_REFUND_OPERATION_PRESENCE/1` | `afterSalesRequestId,afterSalesStatus,successfulRefundOperationCount,approvedRefundAmount,currency,refundPostingSequence` | `afterSalesStatus,closedAt,refundPostingSequence,successfulRefundOperationCount,refundOperationIds` | +| `duplicate_refund_operation` | RefundOperation/laterRefundOperationId/laterRefund.sequence;RefundOperation/laterRefundOperationId | `C08.REFUND_OPERATION_SINGLE_SUCCESS/1` | `afterSalesRequestId,successfulRefundOperationCount,authoritativeRefundOperationId,amount,currency` | `successfulRefundOperationCount,refundOperationIds,postingSequences,amounts,currencies` | +| `refund_succeeded_after_sales_not_updated` | RefundOperation/refundOperationId/refund.sequence;AfterSalesRequest/afterSalesRequestId | `C08.REFUND_AFTER_SALES_TERMINAL/1` | `refundOperationId,refundOperationStatus,afterSalesStatus,amount,currency,refundPostingSequence` | `refundOperationStatus,completedAt,afterSalesStatus,closedAt,refundPostingSequence` | +| `refund_succeeded_wallet_credit_missing` | RefundOperation/refundOperationId/refund.sequence;RefundOperation/refundOperationId | `C08.REFUND_WALLET_CREDIT_PRESENCE/1` | `refundOperationId,buyerId,walletCreditCount,walletCreditAmount,currency,refundPostingSequence` | `walletCreditCount,walletTransactionIds,walletCreditAmounts,walletPostingSequences` | +| `duplicate_wallet_credit` | RefundOperation/refundOperationId/laterWallet.sequence;WalletTransaction/laterWalletTransactionId | `C08.REFUND_WALLET_SINGLE_CREDIT/1` | `refundOperationId,buyerId,walletCreditCount,walletCreditAmount,currency` | `walletCreditCount,walletTransactionIds,walletCreditAmounts,walletPostingSequences` | +| `refund_amount_mismatch` | RefundOperation/refundOperationId/refund.sequence;RefundOperation/refundOperationId | `C08.REFUND_AMOUNT_CONSISTENCY/1` | `afterSalesRequestId,refundOperationId,approvedRefundAmount,refundOperationAmount,walletCreditAmount,paymentRefundedTotalDelta,currency` | `approvedRefundAmount,refundOperationAmount,walletCreditAmount,paymentRefundedTotalDelta,currencies,walletCreditCount` | + +每条差异至少保存能重放同一比较的 DB094 证据。对象存在时引用相应 DB061/DB083/DB085/DB086/DB087/DB088/DB089;对象缺失时保存批次范围、水位、规范主体、固定查询条件和空结果哈希,不能把“没有行”误写成“没有证据”。ID 数组固定按 postingSequence、ID 升序。 + 索引: - `ix_reconciliation_differences_batch_status_created_at(batch_id,status,created_at DESC,id DESC)`。 +- `ix_reconciliation_differences_batch_anchor(batch_id,anchor_posting_sequence,id)`。 - `ix_reconciliation_differences_assignee_claim(current_assignee_user_id,claim_expires_at) WHERE status='in_progress'`。 - `ix_reconciliation_differences_subject(subject_type,subject_id)`。 @@ -1478,27 +1582,61 @@ A412 必须在取得订单锁后调用一次 `decision_time=clock_timestamp()` | `id` | `uuid` | 否 | 尝试 ID | | `refund_operation_id` | `uuid` | 否 | 稳定业务退款 | | `attempt_number` | `integer` | 否 | 从 1 递增 | +| `attempt_kind` | `varchar(24)` | 否 | `initial/automatic_retry/manual_retry` | | `status` | `varchar(24)` | 否 | `executing/unknown/succeeded/definite_failure` | | `executor_kind` | `varchar(24)` | 否 | `merchant/system_recovery` | | `executor_user_id` | `uuid` | 是 | 商家操作者 | | `worker_instance_id` | `varchar(100)` | 是 | 系统恢复实例 | | `execution_token` | `uuid` | 否 | 本次执行身份 | -| `lease_expires_at` | `timestamptz` | 是 | 执行租约 | +| `execution_lease_expires_at` | `timestamptz` | 是 | Executing 最晚确认时间 | | `failure_code` | `varchar(100)` | 是 | 安全失败码 | | `verification_count` | `integer` | 否 | Unknown 核实次数,初始 0 | | `last_verified_at` | `timestamptz` | 是 | 最近核实 | +| `recovery_disposition` | `varchar(32)` | 否 | `none/verify_original/automatic_retry/merchant_retry_required/operator_attention_required/consumed` | +| `next_action_at` | `timestamptz` | 是 | Unknown 核实或自动重试到期时间 | +| `manual_retry_available_at` | `timestamptz` | 是 | 商家最早可人工重试时间 | +| `attention_required_at` | `timestamptz` | 是 | 首次升级系统关注时间 | +| `recovery_lease_owner` | `varchar(100)` | 是 | 恢复 Worker | +| `recovery_lease_token` | `uuid` | 是 | 恢复围栏 Token | +| `recovery_lease_acquired_at` | `timestamptz` | 是 | 本轮恢复租约首次取得时间,续租不改 | +| `recovery_lease_expires_at` | `timestamptz` | 是 | 恢复租约到期 | +| `recovery_lease_takeover_count` | `integer` | 否 | 连续异常接管次数,正常完成一轮后归零 | | `started_at` | `timestamptz` | 否 | 开始 | | `finished_at` | `timestamptz` | 是 | 确定结果时间 | | `trace_id` | `varchar(64)` | 是 | 链路 | - PK `pk_refund_attempts`;FK `refund_operation_id → refund_operations`、可空 `executor_user_id → users`,均 `ON DELETE RESTRICT`。 - Unique `ux_refund_attempts_operation_number(refund_operation_id,attempt_number)`、Unique `ux_refund_attempts_execution_token(execution_token)`。 +- 部分 Unique `ux_refund_attempts_initial(refund_operation_id) WHERE attempt_kind='initial'`;`initial` 必须且只能使用 `attempt_number=1`,并与 DB088 创建及 `attempt_count=1` 同事务提交。 - 部分 Unique `ux_refund_attempts_unresolved(refund_operation_id) WHERE status IN ('executing','unknown')`,每个退款最多一个未决尝试。 -- 建立新尝试时锁 DB088,先令 `refund_operations.attempt_count = attempt_count + 1`,新 DB093 `attempt_number` 必须等于该值;Unknown 核实不增加编号。 -- `merchant` 要求 `executor_user_id` 非空且 Worker 为空;`system_recovery` 要求 Worker 非空且用户为空。 -- `executor_kind='merchant'` 时,A419 锁定 DB088 后继续锁其所属 DB086,校验 `executor_user_id = after_sales_requests.assigned_merchant_user_id`;该跨表归属由共享事务与集成测试保证,不能接受任意 Merchant ID。 -- `executing` 要求租约非空、结束时间和失败码为空;`unknown` 要求结束时间非空、租约/失败码为空;`succeeded` 要求结束时间非空且失败码为空;`definite_failure` 要求结束时间和失败码非空。核实次数非负,正数时最近核实时间非空。 -- Unknown 不新建下一次尝试,恢复任务继续核实原尝试:确认完整原子结果已提交则同一尝试收敛为 Succeeded;只有确认资金、支付累计、库存和售后状态均无副作用后才收敛为 DefiniteFailure;仍无法判定时保持 Unknown 并更新核实计数。 +- 部分 Unique `ux_refund_attempts_active_disposition(refund_operation_id) WHERE recovery_disposition IN ('verify_original','automatic_retry','merchant_retry_required','operator_attention_required')`,每个退款最多一个尚未消费的恢复责任。 +- 首次退款由 RefundOrchestrator 按全局锁序调用 Payment,在一个事务内创建 DB088 与 `initial/executing` DB093;后继尝试先锁 DB061/DB086/DB088/最新 DB093,把旧处置改为 `consumed`,再令 `refund_operations.attempt_count = attempt_count + 1`,新 DB093 `attempt_number` 必须等于该值。Unknown 核实不增加编号;AfterSales、HTTP 和 Worker 均不得直接写 DB088/DB093。 +- `initial/manual_retry` 必须为 `merchant`,`executor_user_id` 非空且 Worker 为空;`automatic_retry` 必须为 `system_recovery`,Worker 非空且用户为空。 +- `executor_kind='merchant'` 时,A419 先只读解析关联 ID,再固定按 DB061 订单 → DB086 申请 → DB088 退款操作 → 最新 DB093 尝试锁定,校验 `executor_user_id = after_sales_requests.assigned_merchant_user_id`;该跨表归属由共享事务与集成测试保证,不能接受任意 Merchant ID,也不能从 DB088 反向先锁 DB086。 +- 所有 `initial/automatic_retry/manual_retry` 新尝试都以同一数据库 `started_at` 写 `execution_lease_expires_at=started_at+interval '60 seconds'`。执行者每 20 秒只可用 `WHERE id=:attemptId AND status='executing' AND execution_token=:token` 条件续租,单次取 `LEAST(clock_timestamp()+interval '60 seconds', started_at+interval '5 minutes')`;不得重写 `started_at`,不得超过绝对 5 分钟上限。续租或完成影响 0 行时必须立即停止并重读。 +- `execution_token` 是每次真实执行尝试从创建到永久保留的不可变身份,所有状态均非空;进入 Unknown 或确定终态时只清空 `execution_lease_expires_at`,不得清空或换发原 Token。新的 AutomaticRetry/ManualRetry 必须新建下一 `attempt_number` 和新 Token,不能复用原行重启执行。 +- `recovery_lease_owner/token/acquired_at/expires_at` 必须全空或全非空;只有 `verify_original/automatic_retry` 可持有恢复租约。固定满足 `expires_at <= acquired_at + interval '5 minutes'`,每 20 秒续租只能延后到该上限且不得重写 `acquired_at`。`recovery_lease_takeover_count >= 0`;只有接管不同 Token 的已过期租约才递增,正常完成并释放一轮恢复租约后归零。完成更新必须匹配 `recovery_lease_token`,租约丢失的旧执行器不得覆盖接管者。 +- 执行租约与恢复租约是不同阶段、不同 Token:`execution_token/execution_lease_expires_at` 保护一次真实退款执行;`recovery_lease_*` 只保护 Unknown 核实或 AutomaticRetry 调度。不得共用列、把执行续租当作恢复续租,或同时让两类执行器提交结果。 +- 原退款执行器只能以 `WHERE id=:refundAttemptId AND status='executing' AND execution_token=:executionToken` 条件写入 Succeeded、DefiniteFailure 或 Unknown。Worker 把 `execution_lease_expires_at <= clock_timestamp()` 或 `started_at+interval '5 minutes' <= clock_timestamp()` 的 Executing 行转为 Unknown 时必须锁定同一 DB093 行,并在一个原子更新中写入 `status='unknown'`、`finished_at=decision_time`、`recovery_disposition='verify_original'`、`next_action_at` 与新的恢复租约;原执行器和接管 Worker 以先提交者为准,后提交方条件更新影响 0 行后必须重读,不得用迟到结果覆盖核实责任。 +- 候选领取严格分两阶段:第一短事务只对 DB093 使用 `FOR UPDATE SKIP LOCKED`,原子写入/接管恢复租约和必要的 `executing → unknown` 后立即提交;该事务绝不等待或锁 DB061/DB086/DB088 等父业务行。第二业务事务再按 DB061 → DB086 → DB088 → DB093 的父子顺序重锁并以 `recovery_lease_token` 重检,之后才核实、创建后继或完成结果,避免“先锁子行再等父行”的死锁。 +- 逐状态和处置字段矩阵: + - `executing`:`execution_lease_expires_at` 非空并满足 `started_at < execution_lease_expires_at <= started_at+5 分钟`,结束/失败/调度/人工重试/关注/恢复租约字段为空,`recovery_disposition='none'`; + - `unknown`:`finished_at` 非空、执行租约和失败码为空,`recovery_disposition='verify_original'`、`next_action_at` 非空;核实次数非负,正数时 `last_verified_at` 非空。持续无法确认时可以设置 `attention_required_at`,但仍保持 Unknown 并按小时继续核实; + - `succeeded`:`finished_at` 非空、失败码为空,处置为 `none`;保留不可变 `execution_token`,`execution_lease_expires_at`、全部恢复租约、调度、人工重试和关注字段为空; + - `definite_failure + automatic_retry`:`finished_at/failure_code/next_action_at` 非空,其他处置专属字段为空; + - `definite_failure + merchant_retry_required`:`finished_at/failure_code/manual_retry_available_at` 非空,调度、关注和租约字段为空; + - `definite_failure + operator_attention_required`:`finished_at/failure_code/attention_required_at` 非空,调度、人工重试和租约字段为空; + - `definite_failure + consumed`:表示已经原子创建下一尝试,所有调度、人工重试、关注和租约字段为空;`none/verify_original` 不允许与 DefiniteFailure 组合。 +- 索引: + - `ix_refund_attempts_execution_expired(execution_lease_expires_at,id) WHERE status='executing'`; + - `ix_refund_attempts_recovery_due(recovery_disposition,next_action_at,id) WHERE recovery_disposition IN ('verify_original','automatic_retry')`; + - `ix_refund_attempts_recovery_lease_expired(recovery_lease_expires_at,id) WHERE recovery_lease_token IS NOT NULL`; + - `ix_refund_attempts_manual_retry(refund_operation_id,manual_retry_available_at) WHERE recovery_disposition='merchant_retry_required'`。 +- Unknown 不新建下一次尝试,恢复任务继续核实原尝试:确认完整原子结果已提交则同一尝试收敛为 Succeeded;只有确认资金、支付累计、库存和售后状态均无副作用后才收敛为 DefiniteFailure 并按固定故障映射填写处置;仍无法判定时保持 Unknown、更新核实计数和下一核实时间。 +- Unknown 的核实计划只由数据库字段计算,不能由各实例自行退避:进入 Unknown 时写 `verification_count=0`、`next_action_at=finished_at+10 秒`;第 1 次仍未知后写 count=1、`next_action_at=last_verified_at+30 秒`;第 2 次后加 2 分钟;第 3 次后加 10 分钟;第 4 次后加 30 分钟;第 5 次后加 60 分钟;第 6 次及以后固定每 60 分钟继续核实。第 6 次仍未知时首次写 `attention_required_at=last_verified_at`,后续不得覆盖该首次升级时间。每次更新计数、最近核实时间、下一动作与关注时间必须在持有有效恢复 fencing token 的同一事务完成。 +- `automatic_retry` 最多创建 3 个 `attempt_kind='automatic_retry'` 的后继尝试,退避固定为 30 秒、2 分钟、10 分钟。创建后继尝试与把旧处置改为 `consumed` 必须在同一事务完成;额度耗尽时不再创建尝试,而是把最新处置改为 `merchant_retry_required` 并同步 DB088/DB086 进入可查询的 `definite_failure/refund_failed`。 +- 最新 DefiniteFailure 被确定映射为 `merchant_retry_required` 时,使用该次处置决策的同一 `final_time` 同时写 `finished_at=final_time` 与 `manual_retry_available_at=final_time+interval '60 seconds'`;不得从 HTTP 到达时间、客户端时间或 Worker 下一轮扫描时间计算冷却。若映射为 `operator_attention_required`,`manual_retry_available_at` 必须为空。 +- A419 只允许最新处置为 `merchant_retry_required`、数据库时间达到 `manual_retry_available_at` 且不存在 Executing/Unknown 时,在按 DB061 → DB086 → DB088 → 最新 DB093 的固定锁序事务中,把旧处置改为 `consumed`、DB088/DB086 恢复 `processing/refunding` 并创建唯一 `manual_retry` 尝试。`operator_attention_required` 永不开放商家重试。 ### 7.14 DB094 `reconciliation_evidence` @@ -1508,7 +1646,7 @@ A412 必须在取得订单锁后调用一次 `decision_time=clock_timestamp()` | `difference_id` | `uuid` | 否 | 差异 | | `phase` | `varchar(16)` | 否 | `detection/resolution` | | `source_type` | `varchar(64)` | 否 | 来源类型 | -| `source_id` | `varchar(200)` | 否 | 来源标识 | +| `source_id` | `varchar(500)` | 否 | 来源标识;覆盖 A425 单项最长 500 字的受控证据引用 | | `observation_hash` | `char(64)` | 否 | 去重哈希 | | `safe_snapshot` | `jsonb` | 否 | 脱敏证据 | | `observed_at` | `timestamptz` | 否 | 观察时间 | @@ -1518,6 +1656,7 @@ A412 必须在取得订单锁后调用一次 `decision_time=clock_timestamp()` - Unique `ux_reconciliation_evidence_observation(difference_id,source_type,source_id,observation_hash)`。 - Check `safe_snapshot` 为 JSON object、`observation_hash` 为 64 位小写十六进制。 - `ix_reconciliation_evidence_difference_created_at(difference_id,created_at,id)`。 +- A425 `Resolve` 的每个 `evidenceRefs` 元素分别写一行 `phase='resolution'`;`source_type` 固定为 `controlled_action_ref` 或 `resolution_reference`,`source_id` 保存完整规范引用,`observation_hash` 对“阶段 + 类型 + 引用 + 脱敏快照”的规范 UTF-8 字节计算。不得把多项引用拼接进单列,也不得只保存第一项。 - 不保存签名、密钥、Token、连接字符串和完整异常堆栈。 ### 7.15 DB095 `reconciliation_actions` @@ -1543,7 +1682,7 @@ A412 必须在取得订单锁后调用一次 `decision_time=clock_timestamp()` - PK `pk_reconciliation_actions`;FK `fk_reconciliation_actions_differences_difference`、`fk_reconciliation_actions_users_actor` 分别约束差异和管理员且 `ON DELETE RESTRICT`。 - Unique `ux_reconciliation_actions_difference_sequence(difference_id,sequence)`。 - `ix_reconciliation_actions_difference_occurred_at(difference_id,occurred_at,sequence)`。 -- 动作边固定为:`created: NULL→pending`、`claimed: pending/in_progress→in_progress`、`released/taken_over/verification_failed: in_progress→in_progress`、`resolved: in_progress→resolved`。 +- 动作边固定为:`created: NULL→pending`、`claimed: pending→in_progress`、`released/taken_over/verification_failed: in_progress→in_progress`、`resolved: in_progress→resolved`。已是 `in_progress` 的差异不能再次 Claim;换人处理必须满足 A425 的 Takeover 条件并追加 `taken_over`。 - `created` 只能为 `system` 且用户为空;其余动作只能为 `admin` 且用户非空。只有 `resolved` 保存解决类型和可选受控动作引用;`verification_failed` 固定保存 `still_mismatched` 与非空剩余原因,其他动作不得伪造解决/复核字段。 - 只追加。A425 的领取、释放、接管、复核失败和解决都必须留痕。 @@ -1560,9 +1699,9 @@ A412 必须在取得订单锁后调用一次 `decision_time=clock_timestamp()` - PK `pk_financial_posting_sequences`;Check `id=1`、`last_sequence >= 0`。 - Seed 必须且只能插入一行。 -- 钱包充值、成功支付、有效回调裁决和完整退款结果在事务末段对单例行取得 `FOR UPDATE`,递增一次,并把同一 sequence 写入该原子结果涉及的全部表;行锁持有至提交,故序号顺序与这些事务的提交顺序一致。 -- 只有取得水位写锁后才调用一次 `posting_time = clock_timestamp()`;它是本系统的权威财务业务时间,而不是 PostgreSQL 物理 COMMIT instant。同一原子结果的业务时间及各表 posting sequence 使用本次提交阶段时间/序号,不得在取得水位锁前预取。极端跨 UTC 零点事务仍按该 `posting_time` 归属,日批次的共享锁会等待它提交后再取水位,因此不会漏账;不启用 `track_commit_timestamp`,也不声称两者时间完全相等。 -- 对账统一 `posted_at` 映射并要求等于该原子结果的 `posting_time`:DB061/DB085 为 `paid_at`,DB082 为 `credited_at`,DB083 为 `created_at`,DB086 仅 Refunded 行为 `closed_at`,DB088 为 `completed_at`,DB089 为 `processed_at`。渠道 `occurred_at` 仍只作外部追踪,不能决定业务日。 +- 钱包充值、成功支付、有效回调裁决和完整退款结果在事务末段对单例行取得 `FOR UPDATE`,递增一次,并把同一 sequence 写入该原子结果涉及的全部表;DB096 必须是该路径最后一个可能阻塞且影响业务裁决的共享锁,行锁持有至提交,故序号顺序与这些事务的提交顺序一致。 +- 只有取得水位写锁后才调用一次 `final_time = clock_timestamp()`;它同时是截止裁决时间和本系统的权威财务 `posting_time`,不是 PostgreSQL 物理 COMMIT instant。同一原子结果的业务时间及各表 posting sequence 使用本次提交阶段时间/序号,不得在取得水位锁前预取,也不得在生成后再调用外部服务或取得新共享业务锁。极端跨 UTC 零点事务仍按该 `final_time` 归属,日批次的共享锁会等待它提交后再取水位,因此不会漏账;不启用 `track_commit_timestamp`,也不声称两者时间完全相等。 +- 对账统一 `posted_at` 映射并要求等于该原子结果的 `final_time`:DB061/DB085 为 `paid_at`,DB082 为 `credited_at`,DB083 为 `created_at`,DB086 仅 Refunded 行为 `closed_at`,DB088 为 `completed_at`,DB089 为 `processed_at`。渠道 `occurred_at` 仍只作外部追踪,不能决定业务日。 - 对账启动时在短事务内对单例行取得 `FOR SHARE`,等待此前写事务提交并阻止后续写事务越过屏障;在同一屏障内读取 `watermark_sequence=last_sequence` 与 `watermark_at=clock_timestamp()` 后立即提交。后续财务事务取得写锁后的 `posting_time` 必然晚于该屏障。 - 对账比较单元必须同时满足 `posted_at >= range_from AND posted_at < range_to` 与 `posting_sequence <= watermark_sequence`。在批次取水位后才取得 posting sequence 的事务自然进入下一业务日/后续批次,不会因事务开始时间较早而漏记。 - 同一 posting sequence 在每张参与事实表内最多出现一次;跨表可以且必须为同一原子结果复用。DB061、DB082、DB083、DB085、DB086、DB088、DB089 均用非空部分 Unique/Unique 约束落实这一点。 @@ -1574,26 +1713,32 @@ A412 必须在取得订单锁后调用一次 `decision_time=clock_timestamp()` 锁顺序: ```text -orders +transaction advisory lock payment-result:{order_id} +→ idempotency processing qualification +→ orders → payments(order_id 唯一检查) → wallet_accounts → financial_posting_sequences → wallet_transactions / payments / outbox / idempotency ``` -锁订单后取得 `decision_time`;只有 `pending_payment AND decision_time < payment_deadline` 可支付。余额、流水、支付、订单、Outbox、DB104 共用一个 posting sequence 和事务。 +1. A405 在任何副作用前取得订单级事务 advisory lock `payment-result:{order_id}`,再锁幂等处理资格、订单、既有支付唯一事实和买家钱包,完成所有不依赖截止时间的归属、金额、余额和幂等校验;A421 使用同一命名空间,A406 尝试取得该锁后才给出确定查询状态。 +2. 最后锁 DB096,立即取得唯一 `final_time`;只有 `pending_payment AND final_time < payment_deadline` 可支付。 +3. 余额、流水、支付、订单、Outbox、DB104 共用同一 posting sequence、`final_time` 和事务。生成 `final_time` 后不得再取得新的共享业务锁或调用外部依赖。 #### 回调处理 -1. 验签和固定字段通过后按 callbackId advisory lock;重查 DB089。 -2. 锁 DB084 流水绑定,再锁订单和支付唯一事实。 -3. 锁后取得 `decision_time`,按 A421 固定矩阵裁决。 -4. 四种合法业务终态都写 DB089;只有 ProcessedSuccess 同事务写 DB085、订单 Paid 和 Outbox。 -5. 锁 DB096 分配 posting sequence,完成回执后提交。 +1. 验签和固定字段通过后先取得订单级事务 advisory lock `payment-result:{claimed_order_id}`,再按 callbackId 和 paymentSerialNumber 的独立 advisory 命名空间串行化并重查 DB089/DB084;该订单级锁与 A405/A406 共用。 +2. 先按请求中的 `order_id` 锁 DB061 订单(订单不存在时记录缺失并跳过),再锁定或创建 DB084 流水绑定,随后锁 DB085 支付唯一事实,准备固定裁决矩阵需要的全部事实;`payment_serial_number` 的独立 advisory lock 必须覆盖 DB084 尚不存在的首次创建竞争。 +3. 最后锁 DB096 并立即取得唯一 `final_time`,用它判断支付截止并按 A421 固定矩阵裁决;等待任一前置共享锁跨过截止点时必须按过期分支处理。 +4. 四种合法业务终态都写 DB089 并分配 posting sequence;只有 ProcessedSuccess 同事务写 DB085、订单 Paid 和 Outbox。成功相关 `paid_at/processed_at/last_processed_at` 全部复用 `final_time`。 +5. 生成 `final_time` 后不得再取得新的共享业务锁或调用外部服务;完成确定回执后提交。 6. 若裁决为 `expired_success` 或 `failure_recorded_after_deadline`,在回调确定终态提交后立即尽力调用 M04/C03 统一过期取消能力:复用既有 DB063、DB026/DB044 与 OrderCancelled Outbox,C08 不直接改订单或库存。取消暂时失败不得回滚已经确定的回调终态,由下单时预建的 due task 继续恢复;若竞争方已推进订单则按统一取消结果收敛。 同一流水可有多个 callbackId;绝不对 DB089 的流水号建唯一约束。 +A406 先按本人归属只读识别订单,再尝试取得 `payment-result:{order_id}`。锁未取得时返回 `Confirming`;取得后锁定订单并查询唯一 DB085,以同一数据库时间派生 `Succeeded/NotPaid/ExpiredOrCancelled/Indeterminate`,事务结束即释放。这样查询不会把已经进入支付裁决但尚未提交的空结果误报为确定失败。 + #### 售后退款 统一锁顺序: @@ -1602,6 +1747,7 @@ orders orders → after_sales_requests → refund_operations +→ refund_attempts(当前执行资格) → payments → wallet_accounts → Catalog/Seckill 原库存聚合 @@ -1615,14 +1761,16 @@ orders - `payments.refunded_amount` 条件增加且不超过支付额; - 按 `stock_return_policy` 写 DB026 或 DB044 并回补; - DB088 Succeeded、DB086 Refunded 和数量从处理中转已退款; -- DB087 时间线、DB102 买家消息事件;`return_to_catalog` 时同事务另写 C07 Immediate 失效事件; +- DB087 时间线、DB102 买家消息事件;`return_to_catalog` 时同事务另写 C07 Immediate/Delayed 两条独立失效责任; - 同一 posting sequence。 结果未知保持 Refunding + unresolved DB093;确定失败只有在确认资金、库存均无副作用后进入 RefundFailed。 +RefundOrchestrator 是唯一外层事务协调者,但不拥有表;它按 DB061 → DB086 → DB088 → 当前 DB093 → DB085 → DB081 → DB026/DB044 所属库存聚合 → DB096 调用各模块公开能力。退款成功路径在取得当前尝试执行资格、原支付、钱包和原库存聚合后,最后锁 DB096 并取得唯一 `final_time`;钱包入账、支付累计、库存回补、DB093/DB088/DB086 成功时间和可靠事实均复用该时间。单次退款执行的确定失败不需要分配财务水位;按 DB093 固定处置决定保持 Refunding 并自动恢复,或进入 MerchantRetryRequired / OperatorAttentionRequired 的 RefundFailed。 + #### A425 对账处置 -Claim 使用条件更新;Release 后仍为 InProgress 但清空当前领取;Takeover 只在无有效领取人时成功。Resolve 必须重跑原比较规则: +Claim 使用条件更新把 Pending 推进为 InProgress,并固定 `claim_expires_at=claimed_at+30 分钟`;本期没有 Renew/心跳续期。Release 后仍为 InProgress 但清空当前领取;Takeover 只在无有效领取人、领取已过期或原领取人禁用时成功,并重新取得独立 30 分钟。Resolve 必须同时满足当前管理员、数据库时间早于到期和版本匹配,再重跑原比较规则: - 仍不一致:写 DB095 `verification_failed`,差异保持 InProgress; - 已一致:写证据和 Resolved;若批次最后一条未决差异同时更新批次 Resolved。 @@ -1659,11 +1807,13 @@ DB091、DB094、DB095、DB090 和 DB104 在一个事务提交;C08 不直接修 - `inbox_messages` 提供 `ux_inbox_messages_id_event_id(id,event_id)` 备用键;复合 FK `(inbox_id,source_event_id) → inbox_messages(id,event_id)` 保证消息没有挂到其他事件的 Inbox。 - 复合 FK `(recipient_user_id,recipient_role) → users(id,role) ON DELETE RESTRICT`,数据库直接保证接收角色一致。 - Unique `ux_messages_event_recipient_type(source_event_id,recipient_user_id,message_type)`。 -- `message_type` 固定为 `order_created/order_cancelled/payment_succeeded/order_shipped/order_completed/after_sales_submitted/after_sales_reviewed/after_sales_pending_return/after_sales_return_submitted/refund_succeeded/refund_failed`;API 映射为已确认的 PascalCase。 +- `message_type` 固定为 `order_created/order_cancelled/payment_succeeded/order_shipped/order_completed/after_sales_submitted/after_sales_reviewed/after_sales_pending_return/after_sales_return_submitted/refund_succeeded/refund_failed/refund_retry_required`;API 映射为已确认的 PascalCase。`refund_retry_required` 只承接恢复流程已确定进入 `MerchantRetryRequired` 的商家提醒,不代表退款已失败终止。 +- `RefundFailedIntegrationEvent + merchant_retry_required` 在同一消费事务生成买家 `refund_failed` 和订单指定商家 `refund_retry_required`;`operator_attention_required` 只生成买家 `refund_failed`,运维告警进入监控系统,不生成商家或管理员 DB101。Unknown、单次自动失败和 AutomaticRetry 排队均不生成失败消息。 - `related_resource_type` 只允许 `order/payment/after_sales`;类型与 ID 同空同非空。 - `recipient_role` 和接收人实际角色必须由 Identity 公开能力在消费事务重检;禁用账号仍保留合法身份和消息,只是不能登录。 - `action_target` 只允许 `order_detail/payment_detail/after_sales_detail`;动作字段全空或全非空。动作目标与 `action_resource_id` 必须按固定消息矩阵派生,接口映射为 `OrderDetail/PaymentDetail/AfterSalesDetail`,不能保存前端路由字符串。 -- 标题、摘要和正文去空白后非空。 +- 应用层在生成消息模板结果后、打开消费写事务前,对 `title/summary` 做 Unicode NFC,并去除首尾全部 Unicode White_Space;随后按 .NET `Rune`/Unicode 标量计数,标题必须为 1~100、摘要必须为 1~200 个标量,任一字符的 Unicode General_Category 为 `Cc` 或 `Cf` 时整事件按模板契约错误处理,不得用替换字符静默落库。数据库以 `varchar(100/200)`、`char_length(title) BETWEEN 1 AND 100`、`char_length(summary) BETWEEN 1 AND 200` 和非空 Check 做第二道边界;NFC、Unicode 空白和类别判断由应用校验与契约测试负责,不能声称 PostgreSQL 原生 Check 已完整覆盖。 +- `body` 去除首尾 Unicode 空白后必须非空且不超过 2000 个 Unicode 标量;完整正文只进入 DB101 和授权 HTTP 详情,不进入实时提示。 - 消息业务字段在插入后不可变,只允许 `read_at` 从空写入一次;Check `read_at IS NULL OR read_at >= created_at`。 - 不保存冗余 `is_read`,API 由 `read_at IS NOT NULL` 派生;首次已读时间不可覆盖。 @@ -1677,7 +1827,14 @@ DB091、DB094、DB095、DB090 和 DB104 在一个事务提交;C08 不直接修 | `ix_messages_recipient_unread_sequence` | `recipient_user_id, server_sequence WHERE read_at IS NULL` | A503/A505 | | `ix_messages_inbox_id` | `inbox_id` | Inbox 追踪与删除保护 | -消息事务按 `recipient_user_id` 稳定顺序取得事务 advisory lock,在锁内从该用户当前 `MAX(server_sequence)+1` 分配连续序号,并持锁至消息与 Inbox 一起提交;不得使用 PostgreSQL Identity/Sequence 冒充提交顺序。A504 使用 `UPDATE ... WHERE recipient_user_id=:me AND id=:id AND read_at IS NULL`,已读重放返回原时间。A505 取得同一用户锁后,在一个事务中捕获当前 `MAX(server_sequence)`,再以同一 `read_at` 更新 `<= watermark` 的未读消息并提交;后续消息只能在该锁释放后取得更大序号,因此保持未读。 +消息事务按 `recipient_user_id` 稳定顺序取得事务 advisory lock,在锁内从该用户当前 `MAX(server_sequence)+1` 分配连续序号,并持锁至消息、Inbox 与每消息实时提示 Outbox 一起提交;不得使用 PostgreSQL Identity/Sequence 冒充接收人内提交顺序。A504 使用 `UPDATE ... WHERE recipient_user_id=:me AND id=:id AND read_at IS NULL`,已读重放返回原时间。 + +A505 取得同一用户 advisory lock 后,在一个事务中先读 `high_watermark=COALESCE(MAX(server_sequence),0)`,再只更新 `recipient_user_id=:me AND read_at IS NULL AND server_sequence <= high_watermark`: + +- 从未有历史消息时 `high_watermark=0`,不读取伪消息、不写 `read_at`,返回 `markedCount=0,readAt=null`; +- 有历史但没有命中未读行时保留现有最大水位,不写任何行,返回 `markedCount=0,readAt=null`; +- 只有至少命中一行时才以一次数据库 `clock_timestamp()` 形成共同 `read_at`,`markedCount` 等于本次由空变非空的真实行数;重复调用不得返回上次批量时间; +- 后续消息只能在该锁释放后取得更大 `server_sequence`,因此不进入本次更新。SQL 只判断 `read_at IS NULL`,不得引用 API 派生的 `isRead/is_read`。 ### 8.2 DB102 `outbox_messages` @@ -1695,9 +1852,12 @@ DB091、DB094、DB095、DB090 和 DB104 在一个事务提交;C08 不直接修 | `occurred_at` | `timestamptz` | 否 | — | 业务发生时间 | | `correlation_id` | `varchar(64)` | 是 | — | 链路关联 | | `status` | `varchar(24)` | 否 | `'pending'` | 投递状态 | +| `schedule_mode` | `varchar(24)` | 否 | `'fixed'` | `fixed/after_commit_delay` | +| `delay_seconds` | `integer` | 是 | — | 提交可见后延迟秒数;仅延迟模式使用 | +| `armed_at` | `timestamptz` | 是 | — | 延迟行提交可见后被数据库时间武装的时刻 | | `attempt_count` | `integer` | 否 | `0` | 尝试数 | -| `available_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 首次可投递 | -| `next_attempt_at` | `timestamptz` | 是 | `CURRENT_TIMESTAMP` | 下次尝试;运行/终态为空 | +| `available_at` | `timestamptz` | 是 | — | 首次可投递;未武装延迟行为空 | +| `next_attempt_at` | `timestamptz` | 是 | — | 下次尝试;未武装、运行或终态为空 | | `lease_owner` | `varchar(100)` | 是 | — | Worker | | `lease_token` | `uuid` | 是 | — | 领取令牌 | | `lease_expires_at` | `timestamptz` | 是 | — | 租约到期 | @@ -1710,33 +1870,67 @@ DB091、DB094、DB095、DB090 和 DB104 在一个事务提交;C08 不直接修 约束: - PK `pk_outbox_messages`;事件 ID 不再另建 `messageId`。 -- Unique `ux_outbox_messages_event_deduplication(event_type,deduplication_key) WHERE deduplication_key IS NOT NULL`;普通来源事务事件可为空,跨事务派生的延迟失效事件必须使用稳定键。 -- 状态:`pending/publishing/published/dead_lettered`。 -- Envelope 字段、payload、headers、occurred_at 创建后不可变;只允许修改投递字段。 +- Unique `ux_outbox_messages_event_deduplication(event_type,deduplication_key) WHERE deduplication_key IS NOT NULL`;普通来源事务事件可为空,C07 两阶段责任及其他需要稳定恢复的事件必须使用已冻结去重键。 +- 状态:`pending/publishing/published/dead_lettered`;`schedule_mode` 只允许 `fixed/after_commit_delay`。 +- Envelope 字段、payload、headers、occurred_at、schedule_mode、delay_seconds 创建后不可变;`armed_at/available_at` 只允许由未武装到已武装一次性填写,提交后不可重算,其他只允许修改投递字段。 - `payload/headers` 必须为 JSON object,`attempt_count >= 0`、schema version 正数。 -- 首次创建固定 `next_attempt_at=available_at`;重试时 `next_attempt_at >= available_at`,未来可用事件不得提前发布。 +- `ck_outbox_messages_schedule` 固定调度组合为 `schedule_mode='fixed'`、`delay_seconds/armed_at` 为空、`available_at` 非空;提交后延迟组合为 `schedule_mode='after_commit_delay'`、`delay_seconds>0`,并且只能是 `armed_at/available_at` 同空的未武装态,或二者同非空且 `available_at=armed_at + delay_seconds * interval '1 second'` 的已武装态。 +- 固定调度首次创建时 `next_attempt_at=available_at`。提交后延迟调度首次创建时 `armed_at/available_at/next_attempt_at` 全空且 `attempt_count=0`;武装事务一次性写 `armed_at=clock_timestamp()`、`available_at=next_attempt_at=armed_at + delay_seconds * interval '1 second'`。重试时 `next_attempt_at >= available_at`,未来可用事件不得提前发布。 - 状态组合: - - `pending`:`next_attempt_at` 非空,lease 与两个终态时间为空; - - `publishing`:三个 lease 字段非空,`next_attempt_at` 与两个终态时间为空; - - `published`:`published_at` 非空,`next_attempt_at`、lease、`dead_lettered_at`、错误码为空; - - `dead_lettered`:`dead_lettered_at/last_error_code` 非空,`next_attempt_at`、lease、`published_at` 为空。 + - `pending + fixed/已武装 after_commit_delay`:`available_at/next_attempt_at` 非空且 `next_attempt_at>=available_at`,lease 与两个终态时间为空; + - `pending + 未武装 after_commit_delay`:`armed_at/available_at/next_attempt_at` 全空、`attempt_count=0`,lease、错误与两个终态时间全空; + - `publishing`:必须已经具备 `available_at`,三个 lease 字段非空,`next_attempt_at` 与两个终态时间为空;延迟模式还必须已经具备 `armed_at`; + - `published`:必须已经具备 `available_at`,`published_at` 非空,`next_attempt_at`、lease、`dead_lettered_at`、错误码为空;延迟模式还必须已经具备 `armed_at`; + - `dead_lettered`:必须已经具备 `available_at`,`dead_lettered_at/last_error_code` 非空,`next_attempt_at`、lease、`published_at` 为空;未武装行不能直接进入 DeadLettered。 索引: -- `ix_outbox_messages_pending(next_attempt_at,id) WHERE status='pending'`。 +- `ix_outbox_messages_unarmed(created_at,id) WHERE status='pending' AND schedule_mode='after_commit_delay' AND armed_at IS NULL`。 +- `ix_outbox_messages_pending(next_attempt_at,id) WHERE status='pending' AND next_attempt_at IS NOT NULL`。 - `ix_outbox_messages_expired_lease(lease_expires_at,id) WHERE status='publishing'`。 - `ix_outbox_messages_aggregate(aggregate_type,aggregate_id,occurred_at)`。 -Worker 用 `FOR UPDATE SKIP LOCKED` 小批量领取,等待 Publisher Confirm。Broker 已确认但数据库标记前崩溃会重复发布,这是允许的故障窗口,由 Inbox 防重。 +Worker 默认每 1 秒先执行武装扫描、再执行发布扫描,两类扫描每批最多 100 条: + +- **武装扫描**:按 `created_at,id` 扫描已提交可见的 `pending + after_commit_delay + armed_at IS NULL`,以 `FOR UPDATE SKIP LOCKED` 互斥领取;在同一短事务内取得一次数据库 `arm_time=clock_timestamp()`,原子写 `armed_at=arm_time`、`available_at=next_attempt_at=arm_time + delay_seconds * interval '1 second'` 和 `updated_at=arm_time`。该阶段不递增 `attempt_count`、不建立 DB106、不取得发布租约。进程在提交前崩溃时事务回滚,原行仍未武装并由后续轮次重扫;提交后崩溃时已武装时间保持不变,由普通发布扫描接管,禁止再次武装或重算到期时间。 +- **发布扫描**:按 `next_attempt_at,id` 扫描 `status='pending' AND next_attempt_at<=clock_timestamp()` 的已到期行,以 `FOR UPDATE SKIP LOCKED` 领取;未武装行因 `next_attempt_at IS NULL` 永不进入发布。领取事务把 DB102 改为 Publishing、`attempt_count+1`,写 30 秒租约并同时插入 DB106 Executing。发布调用和 Publisher Confirm 最长等待 5 秒,长调用每 10 秒续租;DB102/DB106 的完成、失败或接管更新必须匹配 `lease_token`。 + +- 收到 Broker Confirm:同一事务把本次 DB106 改为 Confirmed、DB102 改为 Published;数据库标记前崩溃会重复发布,这是允许的 at-least-once 窗口,由 Inbox 防重。 +- 明确瞬态失败(网络、Broker 不可用、限流、Confirm 超时)把 DB106 改为 Failed,DB102 回到 Pending;退避固定为 1 秒、5 秒、30 秒、2 分钟、10 分钟,此后每 1 小时持续重试,不因次数耗尽丢失业务事件。第 5 次 Warning,第 20 次及以后每 24 小时聚合 Critical。 +- Publishing 租约过期表示结果未知;接管事务先把旧 DB106 Executing 改为 Unknown,再把 DB102 重新置为 Pending 并按同一退避重试。不能把未知当未发送,也不能直接标 Published。 +- 只有事件类型/路由/Schema 等已经确定且不可重试的契约错误才把 DB102 改为 DeadLettered,并同步 DB106 Failed 与 Critical 告警;RabbitMQ 短暂故障、数据库故障或消费者离线不得进入 DeadLettered。人工修复契约后只能通过受控运维动作重置同一 eventId,不能复制成新业务事件掩盖原责任。 + +DB102 承接四类已确认事件,不为实时连接或 C07 另建业务表: + +- `MessagingSourceEventV1`:Ordering、Payment、AfterSales 来源事务保存的 10 种封闭业务事件,由 RabbitMQ 路由到 Messaging;具体联合和规范哈希见 DB103。 +- `MessagingRealtimeHintRequestedV1`:Messaging 成功消费来源事件后,为每条已持久化 DB101 单独建立的 60 秒轻提示。 +- `IdentityRealtimeCredentialInvalidatedV1`:Identity 撤销当前 Token 或使账号全部旧凭证失效时建立的安全广播责任。 +- `CatalogCacheInvalidationRequestedV1`:来源业务事务独立建立的 Immediate/Delayed 缓存失效责任。 -DB102 同时承接两类已确认的可靠事件,不新增 C07 业务表: +`MessagingSourceEventV1` 的来源 DB102 行按统一 Envelope 映射:`id → eventId`、`event_type → type`、数值 `schema_version=1 → schemaVersion="v1"`、`occurred_at → occurredAt`、`aggregate_id → aggregateId`、`correlation_id → correlationId`,`payload` 必须且只能含 `ownership,data` 两个对象;`routing_key` 必须命中 DB103 的 10 种矩阵,`headers` 不得承载任何接收人或业务语义。发布者按这些不可变列组装 Envelope,不能在发布时重新查询当前业务行改变历史快照。 -- 业务集成事件:由 RabbitMQ 路由到 Messaging 等消费者; -- `CatalogCacheInvalidationRequestedV1`:只携带稳定 `operationId`、`phase=immediate/delayed`、受影响 `productIds`、是否失效固定首页和结构版本,不保存 Redis Key 或缓存值。来源业务事务只写 `immediate` 事件,去重键固定 `cache:{operationId}:immediate`。 -- Immediate 消费者先对配置规则派生的范围执行一次幂等 DEL,再在数据库事务中写 Processed DB103 与 `available_at=clock_timestamp()+interval '3 seconds'` 的 delayed DB102,延迟事件去重键固定 `cache:{operationId}:delayed`;提交后才确认原 Broker 消息。若删除后、事务提交前崩溃,Broker 重投只会安全重删;若事务提交后、确认前崩溃,唯一键保证不重复建立延迟事件。 -- Delayed 消费者到期后再次幂等 DEL,写 Processed DB103 后确认。这样二次删除相对首次成功删除延迟至少 3 秒,不占用消费者线程等待,也不长期持有未确认消息;任一阶段中断均可由 Broker + Inbox 恢复。 +`MessagingRealtimeHintRequestedV1` 的 DB102 契约固定为: -C07 Immediate 生产矩阵固定为:A112~A114 分类有效性/名称变化;A123~A128 已完成的商品内容、状态、图片或删除变化;A222 普通库存划拨;A301 普通库存扣减;A304/C03 普通库存取消回补;售后成功 `return_to_catalog`。A122 只创建不可公开的 Draft,不为不存在的公开缓存制造事件;评价不进入 C07 缓存。 +- `event_type='MessagingRealtimeHintRequestedV1'`、`schema_version=1`、`aggregate_type='message'`、`aggregate_id=messageId`、`routing_key='messaging.realtime.hint.v1'`、`deduplication_key='messaging-realtime:{messageId}'`,并令 `occurred_at=available_at=DB101.created_at`;DB102 `id` 是独立提示 `eventId`,不得复用来源业务 `eventId` 或 `messageId`。 +- `payload` 必须且只能包含 `eventId,schemaVersion,messageId,recipientUserId,recipientRole,messageType,title,summary,relatedResourceType,relatedResourceId,actionTarget,actionResourceId,createdAt,expiresAt`。其中 `eventId=DB102.id`、`schemaVersion='v1'`、`messageId=DB101.id`、`recipientUserId=DB101.recipient_user_id`、`createdAt=DB101.created_at`、`expiresAt=createdAt+interval '60 seconds'`;所有 UUID 输出小写 D,所有时间输出 UTC。 +- `recipientRole` 把 DB101 `buyer/merchant` 精确映射为 `Buyer/Merchant`。`messageType` 把 DB101 的 12 个 lower_snake_case 值精确映射为 `OrderCreated/OrderCancelled/PaymentSucceeded/OrderShipped/OrderCompleted/AfterSalesSubmitted/AfterSalesReviewed/AfterSalesPendingReturn/AfterSalesReturnSubmitted/RefundSucceeded/RefundFailed/RefundRetryRequired`,不得接受第 13 种自由值。 +- `title/summary` 必须逐标量等于已按 NFC、Unicode 空白和 `Cc/Cf` 规则持久化的 DB101 安全文本快照,不得在 Publisher 再截断、改写或套模板。`relatedResourceType/relatedResourceId` 同空同非空,并把 DB101 `order/payment/after_sales` 精确映射为 `Order/Payment/AfterSales`;`actionTarget/actionResourceId` 同空同非空,并把 DB101 `order_detail/payment_detail/after_sales_detail` 精确映射为 `OrderDetail/PaymentDetail/AfterSalesDetail`。四个可空字段必须显式输出 JSON `null`,不能省略、嵌套成 `action` 对象或改成 URL。 +- payload 禁止完整 `body`、完整订单/支付/售后对象、地址、手机号、JWT、密码、密钥、支付凭证、异常文本、URL、内部前端路由、连接信息和任何未知属性。Messaging 在同一事务提交一个 DB103、该来源事件全部 DB101,以及每条 DB101 对应的一条提示 DB102;任一提示责任写入失败时整事件消息事务回滚。 +- API 消费者在推送前按 `expiresAt` 重检。到期提示确认并丢弃,不更新 DB101;未到期提示允许至少一次重复,客户端以 `messageId` 去重并用 A501/A503 补查。DB102 Publisher 即使延迟后才发布也不得把 `expiresAt` 延长或复制成新 eventId。 + +`IdentityRealtimeCredentialInvalidatedV1` 的 DB102 契约固定为: + +- 共用 `event_type='IdentityRealtimeCredentialInvalidatedV1'`、`schema_version=1`、`aggregate_type='user'`、`aggregate_id=userId`、`routing_key='identity.realtime.credential-invalidated.v1'`;发布到身份实时失效广播 Exchange,重复广播只导致幂等 Abort。 +- `TokenRevoked` 变体的 payload 必须且只能有 `eventId,type,userId,jti,invalidatedAt,schemaVersion`,固定 `type='TokenRevoked'`、`schemaVersion='v1'`,去重键为 `identity-realtime:token:{jti}`。A003 只在同一数据库 `decisionTime < JWT exp` 时写 `DB004.revoked_at=invalidatedAt=decisionTime`,并在该事务建立 DB102,令 `aggregate_id=userId`、`occurred_at=available_at=invalidatedAt`;任一写入失败时撤销事实与广播责任一起回滚。若 `decisionTime >= exp`,A003 以自然失效收敛且不得建立该事件。 +- `AccountCredentialsInvalidated` 变体的 payload 必须且只能有 `eventId,type,userId,currentTokenVersion,accountStatus,invalidatedAt,schemaVersion`,固定 `type='AccountCredentialsInvalidated'`、`schemaVersion='v1'`,`accountStatus` 只允许 `Normal/Disabled`,去重键为 `identity-realtime:account:{userId}:{currentTokenVersion}`。A006、A016 及其他全部旧凭证失效动作在同一事务写 DB001 新 `token_version/current status` 与 DB102,payload 精确复制提交后的 `currentTokenVersion/accountStatus`,并令 `aggregate_id=userId`、`occurred_at=available_at=invalidatedAt`;A017 启用不递增版本,因此不重复建立本事件。 +- 两种变体均要求 `eventId=DB102.id`、`userId=DB102.aggregate_id`、UUID 小写 D、时间 UTC,未知/重复属性和未定义 `null` 一律拒绝。事件不包含 JWT、手机号、角色授权快照或任意连接 ID;DB001/DB004 仍是权威事实,安全广播不替代每次推送和最长 30 秒周期复核。 + +`CatalogCacheInvalidationRequestedV1` 的封闭 payload 固定包含 `operationId`、`phase=immediate/delayed`、`invalidationBaseTime`、`delayAfterCommitSeconds`、`productIds`、`invalidateHomepage`、`schemaVersion=v1`,不保存 Redis Key、缓存值或预提交绝对投递时间字段。`productIds` 为 1~100 个唯一 UUID,按 UUID 字节升序;`invalidationBaseTime` 只保存来源业务发生时间,不参与 Delayed 调度。 +- 来源业务事务取得一次数据库 `invalidationBaseTime`,同时插入两个不同 eventId 的 DB102:Immediate 使用 `schedule_mode='fixed'`、`delay_seconds/armed_at` 为空、`available_at=next_attempt_at=invalidationBaseTime`、payload `delayAfterCommitSeconds=0`,去重键 `cache:{operationId}:immediate`;Delayed 使用 `schedule_mode='after_commit_delay'`、`delay_seconds=3`、`armed_at/available_at/next_attempt_at` 全空、payload `delayAfterCommitSeconds=3`,去重键 `cache:{operationId}:delayed`。两条 payload 除 phase/delayAfterCommitSeconds 外一致;业务事实与两条责任必须整体提交,不能只写 Immediate,也不能由 Immediate 消费者或调度器事后派生 Delayed。 +- Delayed 行只有在来源事务提交后才对其他事务可见;Outbox 武装扫描看到它后使用上述数据库 `clock_timestamp()` 一次性写 `armed_at/available_at/next_attempt_at`,所以其最早投递时间不早于来源事务提交可见后 3 秒。来源事务持续超过 3 秒不会使 Delayed 在提交时已经到期;调度器观察延迟只会使投递更晚。Immediate 的 Publishing/Published/重试/失败均不得查询同 `operationId` 后创建、武装、取消、推迟或重算 Delayed。 +- 两个消费者分别按配置从 payload 派生 Key 并执行幂等 DEL;删除 0 个 Key 也是成功。只有 Redis 明确返回成功后,才在事务中写本 `consumer_name + event_id` 的 Processed DB103,提交后 ACK;Redis 失败或结果未知时不写 Inbox、不 ACK,Broker 只重投同一阶段 eventId。删除后、Inbox 提交前崩溃会安全重删;Inbox 提交后、ACK 前重投由 DB103 幂等。Delayed 到点执行不依赖 Immediate 是否成功。 + +C07 双阶段生产矩阵固定为:A112 修改分类名称或关系且改变 A103 已缓存公开字段时精确失效受影响详情;A122 创建 Draft 时只失效同 `productId` 的 A103 10 秒短空值,不失效固定首页;A123~A128 已完成的商品内容、状态、图片或删除变化;A222 普通库存划拨;A301 普通库存扣减;A304/C03 普通库存取消回补;售后成功 `return_to_catalog`。上述每个来源都在本事务同时写 Immediate/Delayed;A113/A114 仅改变分类筛选入口,不改变既有 `OnSale` 商品的公开详情与固定首页成员,因此不产生 C07 失效;评价不进入 C07 缓存。 ### 8.3 DB103 `inbox_messages` @@ -1758,11 +1952,49 @@ C07 Immediate 生产矩阵固定为:A112~A114 分类有效性/名称变化 - PK `pk_inbox_messages`。 - Unique `ux_inbox_messages_consumer_event(consumer_name,event_id)`。 - Unique `ux_inbox_messages_id_event_id(id,event_id)` 供消息复合 FK。 -- Check schema version 正数、哈希格式、`message_count >= 0`。`rejected` 固定为 0;`processed` 对消息事件必须等于固定矩阵派生的接收人数,对已登记的明确无消息事实或 C07 失效命令允许为 0 并保存稳定结果码。 +- Check schema version 正数、哈希格式、`message_count >= 0`。`rejected` 固定为 0;`processed` 对 Messaging 10 种来源事件必须等于下方矩阵派生的消息数,对 C07 失效等明确的非消息消费者允许为 0 并保存稳定结果码。 - Rejected 结果码固定为 `unsupported_event_type/unsupported_schema_version/invalid_event_fields/recipient_missing/recipient_role_mismatch/resource_ownership_mismatch`;不得把瞬态依赖失败记录成确定拒绝。 - `ix_inbox_messages_completed_at(completed_at,id)`。 -Messaging 消费时先判断事件是否属于消息矩阵:明确无消息事实写一条 `processed/no_message/0` Inbox;消息事件先校验全部接收人,再在一个事务中写 Processed Inbox 和本事件全部消息。账号 Disabled 仍是合法接收人,只影响登录。 +Messaging V1 只接受下列 10 种 `MessagingSourceEventV1`。发布 Envelope 必须且只能包含 `eventId,type,schemaVersion,occurredAt,aggregateId,correlationId,ownership,data` 八个顶层属性,`schemaVersion` 固定字符串 `v1`;缺失、重复、未知属性或任意 `recipients[]` 都是确定契约错误。 + +| `type` / `routing_key` | `aggregate_id` | `ownership` 精确属性 | `data` 精确属性 | DB101 确定结果 / `message_count` | +|---|---|---|---|---| +| `OrderCreatedIntegrationEvent` / `ordering.order.created.v1` | `orderId` | `buyerId` | `orderId,totalAmount,currency="CNY"` | 买家 `order_created` / 1 | +| `OrderCancelledIntegrationEvent` / `ordering.order.cancelled.v1` | `orderId` | `buyerId` | `orderId,cancelReason=BuyerRequested/PaymentExpired` | 买家 `order_cancelled` / 1 | +| `OrderPaidIntegrationEvent` / `payment.order.paid.v1` | `paymentId` | `buyerId,assignedMerchantUserId` | `orderId,paymentId,amount,currency="CNY"` | 买家、指定商家各 `payment_succeeded` / 2 | +| `OrderShippedIntegrationEvent` / `ordering.order.shipped.v1` | `orderId` | `buyerId` | `orderId,shippedAt` | 买家 `order_shipped` / 1 | +| `OrderCompletedIntegrationEvent` / `ordering.order.completed.v1` | `orderId` | `buyerId` | `orderId,completedAt,completedBy=BuyerConfirmed/AutoCompleted` | 买家 `order_completed` / 1 | +| `AfterSalesApplicationSubmittedIntegrationEvent` / `after-sales.request.submitted.v1` | `requestId` | `buyerId,assignedMerchantUserId` | `requestId,orderId,requestType=RefundOnly/ReturnAndRefund` | 指定商家 `after_sales_submitted` / 1 | +| `AfterSalesApplicationAuditedIntegrationEvent` / `after-sales.request.audited.v1` | `requestId` | `buyerId,assignedMerchantUserId` | `requestId,orderId,decision,status` | 买家 `after_sales_reviewed` 或 `after_sales_pending_return` / 1 | +| `AfterSalesReturnInfoSubmittedIntegrationEvent` / `after-sales.return-info.submitted.v1` | `requestId` | `buyerId,assignedMerchantUserId` | `requestId,orderId,status=PendingReceipt` | 指定商家 `after_sales_return_submitted` / 1 | +| `RefundCompletedIntegrationEvent` / `payment.refund.completed.v1` | `refundOperationId` | `buyerId,assignedMerchantUserId` | `requestId,refundOperationId,amount,currency="CNY"` | 买家 `refund_succeeded` / 1 | +| `RefundFailedIntegrationEvent` / `after-sales.refund.failed.v1` | `refundOperationId` | `buyerId,assignedMerchantUserId` | `requestId,refundOperationId,failureCode,recoveryDisposition`;Merchant 变体另有 `manualRetryAvailableAt` | Merchant:买家 `refund_failed` + 商家 `refund_retry_required` / 2;Operator:仅买家 `refund_failed` / 1 | + +变体约束不能下放给消息模板: + +- 审核只允许 `Reject+Rejected`、`Approve+PendingReturn`、`Approve+Refunding`;前两类分别映射 `after_sales_reviewed/after_sales_pending_return`,第三类映射 `after_sales_reviewed`。 +- `RefundFailed + MerchantRetryRequired` 必须有 UTC `manualRetryAvailableAt`;`OperatorAttentionRequired` 禁止该属性,只生成买家消息,运维告警不进入 DB101。其他恢复处置、Unknown 或 AutomaticRetry 事件一律不属于上述 10 种合法变体。 +- Ordering 事件要求 `aggregateId=data.orderId`,OrderPaid 要求 `aggregateId=data.paymentId`,普通售后事件要求 `aggregateId=data.requestId`,退款事件要求 `aggregateId=data.refundOperationId`。金额为 `0.01~9999999999999999.99`、最多两位且禁止指数,币种固定 CNY;Routing Key 必须与 type 同行精确匹配。 +- `correlationId` 必须匹配 `^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$`;`RefundFailed.failureCode` 必须匹配 `^[A-Z][A-Z0-9_.]{0,63}$`。两者都禁止控制字符、异常正文、Token、手机号、地址和其他敏感信息;不合规按 `invalid_event_fields` 整事件拒绝。 + +`payload_hash` 固定覆盖完整八属性 Envelope,不包含 RabbitMQ Header、Routing Key 或投递次数。对通过结构解析的 Envelope 使用以下 Canonical JSON 后执行 SHA-256: + +1. 对象属性递归按属性名 UTF-8 字节序升序;V1 不允许数组。 +2. UUID 统一小写 D,时间统一 UTC `.NET "O"`,金额统一两位小数且无指数,枚举保留矩阵精确大小写。 +3. 普通字符串先做 Unicode NFC,不擅自 trim;不输出空白、未知属性或该变体未定义的 `null`。 +4. 对无 BOM 的规范 UTF-8 字节计算 SHA-256,保存 64 位小写十六进制;DB103 `schema_version` 保存数值 `1`。 + +只有无歧义完成顶层 JSON 解析、且 `eventId/type/schemaVersion` 各恰好出现一次并可安全取得时,才允许为“更深层字段非法而无法形成 Canonical JSON”的事件写 Rejected;此时 `payload_hash` 使用原始 Broker Body SHA-256,并以 `invalid_event_fields` 标识。JSON 语法不完整、顶层重复/未知属性,或不能安全取得唯一三元组时不写 DB103,直接记录受限原始 Body 哈希、告警并死信。该兜底绝不能用于 10 种合法 Envelope,也不能用占位 eventId、event_type 或 schema_version 伪造 Inbox 行。 + +Messaging 在查询 DB103 前先校验 type、schemaVersion 与 Routing Key。幂等和冲突顺序固定为: + +- `(consumer_name,event_id)` 不存在:完整校验全部接收人、角色和归属,再在一个事务写 Processed DB103、矩阵全部 DB101 与每消息一条实时提示 DB102;确定非法事件只写 Rejected/0。 +- 已存在且 `payload_hash` 相同、类型和 Routing Key 仍匹配:返回原 Processed/Rejected 结果并确认当前投递,不新增消息、提示或未读数。 +- 已存在但 `payload_hash` 不同,或当前 Routing Key 与既有事件类型不匹配:既有 DB103/DB101/DB102 保持不变,当前投递零消息、零提示,记录脱敏双哈希、eventId、Routing Key 与 traceId,Critical 告警并死信;禁止覆盖、自动重试或改用新 eventId 掩盖冲突。 +- 两个首投并发竞争唯一键时,未获胜事务必须回读获胜 DB103 并执行相同哈希判断,不能把唯一冲突直接当成功或 500。 + +账号 Disabled 仍是合法接收人,只影响登录、查询、已读与实时推送,不删除或阻止业务消息保存。 - 字段非法、必需接收人确定不存在、角色错误或业务归属确定错误:同事务写 Rejected/0/稳定结果码并告警,随后对原消息执行“不重新入队”的拒绝,由 RabbitMQ 路由到死信队列,避免毒消息无限重试; - Identity/业务归属暂时无法确认、数据库写失败或其他基础设施未知:整体回滚并让 Broker 重投,不能留下 Rejected; @@ -1808,7 +2040,7 @@ Messaging 消费时先判断事件是否属于消息矩阵:明确无消息事 - PK `pk_idempotency_records`;Unique `ux_idempotency_records_operation_scope_key(operation_code,scope_key,idempotency_key)`。 - `actor_user_id` 存在时 FK → users `RESTRICT`。 - Check `request_hash` 格式、`attempt_count >= 0`、HTTP 状态范围、`work_payload` 为 JSON object,非空 `response_headers/response_body` 也必须为 JSON object。 -- Processing:结果字段、完成时间和 `expires_at` 为空;普通纯数据库操作的工作/租约字段全空,A122 跨对象工作要求工作清单非空、`work_expires_at` 与三个 lease 字段全非空。 +- Processing:结果字段、完成时间和 `expires_at` 为空;普通纯数据库操作的工作/租约字段全空,A122/A141 跨对象工作要求工作清单非空、`work_expires_at` 与三个 lease 字段全非空。 - Completed:`outcome_type/http_status/response_headers/completed_at` 非空;无响应体的 204 等结果以 SQL NULL 明确表示,其他响应体非空。工作清单重置为空对象,错误与工作租约字段和 `work_expires_at` 清空;`expires_at` 为空或严格晚于 `completed_at`,且只能从 `completed_at` 计算。 - `success` 只保存 2xx 且 `error_code` 为空;`business_failure` 只保存已确认 4xx 且 `error_code` 非空。瞬态、依赖未知和提交未知不得伪装为 Completed。 - 三个 lease 字段全空或全非空;`resource_type/resource_id` 同空同非空。 @@ -1824,9 +2056,14 @@ Messaging 消费时先判断事件是否属于消息矩阵:明确无消息事 2. 同 Key 不同请求哈希返回 `IDEMPOTENCY.KEY_REUSED`。 3. 普通纯数据库操作在一个外层事务建立 Processing,执行副作用,完成后才提交 Completed;连接中断/提交未知整体回滚,不留下处理态。 4. 确定业务失败使用 Savepoint 回滚副作用,再在外层事务保存 BusinessFailure。 -5. A122 是例外的跨对象写入:先提交 Processing + 预生成商品/图片 ID、全部原图/缩略图 Key、文件哈希清单、工作期限与新 lease token;上传后最终事务必须以 `WHERE id=:id AND status='processing' AND lease_token=:token` 完成。接管会换发新 token,旧执行者永远不能完成或清理新执行者的工作。 -6. 失败或崩溃时同 Key 按工作清单续传/清理,不重复生成对象;任一对象已写但业务关联未提交时,先登记 DB105 或保留 Processing,不丢弃最后持久化清单。 -7. 24 小时 Completed 记录到期但清理延迟时,同作用域请求可在锁内确认 `status='completed' AND expires_at <= decision_time` 后删除旧记录并重新占用,不能被旧行永久阻塞。 +5. A122/A141 是例外的跨对象写入:先提交 Processing + 预生成资源/图片 ID、全部不可变对象 Key、文件哈希清单、工作期限与新 lease token;上传后最终事务必须以 `WHERE id=:id AND status='processing' AND lease_token=:token` 完成。接管会换发新 token,旧执行者永远不能完成或清理新执行者的工作。 +6. A122/A141 同 Key 同哈希命中有效 Processing 租约时最多有界等待 2 秒;仍未完成则返回 `409 IDEMPOTENCY.REQUEST_IN_PROGRESS` 和 `Retry-After: 1`,该临时响应不写入结果字段。租约到期后才允许基于原工作清单换发新 fencing token 接管,不能并发创建第二份对象集合。 +7. 失败或崩溃时同 Key 按工作清单续传/清理,不重复生成对象;任一对象已写但业务关联未提交时,先登记 DB105 或保留 Processing,不丢弃最后持久化清单。 +8. 24 小时 Completed 记录到期但清理延迟时,同作用域请求可在锁内确认 `status='completed' AND expires_at <= decision_time` 后删除旧记录并重新占用,不能被旧行永久阻塞。A141 还须先确认对应 DB025 未关联图片已经在同一锁序中登记 DB105 并移除;图片已由 A142 变为 Attached 时,A142 同事务把该 A141 记录的 `expires_at` 清空,随评价图片生命周期保留并继续稳定重放原 `imageId`。 + +A122 Processing 的时间公式固定为:首次建立时 `lease_expires_at=decision_time+60 秒`、`work_expires_at=created_at+24 小时`;持有者每 20 秒以 `id + status=processing + lease_token` 条件续租,单次只能续到数据库当前时间后 60 秒且不得超过 `work_expires_at`。到达工作期限后禁止继续上传或最终建商品,恢复 Worker 改为按不可变工作清单登记全部 DB105 清理责任;只有全部 Key 经对象存储核实不存在或已删除后,才可在同一幂等 advisory lock 下删除该 Processing 行。租约、续租、接管、工作期限和清理判断全部使用 PostgreSQL 时间。 + +A141 使用相同 60/20 秒 lease 公式,`work_expires_at` 与 DB025 `upload_expires_at` 都固定为首次预留时间 + 24 小时。到期后 `review.expired_image_upload_cleanup` 在 A141 幂等锁和 `(buyer_id,order_item_id)` 锁内登记 DB105、移除未关联 DB025 与过期 DB104;不得续期、生成第二个 ID/Key或把过期图转 Pending。 `scope_key` 固定采用不含秘密的规范字符串,例如 `buyer:{buyerId}`、`merchant:{merchantId}:order:{orderId}`、`admin:{adminId}:user:{targetUserId}`;同一 operation 不允许多个模块自行发明不同格式。公开 HTTP 通常使用对应 `Axxx` 作为 `operation_code`,内部动作使用稳定 lower_snake_case 名。A016/A017 是已确认例外:共同使用 `operation_code='account_governance'`、`scope_key='admin:{adminUserId}'`,目标账号与启用/禁用动作进入请求指纹,保证两接口共享治理幂等范围。 @@ -1836,7 +2073,8 @@ Messaging 消费时先判断事件是否属于消息矩阵:明确无消息事 |---|---| | A016/A017 | 管理员 + 目标账号治理事实,本期不自动清理 | | A122/A142/A220/A222/A223/A228/A301/A307 | 至少覆盖资源完整生命周期,本期不自动清理 | -| A201、A204~A207 | 首次确定结果起 24 小时 | +| A141 | 未关联时保留至首次预留起 24 小时;Attached 后随评价图片生命周期保留 | +| A201、A205、A206 | 首次确定结果起 24 小时;A204/A207 天然幂等,不写本表 | | A402/A405 | 资金事实完整生命周期,本期不自动清理 | | A412/A415~A417/A419/A434 | 售后事实完整生命周期,本期不自动清理 | | A425 | 对账差异完整生命周期,本期不自动清理 | @@ -1853,13 +2091,16 @@ A421 使用 DB089 的 callbackId 幂等,不重复写本表。 | `source_type` | `varchar(64)` | 否 | — | 来源记录类型 | | `source_id` | `uuid` | 否 | — | 来源 ID 弱引用 | | `reason` | `varchar(64)` | 否 | — | 删除/过期/补偿原因 | +| `generation` | `bigint` | 否 | `1` | 同一 Key 的清理责任代次 | | `status` | `varchar(24)` | 否 | `'pending'` | `pending/running/retry_wait/succeeded/dead_lettered` | | `attempt_count` | `integer` | 否 | `0` | 尝试数 | | `next_attempt_at` | `timestamptz` | 是 | `CURRENT_TIMESTAMP` | 下次处理;运行/终态为空 | | `lease_owner` | `varchar(100)` | 是 | — | Worker | | `lease_token` | `uuid` | 是 | — | 租约令牌 | | `lease_expires_at` | `timestamptz` | 是 | — | 租约到期 | +| `completion_code` | `varchar(100)` | 是 | — | `object_deleted/object_not_found/object_still_referenced` | | `last_error_code` | `varchar(100)` | 是 | — | 安全错误 | +| `requested_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 本代清理责任受理时间 | | `created_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 创建 | | `updated_at` | `timestamptz` | 否 | `CURRENT_TIMESTAMP` | 更新 | | `completed_at` | `timestamptz` | 是 | — | 删除确认 | @@ -1867,14 +2108,24 @@ A421 使用 DB089 的 callbackId 幂等,不重复写本表。 - PK `pk_object_cleanup_tasks`;Unique `ux_object_cleanup_tasks_object_key(object_key)`,重复清理合并为同一任务。 - 状态增加 `running`,并冻结: - - `pending/retry_wait`:`next_attempt_at` 非空,lease 与终态时间为空; - - `running`:三个 lease 字段非空,`next_attempt_at` 与终态时间为空; - - `succeeded`:`completed_at` 非空,其余调度/lease/死信字段为空; + - `pending/retry_wait`:`next_attempt_at` 非空,lease、完成码与终态时间为空; + - `running`:三个 lease 字段非空,`next_attempt_at`、完成码与终态时间为空; + - `succeeded`:`completed_at/completion_code` 非空,其余调度/lease/错误/死信字段为空; - `dead_lettered`:`dead_lettered_at/last_error_code` 非空,其余调度/lease/完成字段为空。 -- Check 尝试数非负、Key 和受控类型非空。 +- Check `generation >= 1`、尝试数非负、Key 和受控类型非空。 - `object_kind` 与 Key 前缀必须一致:`product_image → products/`、`thumbnail → product-thumbnails/`、`review_image → reviews/`,清理任务不能伪造跨类型对象。 - `ix_object_cleanup_tasks_due(status,next_attempt_at,lease_expires_at,id)`。 - 数据库关联删除前在同一事务登记任务;Worker 把对象 NotFound 视为幂等成功。DeadLettered 进入告警但不能恢复已删除业务关联。 +- Worker 删除前必须在同一数据库快照检查所有现有引用: + - 商品原图/缩略图:DB023 Attached/Detached、DB062 历史订单图片快照,以及 DB104 未过期 A122 Processing 工作清单; + - 评价图:DB025 Attached 或未过期 Uploading/Pending,以及 DB104 未过期 A141 Processing 工作清单。作为当前清理来源且已经到期的 DB025 Uploading/Pending 不阻断删除; + - 任一查询失败或结果未知都按可重试依赖错误处理,不能当作“无引用”继续。仍有有效引用时以 `succeeded + object_still_referenced` 完成本代安全无操作;对象不存在为 `object_not_found`,实际删除为 `object_deleted`。 +- 所有登记先取得 objectKey 级事务 advisory lock,并明确选择两种内部模式: + - `EnsureActive`:用于首次解除关联、上传到期或重复恢复扫描。目标行已经 `pending/running/retry_wait` 时只确认活动责任存在,不改变 generation 或当前 lease;目标为 `succeeded/dead_lettered` 时 `generation+1` 并重置为 Pending。 + - `Supersede`:只用于**先前登记之后又发生了新的外部事实**,例如失去上传 Token 的执行者迟到确认 PUT 成功,或先前 `object_still_referenced` 后最后引用刚被解除。无论当前是活动态还是终态,都必须 `generation+1`、更新来源/原因和 `requested_at=decision_time`,清空完成/死信/错误/租约字段、`attempt_count=0` 并重置为 `pending + next_attempt_at=decision_time`。 +- 领取者同时保存 generation 与新 lease token;完成、失败或续租必须匹配 `id + generation + lease_token + status=running`。Supersede 因而立即使旧租约失效:旧 Worker 即使已完成 HEAD/Delete,也不能提交旧 NotFound/Deleted 覆盖新责任,下一代仍会重新核查对象。 +- `object_still_referenced` 和 `object_not_found` 只是当前代次的成功观察,不表示该 Key 永久无需清理。调用方必须区分“重复确保已有责任”和“新事实覆盖旧责任”,不得因唯一键冲突直接忽略,也不得用 EnsureActive 吞掉迟到写入。 +- 对象清理 Worker 默认每 10 秒扫描、每批最多 100 条,领取租约 60 秒、每 20 秒续租;单次对象调用超时 10 秒。瞬态网络、对象存储 5xx/限流和引用检查依赖失败按 10 秒、30 秒、2 分钟、10 分钟、30 分钟退避,此后每 1 小时持续重试,不因次数耗尽删除责任;第 5 次失败 Warning,第 20 次及以后每 24 小时聚合 Critical。只有 Key 格式/类型等确定不可执行的数据错误才进入 `dead_lettered` 并立即 Critical,不能把对象存储暂时不可用转成人工终止。 ### 8.6 DB106 `outbox_delivery_attempts` @@ -1930,11 +2181,30 @@ A421 使用 DB089 的 callbackId 幂等,不重复写本表。 - `succeeded`:开始/完成时间非空,调度/lease/错误为空; - `dead_lettered`:开始/完成时间和错误非空,调度/lease 为空。 - `ix_worker_job_runs_due(status,next_attempt_at,lease_expires_at,id)`。 -- C08 `run_key=UTC business_date`;活动生命周期、过期上传清理、对象清理等使用稳定调度窗口。订单逐笔责任仍由 DB063,Outbox 逐笔责任仍由 DB102,退款责任仍由 DB093。 +- 所有 DB107 任务默认使用 60 秒租约、每 20 秒续租,领取/续租/完成均匹配 fencing token;任务专章有更严格值时以专章为准。固定调度矩阵: + +| `job_name` | `run_key` | 扫描/批量 | 失败收敛 | +|---|---|---|---| +| `seckill.activity_lifecycle` | UTC 5 秒窗口 | 每 5 秒,100 条活动 | 5 秒、30 秒、2 分钟、10 分钟后每小时;下一窗口仍重扫到期活动 | +| `ordering.order_lifecycle` | UTC 5 秒窗口 | 每 5 秒,逐笔责任由 DB063、单批 100 | DB107 只记批次摘要;DB063 按自身退避持续收敛 | +| `catalog.product_create_recovery` | UTC 10 秒窗口 | 每 10 秒,50 条 DB104 A122 Processing | 有效租约只观察;过期租约按原工作清单接管,超过 24 小时则登记全部 DB105 后收敛 | +| `catalog.expired_product_upload_cleanup` | UTC 分钟窗口 | 每 1 分钟,100 条 DB023 | DB107 失败按 1 分钟、5 分钟、15 分钟、1 小时封顶;DB023/DB105 责任不丢失 | +| `review.expired_image_upload_cleanup` | UTC 分钟窗口 | 每 1 分钟,100 条 DB025 | 在订单项锁内登记 DB105 后移除过期暂存;DB025/DB104/DB105 责任不丢失 | +| `storage.object_cleanup` | UTC 10 秒窗口 | 每 10 秒,100 条 DB105 | DB107 只记批次摘要;DB105 按自身退避持续收敛 | +| `storage.orphan_inventory_reconciliation` | UTC 日期 | 每日 02:00 UTC,分页 500 个对象 | 仅登记超过 25 小时且无数据库/有效工作清单引用的对象;1 分钟、5 分钟、15 分钟、1 小时封顶重试 | +| `after_sales.refund_recovery` | UTC 5 秒窗口 | 每 5 秒,50 条 DB093 | DB107 只记批次摘要;DB093 按自身时序持续收敛 | +| `PaymentDailyReconciliation` | `YYYY-MM-DD` UTC business date | 每日 00:05 UTC 触发;每分钟漏跑检查;Worker 启动立即补查,逐日补齐最早 eligible 缺失日 | 1 分钟、5 分钟、15 分钟、1 小时封顶,成功前阻断后续日期 | + +- `PaymentDailyReconciliation` 每个 UTC 业务日一条,`run_key` 只等于该日 `YYYY-MM-DD`,不得附加实例、触发来源或重试次数。正常 00:05、每分钟检查与启动补查只发现同一责任:当前时间达到当日 `00:05Z` 后才允许为前一完整日生成;00:05Z 前最近 eligible 日为前两日,禁止提前生成刚结束日期。按日期升序一次处理一个,空日也建批,某日失败阻断后续。 +- 该任务禁止 `running/retry_wait → dead_lettered`:失败后复用同一行持续重试;第 3 次失败 Warning、第 10 次及以后 Critical,直到成功才允许处理后续日期。 +- 周期扫描类运行行本身可在确定的配置/契约错误时 DeadLettered 并 Critical,但它不能清除 DB023/DB063/DB093/DB102/DB104/DB105 的逐笔责任;后续窗口仍须发现并处理这些责任。数据库/网络等瞬态失败只进入 RetryWait,不能 DeadLetter。 +- 退款恢复固定 `job_name='after_sales.refund_recovery'`、`run_key=UTC 五秒调度窗口`;默认每 5 秒扫描一次、单批最多 50 条,运行摘要只保存扫描、成功、Unknown、自动重试和升级数量。恢复租约 60 秒、每 20 秒续租、单次最多连续持有 5 分钟;逐笔候选、围栏、退避和人工处置仍以 DB093 为权威,DB107 失败或缺失不能清除退款责任。 +- `storage.orphan_inventory_reconciliation` 的 `run_key=YYYY-MM-DD`,当日 02:00 UTC 后首次调度;Worker 启动时若已过当日触发点且该日尚无成功运行则立即补跑。它只列举 `products/`、`product-thumbnails/`、`reviews/` 受控前缀,使用 DB107 checkpoint 保存分页游标;候选必须对象年龄超过 25 小时,并在同一核查中确认 DB023/DB025/DB062 和有效 DB104 工作清单均无引用,才以 `source_type='orphan_inventory'`、当前 DB107 run ID 为 `source_id` 登记 DB105。它不直接删对象,也不把列表失败当作无引用。 +- A122 创建恢复责任仍由 DB104 的 Processing 工作清单,订单逐笔责任仍由 DB063,Outbox 逐笔责任仍由 DB102,退款逐笔责任仍由 DB093。 ### 8.8 C06/C07/C10 不建业务表的内容 -- SignalR 连接与 Redis Backplane 不落 PostgreSQL;消息可查询事实只在 DB101。 +- SignalR 连接与版本化应用级 Redis Pub/Sub 分发命令不落 PostgreSQL;消息可查询事实只在 DB101。 - C07 Cache Key、填充锁、缓存值和 TTL 不落 PostgreSQL;商品事实仍在 DB021~DB027,但来源事务与两阶段可靠失效责任由 DB102/DB103 持久化。 - Redis 撤销镜像从 DB001/DB004 重建;重建完成前受保护鉴权与 Hub 失败关闭。 - EF Core 自带 `__EFMigrationsHistory` 是框架元数据,不分配 DBxxx;A507 只比较目标 Migration 版本和依赖能力,不伪造健康业务表。 @@ -1952,6 +2222,7 @@ A421 使用 DB089 的 callbackId 幂等,不重复写本表。 | 订单、订单项、履约状态 | Ordering | Payment/AfterSales 保存稳定引用和必要金额/来源快照 | | 钱包、资金流水、成功支付、回调、退款 | Payment | Ordering 只保存支付时间/水位;AfterSales 保存原支付引用 | | 售后资格、申请状态、领域时间线 | AfterSales | Ordering 通过公开快照计算能否发货 | +| 评价与订单项评价事实 | Review | Ordering 只通过批量公开能力派生评价摘要,不保存平行评价状态 | | 用户消息与已读 | Messaging | 来源模块只写 Outbox 事件,不写消息表 | | 幂等、Outbox/Inbox、对象补偿、Worker 运行 | M00 | 模块提供受控作用域、事件和业务引用 | @@ -2024,13 +2295,14 @@ erDiagram |---|---|---| | A001 | DB001 | 原子创建 Buyer | | A002 | DB001 | 凭据、状态、token version | -| A003 | DB001、DB004 | 当前 JTI 撤销 | +| A003 | DB001;条件使用 DB004、DB102 | 数据库裁决时未到期才建立当前 JTI 撤销及实时安全失效责任;已到期只确认自然失效 | | A004 | DB001、DB004 | 账号与撤销权威事实 | -| A006 | DB001 | 手机号 + token version | +| A006 | DB001、DB102 | 手机号、token version 及实时安全失效责任 | | A007、A008 | DB001 | 用户名重置/资料 | | A010~A014 | DB003 | 地址及默认切换 | | A015 | DB001 | 后台账号列表 | -| A016、A017 | DB001、DB002、DB104 | 启停、历史、稳定结果;责任通过各模块公开能力复核 | +| A016 | DB001、DB002、DB102、DB104 | 禁用、token version、实时安全失效、历史与稳定结果;责任通过各模块公开能力复核 | +| A017 | DB001、DB002、DB104 | 启用、历史与稳定结果;不回退 token version,不重复产生安全失效事件 | | A018~A020 | DB005 | 收藏;展示实时取 Catalog | | A021 | DB006 | 历史列表 | | A022、A025 | DB007 | 浏览开关 | @@ -2041,17 +2313,18 @@ erDiagram | A110 | DB021、DB022 | 全状态分类和计数 | | A111 | DB021 | 新建两级分类 | | A112 | DB021、DB022、DB027、DB102 | 分类修改、搜索投影同步与受影响详情失效 | -| A113、A114 | DB021、DB102 | 启停分类与受影响首页/详情失效 | +| A113、A114 | DB021 | 启停分类只改变 A101 筛选入口,不产生 C07 失效 | | A115 | DB021、DB022及所有历史引用 | 受约束物理删除 | | A120、A121 | DB021~DB023 | 后台商品列表/详情 | -| A122 | DB021~DB023、DB027、DB104,正库存时 DB026,失败时 DB105 | 幂等 multipart 创建 | +| A122 | DB021~DB023、DB027、DB102、DB104,正库存时 DB026,失败时 DB105 | 幂等 multipart 创建与详情短空值失效 | | A123 | DB021~DB023、DB026、DB027、DB102 | 编辑、库存流水、搜索同步与缓存失效 | | A124 | DB022、DB023、DB026、DB027、DB102、DB105及跨模块引用 | 受约束删除与对象补偿 | | A125 | DB021~DB023、DB102 | 上架完整性与缓存失效 | | A126 | DB022、DB102 | 下架与缓存失效 | -| A127、A128 | DB022、DB023、DB102、DB105 | 图片上传/删除与缓存失效 | +| A127 | DB022、DB023、DB102,失败时 DB105 | 图片上传、图库提交与缓存失效 | +| A128 | DB022、DB023、DB062、DB102、DB105 | 图片移除前保护历史订单快照,必要时转 Detached 或登记对象清理 | | A140 | DB024、DB025 | 评价列表/实时汇总 | -| A141 | DB025、失败/过期时 DB105 | 评价图片暂存 | +| A141 | DB025、DB104,失败/过期时 DB105 | 幂等评价图片持久预留、续传与暂存 | | A142 | DB024、DB025、DB104 | 唯一评价和图片绑定 | | A143 | DB024 | 评价资格/既有结果 | @@ -2061,26 +2334,27 @@ erDiagram |---|---| | A201 | DB041;提供 Key 时 DB104 | | A202、A203、A208 | DB041 + Catalog 实时快照 | -| A204~A207 | DB041、DB104 | +| A204、A207 | DB041 | +| A205、A206 | DB041、DB104 | | A220 | DB042、DB104 | | A221 | DB042 | -| A222 | DB042、DB044、DB026、DB102、DB104 | +| A222 | DB022、DB026、DB042、DB044、DB102、DB104 | | A223 | DB042、DB104 | | A224 | DB042 | | A225 | DB042;订单统计通过 DB061/DB062 公开能力 | | A226 | DB042 + Catalog 展示快照 | | A227 | DB042;已登录买家另读 DB043 | | A228 | DB042~DB044、DB061~DB063、DB102、DB104 | -| A301 | DB041、DB026、DB061~DB063、DB102、DB104 | -| A302 | DB061、DB062 | -| A303 | DB061、DB062 + Payment/AfterSales 公开摘要 | -| A304 | DB061、DB062、DB026 或 DB044、DB102 | -| A305 | DB061、DB062 | -| A306 | DB061、DB062 + AfterSales 公开摘要 | +| A301 | DB001、DB003、DB022、DB026、DB041、DB061~DB063、DB102、DB104 | +| A302 | DB061、DB062 + AfterSales DB086 / Review DB024 批量公开摘要 | +| A303 | DB061、DB062 + Payment DB085 / AfterSales DB086 / Review DB024 公开摘要 | +| A304 | DB061~DB063、DB102;普通订单回补 DB022/DB026,秒杀订单回补 DB042/DB043/DB044 | +| A305 | DB061、DB062 + AfterSales DB086 批量公开摘要 | +| A306 | DB061、DB062 + Payment DB085 公开摘要 + AfterSales DB086 批量明细 | | A307 | DB061~DB063、DB102、DB104 + AfterSales 履约快照 | | A308 | DB061、DB102 | | C01 生命周期 | DB042、DB107 | -| C03 超时取消 | DB061~DB063、原库存流水、DB102 | +| C03 超时取消 | DB061~DB063、DB102;普通订单回补 DB022/DB026,秒杀订单回补 DB042/DB043/DB044 | | 自动完成 | DB061、DB063、DB102 | ### 10.3 Payment、AfterSales、Messaging、运行能力 @@ -2099,10 +2373,10 @@ erDiagram | A413 | DB086 | | A414 | DB086~DB088、DB092、DB093 | | A415 | DB086、DB087、DB104 | -| A416 | DB086~DB088、DB093、DB102、DB104;成功退款另用钱包/库存/DB096 | -| A417 | DB086~DB088、DB092、DB093、DB102、DB104 | -| A419 | DB086~DB088、DB093、DB102、DB104 | -| A421 | DB061、DB084、DB085、DB089、DB096、DB102;到期裁决另触发 DB063 与 DB026/DB044 统一取消 | +| A416 | DB086~DB088、DB093、DB102、DB104;成功退款另用 DB081、DB083、DB085、DB096,并按原通道回补普通 DB022/DB026 或秒杀 DB042/DB043/DB044 | +| A417 | DB086~DB088、DB092、DB093、DB102、DB104;成功退款另用 DB081、DB083、DB085、DB096,并按原通道回补普通 DB022/DB026 或秒杀 DB042/DB043/DB044 | +| A419 | DB086~DB088、DB093、DB102、DB104;成功退款另用 DB081、DB083、DB085、DB096,并按原通道回补普通 DB022/DB026 或秒杀 DB042/DB043/DB044 | +| A421 | DB061、DB084、DB085、DB089、DB096、DB102;到期裁决另触发 DB063,并按原通道取消回补普通 DB022/DB026 或秒杀 DB042/DB043/DB044 | | A422 | DB090 | | A423 | DB090、DB091 | | A424 | DB091、DB094 | @@ -2110,11 +2384,12 @@ erDiagram | A426 | DB091、DB094、DB095 | | A434 | DB086、DB087、DB092、DB102、DB104 | | A501~A505 | DB101 | -| 消息事件消费 | DB103、DB101 | +| 消息事件消费 | DB103、DB101、DB102 | | 全部可靠事件生产者 | DB102;投递尝试 DB106 | | A506 | 无业务表 | | A507 | `__EFMigrationsHistory` + 依赖检查,无伪业务表 | | 对账 Worker | DB096、DB090~DB095、DB107 | +| 退款恢复 Worker | DB086、DB088、DB093、DB107;成功时另用钱包、原库存、DB096、DB102 | | 对象清理 Worker | DB105、DB107 | 已取消的 A005、A009、A023、A144、A229、A230、A418、A431、A432、A433 不建表、不建字段、不重新分配。 @@ -2124,7 +2399,7 @@ erDiagram | 数据 | 保留规则 | |---|---| | 用户、账号状态历史 | 本期永久保留;无用户删除能力 | -| JWT 撤销 | `expires_at` 后加安全余量才可清理;清理不影响 `token_version` | +| JWT 撤销 | JWT 验证固定 `ClockSkew=0`;只按数据库时间清理 `expires_at <= clock_timestamp()`,不增加应用时钟“安全余量”;清理不影响 `token_version` | | 地址、购物车、收藏 | 按明确业务动作物理删除 | | 浏览历史 | 每买家最多 200 条;A024 同事务裁剪 | | 商品/分类 | 仅按受约束删除;合法商品删除先登记对象清理 | @@ -2216,13 +2491,13 @@ Seed 只提供可重复演示事实: 14. 搜索投影 `source_hash` 必须等于商品名、分类名和描述的规范哈希;它可以从权威表重建,库存、价格和状态版本不参与比较。 15. Normal 订单全部订单项都来自 Catalog;Seckill 订单恰有一项,且订单、订单项、活动与商品来源一致。 16. 同一原子财务结果在参与表复用同一 posting sequence 和 posting_time;每张参与事实表内该 sequence 最多一行。 -17. Shipped/Completed 订单的订单项已发货量等于购买量减已退款量,且整单已发货总量大于 0;全部数量已退款的订单不能发货。 +17. 订单从 Paid 迁移为 Shipped 的提交瞬间,每个订单项 `shipped_quantity = quantity - refunded_quantity_at_ship`,且整单已发货总量大于 0;全部数量已退款的订单不能发货。迁移提交后 `shipped_quantity` 是不可变历史快照,后续售后退款不得用“当前累计退款量”反向改写它。 ### 14.2 后续实现必须覆盖的数据库测试 - 唯一约束:手机号、用户名、默认商家、默认地址、收藏、购物车、评价、订单支付、售后退款、回调 ID、幂等作用域。 - Check:状态字段组合、金额/库存非负、活动数量守恒、钱包借贷平衡、售后状态字段。 -- 跨表不变量:发布初始划拨与 DB044 对齐、生命周期任务到期时间绑定、订单头/订单项类型一致、发货量与售后退款量一致且非零、Completed 售后完成时间绑定、状态历史 actor 归属、退款执行人归属、posting sequence 时间与序号一致。 +- 跨表不变量:发布初始划拨与 DB044 对齐、生命周期任务到期时间绑定、订单头/订单项类型一致、发货迁移瞬间的已发货量与当时退款量一致且非零、发货后退款不改写发货快照、Completed 售后完成时间绑定、状态历史 actor 归属、退款执行人归属、posting sequence 时间与序号一致。 - 并发: - 同手机号注册; - 默认地址切换; @@ -2237,7 +2512,7 @@ Seed 只提供可重复演示事实: - 消息整事件消费; - 确定非法消息进入 Rejected/死信、瞬态校验失败重投; - 对账领取/接管/解决; - - C07 Immediate/Delayed 重投、延迟事件唯一去重与不阻塞双删。 + - C07 Immediate/Delayed 唯一去重、长事务后 Delayed 不提前、未武装崩溃重扫、已武装崩溃接管、两阶段独立重投与不阻塞双删。 - 故障恢复:对象写入后数据库失败、Outbox Confirm 后标记前崩溃、Worker lease 过期、退款 Unknown、Redis 丢失后的安全事实重建。 ## 十五、流程与接口审计中采用的最优解 @@ -2257,13 +2532,13 @@ Seed 只提供可重复演示事实: | 秒杀 `frozenCount` 旧说法 | 本期不建;活动库存只维护划拨、剩余、已售守恒 | | 回调 ID 与渠道流水都被误当唯一回调键 | DB084 流水唯一,DB089 允许一流水多 callbackId | | 仅 `watermarkAt` 可能漏并发晚提交 | DB096 严格 posting sequence + 展示 watermarkAt | -| 财务时间无法在事务内取得物理 COMMIT instant | 以持 DB096 写锁后取得的 posting_time 作为权威财务业务时间;共享水位屏障保证不漏账 | +| 财务时间无法在事务内取得物理 COMMIT instant | 以持 DB096 写锁后取得的唯一 `final_time` 作为权威财务业务时间与 `posting_time`;共享水位屏障保证不漏账 | | 订单状态历史是否另建表 | 五态线性且不回退,直接由唯一时间字段生成;不重复建表 | | 售后状态历史是否另建表 | 存在 RefundFailed→Refunding 循环,必须有 DB087 | | 部分售后是否建冗余余额表 | 统一锁 orders + 索引聚合,避免冗余计数漂移 | | A416/A417/A419 重放首次命令又要显示退款进展 | DB104 `resource_current` 固定资源身份,再读同一 RefundOperation 当前态 | | Outbox 发布恰好一次 | 接受至少一次发布窗口;DB103 Inbox 保证业务效果一次 | -| C07 双删若让消费者等待 3 秒会阻塞吞吐 | Immediate 消费后登记唯一 Delayed Outbox;两阶段分别确认、重投和幂等删除 | +| C07 双删若让消费者等待 3 秒会阻塞吞吐,预提交绝对到期又会被长事务吃掉延迟 | 来源业务事务独立写 Immediate 与初始未武装 Delayed;Delayed 提交可见后由 Outbox 调度器以 `clock_timestamp()` 原子武装,`available_at=armed_at+3 秒`。两阶段各自确认、恢复和幂等删除,互不依赖且不提前 | | 跨模块原子性与模块边界 | 公开应用能力加入同一 Npgsql 事务;调用方仍不得取其他模块 DbSet/仓储 | | RLS 是否作为权限方案 | 不采用;服务端 Policy/资源归属校验 + 最小数据库账号权限 | @@ -2282,7 +2557,7 @@ Seed 只提供可重复演示事实: 后续开始真实数据库实施前必须: 1. 主接口的“关联数据表”全部同步到本文; -2. 流程/需求中“待数据库设计”的成熟度说明改为“数据库设计已完成,待实现”; +2. 保持流程、需求和接口中的成熟度为“数据库设计已确认,待 OpenAPI/实现/测试”,不得把文档设计误报为数据库已经落地; 3. 实体与映射逐表对应本文,不新增未评审表或字段; 4. 先生成 Migration SQL 并审查扩展、约束、索引、FK 和删除行为; 5. 用真实 PostgreSQL 执行映射测试、Migration 正反向验证和并发集成测试; diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" index 7f471f0..a7a4df4 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\347\263\273\347\273\237\346\236\266\346\236\204\350\256\276\350\256\241.md" @@ -2,7 +2,7 @@ > 组别:24级1班第7组 编写人:罗皓晨 编写日期:2026-07-22 版本:v0.2 > -> 文档状态:已按业务流程 v1.0 完成架构承接校准,接口已按流程重建,待数据库、实现和全组技术评审后冻结 +> 文档状态:已按业务流程 v1.0 完成架构承接校准,接口与数据库设计已承接;待 OpenAPI、实现、测试和全组技术评审后冻结 > 截止:第 1 周周五 ## 一、架构目标与约束 @@ -25,7 +25,7 @@ - PC Web、Electron 和 Android 必须共用同一套 ASP.NET Core Web API、OpenAPI 契约、鉴权、业务状态和 PostgreSQL 数据,不建设平台专属后端。 - DDD 只用于订单、支付、库存等规则复杂区域,简单 CRUD 不创建多余抽象。 - 简化 CQRS 只分离命令与查询职责,不拆分读写数据库。 -- Redis 首期用于 JWT 退出失效记录,并承担 C06 SignalR 多实例 Backplane 和 C07 商品缓存。 +- Redis 只保存可由 PostgreSQL DB001/DB004 重建的撤销与安全镜像,并承担 C06 版本化实时提示 Pub/Sub 通道和 C07 商品缓存;它不是令牌撤销、账号状态、站内消息或其他业务/安全事实的唯一来源,镜像水位无法确认时受保护能力失败关闭。 - RabbitMQ、Outbox/Inbox 和 S3 兼容对象存储按对应选做或挑战阶段启用,核心下单正确性仍以 PostgreSQL 事务为准。 - 对象存储只依赖 S3 兼容协议,开发环境使用 SeaweedFS,其他环境可替换为兼容实现。 - 本组为 24级1班第7组,6 名成员均由教师分配;架构按六人纵向模块负责制设计。 @@ -61,9 +61,9 @@ | 数据库 | PostgreSQL | 18.4 | 关系模型、事务、约束和统计 SQL 能力适合商城业务;18 系列由官方支持至 2030-11-14 | | 架构模式 | 核心领域使用 DDD + Clean Architecture + 简化 CQRS | — | 复杂业务规则进入领域层;命令与查询职责分开,但不拆读写库 | | 领域协作 | 领域事件 | — | 在同一进程内表达并处理领域事实 | -| 集成可靠性 | RabbitMQ 集成事件 + Outbox/Inbox | RabbitMQ 4.3.2 | Outbox 保证事务后可靠发布,Inbox/消费处理记录保证消费者防重 | +| 集成可靠性 | RabbitMQ 集成事件 + Outbox/Inbox | RabbitMQ 4.3.4 | Outbox 保证事务后可靠发布,Inbox/消费处理记录保证消费者防重 | | 缓存/共享通道 | Redis | 8.2.7 | 8.2 系列为当前具有明确长期支持周期的较新 GA 系列,官方标注支持至 2030-09-01 | -| 后台处理 | .NET Worker Service | .NET 10 | 执行 Outbox 投递、秒杀生命周期、订单超时取消、发货超时自动完成和每日对账任务 | +| 后台处理 | .NET Worker Service | .NET 10 | 执行 Outbox 投递、秒杀生命周期、订单超时取消、发货超时自动完成、退款恢复和逐日对账补偿任务 | | 本地编排 | Aspire | 13.4.4 | 声明 API、Worker 和基础设施依赖;Aspire 不设 LTS 分支,按官方策略使用当前唯一受支持版本 | | 生产部署 | Docker Compose + Nginx | Compose 规范 + 镜像摘要锁定 | 满足 C10 一键部署和至少 2 个 API 实例负载均衡验收 | | 对象存储 | S3 Compatible Object Storage(开发环境:SeaweedFS) | SeaweedFS 4.29 | 业务仅依赖 S3 兼容协议,避免绑定具体存储产品 | @@ -75,7 +75,7 @@ |---|---|---| | PostgreSQL | 18.4 | PostgreSQL 不使用 LTS 名称,但每个主版本支持 5 年;18.4 是 18 系列当前修订版,18 系列支持至 2030-11-14 | | Redis Open Source | 8.2.7 | 8.2 为 GA,官方已给出至 2030-09-01 的支持周期;使用当前 8.2 系列修订版 | -| RabbitMQ | 4.3.2 | 社区版没有独立免费 LTS 分支;4.3.2 是当前最新且处于社区支持期的稳定版,长期商业支持需另购许可证 | +| RabbitMQ | 4.3.4 | 社区版没有独立免费 LTS 分支;4.3.4 是截至 2026-07-25 的 4.3 系列最新社区支持补丁,长期商业支持需另购许可证 | | SeaweedFS | 4.29 | 项目没有官方 LTS 分支;4.29 是当前最新正式 Release,仅用于开发环境 S3 兼容对象存储 | | Aspire | 13.4.4 | Aspire 不提供 LTS 分支且同一时间只支持最新版本;13.4.4 是当前唯一受支持修订版 | @@ -97,14 +97,16 @@ flowchart TB API1 --> PG[(PostgreSQL)] API2 --> PG - API1 --> R[(Redis)] - API2 --> R + API1 -->|"缓存、安全镜像与提示发布"| R[(Redis)] + API2 -->|"缓存、安全镜像与提示发布"| R API1 --> S3[S3 Compatible Object Storage
开发:SeaweedFS] API2 --> S3 API1 --> MQ[(RabbitMQ)] API2 --> MQ - API1 <-->|SignalR Backplane| R - API2 <-->|SignalR Backplane| R + R -->|"eshop:{environment}:signalr:message-hints:v1"| API1 + R -->|"eshop:{environment}:signalr:message-hints:v1"| API2 + MQ -->|"共享实时提示队列
竞争消费一个入口"| API1 + MQ -->|"共享实时提示队列
竞争消费一个入口"| API2 W[Mall.Worker] --> PG W --> MQ W --> R @@ -238,7 +240,7 @@ Mall.Modules..Api | Ordering | 提交订单、订单项快照、订单查询、取消、商家发货、确认收货、自动完成和 C03 超时取消 | `Mall.Modules.Ordering` | F08、F09、F12、C03 | 韦乾强 | | Payment | 小金库、模拟充值、模拟支付、钱包流水、支付记录、回调幂等、退款入账和 C08 对账 | `Mall.Modules.Payment` | F10、C08,并支撑 X04 | 张海洋 | | Review | 已完成订单项评分、文字评价、评价晒图、重复评价和越权拦截 | `Mall.Modules.Review` | X01 | 顾欣月 | -| Engagement | 商品收藏、取消收藏、收藏列表、浏览历史记录和清理 | `Mall.Modules.Engagement` | X02 | 唐宇昊 | +| Engagement | 商品收藏、取消收藏、收藏列表、浏览历史记录与同事务裁剪至最近 200 条 | `Mall.Modules.Engagement` | X02 | 唐宇昊 | | Messaging | 买家和商家站内消息、未读状态、标记已读、事件去重和 C06 实时推送 | `Mall.Modules.Messaging` | X03、C06 | 罗皓晨 | | AfterSales | 退款/退货申请、商家审核、退货确认、售后状态、撤销申请和退款协作 | `Mall.Modules.AfterSales` | X04 | 张海洋 | | Seckill | 秒杀活动、资格校验、秒杀库存、防超卖下单和并发验收 | `Mall.Modules.Seckill` | C01 | 朱惠惠 | @@ -329,18 +331,20 @@ sequenceDiagram - 模拟充值写入钱包流水并原子增加余额;充值请求使用幂等键,重复请求不重复到账。 - 支付请求使用唯一支付流水号和幂等键。 - 只有待支付订单允许支付。 -- 支付还必须满足服务端权威时间早于订单固定 `paymentDeadline`;Wallet 支付、C08 模拟通道回调、买家取消和 C03 到期取消竞争同一 `PendingPayment` 条件,最多一个成功。 +- 支付还必须在取得订单、既有支付、钱包(适用时)和财务提交水位等该路径全部可能阻塞的共享事实后,立即读取唯一数据库 `finalTime`;只有 `finalTime < paymentDeadline` 才能成功。`finalTime` 同时作为该原子资金结果的支付/入账时间,之后不得再等待新共享锁或调用外部服务。Wallet 支付、C08 模拟通道回调、买家取消和 C03 到期取消竞争同一 `PendingPayment` 条件,最多一个成功。 - 钱包余额条件扣减、钱包流水、支付记录、订单状态和 Outbox 在同一数据库事务内更新,余额不得为负。 - 重复成功请求返回原成功结果,不重复写入或重复发布事件。 - 默认 F10 使用 `Wallet` 同步支付;C08 使用不扣钱包的受控 `SimulatedChannel`。订单选择的支付来源必须持久化并出现在支付查询中,不能由另一通道补写成功。 -- 售后退款通过 Payment 的公开应用能力幂等退回原买家的小金库,并写入退款钱包流水;AfterSales 不直接修改钱包数据。 +- 售后退款由进程内 `RefundOrchestrator` 作为唯一外层事务协调者:按订单 → 售后申请 → 退款操作 → 当前尝试 → 原支付 → 钱包 → 原库存聚合 → 财务水位的顺序调用公开应用能力。编排器不拥有表;Payment 独占退款操作/尝试、钱包和资金流水,AfterSales 独占资格/状态/回补策略,Catalog/Seckill 独占库存。首次进入 `Refunding` 必须同事务建立 DB088 与 `Initial/Executing` DB093,AfterSales 不直接修改 Payment 表。 ### 7.3 订单履约与完成 - 商家只能把已支付订单推进为已发货,并记录发货时间;其他状态的发货请求必须拒绝。 - 本期为单店 B2C,不建设多商户商品归属、拆单或结算;Ordering 为每张普通订单和秒杀订单都保存 Identity 解析的唯一启用默认 `assignedMerchantUserId`,活动创建人只用于活动管理。商家查询、发货、售后和消息接收均按订单指定账号精确过滤。 - Identity 对默认商家标记建立唯一约束,并由启动配置/种子数据保证存在一个启用账号;A016 始终拒绝禁用默认商家。非默认商家存在 `PendingPayment`、仍有可履约数量的 `Paid`、`Shipped`、完成后 7 天窗口内的 `Completed`、任一非终态售后申请或未结束秒杀活动时也拒绝禁用;已全量退款、无剩余可履约数量且无非终态售后的 `Paid` 不再单独阻断。本期不做自动重新分配。 -- A307 发货与 A412 提交售后都先通过 Ordering 公开应用契约在当前 PostgreSQL 事务中锁定同一 `orders` 行并复核最新履约状态,锁保持到业务写入提交;随后 A307 通过 AfterSales 公开应用契约取得售后快照。处理中申请阻断发货,已退款数量从可履约数量中扣除,部分退款只发剩余数量,全部退款不再发货;模块之间不得直接读取对方内部表。 +- A307 发货先通过 Ordering 公开应用契约锁定 DB061 订单,再取得 DB086 售后履约快照。A412 为避免与商家禁用责任门形成逆序死锁,固定按 `DB104 售后幂等范围 → 无锁预读 DB061 不可变 assignedMerchantUserId(只定位门行)→ DB001 责任商家门 → DB061 订单并重检买家/商家归属 → DB086 既有申请与 DB062 目标订单项` 锁定;不得照搬 A307 的订单先锁顺序。两条路径都保持业务锁到提交,售后先提交时处理中申请阻断发货,发货先提交时 A412 按最新 `Shipped` 规则重判;已退款数量从可履约数量中扣除,部分退款只发剩余数量,全部退款不再发货。 +- 订单核心状态只保存 `PendingPayment/Paid/Shipped/Completed/Cancelled`。A302/A303/A305/A306 展示的履约、售后和评价状态是请求时派生的组合摘要,不得新增平行订单状态或持久化 `canShip`。 +- Ordering 在读取前开启短生命周期 `REPEATABLE READ READ ONLY` PostgreSQL 事务,先取得本页最多 50 张订单及订单项快照,再以同一 `DbConnection + DbTransaction` 分别调用一次 AfterSales 批量汇总和 Review 批量评价事实能力;默认 `READ COMMITTED` 的逐语句快照不满足该一致性边界。不得逐订单发 HTTP、跨模块直读 DbContext 或把缺失 Key 当零结果。依赖不可用或数量不变量破坏时响应显式 `Degraded`,相关摘要为空并关闭发货/评价等依赖动作,核心订单事实仍可安全展示。 - 买家只能确认本人已发货订单,确认成功后以状态条件把订单推进为已完成,并记录完成时间和“买家确认”方式。 - `Mall.Worker` 扫描发货满 7 天且仍为已发货的订单,以同一状态条件推进为已完成,并记录“自动完成”方式;演示环境可以缩短配置,但不改变正式规则。 - 买家确认与自动完成并发时只能有一个状态更新成功,重复请求返回已有结果,不重复生成通知。 @@ -350,28 +354,33 @@ sequenceDiagram - 领域事件在同一进程内表达领域事实,例如 `OrderPaidDomainEvent`。 - 只有跨进程需求才转换为集成事件,例如 `OrderPaidIntegrationEvent`。 -- 所有进入 M09 固定矩阵的成功业务事实都在来源事务内写入待发布事实;RabbitMQ 只是事务提交后的传输方式。Worker 成功发布后标记已处理,RabbitMQ 故障时 Outbox 保留并暂停投递,不能因消息中间件未启用而省略可靠事实。 +- 所有进入 M09 封闭 `MessagingSourceEventV1` 联合的成功业务事实都在来源事务内写入待发布事实;RabbitMQ 只是事务提交后的传输方式。Worker 成功发布后标记已处理,RabbitMQ 故障时 Outbox 保留并暂停投递,不能因消息中间件未启用而省略可靠事实。 - 消费者在业务事务内写入 Inbox/消费处理记录,并结合消息 ID 唯一约束或业务唯一约束防止重复处理。 - Outbox 负责可靠发布,Inbox 负责可靠消费;二者均不能替代订单、支付、消息等业务表上的最终唯一约束和状态条件。 - 第一条集成事件链确定为 `OrderPaidIntegrationEvent`:Payment 在支付事务成功后通过 Outbox 发布,Messaging 幂等消费并生成买家支付成功、商家待发货通知;不为普通 CRUD 广泛发布事件。 - Messaging 的接收人不是来源模块可任意填写的广播数组,而是由事件类型固定解析:订单创建/取消/发货/完成给买家,支付成功给买家和 `assignedMerchantUserId`,售后申请/退货说明给指定商家,售后审核/退款成功/确定退款失败给买家。支付失败、忽略回调、回调差异、对账差异及处置不生成站内消息。 -- Messaging 在同一事务中写 Inbox 处理结果和该事件全部必需接收人的消息;任一接收人不存在、角色或归属不符时整事件零消息并告警,不能部分成功。 +- Mall.Worker 中的 Messaging 消费者先校验封闭事件变体和规范 JSON 哈希,再在同一 PostgreSQL 事务写 DB103 Inbox、该事件全部 DB101 接收人消息,以及每条消息一条 DB102 `MessagingRealtimeHintRequestedV1`。任一接收人不存在、角色或归属不符时整事件零消息并告警;同 `eventId` 同哈希只重放既有结果,异哈希告警并死信当前冲突投递,不能部分成功。 +- 实时提示在消息 `createdAt + 60 秒` 过期。Outbox Publisher 发布到 RabbitMQ 共享提示队列,由任一 Mall.Api 的入口消费者竞争取得一份提示,再发布到版本化 Redis 频道 `eshop:{environment}:signalr:message-hints:v1`;每个运行中的 API 由自己的 `RealtimeFanoutHostedService` 各接收一次频道消息,只枚举、复核本实例连接并通过 `IHubContext.Clients.Client(connectionId)` 发送。Worker 不持有 WebSocket 连接,也不要求自定义 `HubLifetimeManager`。 +- Redis 发布失败且提示尚未过期时,入口消费者不得 ACK RabbitMQ 消息,必须保留同一提示重投;达到过期时间后确认丢弃。至少一次重复由客户端按 `messageId` 去重,实例离线、队列延迟或推送失败统一由 A501/A503 补查,不修改消息事实。 ### 7.5 图片存储 - 业务层依赖 `IObjectStorage` 一类最小接口。 - Infrastructure 通过 S3 兼容客户端访问对象存储;客户端库在实现阶段锁定,不在架构层绑定具体 SDK。 - 商品图片和评价晒图共用对象存储抽象,使用不同对象键前缀和权限规则。 -- 数据库只保存对象键、访问 URL、排序和媒体类型,不存图片二进制。 +- 数据库只保存对象键、必要媒体元数据和排序,不把可换域名的访问 URL 作为长期业务事实,也不存图片二进制。DB023 Attached/Detached 商品图与 DB025 Attached 评价图才是公开内容:API 以环境配置的只读媒体源和不可变对象键生成稳定 URL,不携带短效签名或上传凭据,保证 A102/A103 的 62 秒最坏缓存窗口内不会先于业务响应失效。媒体源只允许受控 Key 的 `GET/HEAD`,并以数据库引用状态为准拒绝 DB025 Uploading/Pending;对象写入仍只能经服务端。 - 上传接口按 M02、M06 和 M07 已确认的数量、类型、大小及尺寸规则校验;对象存储失败时不写入无效商品或评价图片记录。 +- 订单项保存下单时主图对象 Key 的强引用。商家移除仍被历史订单引用的图片时,图片只从当前图库转为 `Detached`,原图和缩略图继续保留;无历史引用时才在事务内登记对象清理责任。清理 Worker 删除前再次核查当前/归档图片与订单快照,依赖查询失败必须停止删除。 - SeaweedFS 在第二阶段商品图片和 X01 开发开始前启用;当前没有历史图片数据,不建设迁移流程。 ### 7.6 四项选做功能 - **评价晒图**:评价必须在上传和正式提交时分别校验当前用户的本人 `Completed` 订单项且尚未评价;创建时通过 Identity 公开应用契约取得自动用户名的安全脱敏快照,评价、图片关联和唯一资格原子提交并立即公开。评价不触发 C07 缓存失效或 M09 消息;公开列表不逐条跨模块查询用户资料。 -- **收藏/历史**:按用户隔离;收藏使用唯一约束防重,浏览历史对同一用户和商品更新最近时间。 -- **站内消息**:来源模块只提交已确认事件类型和业务标识,Messaging 按固定矩阵解析买家和订单指定商家;按事件 ID 做整事件幂等,全部消息同事务落 PostgreSQL 后再由 SignalR 发送轻提示。已读以消息稳定序列高水位和数据库状态为准,WebSocket 不累加权威未读数。 +- **收藏/历史**:按用户隔离;收藏使用唯一约束防重,浏览历史默认开启,对同一用户和商品更新最近时间,并在同一事务只保留最近 200 条。关闭记录只阻止未来写入且不隐藏旧历史;本期不提供清空历史。 +- **站内消息**:来源模块只提交封闭事件类型和真实业务归属,Messaging 按固定矩阵解析买家和订单指定商家;Mall.Worker 按 `eventId + 规范哈希` 做整事件幂等,并把 DB103、全部 DB101 消息和每消息一条 DB102 短期实时提示同事务提交。任一 API 入口消费者从共享 RabbitMQ 队列取得尚未过期的提示并发布到版本化 Redis 频道,各 API 的 `RealtimeFanoutHostedService` 只向本实例复核通过的 `connectionId` 发送;已读以消息稳定序列高水位和数据库状态为准,WebSocket 不累加权威未读数。 - **售后流程**:建立独立售后状态机,按订单项数量占用申请资格并按实付金额退款;`Paid`、`Shipped` 或完成后 7 天内允许申请。发货前读取非终态申请和已退款数量,退款经 Payment 的稳定退款操作幂等退回小金库并纳入 C08 对账;普通库存回补触发 C07,秒杀库存回原活动,订单核心状态不因售后被覆盖。 +- **退款恢复**:Unknown 永远核实原 `refundAttemptId + executionToken`,不得开启第二笔退款;确定失败按固定映射进入自动重试、商家重试或系统关注。A419/Worker 只提交后继尝试意图,由 RefundOrchestrator 调用 Payment 创建 DB093。自动重试最多三次并按 30 秒、2 分钟、10 分钟退避,商家只可在 60 秒冷却后重试 `MerchantRetryRequired`。 +- **退款围栏**:Initial、AutomaticRetry、ManualRetry 的真实执行租约统一为首次 60 秒、每 20 秒按 `refundAttemptId + executionToken + Executing` 条件续租,且绝对不超过 `startedAt+5 分钟`;到期后 Worker 原子收敛为 Unknown,旧执行者续租或提交影响 0 行后必须停止并重读。Worker 的核实/调度恢复租约使用独立 `recoveryLeaseToken`,同样为 60 秒、20 秒续租和首次取得后 5 分钟上限;执行租约与恢复租约不得共用 Token、字段或提交责任。 ### 7.7 C01 秒杀与防超卖 @@ -381,24 +390,26 @@ sequenceDiagram UPDATE 秒杀库存 SET available_stock = available_stock - quantity WHERE activity_id = @id + AND status = 'Ongoing' AND available_stock >= quantity - AND start_at <= now() - AND end_at > now(); + AND start_at <= @decisionTime + AND end_at > @decisionTime; ``` - 受影响行数为 1 才允许创建秒杀订单。 +- `@decisionTime` 必须在取得活动行锁后通过数据库 `clock_timestamp()` 读取一次,并在同一事务复用;不能使用应用服务器时间、客户端时间、事务开始时的 `now()` 快照或等待行锁前预取的时间。 - 发布活动时通过 Catalog 公开应用契约把普通库存原子划转为独立秒杀配额;取消活动时按本期规则保留已分配配额,不混回普通库存。 -- `Mall.Worker` 按数据库时间幂等推进 `Published → Ongoing → Ended`;秒杀下单同时校验状态和时间窗口。 +- `Mall.Worker` 按数据库时间幂等持久化 `Published → Ongoing → Ended`。公开 GET 只按同一次 `serverTime` 计算 `effectiveStatus`,不得产生状态写入;发布、取消和抢购命令锁定活动后追赶应有状态,再以同一 `decisionTime` 裁决。公开快照版本按 `storedResultVersion * 8 + effectivePhaseCode` 计算,阶段码固定为 `Draft=0、Published=1、Ongoing=2、Ended=3、Cancelled=4`,同时覆盖真实写入和只读时间阶段推进的单调性。 - 单用户限购由 `(activity_id, buyer_id)` 唯一配额事实和条件更新保证,取消成功按订单幂等释放,不以 Redis 或普通聚合查询承担并发正确性。 - 秒杀库存扣减、限购占用、Ordering 共享订单创建和必要 Outbox 写入处于同一受控事务;Seckill 不建立第二套订单状态机。 - Ordering 在秒杀共享订单创建中仍由 Identity 解析唯一启用默认商家并保存 `assignedMerchantUserId`;活动创建人不替代订单履约商家。 -- Redis 可用于活动热点读取和入口削峰,但不能成为唯一库存事实来源。 +- Redis 只可承接入口速率协调和非事实性技术状态,不能缓存或返回秒杀活动列表、状态、库存、售罄和限购事实;这些读取及全部抢购裁决都直接使用 PostgreSQL。 - 压测固定记录并发数、库存、成功/失败数、数据库最终库存和有效订单数,验证不超卖、不少卖。 ### 7.8 C03 订单超时自动取消 - 创建订单时写入固定 `paymentDeadline = createdAt + 当时配置期限`,正式配置为 30 分钟;历史订单不因后续配置变化重新计算。 -- `Mall.Worker` 使用 PostgreSQL 权威时间周期扫描已到固定 `paymentDeadline` 的待支付订单;演示环境只缩短新订单的配置值,不改变规则。 +- `Mall.Worker` 使用 PostgreSQL 权威时间周期扫描已到固定 `paymentDeadline` 的待支付候选订单;演示环境只缩短新订单的配置值,不改变规则。扫描时间只负责选候选,M04 锁定订单后重新取得 `decisionTime` 并最终复核。 - 多 Worker 使用批量领取/跳过已锁定记录或等价机制,取消时执行带 `PendingPayment` 条件的状态更新。 - 状态更新和库存回补同事务;普通订单回补 Catalog,秒杀订单回补原 Seckill 活动库存并释放限购额度。支付也必须带待支付状态条件,因此支付与取消竞争只能一方成功。 - Worker 重试安全,重复扫描不会重复回补库存。 @@ -414,27 +425,35 @@ WHERE activity_id = @id ### 7.10 C06 实时消息推送 - ASP.NET Core SignalR 只提供 PC Web WebSocket 通道并跳过协商,不启用 SSE、长轮询或会话亲和;JWT 用于连接身份认证。 -- PostgreSQL 站内消息表保存通知事实;SignalR 推送失败不回滚订单业务,也不丢失可查询消息。 -- Redis Backplane 在两个 API 实例之间传播 Hub 消息,保证用户连接落在不同实例时仍能接收。 +- PostgreSQL 站内消息表保存通知事实。Mall.Worker 的 Messaging 消费事务原子提交 DB103、全部 DB101 消息和每消息一条 DB102 实时提示 Outbox;事务内不调用 RabbitMQ、Redis 或 Hub。Outbox Publisher 把提示发布到一个 RabbitMQ 共享提示队列,由任一 Mall.Api 的入口消费者竞争消费一次,再发布到版本化 Redis 频道 `eshop:{environment}:signalr:message-hints:v1`。 +- 每个 API 都运行独立 `RealtimeFanoutHostedService` 并订阅该频道,因此每条成功发布的提示会被每个运行实例各收到一次。实例只枚举本地 `connectionId → userId/jti/tokenVersion/expiresAt/Abort` 登记,逐连接复核后调用 `IHubContext.Clients.Client(connectionId)`;禁止入口消费者直接调用 `Clients.User`,也禁止任何广播绕过远端实例自己的安全复核。本期使用应用级 Redis Pub/Sub,不要求自定义 `HubLifetimeManager`。 +- 实时提示固定在消息创建后 60 秒过期。入口消费者在 Redis 发布失败且提示未过期时不 ACK RabbitMQ 消息并重投同一提示;达到过期时间后确认丢弃。Redis Pub/Sub 不补发实例离线期间的提示,至少一次重复由客户端按 `messageId` 去重,过期、离线、队列延迟或本地推送失败统一由 A501/A503 补查,不回滚业务或消息。 - 前端重连节奏固定为立即、2 秒、5 秒、10 秒,四次失败后暂停;重连后先查询权威未读数,再按需查询列表/详情。多标签页各自维持连接,但消息和已读状态共享。 - SignalR 只发送消息标识和必要轻提示,客户端不得用每条推送直接 `badge + 1`;A503/M09 权威未读数负责校正,Redis 恢复也不重放历史推送。 -- 建连、重连和连接存续期间都校验账号状态与撤销事实。退出、JWT 到期、手机号修改、账号禁用、全部旧凭证失效或安全事实无法确认时,关闭既有连接。 +- Hub 固定 `CloseOnAuthenticationExpiration=true` 且 `ClockSkew=0`;`IUserIdProvider` 只接受恰好一个可解析为 UUID 的 `sub` 并返回小写 D 格式,客户端不能覆盖用户标识。每个 API 只登记本实例 `connectionId → userId/jti/tokenVersion/expiresAt/Abort`,本地登记不是唯一在线事实。 +- A003 仅在数据库 `decisionTime < JWT exp` 且首次建立 DB004 时写 `TokenRevoked` 安全失效 Outbox;裁决时已经自然到期则不写伪撤销事实或广播。A006、A016 的令牌版本事务写账号级安全失效 Outbox。Publisher 广播到每个运行 API 各自的排他自动删除队列;实例按 `jti` 或 `userId/tokenVersion` 立即 Abort 本地命中连接。广播只是加速器,每次推送前和最长每 30 秒仍复核有效期、撤销、账号状态和令牌版本,无法确认时失败关闭。 ### 7.11 C07 缓存与性能优化 - 只有固定首页商品摘要和商品自身公开详情使用 Cache-Aside。固定首页严格为 A102 无筛选、第一页 12 条、`OnSale`、`createdAt DESC, productId DESC`;A103 不含 M07 评价/评分。普通列表、分类、搜索/筛选、评价和秒杀活动事实全部直读 PostgreSQL。 -- 正常结果 TTL 60 秒、空结果 TTL 10 秒。跨实例只允许一个回填者;其他请求最多等待 500 ms,仍未命中就直读 PostgreSQL且不竞争回填;唯一回填者只有在 2 秒有效窗口内完成查询才可写入 Redis。 -- 商品公开字段、销售状态或普通库存事务提交后立即删除目标详情和受影响固定首页缓存,并在第 3 秒二次删除。两次删除都失败时,计入 2 秒回填窗后的最长旧值窗口为正常结果 62 秒、空结果 12 秒。 +- 正常结果 TTL 60 秒、空结果 TTL 10 秒。跨实例回填锁固定执行 Redis `SET <128-bit-random-token> NX PX 3000`,不续租;未取得者最多等待 500 ms,仍未命中就直读 PostgreSQL且不竞争回填。 +- 唯一回填者必须在取得锁后才开启新的短生命周期 `READ COMMITTED` 事务读取 PostgreSQL,不能复用等待锁前的旧事务或旧查询结果。只有查询在 2 秒回填窗口内完成且 Lua 比较确认锁值仍等于本 Token 时,才原子写缓存;释放同样用 Lua 比较 Token 后删除,锁丢失或超时只能返回数据库结果,不能覆盖后来者。 +- 每个会改变 C07 响应的源业务事务独立写入 Immediate 和 Delayed 两条 DB102 Outbox 责任:两者共享稳定 `operationId/invalidationBaseTime`,各有独立事件 ID 和幂等键。Immediate 使用 `fixed` 调度并立即到期;Delayed 使用 `after_commit_delay` 调度、固定 `delay_seconds=3`,来源事务内保持未武装且 `armed_at/available_at/next_attempt_at` 为空。Outbox 调度器只能扫描已提交可见的未武装行,以数据库 `clock_timestamp()` 在短事务内原子写 `armed_at`、`available_at=next_attempt_at=armed_at+3 秒`;武装回滚后原行可重扫,武装提交后普通到期扫描可接管。Immediate 消费成功与否不能创建、武装、取消或推迟 Delayed,调度器也不得事后创建 Delayed。 +- Immediate 与 Delayed 都删除目标详情和受影响固定首页缓存;DEL 遇到 Key 不存在视为成功,失败或结果未知时重投同一阶段。Delayed 最早不早于来源事务提交可见后 3 秒,调度延迟只会使其更晚。即使 Delayed 武装或两阶段删除持续失败,计入 2 秒回填窗后的最长旧值窗口仍为正常结果 62 秒、空结果 12 秒。 - 失效来源固定为商品名称、价格、普通库存、描述、分类展示、图片/主图/排序、销售状态和删除,以及普通订单扣减/取消回补、C01 发布时普通库存划转、M10 普通库存回补。C01 活动内部库存与原活动回补、M07 评价变化不触发 C07。 +- A102/A103 以及不进入 C07 的 A226 JSON 响应固定返回 `Cache-Control: no-store`;A227 因有效 Buyer Token 可增加本人限购字段,固定返回 `Cache-Control: private, no-store`。Nginx、CDN、浏览器和 Service Worker 不得建立第二层业务 JSON 缓存;只有不可变媒体 URL 可按媒体策略长期缓存。 - Redis 故障时公开查询直接回退 PostgreSQL;数据库始终为价格、库存、上下架和商品内容事实来源,F08/C01/M10 不读缓存做交易判断。 - 压测报告对比缓存启用前后 P50/P95、吞吐量、命中率和数据库查询次数。 ### 7.12 C08 支付回调幂等与对账 -- C08 是不扣小金库的受控 `SimulatedChannel`,不新增买家前台支付入口。回调使用 HMAC 验证来源,幂等身份为全局唯一 `callbackId + 请求指纹`;同一支付流水允许多个不同回调以表达先失败后成功或先成功后失败。 -- 回调终态固定为 `ProcessedSuccess`、`ProcessedFailure`、`Ignored`、`Difference`。只有服务端处理时间早于 `paymentDeadline` 且订单仍为 `PendingPayment` 的成功回调,才原子写模拟通道支付事实、订单 `Paid`、回调终态和 Outbox;不扣 Wallet。 +- C08 是不扣小金库的受控 `SimulatedChannel`,不新增买家前台支付入口。回调使用 HMAC 验证来源,幂等身份为全局唯一 `callbackId + 请求指纹`;同一支付流水允许多个不同回调以表达先失败后成功或先成功后失败。Nginx 回调 location、Kestrel 端点和应用流式读取统一限制原始 UTF-8 Body 为 16384 字节,超限在 HMAC、JSON 和数据库前返回 413;三类签名头使用唯一 ASCII 格式/长度,金额严格处于 `numeric(18,2)` 正数范围且禁止指数。 +- 回调终态固定为 `ProcessedSuccess`、`ProcessedFailure`、`Ignored`、`Difference`。处理器先取得回调聚合、订单、既有支付等全部共享事实,最后取得财务水位锁并立即生成唯一 `finalTime`;只有此时仍早于 `paymentDeadline` 且订单为 `PendingPayment` 的成功回调,才原子写模拟通道支付事实、订单 `Paid`、回调终态和 Outbox。生成后不再取新共享锁或调用外部服务,也不扣 Wallet。 - 订单状态机拒绝迟到或逆序更新;金额、币种、订单关联不一致、订单不存在,以及 `Cancelled` 或其他通道成功后的迟到成功都保存为完整 `Difference` 来源事实,不改订单也不伪造成功支付记录。 -- 回调接收只保存来源终态;`Mall.Worker` 按固定 UTC 范围和稳定水位生成每日对账批次,统一聚合支付、订单、售后退款与钱包入账事实,输出 `Matched`、`HasDifferences` 或 `Resolved`。 +- 回调接收只保存来源终态;`Mall.Worker` 固定在每日 `00:05Z` 触发、每分钟检查漏跑,并在进程启动时立即补查。三种入口都使用 `jobName=PaymentDailyReconciliation、runKey=YYYY-MM-DD` 的同一日期责任,只处理已经到达次日 00:05 的完整 UTC 日。 +- Worker 从最早缺失日期开始按升序逐日处理,失败日复用同一 DB107 行持续有界退避并阻断后续日期,不允许永久 DeadLetter 闭锁日期链。每个日期先取得独立财务 `watermarkSequence/watermarkAt`,再开启新的 `REPEATABLE READ` 只读快照;无交易日也生成空 `Matched` 批次,既有 `Matched/HasDifferences/Resolved` 不重开或被规则升级静默改写。 +- 唯一财务 `postingSequence` 是比较锚点;同一原子结果按 `RefundOperation > Payment > Callback` 选择锚点类型。`totalCount` 统计去重锚点,`matchedCount` 统计零差异锚点,`differenceCount` 统计至少命中一条规则的锚点,固定满足 `totalCount=matchedCount+differenceCount`;同一锚点命中多条规则只让 `differenceCount` 加一。 +- `differenceCountsByType` 按实际差异行分组,不按锚点去重,因此其合计可以大于 `differenceCount`;同一锚点、规则码和规则版本只形成一条差异并合并证据,不同规则不得互相覆盖。 - 差异必须可稳定分页和查看完整证据,由管理员领取、释放/接管、选择受控处置、引用证据,并在关闭前重新读取权威事实、重跑原比较规则。只有当前有效领取人可关闭;最后一条差异和批次 `Resolved` 原子提交,仅填写说明不能关闭。 - 验收脚本随机重复并打乱回调顺序,验证最终状态和对账差异。 @@ -442,18 +461,21 @@ WHERE activity_id = @id - Docker Compose 定义 Nginx、Vue 静态站点、2 个 Mall.Api、一次性 Migrator、Mall.Worker、PostgreSQL、Redis、RabbitMQ 和 SeaweedFS。 - 同一版本只允许 Migrator 执行 Migration:PostgreSQL 就绪后运行 Migrator,成功后才启动 API/Worker;API 和 Worker 禁止并发自动迁移。目标 Migration 失败或运行版本不兼容时,应用不得进入就绪。 -- 两个 API 镜像和配置一致,不使用本地内存 Session;JWT 验签配置一致,失效记录和 SignalR Backplane 共享 Redis。 +- 两个 API 镜像和配置一致,不使用本地内存 Session。每个实例分别从 HTTP Bearer 与 SignalR Hub 的**实际生效配置**生成同一脱敏认证摘要:`Issuer`、`Audience`、验签材料 `keyFingerprint`、`accessTokenLifetimeSeconds`、`clockSkewSeconds=0`、`tokenVersionValidationRule`;按固定属性名和规范 JSON 执行 SHA-256。`keyFingerprint` 只使用非秘密 KeyId 或验签公钥指纹,摘要不得包含原始 Secret、私钥或对称密钥哈希。 +- Compose 注入同版本期望摘要,A507 要求本实例 HTTP 摘要、Hub 摘要和期望摘要三者相等;部署门再要求两个 API 返回相同摘要。任一实例/Hub 不一致时 C10 全局为 `NotReady`,Nginx 不开放双实例业务流量,不能把错误实例仅标成能力降级。DB001 的账号状态/令牌版本与 DB004 的单令牌撤销记录是 PostgreSQL 权威事实;Redis 只保存可由这些事实重建的安全镜像,并承载版本化实时提示 Pub/Sub 基础设施。 - Nginx 负责 API 负载均衡和 WebSocket Upgrade;Health Check 不通过的实例不应继续接收新请求。SignalR 使用 WebSockets-only + skip negotiation,不依赖会话亲和。 -- 全局就绪只检查安全配置、运行版本兼容、目标 Migration 匹配和 PostgreSQL;Redis、RabbitMQ、SeaweedFS 作为能力级状态暴露,单项故障不直接把整个 API 判为未就绪。 +- 全局就绪检查认证配置摘要一致、安全配置完整、运行版本兼容、目标 Migration 匹配和 PostgreSQL;Redis、RabbitMQ、SeaweedFS 作为能力级状态暴露,单项故障不直接把整个 API 判为未就绪。 - Redis 故障时公开缓存回退 PostgreSQL、实时推送关闭、无法确认令牌撤销/账号禁用/手机号变更/旧凭证失效的受保护 HTTP 与 Hub 请求失败关闭;只有撤销事实重建且安全健康通过后才恢复受保护能力。RabbitMQ 故障时 Outbox 保留且投递暂停;SeaweedFS 故障时对象写入失败,其他能力继续。 - 镜像使用 Commit SHA/版本 Tag,不只使用 `latest`;Secret 通过环境变量或受控文件注入。 - 验收演示包含请求分布证明、停止一个 API 实例后的可用性和登录态连续性。 -- 单 API 停止顺序为:Nginx 停止向目标实例转发 → 目标实例有界排空 → 关闭该实例 Hub 并刷新日志 → 仅目标 API 停止;Worker 和共享依赖继续。整套停止时再依次停止全部新流量、排空 API、关闭 Hub、停止 Worker 领取并完成或安全释放任务、刷新遥测、停止应用和共享依赖,始终保留数据卷。 +- 单 API 停止顺序为:Nginx 停止向目标实例转发 → 目标实例停止实时提示/安全广播消费并有界排空 → 通过本地连接登记逐一服务端 Abort Hub 连接并刷新日志 → 仅目标 API 停止;Worker 和共享依赖继续。整套停止时再依次停止全部新流量、排空 API、关闭所有本地 Hub、停止 Worker 领取并完成或安全释放任务、刷新遥测、停止应用和共享依赖,始终保留数据卷。 ## 八、安全设计 - 密码采用 ASP.NET Core PasswordHasher 或等价可靠算法,不自行实现加密。 -- JWT 包含用户 ID、角色、`jti` 和过期时间,不包含密码或敏感资料;退出时把 `jti` 写入 Redis 至令牌自然过期。 +- JWT 包含用户 ID、角色、`jti`、账号令牌版本和过期时间,不包含密码或敏感资料。A003 进入处理前按 `ClockSkew=0` 校验自然有效期,随后在 PostgreSQL 事务只取得一次 `decisionTime`:早于 `exp` 时以 `jti` 幂等写 DB004 和安全 Outbox,`revokedAt=decisionTime`、`expiresAt=exp`;达到 `exp` 时按自然失效确定收敛,不写 DB004、Outbox 或 Redis 镜像。有效期内撤销事务提交后再写入/刷新 Redis 撤销镜像;Redis 写入失败不得撤销已经提交的退出结果,也不得把 Redis 当成唯一撤销事实。 +- JWT 有效期验证统一 `ClockSkew=0`,达到 `exp` 立即失效;HTTP、Hub 和两个 API 的脱敏认证配置摘要必须一致。Hub 固定启用 `CloseOnAuthenticationExpiration=true`,配置摘要不一致属于全局未就绪,不得用 DB004 延长留存或 Redis 镜像掩盖漂移。 +- 受保护 HTTP 请求与 Hub 连接先完成签名、有效期与账号身份校验,再依据 DB001/DB004 的安全水位校验令牌版本和撤销状态;Redis 镜像命中可快速拒绝,未命中只有在镜像已完成权威重建且安全健康通过时才能作为安全否定。Redis 重启、分区或镜像水位未知时失败关闭,由 PostgreSQL 重建有效期内撤销事实后再恢复。 - 用户禁用或修改手机号后,原有登录凭证立即失效且各 API 实例结果一致;重新启用账号不会恢复旧凭证。 - 令牌撤销、账号禁用、手机号变更或全部旧凭证失效事实无法安全确认时,受保护 HTTP 请求和 Hub 连接必须失败关闭;Redis 仅恢复连通但安全事实尚未重建时仍不得放行。 - Policy 至少包括 `BuyerOnly`、`MerchantOnly`、`AdminOnly`。 @@ -485,9 +507,9 @@ WHERE activity_id = @id | 端点 | 用途 | 检查内容 | |---|---|---| | `/health/live` | 存活检查 | 进程能够响应 | -| `/health/ready` | 全局就绪检查 | 安全配置有效、运行版本兼容、目标 Migration 匹配、PostgreSQL 可用 | +| `/health/ready` | 实例就绪检查与部署门证据 | 本实例 HTTP/Hub 摘要与部署预期摘要一致、安全配置有效、运行版本兼容、目标 Migration 匹配、PostgreSQL 可用;部署门再比较两个实例的摘要 | -Redis、RabbitMQ 和 SeaweedFS 不决定全局 200/503,而在响应中按能力报告:C07 `fallback`、C06 `disabled`、受保护鉴权 `failClosed`、Outbox 投递 `paused`、对象写入 `disabled`。Redis 基础连通恢复与撤销事实重建/安全健康恢复必须分开表达;后者完成前受保护能力和实时连接不能恢复。 +A507 固定返回 `authenticationConfiguration { digestAlgorithm, expectedDigest, httpDigest, hubDigest, matchesExpected }`,不返回任何原始认证配置。`matchesExpected` 只表达本实例三份摘要是否相同;任一摘要缺失、格式错误或不相同时,本实例返回 503/`NotReady`。两个实例是否一致不伪装成单实例响应字段,部署门必须分别读取两个 A507 响应并确认两边的 `expectedDigest/httpDigest/hubDigest` 全部相同后,才开放双实例业务流量。Redis、RabbitMQ 和 SeaweedFS 不决定全局 200/503,而在响应中按能力报告:C07 `fallback`、C06 `disabled`、受保护鉴权 `failClosed`、Outbox 投递 `paused`、对象写入 `disabled`。Redis 基础连通恢复与撤销事实重建/安全健康恢复必须分开表达;后者完成前受保护能力和实时连接不能恢复。 ## 十一、环境与部署 -- Gitee From 8fbd0141a636814927bd82b48d9924288e1af86a Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Sat, 25 Jul 2026 16:58:35 +0800 Subject: [PATCH 114/118] =?UTF-8?q?docs(rules):=20=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E8=AE=BE=E8=AE=A1=E4=BA=8B=E5=AE=9E=E6=BA=90?= =?UTF-8?q?=EF=BC=9B=E6=9B=B4=E6=96=B0=E6=8E=A5=E5=8F=A3=E4=B8=8E=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../document-routing.reference.md | 25 +++++++++---------- .../eshop-align-docs.SKILL.md | 2 +- .../eshop-project-workflow.SKILL.md | 2 +- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/eshop-project-rules-upload/document-routing.reference.md b/eshop-project-rules-upload/document-routing.reference.md index 9dc41e2..e449c95 100644 --- a/eshop-project-rules-upload/document-routing.reference.md +++ b/eshop-project-rules-upload/document-routing.reference.md @@ -198,19 +198,18 @@ 4. `五、汇总审计与冻结条件` 中目标负责人的缺口、冲突和冻结阻塞项。 5. 实际 OpenAPI、Endpoint、DTO、调用方和测试。 -只有接口清单、没有详细 Axxx 时标记为 `部分定义`;字段、失败响应和业务规则未确认前不得直接编码。当前六份个人原稿已保存在 `docs/02-设计文档/interface/`,主接口文档保留 107 个不重复 Axxx 追踪编号,其中 103 个为有效 HTTP 契约;A229、A230、A418、A431 均为已取消历史编号。整体仍为“部分定义,未冻结”:需继续完成数据库反查、真实 OpenAPI、跨模块公开契约和交叉评审。每次任务开始时重新检查,不永久假设此状态。 +只有接口清单、没有详细 Axxx 时标记为 `部分定义`;字段、失败响应和业务规则未确认前不得直接编码。当前六份个人原稿已保存在 `docs/02-设计文档/interface/`,仅作贡献追踪;主接口文档保留 109 个不重复 Axxx 追踪编号,其中 99 个为活动 HTTP 详细定义,A005、A009、A023、A144、A229、A230、A418、A431、A432、A433 为 10 个已取消历史编号。当前整体为“完整定义,数据库设计已确认,待真实 OpenAPI、实现、测试和交叉评审,尚未冻结”;活动定义不等于已实现或已冻结。每次任务开始时重新检查,不永久假设此状态。 ### 6.6 `docs/02-设计文档/数据库设计.md` -读取设计说明、DBxxx 分工、个人原稿规则、单表模板、统一表清单、跨模块关系、ER 图和冻结条件。并行设计阶段读取目标负责人的 `docs/02-设计文档/database/database-<姓名拼音首字母>.md`;汇总完成后以主文档中的统一表清单和完整定义为事实源,再核对实际实体、映射、DbContext、Migration、约束、索引与 Seed。 +读取设计说明、44 表统一清单、目标表完整定义、跨模块关系、ER 图、事务与恢复协议和实现前检查。数据库不按成员拆分,不创建、读取或等待 `database-<姓名拼音首字母>.md`;始终以主文档为唯一设计事实源,再核对实际实体、映射、DbContext、Migration、约束、索引与 Seed。 -当前已建立 `docs/02-设计文档/database/` 原稿目录,但六份个人原稿尚未创建,主文档仍为“模板/占位,未冻结”。因此: +当前主文档已从需求与流程派生并完整定义 44 张表,数据库设计已确认,但尚未创建真实实体、DbContext、Migration、初始化和数据库测试。因此: -- 不能把标题当作完整表定义。 -- 不能从需求直接猜字段、类型或状态码。 -- 表设计不完整时先补齐并评审,再实现 Migration。 -- 个人原稿汇总后继续保留,但不能覆盖主文档。 -- 数据库文档与真实 Migration 冲突时明确记录偏离。 +- 实现前读取目标表全部字段、约束、索引、关系、状态、事务和恢复规则,不能只看表标题或追踪表。 +- 不得从接口清单、旧实现或个人草稿反向猜字段、类型或状态码。 +- 只有主文档标记为已确认的表才可实现 Migration;设计已确认不等于 Migration 已存在或已验证。 +- 数据库文档与真实实体、Migration 或测试冲突时明确记录偏离,先按需求和流程判断应修设计还是修实现。 ### 6.7 `docs/03-测试文档/` @@ -232,16 +231,16 @@ | 模块 | 需求定位 | 架构重点 | 常见关联 | |---|---|---|---| | M00 公共基建 | M00、需求 8/9 | 架构 4/5/10/11/14/15 | 全模块组合根、公共契约 | -| M01 用户鉴权 | M01-01~03、M06-03、M08 | 架构 5/8/9 | A001~A025 已登记,账号状态、Policy、令牌撤销 | -| M02 商品 | M02-01/02、M06-01、M07、C04 | 架构 6/7.5/7.6/7.9/7.11 | A101~A103、A110~A114、A120~A128、A140~A144 已登记,图片与订单项协作 | +| M01 用户鉴权 | M01-01~03、M06-03、M08 | 架构 5/8/9 | 活动 A001~A004、A006~A008、A010~A022、A024、A025;A005/A009/A023 已取消;账号状态、Policy、令牌撤销 | +| M02 商品 | M02-01/02、M06-01、M07、C04 | 架构 6/7.5/7.6/7.9/7.11 | 活动 A101~A103、A110~A115、A120~A128、A140~A143;A144 已取消;图片与订单项协作 | | M03 购物车 | M03-01、C01 | 架构 7.1/7.7 | A201~A208、A220~A228 已登记;秒杀绕过购物车;A229/A230 已取消并复用 A302/A303;活动状态由 Worker 推进 | | M04 订单 | M04-01~04、M06-02、C03 | 架构 7.1/7.3/7.8 | A301~A308 已登记;按原通道回补库存;`assignedMerchantUserId` 控制商家范围;超时取消与自动完成由 Worker 执行 | -| M05 支付 | M05-01、C08 | 架构 7.2/7.12 | A401~A408、A421~A425、A432/A433;退款入账使用 Payment 应用契约,A431 已取消;每日对账由 Worker 生成批次 | +| M05 支付 | M05-01、C08 | 架构 7.2/7.12 | 活动 A401~A408、A421~A426;A431~A433 已取消;退款入账使用 Payment 应用契约,每日对账由 Worker 生成批次 | | M06 后台 | M06-01~03 | 架构 5/6/8 | 商品、订单、账号各自主责 | | M07 评价 | M07 | 架构 7.5/7.6 | 订单项、对象存储 | | M08 收藏历史 | M08 | 架构 7.6 | 用户隔离、商品引用 | -| M09 消息 | M09、C06 | 架构 7.4/7.6/7.10 | A501~A505 已登记,来源事件明确 `recipients[]`,PostgreSQL 持久化后再经 SignalR 推送 | -| M10 售后 | M10、C08 | 架构 7.6/7.12 | A411~A417、A419、A432~A434;A418/A431 已取消,退款使用 Payment 应用契约 | +| M09 消息 | M09、C06 | 架构 7.4/7.6/7.10 | A501~A505;来源只携带稳定业务归属,Messaging 按固定矩阵整事件派生接收人,PostgreSQL 持久化后再经 SignalR 推送 | +| M10 售后 | M10、C08 | 架构 7.6/7.12 | 活动 A411~A417、A419、A434;A418/A431~A433 已取消;退款使用 Payment 应用契约 | | C07 缓存 | C07 | 架构 7.11 | Catalog 规则与 M00 基础设施 | | C10 部署 | C10 | 架构 7.13/10/11/12/15 | A506/A507 已登记,Compose、Nginx、多实例 | diff --git a/eshop-project-rules-upload/eshop-align-docs.SKILL.md b/eshop-project-rules-upload/eshop-align-docs.SKILL.md index 9d83f5b..9d087a1 100644 --- a/eshop-project-rules-upload/eshop-align-docs.SKILL.md +++ b/eshop-project-rules-upload/eshop-align-docs.SKILL.md @@ -55,7 +55,7 @@ description: 维护 E-Shop 教师要求、需求、架构、数据库、接口 ## 维护数据库汇总 - `docs/02-设计文档/数据库设计.md` 是实体、映射、Migration、初始化和测试的唯一数据库设计事实源。 -- 六份 `docs/02-设计文档/database/database-<姓名拼音首字母>.md` 作为个人贡献原稿保留;并行编写阶段只改个人原稿,统一汇总后再同步主文档。 +- 数据库由统一设计者直接维护主文档,不创建、不读取、不等待个人数据库原稿;当前主文档的表设计仍需与需求、流程、接口及真实实体、Migration、Seed 和测试交叉核对。 - 汇总时检查 DBxxx、表名、约束名、索引名、数据所有权和跨模块关系;缺少字段、约束、索引、状态或评审的表不得标记为“已确认”。 ## 建立一致性追踪 diff --git a/eshop-project-rules-upload/eshop-project-workflow.SKILL.md b/eshop-project-rules-upload/eshop-project-workflow.SKILL.md index b3f98a1..d7552e8 100644 --- a/eshop-project-rules-upload/eshop-project-workflow.SKILL.md +++ b/eshop-project-rules-upload/eshop-project-workflow.SKILL.md @@ -31,7 +31,7 @@ description: 作为 E-Shop 仓库所有任务的项目级总入口,统一完 5. 同一事实冲突时,按教师基线、用户当前确认、真实实现与验证证据、已确认设计、模板与计划的顺序判断。 6. 新增功能或改变业务行为时,先由需求确认角色、规则和验收,再在 `docs/02-设计文档/process/` 确认业务流程、状态、异常和模块出入口;接口、数据库和架构从流程派生,不能按现有 Axxx 或 DBxxx 反向拼接流程。 7. 业务流程确认后,接口任务以 `docs/02-设计文档/接口设计.md` 为唯一实施契约;“接口先行”只表示先于代码和调用方修改。`docs/02-设计文档/interface/` 中的个人原稿只用于贡献追踪,修改后必须同步总文档。 -8. 数据库任务以 `docs/02-设计文档/数据库设计.md` 为唯一实施契约;`docs/02-设计文档/database/` 中的个人原稿用于并行设计和评审,未确认表不得生成 Migration。 +8. 数据库任务只以 `docs/02-设计文档/数据库设计.md` 为唯一实施契约;本项目不再创建、读取或等待个人数据库原稿。只有主文档标记为已确认且完成实现前复核的表才能生成 Migration。 ## 选择专项 Skill -- Gitee From 4f65e8e9e265246567d3d547e5b1bb11fec80093 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Sat, 25 Jul 2026 18:40:11 +0800 Subject: [PATCH 115/118] =?UTF-8?q?docs(rules):=20=E5=86=BB=E7=BB=93?= =?UTF-8?q?=E5=9B=9B=E7=B1=BB=E8=AE=BE=E8=AE=A1=E5=9F=BA=E7=BA=BF=EF=BC=9B?= =?UTF-8?q?=E5=BD=92=E6=A1=A3=E6=8E=A5=E5=8F=A3=E4=B8=BB=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=B9=B6=E7=BB=9F=E4=B8=80=E5=AE=9E=E6=96=BD=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...17\344\275\234\346\265\201\347\250\213.md" | 2 +- .../interface/interface-gxy.md" | 4 +- .../interface/interface-lhc.md" | 6 +- .../interface/interface-tyh.md" | 4 +- .../interface/interface-wqq.md" | 4 +- .../interface/interface-zhh.md" | 4 +- .../interface/interface-zhy.md" | 8 +- ...45\345\217\243\350\256\276\350\256\241.md" | 30 +++---- ...01\347\250\213\350\256\276\350\256\241.md" | 2 +- ...75\345\220\215\350\247\204\350\214\203.md" | 4 +- eshop-project-rules-upload/AGENTS.md | 58 ++++++++------ .../document-routing.reference.md | 79 +++++++++++-------- .../eshop-align-docs.SKILL.md | 50 ++++++------ .../eshop-align-docs.openai.yaml | 4 +- .../eshop-deliver-feature.SKILL.md | 29 +++---- .../eshop-deliver-feature.openai.yaml | 2 +- .../eshop-fix-bug.SKILL.md | 9 ++- .../eshop-fix-bug.openai.yaml | 2 +- .../eshop-project-workflow.SKILL.md | 16 ++-- .../eshop-project-workflow.openai.yaml | 2 +- .../eshop-verify-acceptance.SKILL.md | 14 ++-- .../eshop-verify-acceptance.openai.yaml | 2 +- 22 files changed, 184 insertions(+), 151 deletions(-) rename "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" => "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/\346\216\245\345\217\243\350\256\276\350\256\241.md" (99%) diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/Git\345\233\242\351\230\237\345\215\217\344\275\234\346\265\201\347\250\213.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/Git\345\233\242\351\230\237\345\215\217\344\275\234\346\265\201\347\250\213.md" index 7a22cd2..8b5a98c 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/Git\345\233\242\351\230\237\345\215\217\344\275\234\346\265\201\347\250\213.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/Git\345\233\242\351\230\237\345\215\217\344\275\234\346\265\201\347\250\213.md" @@ -190,7 +190,7 @@ feature/week-two-work # 按时间而不是任务划分 - 预计修改的数据库、接口、页面和测试。 - 是否影响公共文件、配置或其他模块。 -接口发生变化时,必须先更新 `docs/02-设计文档/接口设计.md`,再修改代码。 +实现接口时,必须以冻结的 `docs/02-设计文档/interface/接口设计.md` 为准;发现契约冲突时停止并报告,不得先改接口基线再迁就代码。 ### 2. 检查本地工作区 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" index 5e7c597..66dfe82 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-gxy.md" @@ -1,13 +1,13 @@ # 接口设计(顾欣月)— Catalog、Review -> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`../接口设计.md`](../接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 +> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`接口设计.md`](接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 > 组别:24级1班第7组 编写人:顾欣月 编写日期:2026-07-24 版本:v0.1 > 编号区间:`A101`~`A200` 负责模块:Catalog(商品目录)、Review(评价) ## 一、说明与约定引用 -- 本文件是《[接口设计.md](../接口设计.md)》第二章要求的个人协作文件,只登记本人 `A101`~`A200` 区间和本人负责模块的 HTTP 接口。汇总后继续保留用于贡献与评审追踪;实现、OpenAPI 和联调一律以总《接口设计》为准。 +- 本文件是《[接口设计.md](接口设计.md)》的历史个人协作文件,只登记本人 `A101`~`A200` 区间和本人负责模块的 HTTP 接口。实现、OpenAPI 和联调一律以总《接口设计》为准。 - 通用约定(前缀、鉴权、成功/失败响应包装、ProblemDetails、分页、幂等、状态码等)一律以总《接口设计》第一章为准,本文件不重复,只在接口内标注差异。 - 命名(模块词根、路径、`operationId`、Schema、错误码、字段大小写)以《[命名规范.md](../命名规范.md)》为准:Catalog 词根 `categories`/`products`/`product_images`,Review 词根 `reviews`/`review_images`。 - 状态枚举对外统一使用英文 `PascalCase`(规范 2.2),不暴露整数序号;数据库落库使用 `lower_snake_case`,二者映射见第六章枚举附录。本文件所有 `status`、`stockStatus`、`reason` 字段值均为对外 PascalCase。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" index 92a416d..164190b 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-lhc.md" @@ -1,6 +1,6 @@ # 罗皓晨接口设计 -> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`../接口设计.md`](../接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 +> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`接口设计.md`](接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 > 负责人:罗皓晨 > 负责范围:M00 公共基建与集成、M09 站内消息通知(X03)、C06 实时消息推送、C07 缓存与性能优化、C10 容器化部署与负载均衡 @@ -11,7 +11,7 @@ ## 一、范围与设计结论 -本文件只登记罗皓晨负责的 Messaging HTTP 接口和 M00 公共健康检查接口。全部接口遵循[《接口设计》](../接口设计.md)第一章通用约定;本文未重复定义的认证、响应包装、ProblemDetails、分页和安全规则均以该文档为准。汇总后本文件继续保留用于贡献与评审追踪,但不得覆盖总文档中的最终契约。 +本文件只登记罗皓晨负责的 Messaging HTTP 接口和 M00 公共健康检查接口。全部接口遵循[《接口设计》](接口设计.md)第一章通用约定;本文未重复定义的认证、响应包装、ProblemDetails、分页和安全规则均以该文档为准。汇总后本文件继续保留用于贡献与评审追踪,但不得覆盖总文档中的最终契约。 本轮范围结论: @@ -723,7 +723,7 @@ Ordering、Payment 和 AfterSales 使用统一 `MessagingSourceEventV1` 信封 |---|---:|---| | `MESSAGE.NOT_FOUND` | 404 | 消息不存在或不属于当前用户 | -认证、验证、限流、依赖不可用和未知错误复用[《接口设计》](../接口设计.md)第一章登记的通用错误码,不创建同义错误码。 +认证、验证、限流、依赖不可用和未知错误复用[《接口设计》](接口设计.md)第一章登记的通用错误码,不创建同义错误码。 ## 七、跨模块影响与待确认项 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" index a2a8836..6c64513 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-tyh.md" @@ -1,12 +1,12 @@ # 个人接口文件 — 唐宇昊(Identity、Engagement) -> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`../接口设计.md`](../接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 +> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`接口设计.md`](接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 > 组别:24级1班第7组 负责人:唐宇昊(tyh) 接口编号区间:`A001`~`A100` > 负责模块:Identity(注册、登录退出、JWT、用户资料、收货地址、后台账号治理)、Engagement(收藏、浏览历史) > 关联教师验收编号:F01、F02、F03、F13、X02 > 当前状态:部分定义;清单与详细定义已完成,待数据库、OpenAPI 和交叉评审 -> 本文件为协作与评审追踪材料;汇总后继续保留,实现、OpenAPI 和联调以 `../接口设计.md` 为准 +> 本文件为协作与评审追踪材料;汇总后继续保留,实现、OpenAPI 和联调以 `接口设计.md` 为准 ## 一、接口清单 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" index bea6026..40ba298 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-wqq.md" @@ -1,13 +1,13 @@ # 韦乾强 - 订单模块接口详细定义 -> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`../接口设计.md`](../接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 +> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`接口设计.md`](接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 > 负责人:韦乾强 > 模块:Ordering(含买家订单与商家订单管理) > 接口编号范围:A301~A308 > 编写日期:2026-07-24 > 版本:v0.1 -> 本文件为协作与评审追踪材料;汇总后继续保留,实现、OpenAPI 和联调以 `../接口设计.md` 为准 +> 本文件为协作与评审追踪材料;汇总后继续保留,实现、OpenAPI 和联调以 `接口设计.md` 为准 ## 接口清单 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" index 6a23b64..be5d7b3 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhh.md" @@ -1,12 +1,12 @@ # 个人接口文件 — 朱惠惠(Cart、Seckill) -> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`../接口设计.md`](../接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 +> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`接口设计.md`](接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 > 组别:24级1班第7组 负责人:朱惠惠(zhh) 接口编号区间:`A201`~`A300` > 负责模块:Cart(购物车 CRUD、选中、结算预览、清空)、Seckill(商家活动维护、买家抢购下单;订单查询复用 Ordering 的 A302/A303) > 关联教师验收编号:F07、C01 > 当前状态:部分定义;清单与详细定义已完成,待数据库、OpenAPI 和交叉评审 -> 本文件为协作与评审追踪材料;汇总后继续保留,实现、OpenAPI 和联调以 `../接口设计.md` 为准 +> 本文件为协作与评审追踪材料;汇总后继续保留,实现、OpenAPI 和联调以 `接口设计.md` 为准 ## 一、接口清单 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" index d90c732..00512fb 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/interface-zhy.md" @@ -1,14 +1,14 @@ # 张海洋个人接口文件(A401-A500) -> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`../接口设计.md`](../接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 +> **文档定位**:本文件是历史贡献快照,仅用于贡献与交叉评审追踪,不是开发、OpenAPI、联调或数据库实现依据。完整接口契约只以 [`接口设计.md`](接口设计.md) 为准,完整数据库设计只以 [`../数据库设计.md`](../数据库设计.md) 为准;本文遗留的旧字段、旧表名、旧 DBxxx 或成熟度描述均不得覆盖两个主文档,本项目不再维护个人数据库原稿。 > **模块**:Payment(含支付对账) / AfterSales > **负责人**:张海洋(zhy) > **范围**:M05-01 模拟支付 + M10 售后流程 + C08 支付回调幂等与对账 > **创建日期**:2026-07-24 > **版本**:v0.1 -> **当前状态**:已汇总到主文档,个人文件继续保留用于贡献与评审追踪;实现、OpenAPI 和联调以 `../接口设计.md` 为准 -> **关联规范**:`docs/02-设计文档/接口设计.md` v0.1 + `docs/02-设计文档/命名规范.md` + `docs/02-设计文档/Git团队协作流程.md` +> **当前状态**:已汇总到主文档,个人文件继续保留用于贡献与评审追踪;实现、OpenAPI 和联调以 `接口设计.md` 为准 +> **关联规范**:`docs/02-设计文档/interface/接口设计.md` + `docs/02-设计文档/命名规范.md` + `docs/02-设计文档/Git团队协作流程.md` > **关联根命名空间**:`Mall.Modules.Payment`、`Mall.Modules.AfterSales` > **关联数据库**:DB081~DB100(**待评审**:`docs/02-设计文档/database/database-zhy.md` 尚未创建,本文档字段暂时按命名规范推断) @@ -2245,6 +2245,6 @@ RefundDetailResponse { ### 4.3 汇总时机 -- 全部接口评审通过后由罗皓晨按编号汇总到 `docs/02-设计文档/接口设计.md` 第三章 +- 本文件仅保留历史贡献记录;实现统一读取 `docs/02-设计文档/interface/接口设计.md` 第三章 - 个人文件 `interface-zhy.md` 汇总后继续保留,不单独作为实现事实源 - 历史贡献通过 Git 记录保留 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/\346\216\245\345\217\243\350\256\276\350\256\241.md" similarity index 99% rename from "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" rename to "docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/\346\216\245\345\217\243\350\256\276\350\256\241.md" index 7c3034a..aab4c42 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/interface/\346\216\245\345\217\243\350\256\276\350\256\241.md" @@ -3,7 +3,7 @@ > 组别:24级1班第7组 编写人:罗皓晨 编写日期:2026-07-24 版本:v1.0 > 截止:第 2 周周三(开发过程中持续更新,保持与代码一致) > -> 当前状态:已按业务流程 v1.0 重建并完成与统一数据库设计的全链路校准;共 109 个追踪编号,其中 99 个活动 HTTP 定义、10 个历史取消编号;待 OpenAPI、实现、测试和正式交叉评审承接 +> 当前状态:已按业务流程 v1.0 重建并完成与统一数据库设计的全链路校准;共 109 个追踪编号,其中 99 个活动 HTTP 定义、10 个历史取消编号;现按用户确认冻结为唯一接口实施基线,明细中的“待交叉评审”仅保留历史阶段信息,不构成修改授权;OpenAPI、实现和测试仍待承接 ## 一、通用约定 @@ -51,7 +51,7 @@ #### 1.2.2 字段命名 -- 本文所有接口命名均以同目录下的[《命名规范》](命名规范.md)为准;本文只补充接口层约束,不另建第二套命名规则。 +- 本文所有接口命名均以设计文档目录的[《命名规范》](../命名规范.md)为准;本文只补充接口层约束,不另建第二套命名规则。 - 模块名和业务对象名必须使用《命名规范》中的标准词根,例如 `Identity`、`Catalog`、`Cart`、`Ordering`、`User`、`Product`、`CartItem`、`Order`。 - JSON 属性、Query 参数和 Route 参数统一使用 `camelCase`。 - C# 类型和公开成员使用 `PascalCase`,由序列化配置转换为 `camelCase`。 @@ -604,12 +604,12 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 | 负责人 | 个人接口文件 | 追踪编号 | 有效 HTTP | 接口编号范围 | |---|---|---:|---:|---| -| 唐宇昊 | [`interface-tyh.md`](interface/interface-tyh.md) | 25 | 22 | `A001~A100` | -| 顾欣月 | [`interface-gxy.md`](interface/interface-gxy.md) | 23 | 22 | `A101~A200` | -| 朱惠惠 | [`interface-zhh.md`](interface/interface-zhh.md) | 19 | 17 | `A201~A300` | -| 韦乾强 | [`interface-wqq.md`](interface/interface-wqq.md) | 8 | 8 | `A301~A400` | -| 张海洋 | [`interface-zhy.md`](interface/interface-zhy.md) | 27 | 23 | `A401~A500` | -| 罗皓晨 | [`interface-lhc.md`](interface/interface-lhc.md) | 7 | 7 | `A501~A600` | +| 唐宇昊 | [`interface-tyh.md`](interface-tyh.md) | 25 | 22 | `A001~A100` | +| 顾欣月 | [`interface-gxy.md`](interface-gxy.md) | 23 | 22 | `A101~A200` | +| 朱惠惠 | [`interface-zhh.md`](interface-zhh.md) | 19 | 17 | `A201~A300` | +| 韦乾强 | [`interface-wqq.md`](interface-wqq.md) | 8 | 8 | `A301~A400` | +| 张海洋 | [`interface-zhy.md`](interface-zhy.md) | 27 | 23 | `A401~A500` | +| 罗皓晨 | [`interface-lhc.md`](interface-lhc.md) | 7 | 7 | `A501~A600` | 保留与同步规则: @@ -764,7 +764,7 @@ HTTP 接口使用 `A` 加三位数字作为文档追踪编号。编号不进入 本章只收录 99 个活动 HTTP 定义。已取消编号和内部应用契约统一放在第四章,避免被误实现为公开端点;本章“活动”表示未取消,不等于已经完成正式冻结。 -> 来源:[`interface-tyh.md`](interface/interface-tyh.md)。F03 已统一为买家专属;A024/A025 已闭合浏览记录写入与设置查询并由 DB006/DB007 承接,待 OpenAPI、实现和交叉评审。 +> 来源:[`interface-tyh.md`](interface-tyh.md)。F03 已统一为买家专属;A024/A025 已闭合浏览记录写入与设置查询并由 DB006/DB007 承接,待 OpenAPI、实现和交叉评审。 ### A001 买家注册 @@ -2154,7 +2154,7 @@ BrowsingHistorySettingResponse { - 已通过 A022 关闭过 → 200,`enabled=false`。 - 商家账号调用 → 403。 -> 来源:[`interface-gxy.md`](interface/interface-gxy.md)。商品与评价图片顺序、公开字段和 PostgreSQL 搜索边界已按流程统一;A144 因没有独立业务入口转为历史取消号。 +> 来源:[`interface-gxy.md`](interface-gxy.md)。商品与评价图片顺序、公开字段和 PostgreSQL 搜索边界已按流程统一;A144 因没有独立业务入口转为历史取消号。 ### A101 购物端有效分类列表 @@ -3469,7 +3469,7 @@ BrowsingHistorySettingResponse { - 已评价返回 `eligible=false, reason=AlreadyReviewed`;未完成返回 `eligible=false, reason=OrderNotCompleted`。A143 只决定订单详情入口提示,A142 仍执行完整资格重检。 -> 来源:[`interface-zhh.md`](interface/interface-zhh.md)。A229/A230 已取消并由 A302/A303 承接秒杀订单查询;活动、库存与 Ordering 创建契约已反查统一数据库设计,待 OpenAPI、实现和联调确认。 +> 来源:[`interface-zhh.md`](interface-zhh.md)。A229/A230 已取消并由 A302/A303 承接秒杀订单查询;活动、库存与 Ordering 创建契约已反查统一数据库设计,待 OpenAPI、实现和联调确认。 ### A201 加入购物车 @@ -4692,7 +4692,7 @@ PlaceSeckillOrderResponse { - 地址在确认后被编辑、切换默认状态或发生其他版本变化 → 409 / `IDENTITY.ADDRESS_VERSION_CONFLICT`,活动库存、限购和订单均不变化;刷新 A010/A227 后由买家重新确认并使用新 Key。 - Identity 权威读取成功且确定默认商家缺失、重复或禁用 → 409 / `ORDER.DEFAULT_MERCHANT_UNAVAILABLE`,不创建无人负责订单;Identity 本身不可用则返回 503 / `COMMON.DEPENDENCY_UNAVAILABLE`,同 Key 可安全重试。 -> 来源:[`interface-wqq.md`](interface/interface-wqq.md)。A301~A308 已统一归入 Ordering,A302/A303 已承接秒杀订单查询;接口与 DB061/DB062、跨模块应用契约和状态字段已经对齐,待 OpenAPI、实现、测试和交叉评审。 +> 来源:[`interface-wqq.md`](interface-wqq.md)。A301~A308 已统一归入 Ordering,A302/A303 已承接秒杀订单查询;接口与 DB061/DB062、跨模块应用契约和状态字段已经对齐,待 OpenAPI、实现、测试和交叉评审。 ### A301 提交订单 @@ -5614,7 +5614,7 @@ ReviewSummary { --- -> 来源:[`interface-zhy.md`](interface/interface-zhy.md)。A418 已取消并入 A414,A431 已取消且退款改用 Payment 应用契约,A434 已补齐退货信息;资金、售后状态、退款恢复和对账字段已反查统一数据库设计,待 OpenAPI、实现与交叉评审。 +> 来源:[`interface-zhy.md`](interface-zhy.md)。A418 已取消并入 A414,A431 已取消且退款改用 Payment 应用契约,A434 已补齐退货信息;资金、售后状态、退款恢复和对账字段已反查统一数据库设计,待 OpenAPI、实现与交叉评审。 ### A401 查询钱包余额 @@ -7919,7 +7919,7 @@ ReconciliationDifferenceDetailResponse { --- -> 来源:[`interface-lhc.md`](interface/interface-lhc.md)。HTTP 主体能力已覆盖;Messaging 集成事件、SignalR 契约及 DB101~DB107 映射已登记,仍待来源模块评审与真实 OpenAPI。 +> 来源:[`interface-lhc.md`](interface-lhc.md)。HTTP 主体能力已覆盖;Messaging 集成事件、SignalR 契约及 DB101~DB107 映射已登记,仍待来源模块评审与真实 OpenAPI。 ### A501 查询本人消息列表 @@ -9101,7 +9101,7 @@ Worker 在查询或写入 DB103 前,对完整 Envelope 生成规范 JSON:对 ### 5.1 当前成熟度 -本次以已确认需求和业务流程派生唯一主接口契约,并仅使用六份个人原稿核对成员贡献范围与遗漏;旧原稿不参与反向拼接业务语义,也不能覆盖本文件。有效接口的请求、响应、错误、鉴权、幂等、并发和依赖边界已经闭合,99 个活动 HTTP 契约及非 HTTP 协作已经逐项反查统一数据库设计。当前状态为 **完整定义,数据库设计已确认,待 OpenAPI、实现与交叉评审,尚未冻结**:DBxxx 映射已经完成,但真实 OpenAPI、跨模块实现签名、契约测试和交叉评审证据仍未完成。 +本次以已确认需求和业务流程派生唯一主接口契约,并仅使用六份个人原稿核对成员贡献范围与遗漏;旧原稿不参与反向拼接业务语义,也不能覆盖本文件。有效接口的请求、响应、错误、鉴权、幂等、并发和依赖边界已经闭合,99 个活动 HTTP 契约及非 HTTP 协作已经逐项反查统一数据库设计。当前状态为 **完整定义、数据库设计已确认、已冻结为唯一接口实施基线,待 OpenAPI、实现和测试承接**:DBxxx 映射已经完成,但真实 OpenAPI、跨模块实现签名和契约测试证据仍未完成。 | 负责人 | 追踪编号 | 有效 HTTP | 本次已闭合 | 仍需完成 | |---|---:|---:|---|---| diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" index f4d7eeb..18d9290 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/process/\344\270\232\345\212\241\346\265\201\347\250\213\350\256\276\350\256\241.md" @@ -17,7 +17,7 @@ - [`../../01-需求文档/需求规格说明书.md`](../../01-需求文档/需求规格说明书.md) 定义正式范围、角色、业务规则、异常和验收,是需求事实源。 - 本文档把已确认需求转换为可评审的参与者、主流程、状态、异常和模块出入口,不新增、删除或改变需求。 - [`../系统架构设计.md`](../系统架构设计.md) 说明 API、Application、数据库、Worker、Redis、RabbitMQ 和事务等技术实现关系。 -- [`../接口设计.md`](../接口设计.md) 根据已确认流程中的业务动作和模块交接,定义 HTTP 路径、请求响应、鉴权、错误码和幂等契约。 +- [`../interface/接口设计.md`](../interface/接口设计.md) 根据已确认流程中的业务动作和模块交接,定义 HTTP 路径、请求响应、鉴权、错误码和幂等契约。 - [`../数据库设计.md`](../数据库设计.md) 定义表、字段、约束、索引和状态存储。 发生冲突时,以教师只读基线和主需求文档中已经确认的业务语义为准;本文档必须随主需求修正,不能反向用图覆盖文字规则。 diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" index 8ba637b..8b60864 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\345\221\275\345\220\215\350\247\204\350\214\203.md" @@ -94,9 +94,9 @@ - Markdown业务文档可使用清晰中文名称;同一目录存在编号体系时延续现有编号。 - 图片和附件使用小写 `kebab-case`,例如 `order-checkout-flow.png`,不使用 `截图1.png`、`最终版2.png`。 - 文件名不得包含姓名、日期或版本,除非日报、周报、Migration、发布材料等规则明确要求。 -- 接口并行设计阶段统一在 `docs/02-设计文档/interface/` 保存个人原稿,固定使用 `interface-<姓名拼音首字母>.md`,例如 `interface-tyh.md`;首字母必须全小写。 +- 主接口设计固定为 `docs/02-设计文档/interface/接口设计.md`;同目录保留的个人历史原稿使用 `interface-<姓名拼音首字母>.md`,例如 `interface-tyh.md`,首字母必须全小写。 - 数据库不按成员拆分个人原稿,只维护 `docs/02-设计文档/数据库设计.md`;不得新建 `database-<姓名拼音首字母>.md`,也不得等待个人稿后再实现。 -- 个人接口原稿汇总后继续保留,仅用于贡献追踪,不能覆盖 `接口设计.md`;数据库只以统一主文档为事实源。 +- 个人接口原稿仅用于历史贡献追踪,不能覆盖同目录的 `接口设计.md`;数据库只以统一主文档为事实源。 ## 四、Vue 3、TypeScript、Vite、Pinia、Axios与UI diff --git a/eshop-project-rules-upload/AGENTS.md b/eshop-project-rules-upload/AGENTS.md index eeb06bb..cdccdc1 100644 --- a/eshop-project-rules-upload/AGENTS.md +++ b/eshop-project-rules-upload/AGENTS.md @@ -9,12 +9,21 @@ 1. 在回答项目问题、制定计划、修改代码、修改文档或创建文件前,必须先完整阅读根目录 `README.md`,然后使用中文回答。 2. 每次任务必须先使用项目级入口 Skill:`.agents/skills/eshop-project-workflow/SKILL.md`,再按任务主要目标选择一个专项 Skill。 3. 不得读取、引用或使用用户目录、系统目录、其他仓库中的全局 skill 或全局 AGENTS 作为本项目规则。 -4. `docs/00-项目要求/` 是教师发布的只读基线,不得修改其中任何文件。 -5. 优先完成当前明确需求,采用符合现有结构的最小可行方案,不得过度设计。 -6. 只修改与当前任务直接相关的文件,不得顺手重构、格式化或清理无关内容。 -7. 不得覆盖、删除、暂存或提交工作区中不属于当前任务的已有改动。 -8. `docs/02-设计文档/命名规范.md` 是项目跨技术栈命名的统一事实源;新增或重命名代码、API、DTO、表、字段、索引、Key、事件、配置、容器、测试或文档前必须读取并遵守。 -9. `.agents/skills/eshop-project-workflow/references/document-routing.md` 是项目文档读取的统一路由;涉及需求、设计、代码、测试或完成度表述时,按稳定编号和标题读取与任务相关的完整章节,不默认全文加载,也不得只看单个文件推断完整链路。 +4. `docs/00-项目要求/` 是教师发布的只读外部约束,不得修改其中任何文件。 +5. 以下四类文档是当前唯一、已确认且冻结的业务与实施设计基线: + - 需求:`docs/01-需求文档/需求规格说明书.md` + - 流程:`docs/02-设计文档/process/` 下全部现有文件 + - 接口:`docs/02-设计文档/interface/接口设计.md` + - 数据库:`docs/02-设计文档/数据库设计.md` + 教师只读文件中遗留的旧接口路径 `docs/02-设计文档/接口设计.md` 统一解释为上述新路径,不修改教师文件本身。 +6. 四类冻结基线不得修改、补写、同步、格式化、移动、重命名或删除。普通功能、缺陷、文档、测试、验收、实现和联调任务均不构成修改授权;只有用户以后明确发出“解冻并重新评审基线”的独立指令时,才可另开任务处理。 +7. OpenAPI、实体、Mapping、Migration、Seed、后端、前端、测试、系统架构、命名映射、README、报告和其他下游资产必须向四类冻结基线对齐。现有代码、测试或其他文档只能证明实现状态或偏离,不能覆盖、反推或改写基线。 +8. 发现四类基线之间存在缺漏、歧义或冲突时,立即停止相关实现,记录准确文件、编号和冲突两端并请求用户裁决;不得自行选择一方、补齐基线或用实现便利性消解冲突。 +9. 优先完成当前明确需求,采用符合现有结构的最小可行方案,不得过度设计。 +10. 只修改与当前任务直接相关的文件,不得顺手重构、格式化或清理无关内容。 +11. 不得覆盖、删除、暂存或提交工作区中不属于当前任务的已有改动。 +12. `docs/02-设计文档/命名规范.md` 只约束跨技术栈命名;新增或重命名代码、API、DTO、表、字段、索引、Key、事件、配置、容器、测试或文档前必须读取并遵守。命名规范与四类冻结基线冲突时,以冻结基线中的业务名称和契约为准。 +13. `.agents/skills/eshop-project-workflow/references/document-routing.md` 是项目文档读取的统一路由;涉及需求、设计、代码、测试或完成度表述时,按稳定编号和标题读取与任务相关的完整章节,不默认全文加载,也不得只看单个文件推断完整链路。 如果 `README.md` 不存在,必须明确说明: @@ -31,7 +40,9 @@ - 修改某个文件前,必须从仓库根目录沿目标路径检查所有 `AGENTS.md`。 - 同一事项发生冲突时,模块级 `AGENTS.md` 优先于根目录 `AGENTS.md`。 - 模块级规则不得推翻教师要求、项目核心目标、模块负责制、长期分支保护和禁止使用全局规则等硬约束。 -- 教师基线与其他项目文档冲突时,以 `docs/00-项目要求/` 中的项目要求、验收标准和评分标准为准。 +- `docs/00-项目要求/` 只提供不可突破的外部范围和验收约束;四类冻结基线提供唯一的项目业务与实施设计。两者都只读,发生冲突时停止并报告,不修改任一方。 +- 四类冻结基线的职责固定为:需求定义范围、角色、规则和验收语义;流程定义业务动作、状态、异常和模块交接;接口定义公开与内部契约;数据库定义持久化事实、约束和事务。下游资产不得越级解释或反向改写上游语义。 +- 系统架构、命名规范、实际代码、OpenAPI、Migration、测试和运行结果不是新的业务设计事实源;冲突时标记为下游偏离并修正下游,不能修改冻结基线。 - 用户的新要求会改变任务范围时,先指出影响;不得自行扩大到其他成员模块或其他任务。 ## 三、每次任务的必读内容 @@ -43,9 +54,10 @@ 3. 完整阅读 `.agents/skills/eshop-project-workflow/SKILL.md`。 4. 检查当前分支、远程基线、工作区改动和未跟踪文件,至少确认 `git status --short --branch` 与 `git diff --name-only`。 5. 检查目标目录及其父目录中是否存在模块级 `AGENTS.md`;存在时必须完整阅读。 -6. 根据任务类型继续读取第四节列出的教师基线、需求、设计、实现和测试文件;读取与当前任务相关的完整章节,不为简单任务加载无关内容,也不得只看单个文件就推断完整链路。 +6. 根据任务类型继续读取第四节列出的教师约束、四类冻结基线、实现和测试文件;读取与当前任务相关的完整章节,不为简单任务加载无关内容,也不得只看单个文件就推断完整链路。 7. 任务涉及新增或重命名任何标识符、文件、目录、接口、数据库对象、基础设施资源或测试资产时,完整阅读 `docs/02-设计文档/命名规范.md`,并先搜索现有同义名称。 8. 除纯 Git 状态或分支操作外,读取 `.agents/skills/eshop-project-workflow/references/document-routing.md`,识别任务的 M/F/X/C/N/D/A 编号、负责人、目标资产、必读章节和文档成熟度。 +9. 修改前确认目标文件不属于四类冻结基线路径;提交前再次检查工作区、暂存区和相对任务基线的提交差异均未包含这些路径。 已经在当前连续任务中完整读取且内容未发生变化的文件可以不重复输出,但在修改前仍须确认其状态没有变化。 @@ -70,11 +82,12 @@ - 长文档按文档路由读取公共章节、目标模块完整章节和直接关联章节;只有全局审查、整体验收或确有跨模块影响时才全文读取。 - 每份关键文档都要判断为 `完整定义`、`部分定义`、`模板/占位`、`实现偏离` 或 `缺失`,并在计划、实施或结论中体现。 - 接口只有清单而没有对应 Axxx 详细定义时,属于契约缺口;数据库只有表名而没有字段、约束和索引时,属于设计缺口;测试计划或报告只有模板时,不构成已执行证据。 -- 同一事实冲突时,优先级为:教师只读基线 > 用户当前明确确认 > 实际代码与真实验证证据 > 已确认设计文档 > 模板、示例和未来计划。 +- 四类冻结基线统一标记为 `完整定义/已确认/冻结`。真实代码与验证证据只用于判断 `符合基线`、`实现偏离`、`缺失` 或 `未验证`,不能改变预期结果。 +- 四类冻结基线内部或与教师只读约束冲突时标记为 `基线冲突(阻塞)`,停止实现并请求用户裁决;不得在当前任务中修订任何基线文件。 ## 四、按任务类型读取文件 -本节编号用于资料分类,不代表设计先后。新增功能或改变业务行为时,设计顺序固定为:教师要求与已确认需求 → 业务流程、状态、异常和模块出入口 → 接口、数据库与架构落地 → 代码与测试。现有接口、表或实现只能用于校验可实施性和发现偏离,不能反向拼接或覆盖已确认的业务流程。 +本节编号用于资料分类,不代表设计先后。当前设计阶段已经结束,实施顺序固定为:读取教师只读约束和四类冻结基线 → 按需求与流程理解业务 → 按接口与数据库实现 → 用测试验证。不得在实现任务中重新设计或修改需求、流程、接口、数据库;现有代码、OpenAPI、表、测试或架构只能用于发现下游偏离。 ### 1. 需求、范围与验收 @@ -110,27 +123,25 @@ 不得随意删除字段、破坏历史数据、绕过模块边界直接改表,或假设 Migration 可以无风险执行。 -`数据库设计.md` 当前仍可能包含字段不完整的模板内容和需要按 PostgreSQL/Npgsql 复核的示例。必须以目标表的完整设计及实际实体、映射和 Migration 交叉确认,不能把表标题当作可实施定义。 - -六份个人数据库原稿保存在 `docs/02-设计文档/database/database-<姓名拼音首字母>.md`,仅用于贡献和交叉评审追踪。`docs/02-设计文档/数据库设计.md` 是实体、映射、Migration、初始化和测试的唯一数据库设计事实源;未在主文档中标记为“已确认”的表不得创建 Migration。 +`数据库设计.md` 已完整定义并冻结,是实体、映射、Migration、初始化、Seed 和数据库测试的唯一数据设计事实源。不得创建、读取或等待个人数据库原稿,不得用实际实体、Migration 或数据库现状反向修改主文档;存在偏离时修改下游实现。 ### 4. Web API、DTO 与模块间协作 涉及接口、请求响应、错误码、分页、鉴权或模块间调用时,读取: - `docs/02-设计文档/process/` 中目标模块已经确认的业务动作、状态、异常和直接出入口; -- `docs/02-设计文档/接口设计.md`; +- `docs/02-设计文档/interface/接口设计.md`; - 对应需求、数据库设计和验收标准; - 现有 Controller/Endpoint、DTO、应用服务、领域逻辑和测试; - OpenAPI/Swagger 契约文件(存在时)。 -接口必须由已确认业务流程中的动作、输入输出、状态和异常结果派生。不得按 Axxx 清单拼接流程,也不得用现有接口缺口反向修改已确认业务语义;发生冲突时先记录接口设计缺口,只有需求变化时才重新评审流程。 +接口已经由冻结流程派生并冻结。不得按 Axxx 清单反向拼接流程,也不得用现有 OpenAPI、DTO、代码或调用方缺口修改接口或流程;存在偏离时修改下游实现。 -必须遵守“接口文档先行于代码”:模块间接口需要变化时,在业务流程确认后先更新并确认接口契约,再修改实现和调用方。这里的“先行”只针对代码和调用方,不表示接口先于需求或业务流程。不得通过跨模块 DbContext、内部仓储或直接改表代替公开接口。 +“接口文档先行于代码”现在表示 OpenAPI、实现和调用方必须先读取并严格承接冻结接口契约,不表示可以在实现任务中更新接口文档。不得通过跨模块 DbContext、内部仓储或直接改表代替公开接口。 -读取接口设计时,必须同时定位第一章相关通用约定、第二章目标 Axxx 清单和第三章同编号详细定义;缺少请求字段、响应、错误、鉴权或业务规则时先记录并补齐契约,不得凭清单名称猜测实现。 +读取接口设计时,必须同时定位第一章相关通用约定、第二章目标 Axxx 清单和第三章同编号详细定义。若仍发现请求字段、响应、错误、鉴权或业务规则无法唯一实施,标记为 `基线冲突(阻塞)` 并请求用户裁决,不得自行补齐契约。 -六份个人接口原稿保存在 `docs/02-设计文档/interface/interface-<姓名拼音首字母>.md`,仅用于贡献和交叉评审追踪,不得作为实现事实源。`docs/02-设计文档/接口设计.md` 是实现、OpenAPI、联调和测试的唯一接口契约;修改个人原稿时必须在同一任务中同步总文档,未在总文档中标记为“已确认”的接口不得直接进入实现。 +`docs/02-设计文档/interface/interface-<姓名拼音首字母>.md` 六份个人原稿仅是历史贡献记录,不再读取、修改或同步,也不得作为实现事实源。`docs/02-设计文档/interface/接口设计.md` 是 OpenAPI、实现、联调和测试的唯一接口契约。 ### 5. 前端页面与交互 @@ -228,6 +239,7 @@ 5. 判断功能应放在哪个目录、哪一层、哪个文件。 6. 选择最小且安全的文件集合。 7. 用中文简要说明准备修改什么以及为什么。 +8. 确认文件集合不包含需求规格、`process/`、接口设计或数据库设计四类冻结基线;若包含则停止,不得编辑。 如果相关链路没有理解清楚,不得直接修改。 @@ -241,6 +253,7 @@ - 不创建无必要的接口、抽象类、基类、通用仓储或额外分层。 - 不为未来可能出现的需求提前增加扩展点。 - 不修改 `docs/00-项目要求/`。 +- 不修改 `docs/01-需求文档/需求规格说明书.md`、`docs/02-设计文档/process/`、`docs/02-设计文档/interface/接口设计.md`、`docs/02-设计文档/数据库设计.md`。 - 不暴露或硬编码敏感信息。 - 密码使用可靠哈希,数据库查询参数化,权限和资源归属必须由服务端校验,不能只依赖前端隐藏入口。 - 不生成并提交 `node_modules`、`dist`、`bin`、`obj`、缓存、日志和临时文件。 @@ -252,10 +265,11 @@ 2. 不确定命令时,先检查 README、`package.json`、解决方案/项目文件、Makefile、脚本目录和 CI 配置。 3. 先运行与改动最相关的检查,再根据风险运行更完整的构建或测试。 4. 文档或规则修改至少检查 `git diff --check`、实际差异、文件编码、路径、链接和扁平上传包同步状态;涉及命名时检查跨技术栈词根和主规范一致性。 -5. 接口、权限、订单、库存、支付、并发、Migration、上传、Docker 和生产配置属于高风险变更,必须增加针对性验证。 -6. 不得伪造命令、输出、测试数量、覆盖率或运行环境。 -7. 修改项目级 Skill 后,必须运行 `python -X utf8 .agents/skills/eshop-project-workflow/scripts/validate_project_skills.py`。 -8. 当前环境可用官方 Skill 校验器时,再对每个已修改 Skill 运行 `quick_validate.py`,但不得依赖团队成员电脑上的全局 Skill 内容作为项目规则。 +5. 所有非基线解冻任务必须检查工作区差异、暂存区差异和当前分支相对任务基线的提交范围,确认四类冻结基线路径没有变化;发现变化立即停止暂存和提交。 +6. 接口、权限、订单、库存、支付、并发、Migration、上传、Docker 和生产配置属于高风险变更,必须增加针对性验证。 +7. 不得伪造命令、输出、测试数量、覆盖率或运行环境。 +8. 修改项目级 Skill 后,必须运行 `python -X utf8 .agents/skills/eshop-project-workflow/scripts/validate_project_skills.py`。 +9. 当前环境可用官方 Skill 校验器时,再对每个已修改 Skill 运行 `quick_validate.py`,但不得依赖团队成员电脑上的全局 Skill 内容作为项目规则。 ### Python 工具环境 diff --git a/eshop-project-rules-upload/document-routing.reference.md b/eshop-project-rules-upload/document-routing.reference.md index e449c95..52f069e 100644 --- a/eshop-project-rules-upload/document-routing.reference.md +++ b/eshop-project-rules-upload/document-routing.reference.md @@ -25,15 +25,20 @@ 不要把本文中的章节名称替换为固定行号。文档持续更新时行号会变化,稳定编号和标题才是定位依据。 -## 2. 事实来源优先级 +## 2. 冻结事实来源 -同一事项冲突时按以下顺序判断: +项目事实按固定职责使用,不再用可变优先级覆盖: -1. `docs/00-项目要求/` 中教师发布的项目要求、验收标准和评分标准;该目录只读。 -2. 用户在当前任务中明确确认的范围、负责人和取舍。 -3. 实际代码、配置、Migration、OpenAPI、运行结果、测试结果和 Git 证据。 -4. 已确认的需求规格、系统架构、命名规范、数据库设计、接口设计和测试计划。 -5. 模板、占位、示例和未来规划。 +1. `docs/00-项目要求/` 是不可修改、不可突破的教师外部范围和验收约束。 +2. 以下四类文档是唯一、已确认且冻结的业务与实施设计基线: + - `docs/01-需求文档/需求规格说明书.md` + - `docs/02-设计文档/process/` 下全部现有文件 + - `docs/02-设计文档/interface/接口设计.md` + - `docs/02-设计文档/数据库设计.md` +3. 系统架构、命名规范、代码、配置、Migration、OpenAPI、测试和运行结果是下游约束或证据,只能判断符合、偏离、缺失或未验证,不能覆盖冻结基线。 +4. 模板、占位、示例和未来规划只能作为待办。 + +教师外部约束与冻结基线冲突,或四类冻结基线彼此冲突时,统一标记 `基线冲突(阻塞)`,停止相关实现并请求用户裁决,不修改任一基线。只有用户明确发起独立的“解冻并重新评审基线”任务时,才允许调整指定基线。 同时记录文档成熟度: @@ -56,6 +61,7 @@ | 必读章节 | 文档路径和稳定章节标题 | | 文档成熟度 | 完整、部分、模板、偏离或缺失 | | 排除项 | 本次明确不处理的模块和能力 | +| 冻结路径检查 | 目标文件、工作区、暂存区和分支提交范围是否包含四类冻结基线 | 编号、功能名称、负责人或验收项发生冲突时停止实施并确认,不自行重编号或合并任务。 @@ -68,6 +74,7 @@ - `.agents/skills/eshop-project-workflow/SKILL.md`:统一预检和专项 Skill 选择。 - 目标路径上的模块级 `AGENTS.md`(存在时)。 - `git status --short --branch`、`git diff --name-only` 和未跟踪文件。 +- 与当前任务有关的四类冻结基线章节;只读,不在普通任务中编辑。 纯 Git 操作按 Git Skill 读取协作流程即可。涉及需求、设计、代码、测试或项目说明时,再使用本文后续路由。 @@ -76,14 +83,14 @@ | 任务类型 | 最小文档范围 | 需要扩展时 | |---|---|---| | 项目理解 | README、需求 2.4/8/9、架构 1/3/5/14/15 | 再读目标模块七节需求和架构 7.x | -| 新功能或行为变化 | 教师对应编号、需求目标模块七节、需求 5/9、相关业务流程、相关设计、命名相关章节 | 跨模块时加入提供方契约和调用方 | -| 业务流程/流程图 | 需求目标模块七节、需求 5/8/9、业务流程设计目标章节 | 先确认业务动作、状态、异常和模块出入口;完成后再映射架构 7.x、接口 Axxx 和数据库 DBxxx | -| API/DTO | 需求目标模块、业务流程目标章节、接口 1.x 相关约定、接口清单与对应 Axxx、命名 2/7/17/19 | 先从流程提取接口能力;鉴权读接口 1.6,列表读 1.11,幂等读 1.12 | -| 数据库/Migration | 需求目标模块、需求 5、数据库对应表、命名 2/6/17/19、实际实体/Migration | 并发事务再读架构 7.x 和接口 1.12 | +| 新功能或行为变化 | 教师对应编号和四类冻结基线中的目标模块、流程、Axxx、DBxxx | 若基线未定义该变化则阻塞,不在功能任务中补设计 | +| 业务流程理解 | 冻结需求目标模块和冻结流程目标章节 | 只读确认业务动作、状态、异常和模块出入口,再检查下游承接 | +| API/DTO | 冻结需求、流程、`interface/接口设计.md` 1.x 约定与目标 Axxx、命名 2/7/17/19 | OpenAPI/DTO 向契约对齐;鉴权读 1.6,列表读 1.11,幂等读 1.12 | +| 数据库/Migration | 冻结需求、流程、接口和数据库目标 DBxxx,命名 2/6/17/19、实际实体/Migration | 实体/Migration 向数据库基线对齐;并发事务再读架构 7.x | | 前端页面 | 需求目标模块、需求 7、业务流程目标章节、架构 6、接口对应 Axxx、命名 2/4/7/17/19 | 多客户端再读架构 6.4 和接口 1.15 | | 后端业务 | 需求目标模块、业务流程目标章节、架构 4/5/7、接口对应 Axxx、数据库相关 DBxxx、命名 2/5/6/7/17/19 | 安全读架构 8,错误读架构 9 | | 缺陷诊断 | 预期行为来源、实际调用链、现有测试、日志/响应/只读数据 | 契约或设计冲突时再读对应设计章节 | -| 文档维护 | 目标文档、直接上游事实源、直接下游消费者 | 完成度表述再读实现、测试与 Git 证据 | +| 文档维护 | 允许修改的下游目标文档、四类冻结上游基线、直接消费者 | 四类冻结基线不属于普通文档维护目标 | | 测试/验收 | 教师验收/评分、需求编号、测试计划/报告、实际实现与命令 | 挑战读需求 4、架构 7.x 和原始脚本 | | 部署/CI | 需求 C10/N 项、架构 10/11/12、命名 12/13/14/16、真实部署与 CI 文件 | 发布再读 Git 流程和回滚说明 | | Git/PR | README Git 章节、完整 Git 流程 | 命名争议读命名规范 3/16/19 | @@ -92,7 +99,7 @@ ### 6.1 `docs/01-需求文档/需求规格说明书.md` -按任务选择: +本文件已冻结。按任务只读选择: - `二、总体描述` - `2.1 用户角色`:角色与入口。 @@ -133,11 +140,12 @@ ### 6.3 `docs/02-设计文档/process/` -本目录集中维护复杂业务流程图,避免在总需求正文中堆叠同一套详细图示: +本目录是已确认且冻结的复杂业务流程基线: -- 修改流程前先读 `docs/02-设计文档/process/README.md`,确认负责人、补充位置、模板、成熟度和检查清单。 -- `业务流程设计.md`:只维护全局核心基线、公共状态、跨模块交接、追踪矩阵和扩展登记。 -- `<负责人缩写>/<模块编号>-<模块名称>流程.md`:由负责人维护单个模块的主流程、状态、异常、模块出入口、由流程派生的接口映射和待评审项;根文档只链接,不重复保存细节。 +- `process/README.md`、`业务流程设计.md` 和 21 份负责人模块流程均只读。 +- `业务流程设计.md` 定义全局核心基线、公共状态、跨模块交接和追踪矩阵。 +- `<负责人缩写>/<模块编号>-<模块名称>流程.md` 定义单个模块的主流程、状态、异常、模块出入口和接口映射。 +- 目录内原有“负责人维护”“统稿修正”等文字只保留历史背景,不再构成修改授权。 - `一、文档定位与事实来源`:需求、流程、架构、接口和数据库的职责边界。 - `二、绘图与维护约定`:图类型、核心 F 扩展规则、状态和跨模块评审要求。 - `三、F01~F13 核心业务流程`:商城核心闭环、核心状态、模块直接出入口,以及注册登录、资料地址、商品、购物车下单、支付、订单履约和后台角色流程。 @@ -145,7 +153,7 @@ - `五、选做与挑战流程登记`:X01~X04 和已选 C 项的基础 F、扩展入口、不可变核心结果和状态。 - `六、维护与评审规则`:主需求、流程图和下游设计的同步顺序。 -业务语义仍以主需求为准;流程目录把需求转换为可评审的业务动作、状态、异常和模块交接。核心流程已经完成基线校准但仍待各主责人交叉评审;X/C 扩展必须从核心状态和直接模块出口接入。流程确认后,再派生 API、数据库和架构技术时序;Axxx、HTTP 状态、DTO、DBxxx、Worker、缓存和消息名不得用来拼接或反向覆盖业务流程。接口文档仍是实现阶段的唯一 API 契约,但它先行于代码,不先行于需求和业务流程。 +业务语义以冻结需求为准;冻结流程把需求转换为业务动作、状态、异常和模块交接。X/C 扩展必须从核心状态和直接模块出口接入。冻结接口和数据库已经承接流程;Axxx、HTTP 状态、DTO、DBxxx、Worker、缓存、消息名或现有代码不得反向覆盖流程。发现无法唯一实施之处时标记阻塞,不补流程。 ### 6.4 `docs/02-设计文档/命名规范.md` @@ -172,7 +180,7 @@ 不要求每次全文读取 658 行。只读取公共三节与目标资产章节,并搜索现有同义名称。 -### 6.5 `docs/02-设计文档/接口设计.md` +### 6.5 `docs/02-设计文档/interface/接口设计.md` 先按接口特征选择第一章: @@ -193,23 +201,23 @@ 然后读取: 1. `二、接口清单` 中的编号分配、个人文件规则、目标负责人区间和汇总状态。 -2. 以本文件中的统一清单和同编号详细定义作为实现事实源;需要核对负责人原始设计或交叉评审记录时,再读 `docs/02-设计文档/interface/interface-<姓名拼音首字母>.md`。 +2. 以本文件中的统一清单和同编号详细定义作为唯一接口契约;不再读取个人原稿兜底。 3. `四、非 HTTP 契约与模块协作` 中目标模块的公开应用契约、事件、SignalR 与 Worker 边界。 4. `五、汇总审计与冻结条件` 中目标负责人的缺口、冲突和冻结阻塞项。 5. 实际 OpenAPI、Endpoint、DTO、调用方和测试。 -只有接口清单、没有详细 Axxx 时标记为 `部分定义`;字段、失败响应和业务规则未确认前不得直接编码。当前六份个人原稿已保存在 `docs/02-设计文档/interface/`,仅作贡献追踪;主接口文档保留 109 个不重复 Axxx 追踪编号,其中 99 个为活动 HTTP 详细定义,A005、A009、A023、A144、A229、A230、A418、A431、A432、A433 为 10 个已取消历史编号。当前整体为“完整定义,数据库设计已确认,待真实 OpenAPI、实现、测试和交叉评审,尚未冻结”;活动定义不等于已实现或已冻结。每次任务开始时重新检查,不永久假设此状态。 +主接口文档保留 109 个不重复 Axxx 追踪编号,其中 99 个为活动 HTTP 详细定义,A005、A009、A023、A144、A229、A230、A418、A431、A432、A433 为 10 个已取消历史编号。当前整体为“完整定义、已确认、冻结,待真实 OpenAPI、实现和测试承接”;活动定义不等于已实现。若仍发现清单与详情、字段、失败响应或业务规则冲突,标记 `基线冲突(阻塞)`,不得修改主文档或采用个人原稿。 ### 6.6 `docs/02-设计文档/数据库设计.md` -读取设计说明、44 表统一清单、目标表完整定义、跨模块关系、ER 图、事务与恢复协议和实现前检查。数据库不按成员拆分,不创建、读取或等待 `database-<姓名拼音首字母>.md`;始终以主文档为唯一设计事实源,再核对实际实体、映射、DbContext、Migration、约束、索引与 Seed。 +只读设计说明、44 表统一清单、目标表完整定义、跨模块关系、ER 图、事务与恢复协议和实现前检查。数据库不按成员拆分,不创建、读取或等待 `database-<姓名拼音首字母>.md`;始终以冻结主文档为唯一数据设计,再核对实际实体、映射、DbContext、Migration、约束、索引与 Seed。 -当前主文档已从需求与流程派生并完整定义 44 张表,数据库设计已确认,但尚未创建真实实体、DbContext、Migration、初始化和数据库测试。因此: +当前主文档已从需求、流程和接口派生并完整定义 44 张表,数据库设计已确认并冻结,但尚未创建真实实体、DbContext、Migration、初始化和数据库测试。因此: - 实现前读取目标表全部字段、约束、索引、关系、状态、事务和恢复规则,不能只看表标题或追踪表。 - 不得从接口清单、旧实现或个人草稿反向猜字段、类型或状态码。 - 只有主文档标记为已确认的表才可实现 Migration;设计已确认不等于 Migration 已存在或已验证。 -- 数据库文档与真实实体、Migration 或测试冲突时明确记录偏离,先按需求和流程判断应修设计还是修实现。 +- 数据库文档与真实实体、Migration 或测试冲突时明确记录下游偏离并修正实现,不修改数据库基线。 ### 6.7 `docs/03-测试文档/` @@ -252,11 +260,11 @@ 1. 教师对应验收编号。 2. 需求 2.4、目标模块完整七节、需求 5/8/9。 -3. 业务流程设计中的目标流程;缺失时先登记并补充业务图。 +3. 冻结业务流程中的目标流程;缺失时阻塞并报告,不补充业务图。 4. 架构 4/5 和对应 7.x。 5. 命名公共三节与目标资产章节。 -6. 根据流程动作和模块出入口核对接口通用小节、Axxx 清单与详细定义。 -7. 根据流程中的业务事实和状态核对数据库目标表及实际实体/Migration。 +6. 读取冻结接口通用小节、Axxx 清单与详细定义,核对 OpenAPI 和实现承接。 +7. 读取冻结数据库目标表,核对实际实体/Migration 承接。 8. 目标前后端代码和现有测试。 ### 修复一个缺陷 @@ -266,10 +274,10 @@ 3. 实际调用链、失败测试、日志、响应和只读数据。 4. 只在需要改名或新增资产时读取命名规范相关章节。 -### 修改一份项目文档 +### 修改一份下游项目文档 -1. 目标文档。 -2. 目标陈述的上游事实源。 +1. 确认目标不属于四类冻结基线。 +2. 只读目标陈述对应的冻结上游基线。 3. 使用该陈述的直接下游文档或实现。 4. Git、测试和运行证据。 5. 不为保持表面一致而修改无关代码。 @@ -284,13 +292,14 @@ ## 9. 缺失、占位与冲突处理 -- 教师基线与团队文档冲突:教师基线优先,记录团队文档待修正项。 +- 教师外部约束与冻结基线冲突:标记 `基线冲突(阻塞)`,请求用户裁决,不修改任一方。 +- 四类冻结基线彼此冲突:记录准确文件、编号和冲突两端,停止相关实现,不自行修订。 - 编号与功能名称冲突:停止并请求确认。 - 负责人冲突:以当前确认的需求分工和用户明确指示为准,不自行转交。 -- 接口只有清单没有详情:先补契约,不猜 DTO 和失败响应。 -- 数据库只有表名没有字段:先补表设计,不直接生成 Migration。 +- 冻结接口无法提供唯一契约:阻塞,不猜 DTO、不读个人原稿、不补接口。 +- 冻结数据库无法提供唯一表设计:阻塞,不猜字段、不补表设计、不生成 Migration。 - 测试只有模板:标记未执行,不生成虚假统计。 -- 设计与实现冲突:报告偏离、兼容影响和建议事实源,不静默改写历史。 +- 冻结设计与实现冲突:报告下游偏离和兼容影响,修正实现,不改基线。 - 当前阶段不存在目标目录:说明前置脚手架缺口,不虚构文件。 ## 10. 搜索与交付记录 @@ -302,7 +311,7 @@ rg -n "^### M04-02 " docs/01-需求文档/需求规格说明书.md rg -n "F12|M06-02" docs/00-项目要求 docs/01-需求文档 rg -n "F08|M04-01" docs/02-设计文档/process/业务流程设计.md rg -n "^### 7\\.8 " docs/02-设计文档/系统架构设计.md -rg -n "A201~A300|Cart_AddCartItem" docs/02-设计文档/接口设计.md +rg -n "A201~A300|Cart_AddCartItem" docs/02-设计文档/interface/接口设计.md rg -n "cart_item|CartItem" docs backend frontend ``` diff --git a/eshop-project-rules-upload/eshop-align-docs.SKILL.md b/eshop-project-rules-upload/eshop-align-docs.SKILL.md index 9d087a1..920e358 100644 --- a/eshop-project-rules-upload/eshop-align-docs.SKILL.md +++ b/eshop-project-rules-upload/eshop-align-docs.SKILL.md @@ -1,6 +1,6 @@ --- name: eshop-align-docs -description: 维护 E-Shop 教师要求、需求、架构、数据库、接口、实现、测试与过程材料之间的一致性。编写、修改或审查 README、需求/设计/测试文档、日报、周报、会议纪要、总结答辩、项目说明或完成度表述时使用。 +description: 只读 E-Shop 冻结的需求、流程、接口和数据库基线,维护架构、README、实现、测试与过程材料等下游资产的一致性。编写、修改或审查允许调整的项目文档和完成度表述时使用。 --- # E-Shop 文档一致性 @@ -13,14 +13,15 @@ description: 维护 E-Shop 教师要求、需求、架构、数据库、接口 ## 确定事实来源 -按以下优先级核对内容: +按固定职责核对内容: -1. `docs/00-项目要求/` 中教师发布的项目要求、验收标准和评分标准;该目录只读。 -2. 实际存在的代码、配置、数据库、运行结果、测试结果和 Git 证据。 -3. 已确认的需求规格、架构、数据库、接口和测试计划。 -4. 模板、占位内容和未来计划只能作为待办,不能证明已经实现。 +1. `docs/00-项目要求/` 是不可修改、不可突破的教师外部约束。 +2. `docs/01-需求文档/需求规格说明书.md`、`docs/02-设计文档/process/` 全目录、`docs/02-设计文档/interface/接口设计.md`、`docs/02-设计文档/数据库设计.md` 是唯一、已确认且冻结的业务与实施设计基线。 +3. 实际代码、配置、OpenAPI、Migration、数据库、运行结果、测试结果和 Git 证据只用于判断是否符合冻结基线,不产生新的业务设计。 +4. 系统架构、命名规范、README、测试计划、报告和其他文档都是下游说明或约束,必须向冻结基线对齐。 +5. 模板、占位内容和未来计划只能作为待办,不能证明已经实现。 -教师基线与其他文档冲突时以教师基线为准;设计文档与真实实现冲突时必须明确指出差异,不能静默选择更好看的说法。 +教师外部约束与冻结基线冲突,或四类冻结基线彼此冲突时,标记 `基线冲突(阻塞)` 并请求用户裁决,不修改任一基线。实现或其他文档与冻结基线冲突时,明确记录下游偏离并只修正下游。 使用 `$eshop-project-workflow` 的 `references/document-routing.md` 按稳定章节定位资料,并先标记每项来源为 `完整定义`、`部分定义`、`模板/占位`、`实现偏离` 或 `缺失`。 @@ -29,11 +30,11 @@ description: 维护 E-Shop 教师要求、需求、架构、数据库、接口 | 目标文档 | 上游事实源 | 必查下游 | |---|---|---| | README/项目范围 | 教师项目要求、需求 2.2/2.4/8/9、架构 1/3/15 | 实际目录、入口、依赖、部署和可运行命令 | -| 需求规格 | 教师项目要求、验收/评分编号、用户确认 | 业务流程、架构、API、数据库、实现与测试追踪 | -| 业务流程设计 | 已确认需求的角色、主流程、状态、异常和模块边界 | 架构技术时序、接口、数据库、页面和流程测试 | -| 系统架构 | 需求目标模块、已确认业务流程、当前交付边界、真实项目/依赖/配置 | 各模块实现、部署和测试策略 | -| 数据库设计 | 已确认流程中的业务事实/状态、接口字段、实际实体和 Migration | 查询调用方、Seed、测试与兼容性 | -| 接口设计 | 已确认流程中的动作、输入输出、状态和异常,数据库与架构约束 | OpenAPI、Endpoint、DTO、客户端和契约测试 | +| 冻结需求规格 | 教师外部约束 | 只读;用于校验流程、接口、数据库、架构、实现与测试 | +| 冻结业务流程 | 冻结需求中的角色、主流程、状态、异常和模块边界 | 只读;用于校验接口、数据库、架构、页面和流程测试 | +| 系统架构 | 四类冻结基线、当前交付边界、真实项目/依赖/配置 | 各模块实现、部署和测试策略;冲突时修架构,不改基线 | +| 冻结数据库设计 | 冻结需求、流程和接口 | 只读;用于校验实体、Migration、Seed、查询和测试 | +| 冻结接口设计 | 冻结需求与流程、数据库约束 | 只读;用于校验 OpenAPI、Endpoint、DTO、客户端和契约测试 | | 测试计划/报告 | 验收标准、实际测试入口、环境、命令和原始结果 | 缺陷闭环、完成度和发布判断 | | 日报/周报 | 对应 README、成员真实 Git、工作区和验证证据 | 汇总、计划和风险,不冒用他人成果 | | 会议纪要 | 模板和真实参会、决策、负责人、截止时间 | 后续任务、需求确认和未决项 | @@ -41,22 +42,22 @@ description: 维护 E-Shop 教师要求、需求、架构、数据库、接口 接口清单没有对应 Axxx 详情、数据库章节只有表名、测试计划或报告仍为空白模板时,必须保留为“部分/模板/缺失”,不能为了文档看起来完整而虚构字段、统计或结论。 -复杂业务流程图集中维护在 `docs/02-设计文档/process/`。`业务流程设计.md` 只保留全局核心基线、公共状态、跨模块交接和追踪索引;各负责人按 `process/README.md` 在本人目录中以“一模块一文档”维护细节。主需求保留功能规则、文字步骤与流程链接,不重复嵌入同一张详细图;需求文字与流程图冲突时先修正业务语义,再同步图示。核心流程必须标明业务状态、直接模块入口和确定出口;X/C 扩展必须绑定基础 F、接入状态和不可变核心结果,不能另起一套主链路。 +复杂业务流程基线冻结在 `docs/02-设计文档/process/`,包括 `README.md`、`业务流程设计.md` 和 21 份模块流程。目录中的维护说明仅保留历史背景,不再授权负责人、统稿人或实现任务修改流程。需求与流程、总流程与模块流程或流程与接口/数据库发生冲突时,只记录冲突并请求用户裁决。 -设计顺序固定为“已确认需求 → 业务流程 → 接口/数据库/架构落地 → 实现与测试”。业务图先使用业务动作确定状态和结果,再建立“流程步骤 → Axxx/DBxxx/技术时序”映射。不得把接口清单拼成流程,也不得把 HTTP 状态码、DTO、字段或事件名当作业务节点。“接口文档先行”只表示已派生的接口契约必须先于代码和调用方修改完成确认。 +实施读取顺序固定为“冻结需求 → 冻结业务流程 → 冻结接口/数据库 → 下游架构、实现与测试”。不得把接口清单拼成流程,也不得把 HTTP 状态码、DTO、字段、现有代码或事件名反向改写业务节点。 -## 维护接口汇总 +## 使用冻结接口基线 -- `docs/02-设计文档/接口设计.md` 是实现、OpenAPI、联调和测试的唯一接口契约,但其业务能力必须由已确认流程派生。 -- 六份 `docs/02-设计文档/interface/interface-<姓名拼音首字母>.md` 作为个人贡献原稿长期保留,用于自审和交叉评审,但不能覆盖总文档。 -- 负责人修改个人原稿时,同一任务必须同步总文档中的统一清单、同编号详细定义、需求追踪状态和未决项;不得只改个人文件。 -- 汇总时检查 Axxx、`operationId` 和“HTTP 方法 + 路径”全局唯一,并把缺少字段、状态机、鉴权、DBxxx 或跨模块契约的接口标为“部分定义”或“待交叉评审”,不得为了凑齐数量改成“已确认”。 +- `docs/02-设计文档/interface/接口设计.md` 是实现、OpenAPI、联调和测试的唯一接口契约,已冻结且不得修改。 +- 六份 `interface-<姓名拼音首字母>.md` 仅是历史贡献快照,不再读取、修改、汇总或同步,也不能覆盖主接口文档。 +- OpenAPI、Endpoint、DTO、客户端和契约测试必须严格承接主文档中的 Axxx、`operationId`、“HTTP 方法 + 路径”、鉴权、错误和业务规则。 +- 若主文档无法为实现提供唯一答案,标记 `基线冲突(阻塞)`,不得在当前任务中补接口或采用个人原稿兜底。 -## 维护数据库汇总 +## 使用冻结数据库基线 -- `docs/02-设计文档/数据库设计.md` 是实体、映射、Migration、初始化和测试的唯一数据库设计事实源。 -- 数据库由统一设计者直接维护主文档,不创建、不读取、不等待个人数据库原稿;当前主文档的表设计仍需与需求、流程、接口及真实实体、Migration、Seed 和测试交叉核对。 -- 汇总时检查 DBxxx、表名、约束名、索引名、数据所有权和跨模块关系;缺少字段、约束、索引、状态或评审的表不得标记为“已确认”。 +- `docs/02-设计文档/数据库设计.md` 是实体、映射、Migration、初始化、Seed 和测试的唯一数据设计,已冻结且不得修改。 +- 不创建、读取或等待个人数据库原稿;真实实体、Migration、Seed 和数据库现状只能用于发现实现偏离。 +- 实现必须逐项承接 DBxxx、表名、字段、约束、索引、数据所有权、事务和跨模块关系;缺少唯一答案时标记 `基线冲突(阻塞)`,不得补表设计。 ## 建立一致性追踪 @@ -82,13 +83,14 @@ F/X/C/N/D 编号 → 负责人 → 需求与边界 → 业务流程/状态/模 - 只修改目标文档及保持直接一致所必需的关联文档。 - 不批量重写教师文件,不改变已确认项目方向和成员归属。 +- 不修改四类冻结基线;文档一致性任务只修正架构、README、测试、报告等下游资产。 - 保留原模板结构和仓库命名风格,不为排版引入无关生成工具。 - 日报、周报和总结只写真实工作,不补造时间、测试数量、提交或他人成果。 - 量化数字必须能追溯到当前文件、命令或原始结果。 ## 验证文档 -检查 Markdown 标题、表格、编号、路径、相对链接、UTF-8 编码、术语和负责人一致性,并运行 `git diff --check`。若仓库存在文档检查脚本,再运行真实脚本并记录结果。 +检查 Markdown 标题、表格、编号、路径、相对链接、UTF-8 编码、术语和负责人一致性,并运行 `git diff --check`。同时确认工作区、暂存区和提交范围未包含四类冻结基线;只有用户明确授权的独立基线解冻任务例外。若仓库存在文档检查脚本,再运行真实脚本并记录结果。 ## 交付结果 diff --git a/eshop-project-rules-upload/eshop-align-docs.openai.yaml b/eshop-project-rules-upload/eshop-align-docs.openai.yaml index dcf55dc..de5b489 100644 --- a/eshop-project-rules-upload/eshop-align-docs.openai.yaml +++ b/eshop-project-rules-upload/eshop-align-docs.openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "E-Shop 文档一致性" - short_description: "基于需求、实现和验证证据维护 E-Shop 项目文档一致性" - default_prompt: "使用 $eshop-align-docs 检查并维护这项 E-Shop 文档的一致性。" + short_description: "只读冻结需求流程接口数据库并校准 E-Shop 下游文档一致性" + default_prompt: "使用 $eshop-align-docs 只读冻结基线,检查并维护这项 E-Shop 下游文档的一致性。" diff --git a/eshop-project-rules-upload/eshop-deliver-feature.SKILL.md b/eshop-project-rules-upload/eshop-deliver-feature.SKILL.md index e67d0d6..701ad3e 100644 --- a/eshop-project-rules-upload/eshop-deliver-feature.SKILL.md +++ b/eshop-project-rules-upload/eshop-deliver-feature.SKILL.md @@ -1,6 +1,6 @@ --- name: eshop-deliver-feature -description: 按业务模块纵向交付 E-Shop 新功能或预期行为变更,覆盖需求编号、数据库、API/OpenAPI、后端、前端、测试和必要文档。新增或修改 F01-F13、X01-X04、挑战模块、M00 公共能力、页面、DTO、数据表、Migration 或跨模块公开契约时使用。 +description: 按冻结的需求、流程、接口和数据库基线纵向实现 E-Shop 业务模块,覆盖 OpenAPI、实体与 Migration、后端、前端、测试和允许调整的下游文档。实现 F01-F13、X01-X04、挑战模块、M00 公共能力、页面、DTO、数据表或跨模块公开契约时使用。 --- # E-Shop 功能纵向交付 @@ -13,10 +13,10 @@ description: 按业务模块纵向交付 E-Shop 新功能或预期行为变更 ## 建立任务卡 -1. 按 `$eshop-project-workflow` 的 `references/document-routing.md` 建立最小事实包,从教师基线和需求规格确认需求编号、负责人、角色、业务规则和验收条件。 -2. 需求编号、功能名称、负责人或验收项相互冲突时停止实施并请求确认,不自行合并、重编号或替换负责人。 +1. 按 `$eshop-project-workflow` 的 `references/document-routing.md` 建立最小事实包,从教师只读约束和四类冻结基线确认需求编号、负责人、角色、业务规则、流程、接口、数据和验收条件。 +2. 需求编号、功能名称、负责人、流程、接口、数据库或验收项相互冲突时标记 `基线冲突(阻塞)`,停止实施并请求确认,不自行合并、重编号、补设计或替换负责人。 3. 写明本次包含项、明确排除项、依赖模块和受影响用户流程。 -4. 确认 `docs/02-设计文档/process/` 中目标流程已覆盖角色、状态、异常和模块出入口;缺失时先补流程,不从现有接口或代码反推预期业务。 +4. 确认冻结的 `docs/02-设计文档/process/` 已覆盖角色、状态、异常和模块出入口;缺失时停止并报告,不补流程,也不从现有接口或代码反推预期业务。 5. 检查相关实现是否真实存在;不存在时先说明脚手架或契约缺口,不虚构代码结构。 6. M00 或公共脚手架缺失时将其记录为前置依赖;除非当前任务明确属于 M00,不由单个业务功能任务顺手搭建全仓或代写公共负责人工作。 7. 区分计划、已实现、已验证和缺失状态。 @@ -29,13 +29,13 @@ description: 按业务模块纵向交付 E-Shop 新功能或预期行为变更 | 流程 | `process/README.md`、全局核心基线和目标负责人模块流程;确认状态、异常、直接输入输出和扩展接入点 | | 架构 | 架构 4/5,以及目标能力对应的 6、7.x、8、9、14、15 章 | | 命名 | 命名规范 1/2/19,再读取代码、API、数据库、事件、配置或测试对应资产章节 | -| 契约 | 接口第一章相关约定、第二章目标 Axxx 清单、第三章同编号详情、实际 OpenAPI 和调用方 | +| 契约 | `interface/接口设计.md` 第一章相关约定、第二章目标 Axxx 清单、第三章同编号详情、实际 OpenAPI 和调用方 | | 数据 | 数据库目标表、实际实体/映射/DbContext/Migration/Seed 和历史兼容性 | | 质量 | 对应验收编号、测试计划、真实测试入口、现有测试与原始结果 | - 只按稳定编号和标题定位,不依赖行号,也不默认加载其他成员的全部模块。 -- 先以需求和流程确定业务动作、状态、异常与模块交接,再派生接口能力和数据事实;接口只有清单没有 Axxx 详情时补齐契约,数据库只有表名没有字段、约束和索引时补齐设计。 -- 文档与实现不一致时记录 `实现偏离` 和兼容影响,不静默采用更方便的一方。 +- 需求、流程、接口和数据库均已冻结;按其固定职责理解业务并实施,不在功能任务中重新派生、补齐或修改基线。 +- 冻结基线与实现不一致时记录 `实现偏离` 和兼容影响,并修改下游实现;冻结基线内部无法唯一实施时停止并请求用户裁决。 - 纵向任务只覆盖用户确认的模块和链路;用户只要求其中一层时,明确其余层尚未交付。 ## 建立纵向影响清单 @@ -44,8 +44,8 @@ description: 按业务模块纵向交付 E-Shop 新功能或预期行为变更 |---|---| | 需求 | 对应 F/X/C/M 编号、角色、权限、主流程、异常和验收条件 | | 流程 | 参与者、业务动作、判断分支、状态转换、模块直接出入口、失败和回归结果 | -| 数据 | `数据库设计.md`、实体、映射、约束、索引、Migration、Seed 和历史兼容性 | -| 契约 | `接口设计.md`、OpenAPI、DTO、错误码、分页、鉴权和调用方 | +| 数据 | 冻结的 `数据库设计.md`、实体、映射、约束、索引、Migration、Seed 和历史兼容性 | +| 契约 | 冻结的 `interface/接口设计.md`、OpenAPI、DTO、错误码、分页、鉴权和调用方 | | 后端 | API/Endpoint、Application、Domain、Infrastructure、事务、事件和后台任务 | | 前端 | 路由、页面、组件、Store、API 客户端、类型、加载/空态/错误反馈 | | 质量 | 单元测试、集成测试、前端测试、必要场景验证和文档同步 | @@ -54,15 +54,16 @@ description: 按业务模块纵向交付 E-Shop 新功能或预期行为变更 ## 按安全顺序实施 -1. 先固定需求与验收边界。 -2. 先确认业务流程、状态、异常和模块直接出入口;流程图使用业务动作,不使用 Axxx、HTTP 状态或 DTO 拼接流程。 -3. 从已确认流程派生所需接口能力、数据事实和技术时序;发现现有契约冲突时先修正设计缺口,不反向覆盖流程。 -4. 需要改变模块间接口时先更新接口设计或 OpenAPI 契约,再改实现与调用方。 -5. 需要持久化变更时同步实体、映射、Migration、约束、兼容性、Seed 和测试。 +1. 读取冻结需求,固定范围、角色、规则和验收边界。 +2. 读取冻结流程,确认状态、异常和模块直接出入口;不得修改流程图。 +3. 读取冻结接口和数据库,形成实现清单;发现缺漏或冲突时停止,不修正基线。 +4. OpenAPI、实现和调用方必须承接冻结接口;需要改变契约才能完成的请求属于范围变化,先报告并等待用户另行解冻基线。 +5. 持久化实现必须承接冻结数据库设计,同步实体、映射、Migration、约束、兼容性、Seed 和测试,但不得修改数据库设计文档。 6. 后端实现服务端参数校验、Policy、资源归属、事务、一致性、幂等和错误处理。 7. 前端实现真实 API 调用、类型、角色入口和加载、空数据、成功、失败、无权限反馈。 8. 通过公开 API、应用接口或集成事件协作,不跨模块直接使用内部 DbContext、仓储或表。 9. 只为已经确认的共同需求建设公共能力;单模块逻辑留在本模块。 +10. 任何普通功能任务都不得修改需求规格、`process/`、`interface/接口设计.md` 或 `数据库设计.md`。 简单 CRUD 保持简单。只有架构文档已确认的复杂规则才使用 DDD、CQRS、Outbox、缓存或消息等机制。 diff --git a/eshop-project-rules-upload/eshop-deliver-feature.openai.yaml b/eshop-project-rules-upload/eshop-deliver-feature.openai.yaml index d78ef78..dd5d350 100644 --- a/eshop-project-rules-upload/eshop-deliver-feature.openai.yaml +++ b/eshop-project-rules-upload/eshop-deliver-feature.openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "E-Shop 功能纵向交付" - short_description: "按需求编号完成数据库、接口、后端、前端与测试纵向交付" + short_description: "严格依据冻结需求流程接口数据库完成 E-Shop 实现与测试纵向交付" default_prompt: "使用 $eshop-deliver-feature 按模块完成这项 E-Shop 功能的纵向交付。" diff --git a/eshop-project-rules-upload/eshop-fix-bug.SKILL.md b/eshop-project-rules-upload/eshop-fix-bug.SKILL.md index b078acf..cf24d0f 100644 --- a/eshop-project-rules-upload/eshop-fix-bug.SKILL.md +++ b/eshop-project-rules-upload/eshop-fix-bug.SKILL.md @@ -1,6 +1,6 @@ --- name: eshop-fix-bug -description: 先确认、复现并定位 E-Shop 已有行为中的缺陷,再实施最小根因修复和回归验证。用户报告 Bug、异常、白屏、接口错误、截图问题、测试失败、权限/数据/状态流转异常或行为回归时使用;只要求诊断时保持只读。 +description: 以冻结的需求、流程、接口和数据库为预期结果,先确认、复现并定位 E-Shop 已有行为中的缺陷,再实施最小根因修复和回归验证。用户报告 Bug、异常、白屏、接口错误、测试失败、权限、数据或状态流转异常时使用。 --- # E-Shop 缺陷修复 @@ -8,13 +8,13 @@ description: 先确认、复现并定位 E-Shop 已有行为中的缺陷,再 ## 先使用总入口 - 先执行 `$eshop-project-workflow` 的规则读取、工作区预检和范围确认。 -- 如果需要改变原本预期的业务规则或公开契约,转用 `$eshop-deliver-feature` 重新确认需求。 +- 如果修复必须改变冻结需求、流程、接口或数据库,标记 `基线冲突(阻塞)` 并请求用户另行发起基线解冻任务;不得在缺陷任务中改预期。 - 只有用户要求完整验收或 Git 操作时,才叠加对应专项 Skill。 ## 确认问题成立 1. 按 `$eshop-project-workflow` 的 `references/document-routing.md` 读取对应编号、目标模块章节和实际链路。 -2. 按教师基线、用户当前明确确认、现有通过测试与真实行为、已确认需求/接口设计的顺序确定预期结果;模板和未来计划不能单独证明预期。 +2. 按教师只读约束和四类冻结基线确定唯一预期结果;现有通过测试、真实行为、代码和数据库只用于证明符合或偏离,不能覆盖预期。模板和未来计划不能单独证明预期。 3. 记录实际结果、复现步骤、账号角色、数据前置条件、浏览器/服务版本和环境。 4. 优先使用失败测试、日志、浏览器操作、API 响应或数据库只读查询复现。 5. 截图驱动的前端问题要检查真实页面入口和交互,不只看静态 DOM 或代码。 @@ -40,7 +40,7 @@ description: 先确认、复现并定位 E-Shop 已有行为中的缺陷,再 检查相关模块级 `AGENTS.md`、需求与验收编号、接口/数据库设计、完整代码链路、现有测试和必要 Git 历史。不要因为症状出现在前端就默认根因也在前端。 -如果接口只有清单而没有详细 Axxx,或数据库只有表名没有字段约束,先判定为设计/契约缺口,不把尚未定义的行为武断归为代码 Bug。只有修复需要新增或重命名资产时,才读取命名规范对应章节。 +如果冻结接口或数据库仍无法给出唯一实施答案,判定为 `基线冲突(阻塞)`,记录准确 Axxx、DBxxx 和缺失定义后停止;不得读取个人原稿、补接口、补表设计或用现有代码猜测。只有修复需要新增或重命名下游资产时,才读取命名规范对应章节。 ## 实施最小修复 @@ -50,6 +50,7 @@ description: 先确认、复现并定位 E-Shop 已有行为中的缺陷,再 4. 不用吞异常、硬编码数据、关闭校验、扩大权限或跳过事务来伪造成功。 5. 在最接近根因的层增加或更新回归测试;无法自动化时记录可重复的手工步骤。 6. 若根因属于其他成员模块,说明证据和影响边界,不越权批量改写该模块。 +7. 只修改实现、配置、测试或允许调整的下游文档;不得修改需求规格、`process/`、`interface/接口设计.md` 或 `数据库设计.md`。 ## 验证修复 diff --git a/eshop-project-rules-upload/eshop-fix-bug.openai.yaml b/eshop-project-rules-upload/eshop-fix-bug.openai.yaml index 4ecbe62..52ba0cf 100644 --- a/eshop-project-rules-upload/eshop-fix-bug.openai.yaml +++ b/eshop-project-rules-upload/eshop-fix-bug.openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "E-Shop 缺陷修复" - short_description: "先复现并定位 E-Shop 缺陷,再做最小修复和回归验证" + short_description: "严格依据冻结需求流程接口数据库复现定位 E-Shop 缺陷并完成回归验证" default_prompt: "使用 $eshop-fix-bug 复现、定位并最小修复这个 E-Shop 问题。" diff --git a/eshop-project-rules-upload/eshop-project-workflow.SKILL.md b/eshop-project-rules-upload/eshop-project-workflow.SKILL.md index d7552e8..53d86ca 100644 --- a/eshop-project-rules-upload/eshop-project-workflow.SKILL.md +++ b/eshop-project-rules-upload/eshop-project-workflow.SKILL.md @@ -1,6 +1,6 @@ --- name: eshop-project-workflow -description: 作为 E-Shop 仓库所有任务的项目级总入口,统一完成规则读取、Git 与工作区预检、任务边界确认、专项 Skill 路由和最终交付检查。凡在本仓库进行项目理解、计划、修改、验证、文档或 Git 操作时都使用,并根据主要目标再选择一个专项 Skill。 +description: 作为 E-Shop 仓库所有任务的项目级总入口,以冻结的需求、流程、接口和数据库为唯一实施基线,统一完成规则读取、Git 与工作区预检、任务边界确认、专项 Skill 路由和最终交付检查。凡在本仓库进行项目理解、计划、修改、验证、文档或 Git 操作时都使用。 --- # E-Shop 项目工作流 @@ -13,6 +13,7 @@ description: 作为 E-Shop 仓库所有任务的项目级总入口,统一完 4. 从仓库根目录解析相对路径,不假设 `frontend/`、`backend/`、测试、部署或 CI 文件已经存在。 5. 始终使用中文说明计划、修改、验证结果和风险。 6. 任务涉及需求、设计、代码、测试或完成度表述时,读取 `references/document-routing.md`;纯 Git 状态和分支操作可以只按 Git 协作规则执行。 +7. 将需求规格、`process/` 全目录、`interface/接口设计.md` 和 `数据库设计.md` 视为已确认且冻结的四类唯一实施基线;除用户明确发起独立的基线解冻任务外,不得编辑这些文件。 ## 完成统一预检 @@ -21,6 +22,7 @@ description: 作为 E-Shop 仓库所有任务的项目级总入口,统一完 3. 区分本任务改动与工作区原有改动;不得覆盖、暂存、提交或清理无关改动。 4. 根据根 `AGENTS.md` 的读取矩阵,只读取与当前任务有关的教师基线、需求、设计、实现和测试章节。 5. 若关键文件或实现不存在,明确说明当前阶段,不虚构目录、命令或完成度。 +6. 编辑前核对目标路径;若命中四类冻结基线则停止。提交前再检查工作区、暂存区和分支提交范围没有包含冻结路径。 ## 按章节路由文档 @@ -28,10 +30,11 @@ description: 作为 E-Shop 仓库所有任务的项目级总入口,统一完 2. 默认读取公共章节、目标模块完整章节和直接关联章节;只有整体验收或确认存在跨模块影响时才扩展范围。 3. 将关键文档标记为 `完整定义`、`部分定义`、`模板/占位`、`实现偏离` 或 `缺失`。 4. 接口只有清单没有 Axxx 详情、数据库只有表名没有字段约束、测试文件只有模板时,先作为缺口报告,不把它们当作可实施契约或通过证据。 -5. 同一事实冲突时,按教师基线、用户当前确认、真实实现与验证证据、已确认设计、模板与计划的顺序判断。 -6. 新增功能或改变业务行为时,先由需求确认角色、规则和验收,再在 `docs/02-设计文档/process/` 确认业务流程、状态、异常和模块出入口;接口、数据库和架构从流程派生,不能按现有 Axxx 或 DBxxx 反向拼接流程。 -7. 业务流程确认后,接口任务以 `docs/02-设计文档/接口设计.md` 为唯一实施契约;“接口先行”只表示先于代码和调用方修改。`docs/02-设计文档/interface/` 中的个人原稿只用于贡献追踪,修改后必须同步总文档。 -8. 数据库任务只以 `docs/02-设计文档/数据库设计.md` 为唯一实施契约;本项目不再创建、读取或等待个人数据库原稿。只有主文档标记为已确认且完成实现前复核的表才能生成 Migration。 +5. `docs/00-项目要求/` 是不可修改、不可突破的外部约束;四类冻结基线是唯一业务与实施设计。两者冲突时标记 `基线冲突(阻塞)` 并请求用户裁决,不修改任一方。 +6. 固定读取顺序为:需求定义范围、角色、规则和验收语义;流程定义业务动作、状态、异常和模块交接;接口定义公开与内部契约;数据库定义持久化事实、约束和事务。系统架构、命名规范、代码、OpenAPI、Migration、测试和运行结果都是下游约束或证据,不能反向改写四类基线。 +7. 接口任务只以 `docs/02-设计文档/interface/接口设计.md` 为唯一契约。`interface-<姓名拼音首字母>.md` 个人文件仅是历史贡献记录,不再读取、修改或同步。 +8. 数据库任务只以 `docs/02-设计文档/数据库设计.md` 为唯一数据设计;不创建、读取或等待个人数据库原稿。 +9. 若冻结基线缺少实现所需定义、彼此不一致或与教师约束冲突,停止相关实现并报告准确文件与编号;不得补流程、补接口、补表设计或选择更方便的实现口径。 ## 选择专项 Skill @@ -59,6 +62,7 @@ description: 作为 E-Shop 仓库所有任务的项目级总入口,统一完 - 只选择完成当前目标所需的最小文件集合,不顺手处理范围外问题。 - 保持模块纵向负责和公开协作边界,不代写其他成员模块。 - 不修改 `docs/00-项目要求/`,不更换技术栈,不进行全仓格式化或大范围重构。 +- 不修改四类冻结基线;所有 OpenAPI、实体、Migration、代码、测试、架构和其他文档只向基线对齐。 - 不提交密码、Token、密钥、生产配置、个人绝对路径或生成目录。 开始编辑前,用一句简短中文说明准备修改哪些文件以及原因。 @@ -66,7 +70,7 @@ description: 作为 E-Shop 仓库所有任务的项目级总入口,统一完 ## 完成统一收尾 1. 运行专项 Skill 要求且仓库真实存在的验证命令。 -2. 检查 `git diff --check`、实际差异、当前状态和无关改动是否保持原样。 +2. 检查 `git diff --check`、实际差异、当前状态和无关改动是否保持原样,并确认非基线解冻任务没有改动四类冻结路径。 3. 把当前成果判断为一个可独立说明、验证和回滚的阶段;阶段未完成、验证失败或改动归属不清时不得自动提交。 4. 阶段完成且安全条件满足时,使用 `$eshop-manage-git` 按精确路径自动暂存并提交本阶段文件,不再逐次请求 Commit 授权;不得混入工作区已有的其他任务改动。 5. 自动提交信息使用 `git commit -m "(): <任务概要>;<主要改动>"`。任务概要说明本阶段目标,主要改动说明实际完成内容;两部分不得使用“更新”“改一下”等模糊词。 diff --git a/eshop-project-rules-upload/eshop-project-workflow.openai.yaml b/eshop-project-rules-upload/eshop-project-workflow.openai.yaml index f8dbad2..5414dbf 100644 --- a/eshop-project-rules-upload/eshop-project-workflow.openai.yaml +++ b/eshop-project-rules-upload/eshop-project-workflow.openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "E-Shop 项目协作入口" - short_description: "统一预检并路由 E-Shop 功能、缺陷、文档、验收与 Git 任务" + short_description: "以冻结四基线统一预检并路由 E-Shop 仓库任务" default_prompt: "使用 $eshop-project-workflow 预检并路由这项 E-Shop 仓库任务。" diff --git a/eshop-project-rules-upload/eshop-verify-acceptance.SKILL.md b/eshop-project-rules-upload/eshop-verify-acceptance.SKILL.md index 1bb255d..ae5505e 100644 --- a/eshop-project-rules-upload/eshop-verify-acceptance.SKILL.md +++ b/eshop-project-rules-upload/eshop-verify-acceptance.SKILL.md @@ -1,6 +1,6 @@ --- name: eshop-verify-acceptance -description: 为 E-Shop 构建、测试、烟测、验收、压测和发布就绪判断提供真实、可复现的证据。运行前端或后端构建测试、API/数据库/浏览器/Docker 全链路验证、F/X/C/N/D 编号验收、挑战模块边界验证或更新测试报告时使用。 +description: 依据冻结的需求、流程、接口和数据库,为 E-Shop 构建、测试、烟测、验收、压测和发布就绪判断提供真实、可复现的证据。运行构建测试、API、数据库、浏览器、Docker 全链路验证或 F/X/C/N/D 编号验收时使用。 --- # E-Shop 测试与验收 @@ -9,14 +9,15 @@ description: 为 E-Shop 构建、测试、烟测、验收、压测和发布就 1. 先执行 `$eshop-project-workflow` 的规则读取、工作区预检和任务范围确认。 2. 核对 `docs/00-项目要求/` 教师基线是否完整、是否相对初始基线出现未经确认的改动。 -3. 检查真实实现、解决方案或项目清单、依赖文件、测试入口、部署入口和可发现命令是否存在。 -4. 全项目验收先按 F、X、C、N、D 分组给出门禁结论;只有具备运行条件的分组或场景,再展开七字段验收矩阵。 -5. 如果没有实现或测试入口,记录为 `阻塞(无实现入口)` 或 `阻塞(无测试入口)`,不要运行虚构命令,也不要把未运行误写成业务失败。 +3. 读取四类冻结基线,并检查待验收分支相对任务基线、工作区和暂存区是否改动了需求规格、`process/`、`interface/接口设计.md` 或 `数据库设计.md`;普通任务出现这些改动直接阻塞验收。 +4. 检查真实实现、解决方案或项目清单、依赖文件、测试入口、部署入口和可发现命令是否存在。 +5. 全项目验收先按 F、X、C、N、D 分组给出门禁结论;只有具备运行条件的分组或场景,再展开七字段验收矩阵。 +6. 如果没有实现或测试入口,记录为 `阻塞(无实现入口)` 或 `阻塞(无测试入口)`,不要运行虚构命令,也不要把未运行误写成业务失败。 ## 确认验证边界 1. 按 `$eshop-project-workflow` 的 `references/document-routing.md`,先读 `docs/00-项目要求/验收标准.md`、项目要求和相关评分项,确定编号与交付门槛。 -2. 再按未通过门禁或准备运行的分组,读取对应需求、设计、实现和测试章节;不为零实现门禁一次性加载全部材料。 +2. 再按未通过门禁或准备运行的分组,读取冻结需求、流程、接口、数据库以及对应实现和测试章节;不为零实现门禁一次性加载全部材料。 3. 读取 `docs/03-测试文档/测试计划.md`、`docs/03-测试文档/测试报告.md`,以及实际测试项目、脚本、CI、依赖和配置。 4. 从 README、项目清单、脚本和 CI 中发现真实命令;不得把架构文档中的计划命令当作已存在命令。 5. 本 Skill 负责验证和报告,不默认修改实现。发现失败时先记录证据;用户明确要求修复后再使用 `$eshop-fix-bug`。 @@ -42,7 +43,7 @@ description: 为 E-Shop 构建、测试、烟测、验收、压测和发布就 | 场景 | 可复现的业务或技术场景 | | 前置条件 | 角色、数据、服务、浏览器和依赖状态 | | 输入与步骤 | 实际使用的参数和操作 | -| 预期 | 来自需求、接口或验收标准的结果 | +| 预期 | 来自教师只读约束和四类冻结基线的唯一结果 | | 实际 | 真实输出、状态码、页面状态或数据库状态 | | 证据 | 命令、日志、截图、响应或查询结果 | @@ -57,6 +58,7 @@ description: 为 E-Shop 构建、测试、烟测、验收、压测和发布就 5. 涉及页面时使用真实浏览器验证主流程、空状态、加载状态、错误反馈和目标浏览器兼容性。 6. 涉及库存、订单、支付、权限、并发、消息、缓存、上传或挑战模块时,执行对应的竞争、幂等、资源归属、恢复和降级场景。 7. 涉及部署时验证 Compose、环境变量、健康检查、Migration、日志和回滚说明;不得使用或暴露生产密钥。 +8. 验证发现实现与冻结基线不一致时记录为失败或阻塞并定位下游偏离;不得通过修改基线、降低断言或改写预期制造通过。 先执行最小相关集合,再根据失败影响面和交付风险扩大范围。不要为了数量运行与任务无关的检查。 diff --git a/eshop-project-rules-upload/eshop-verify-acceptance.openai.yaml b/eshop-project-rules-upload/eshop-verify-acceptance.openai.yaml index 5615117..a954530 100644 --- a/eshop-project-rules-upload/eshop-verify-acceptance.openai.yaml +++ b/eshop-project-rules-upload/eshop-verify-acceptance.openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "E-Shop 测试与验收" - short_description: "按验收编号执行构建、测试、接口、数据库、浏览器和场景验证" + short_description: "严格依据冻结需求流程接口数据库执行 E-Shop 构建测试与全链路验收" default_prompt: "使用 $eshop-verify-acceptance 验证这项 E-Shop 交付是否达到验收要求。" -- Gitee From 8dc981032133d66988825c2f2fec7704ea6af221 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Sat, 25 Jul 2026 19:53:29 +0800 Subject: [PATCH 116/118] =?UTF-8?q?feat(infra):=20=E9=87=8D=E5=BB=BA?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E5=9F=BA=E7=A1=80=E9=AA=A8=E6=9E=B6=EF=BC=9B?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=AE=BF=E4=B8=BB=E3=80=81=E5=81=A5=E5=BA=B7?= =?UTF-8?q?=E9=97=A8=E7=A6=81=E4=B8=8E=E8=B4=A8=E9=87=8F=E5=9F=BA=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/Directory.Build.props | 20 + backend/Directory.Packages.props | 42 + backend/Mall.sln | 226 +++-- backend/README.md | 66 +- backend/global.json | 7 + .../Authentication/AuthorizationPolicies.cs | 8 + backend/src/Mall.Api/DependencyInjection.cs | 147 +++ .../src/Mall.Api/Health/HealthContracts.cs | 37 + .../Health/HealthEndpointExtensions.cs | 63 ++ .../Health/IReadinessSnapshotProvider.cs | 6 + .../Health/ReadinessSnapshotProvider.cs | 213 +++++ backend/src/Mall.Api/Mall.Api.csproj | 18 +- backend/src/Mall.Api/Mall.Api.http | 9 +- backend/src/Mall.Api/Program.cs | 74 +- .../Mall.Api/Properties/launchSettings.json | 17 +- .../src/Mall.Api/appsettings.Development.json | 17 +- backend/src/Mall.Api/appsettings.json | 64 +- backend/src/Mall.Api/packages.lock.json | 363 +++++++ backend/src/Mall.AppHost/Mall.AppHost.csproj | 21 + backend/src/Mall.AppHost/Program.cs | 49 + .../Properties/launchSettings.json | 16 + backend/src/Mall.AppHost/appsettings.json | 8 + backend/src/Mall.AppHost/packages.lock.json | 863 +++++++++++++++++ .../Mall.Application/ApplicationAssembly.cs | 6 + .../Mall.Application/Mall.Application.csproj | 10 +- .../src/Mall.Application/packages.lock.json | 10 + backend/src/Mall.Domain/DomainAssembly.cs | 6 + backend/src/Mall.Domain/Mall.Domain.csproj | 9 +- backend/src/Mall.Domain/packages.lock.json | 6 + .../AuthenticationConfigurationProvider.cs | 15 + .../AuthenticationConfigurationSnapshot.cs | 9 + .../AuthenticationContractOptions.cs | 19 + .../AuthenticationDigestCalculator.cs | 82 ++ .../AuthenticationSecurityOptions.cs | 10 + .../IAuthenticationConfigurationProvider.cs | 6 + .../Caching/RedisOptions.cs | 10 + .../Configuration/DeploymentOptions.cs | 19 + .../DependencyInjection.cs | 112 +++ .../Integration/RabbitMqOptions.cs | 10 + .../Mall.Infrastructure.csproj | 30 +- .../ObjectStorage/ObjectStorageOptions.cs | 18 + .../Observability/ObservabilityExtensions.cs | 90 ++ .../Observability/ObservabilityOptions.cs | 8 + .../Persistence/MallDbContext.cs | 12 + .../Persistence/MallDbContextFactory.cs | 25 + .../Mall.Infrastructure/packages.lock.json | 714 ++++++++++++++ .../src/Mall.Migrator/Mall.Migrator.csproj | 14 + backend/src/Mall.Migrator/MigrationRunner.cs | 59 ++ backend/src/Mall.Migrator/Program.cs | 33 + backend/src/Mall.Migrator/appsettings.json | 26 + backend/src/Mall.Migrator/packages.lock.json | 565 +++++++++++ backend/src/Mall.Worker/Mall.Worker.csproj | 15 +- backend/src/Mall.Worker/Program.cs | 13 +- .../Properties/launchSettings.json | 2 +- .../Mall.Worker/appsettings.Development.json | 9 +- backend/src/Mall.Worker/appsettings.json | 33 +- backend/src/Mall.Worker/packages.lock.json | 565 +++++++++++ .../Mall.IntegrationTests/GlobalUsings.cs | 1 + .../HealthEndpointsTests.cs | 58 +- .../Mall.IntegrationTests.csproj | 29 +- .../Mall.IntegrationTests/MallApiFactory.cs | 25 + .../OpenApiEndpointsTests.cs | 25 + .../ProblemDetailsTests.cs | 27 + .../Mall.IntegrationTests/packages.lock.json | 891 ++++++++++++++++++ .../ArchitectureDependencyTests.cs | 46 +- .../AuthenticationDigestCalculatorTests.cs | 60 ++ backend/tests/Mall.UnitTests/GlobalUsings.cs | 1 + .../Mall.UnitTests/Mall.UnitTests.csproj | 31 +- .../tests/Mall.UnitTests/packages.lock.json | 726 ++++++++++++++ 69 files changed, 6598 insertions(+), 246 deletions(-) create mode 100644 backend/Directory.Build.props create mode 100644 backend/Directory.Packages.props create mode 100644 backend/global.json create mode 100644 backend/src/Mall.Api/Authentication/AuthorizationPolicies.cs create mode 100644 backend/src/Mall.Api/DependencyInjection.cs create mode 100644 backend/src/Mall.Api/Health/HealthContracts.cs create mode 100644 backend/src/Mall.Api/Health/HealthEndpointExtensions.cs create mode 100644 backend/src/Mall.Api/Health/IReadinessSnapshotProvider.cs create mode 100644 backend/src/Mall.Api/Health/ReadinessSnapshotProvider.cs create mode 100644 backend/src/Mall.Api/packages.lock.json create mode 100644 backend/src/Mall.AppHost/Mall.AppHost.csproj create mode 100644 backend/src/Mall.AppHost/Program.cs create mode 100644 backend/src/Mall.AppHost/Properties/launchSettings.json create mode 100644 backend/src/Mall.AppHost/appsettings.json create mode 100644 backend/src/Mall.AppHost/packages.lock.json create mode 100644 backend/src/Mall.Application/ApplicationAssembly.cs create mode 100644 backend/src/Mall.Application/packages.lock.json create mode 100644 backend/src/Mall.Domain/DomainAssembly.cs create mode 100644 backend/src/Mall.Domain/packages.lock.json create mode 100644 backend/src/Mall.Infrastructure/Authentication/AuthenticationConfigurationProvider.cs create mode 100644 backend/src/Mall.Infrastructure/Authentication/AuthenticationConfigurationSnapshot.cs create mode 100644 backend/src/Mall.Infrastructure/Authentication/AuthenticationContractOptions.cs create mode 100644 backend/src/Mall.Infrastructure/Authentication/AuthenticationDigestCalculator.cs create mode 100644 backend/src/Mall.Infrastructure/Authentication/AuthenticationSecurityOptions.cs create mode 100644 backend/src/Mall.Infrastructure/Authentication/IAuthenticationConfigurationProvider.cs create mode 100644 backend/src/Mall.Infrastructure/Caching/RedisOptions.cs create mode 100644 backend/src/Mall.Infrastructure/Configuration/DeploymentOptions.cs create mode 100644 backend/src/Mall.Infrastructure/DependencyInjection.cs create mode 100644 backend/src/Mall.Infrastructure/Integration/RabbitMqOptions.cs create mode 100644 backend/src/Mall.Infrastructure/ObjectStorage/ObjectStorageOptions.cs create mode 100644 backend/src/Mall.Infrastructure/Observability/ObservabilityExtensions.cs create mode 100644 backend/src/Mall.Infrastructure/Observability/ObservabilityOptions.cs create mode 100644 backend/src/Mall.Infrastructure/Persistence/MallDbContext.cs create mode 100644 backend/src/Mall.Infrastructure/Persistence/MallDbContextFactory.cs create mode 100644 backend/src/Mall.Infrastructure/packages.lock.json create mode 100644 backend/src/Mall.Migrator/Mall.Migrator.csproj create mode 100644 backend/src/Mall.Migrator/MigrationRunner.cs create mode 100644 backend/src/Mall.Migrator/Program.cs create mode 100644 backend/src/Mall.Migrator/appsettings.json create mode 100644 backend/src/Mall.Migrator/packages.lock.json create mode 100644 backend/src/Mall.Worker/packages.lock.json create mode 100644 backend/tests/Mall.IntegrationTests/GlobalUsings.cs create mode 100644 backend/tests/Mall.IntegrationTests/MallApiFactory.cs create mode 100644 backend/tests/Mall.IntegrationTests/OpenApiEndpointsTests.cs create mode 100644 backend/tests/Mall.IntegrationTests/ProblemDetailsTests.cs create mode 100644 backend/tests/Mall.IntegrationTests/packages.lock.json create mode 100644 backend/tests/Mall.UnitTests/AuthenticationDigestCalculatorTests.cs create mode 100644 backend/tests/Mall.UnitTests/GlobalUsings.cs create mode 100644 backend/tests/Mall.UnitTests/packages.lock.json diff --git a/backend/Directory.Build.props b/backend/Directory.Build.props new file mode 100644 index 0000000..c72c497 --- /dev/null +++ b/backend/Directory.Build.props @@ -0,0 +1,20 @@ + + + net10.0 + latest + enable + enable + true + latest-recommended + true + true + true + 0.1.0 + https://gitee.com/eshop-class1-group7/eshop-class1-group7 + + + + true + true + + diff --git a/backend/Directory.Packages.props b/backend/Directory.Packages.props new file mode 100644 index 0000000..47f9cb8 --- /dev/null +++ b/backend/Directory.Packages.props @@ -0,0 +1,42 @@ + + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Mall.sln b/backend/Mall.sln index eb83ef0..b171769 100644 --- a/backend/Mall.sln +++ b/backend/Mall.sln @@ -5,21 +5,25 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mall.Domain", "src\Mall.Domain\Mall.Domain.csproj", "{04126612-3F71-434B-967B-5CADA3A27AA8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mall.Domain", "src\Mall.Domain\Mall.Domain.csproj", "{849AD5B1-57E6-4C93-B26A-9DF703CAE67D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mall.Application", "src\Mall.Application\Mall.Application.csproj", "{D0888052-747D-47E5-9C6F-0ECA52647781}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mall.Application", "src\Mall.Application\Mall.Application.csproj", "{9C9C5EFB-941B-4B2C-B8AE-0486B79E3256}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mall.Infrastructure", "src\Mall.Infrastructure\Mall.Infrastructure.csproj", "{4AEF0A7C-503A-44BB-BFE9-D0B83C2ED9D8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mall.Infrastructure", "src\Mall.Infrastructure\Mall.Infrastructure.csproj", "{FA943FB1-F77B-4C7D-BFD2-26FDC6C62751}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mall.Api", "src\Mall.Api\Mall.Api.csproj", "{D8715588-C2E6-4B90-9B1D-0613649E228B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mall.Api", "src\Mall.Api\Mall.Api.csproj", "{22A8AA93-D63F-41EC-B729-595FE817E525}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mall.Worker", "src\Mall.Worker\Mall.Worker.csproj", "{86E04BA4-5837-4782-AD78-04FC3C414E0F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mall.Worker", "src\Mall.Worker\Mall.Worker.csproj", "{BC106469-CD50-49E5-9CBB-FFC67BD8E2B7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mall.Migrator", "src\Mall.Migrator\Mall.Migrator.csproj", "{40F03B32-AAA9-45F6-989E-2044CB61EB43}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mall.AppHost", "src\Mall.AppHost\Mall.AppHost.csproj", "{1913E4BD-9EBE-4032-9569-10FCFF7D5F0A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mall.UnitTests", "tests\Mall.UnitTests\Mall.UnitTests.csproj", "{8F61803F-6AC5-4F70-9124-C57C895A20AE}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mall.UnitTests", "tests\Mall.UnitTests\Mall.UnitTests.csproj", "{30B21D7F-E10F-40E6-B356-F787BE3FD1B4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mall.IntegrationTests", "tests\Mall.IntegrationTests\Mall.IntegrationTests.csproj", "{9943384E-DA05-4C70-9773-613754102C5F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mall.IntegrationTests", "tests\Mall.IntegrationTests\Mall.IntegrationTests.csproj", "{B5C65510-B04C-4F09-B15E-AB98E675F70C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -31,101 +35,127 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {04126612-3F71-434B-967B-5CADA3A27AA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {04126612-3F71-434B-967B-5CADA3A27AA8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {04126612-3F71-434B-967B-5CADA3A27AA8}.Debug|x64.ActiveCfg = Debug|Any CPU - {04126612-3F71-434B-967B-5CADA3A27AA8}.Debug|x64.Build.0 = Debug|Any CPU - {04126612-3F71-434B-967B-5CADA3A27AA8}.Debug|x86.ActiveCfg = Debug|Any CPU - {04126612-3F71-434B-967B-5CADA3A27AA8}.Debug|x86.Build.0 = Debug|Any CPU - {04126612-3F71-434B-967B-5CADA3A27AA8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {04126612-3F71-434B-967B-5CADA3A27AA8}.Release|Any CPU.Build.0 = Release|Any CPU - {04126612-3F71-434B-967B-5CADA3A27AA8}.Release|x64.ActiveCfg = Release|Any CPU - {04126612-3F71-434B-967B-5CADA3A27AA8}.Release|x64.Build.0 = Release|Any CPU - {04126612-3F71-434B-967B-5CADA3A27AA8}.Release|x86.ActiveCfg = Release|Any CPU - {04126612-3F71-434B-967B-5CADA3A27AA8}.Release|x86.Build.0 = Release|Any CPU - {D0888052-747D-47E5-9C6F-0ECA52647781}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D0888052-747D-47E5-9C6F-0ECA52647781}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D0888052-747D-47E5-9C6F-0ECA52647781}.Debug|x64.ActiveCfg = Debug|Any CPU - {D0888052-747D-47E5-9C6F-0ECA52647781}.Debug|x64.Build.0 = Debug|Any CPU - {D0888052-747D-47E5-9C6F-0ECA52647781}.Debug|x86.ActiveCfg = Debug|Any CPU - {D0888052-747D-47E5-9C6F-0ECA52647781}.Debug|x86.Build.0 = Debug|Any CPU - {D0888052-747D-47E5-9C6F-0ECA52647781}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D0888052-747D-47E5-9C6F-0ECA52647781}.Release|Any CPU.Build.0 = Release|Any CPU - {D0888052-747D-47E5-9C6F-0ECA52647781}.Release|x64.ActiveCfg = Release|Any CPU - {D0888052-747D-47E5-9C6F-0ECA52647781}.Release|x64.Build.0 = Release|Any CPU - {D0888052-747D-47E5-9C6F-0ECA52647781}.Release|x86.ActiveCfg = Release|Any CPU - {D0888052-747D-47E5-9C6F-0ECA52647781}.Release|x86.Build.0 = Release|Any CPU - {4AEF0A7C-503A-44BB-BFE9-D0B83C2ED9D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4AEF0A7C-503A-44BB-BFE9-D0B83C2ED9D8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4AEF0A7C-503A-44BB-BFE9-D0B83C2ED9D8}.Debug|x64.ActiveCfg = Debug|Any CPU - {4AEF0A7C-503A-44BB-BFE9-D0B83C2ED9D8}.Debug|x64.Build.0 = Debug|Any CPU - {4AEF0A7C-503A-44BB-BFE9-D0B83C2ED9D8}.Debug|x86.ActiveCfg = Debug|Any CPU - {4AEF0A7C-503A-44BB-BFE9-D0B83C2ED9D8}.Debug|x86.Build.0 = Debug|Any CPU - {4AEF0A7C-503A-44BB-BFE9-D0B83C2ED9D8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4AEF0A7C-503A-44BB-BFE9-D0B83C2ED9D8}.Release|Any CPU.Build.0 = Release|Any CPU - {4AEF0A7C-503A-44BB-BFE9-D0B83C2ED9D8}.Release|x64.ActiveCfg = Release|Any CPU - {4AEF0A7C-503A-44BB-BFE9-D0B83C2ED9D8}.Release|x64.Build.0 = Release|Any CPU - {4AEF0A7C-503A-44BB-BFE9-D0B83C2ED9D8}.Release|x86.ActiveCfg = Release|Any CPU - {4AEF0A7C-503A-44BB-BFE9-D0B83C2ED9D8}.Release|x86.Build.0 = Release|Any CPU - {D8715588-C2E6-4B90-9B1D-0613649E228B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D8715588-C2E6-4B90-9B1D-0613649E228B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D8715588-C2E6-4B90-9B1D-0613649E228B}.Debug|x64.ActiveCfg = Debug|Any CPU - {D8715588-C2E6-4B90-9B1D-0613649E228B}.Debug|x64.Build.0 = Debug|Any CPU - {D8715588-C2E6-4B90-9B1D-0613649E228B}.Debug|x86.ActiveCfg = Debug|Any CPU - {D8715588-C2E6-4B90-9B1D-0613649E228B}.Debug|x86.Build.0 = Debug|Any CPU - {D8715588-C2E6-4B90-9B1D-0613649E228B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D8715588-C2E6-4B90-9B1D-0613649E228B}.Release|Any CPU.Build.0 = Release|Any CPU - {D8715588-C2E6-4B90-9B1D-0613649E228B}.Release|x64.ActiveCfg = Release|Any CPU - {D8715588-C2E6-4B90-9B1D-0613649E228B}.Release|x64.Build.0 = Release|Any CPU - {D8715588-C2E6-4B90-9B1D-0613649E228B}.Release|x86.ActiveCfg = Release|Any CPU - {D8715588-C2E6-4B90-9B1D-0613649E228B}.Release|x86.Build.0 = Release|Any CPU - {86E04BA4-5837-4782-AD78-04FC3C414E0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {86E04BA4-5837-4782-AD78-04FC3C414E0F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {86E04BA4-5837-4782-AD78-04FC3C414E0F}.Debug|x64.ActiveCfg = Debug|Any CPU - {86E04BA4-5837-4782-AD78-04FC3C414E0F}.Debug|x64.Build.0 = Debug|Any CPU - {86E04BA4-5837-4782-AD78-04FC3C414E0F}.Debug|x86.ActiveCfg = Debug|Any CPU - {86E04BA4-5837-4782-AD78-04FC3C414E0F}.Debug|x86.Build.0 = Debug|Any CPU - {86E04BA4-5837-4782-AD78-04FC3C414E0F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {86E04BA4-5837-4782-AD78-04FC3C414E0F}.Release|Any CPU.Build.0 = Release|Any CPU - {86E04BA4-5837-4782-AD78-04FC3C414E0F}.Release|x64.ActiveCfg = Release|Any CPU - {86E04BA4-5837-4782-AD78-04FC3C414E0F}.Release|x64.Build.0 = Release|Any CPU - {86E04BA4-5837-4782-AD78-04FC3C414E0F}.Release|x86.ActiveCfg = Release|Any CPU - {86E04BA4-5837-4782-AD78-04FC3C414E0F}.Release|x86.Build.0 = Release|Any CPU - {8F61803F-6AC5-4F70-9124-C57C895A20AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8F61803F-6AC5-4F70-9124-C57C895A20AE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8F61803F-6AC5-4F70-9124-C57C895A20AE}.Debug|x64.ActiveCfg = Debug|Any CPU - {8F61803F-6AC5-4F70-9124-C57C895A20AE}.Debug|x64.Build.0 = Debug|Any CPU - {8F61803F-6AC5-4F70-9124-C57C895A20AE}.Debug|x86.ActiveCfg = Debug|Any CPU - {8F61803F-6AC5-4F70-9124-C57C895A20AE}.Debug|x86.Build.0 = Debug|Any CPU - {8F61803F-6AC5-4F70-9124-C57C895A20AE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8F61803F-6AC5-4F70-9124-C57C895A20AE}.Release|Any CPU.Build.0 = Release|Any CPU - {8F61803F-6AC5-4F70-9124-C57C895A20AE}.Release|x64.ActiveCfg = Release|Any CPU - {8F61803F-6AC5-4F70-9124-C57C895A20AE}.Release|x64.Build.0 = Release|Any CPU - {8F61803F-6AC5-4F70-9124-C57C895A20AE}.Release|x86.ActiveCfg = Release|Any CPU - {8F61803F-6AC5-4F70-9124-C57C895A20AE}.Release|x86.Build.0 = Release|Any CPU - {9943384E-DA05-4C70-9773-613754102C5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9943384E-DA05-4C70-9773-613754102C5F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9943384E-DA05-4C70-9773-613754102C5F}.Debug|x64.ActiveCfg = Debug|Any CPU - {9943384E-DA05-4C70-9773-613754102C5F}.Debug|x64.Build.0 = Debug|Any CPU - {9943384E-DA05-4C70-9773-613754102C5F}.Debug|x86.ActiveCfg = Debug|Any CPU - {9943384E-DA05-4C70-9773-613754102C5F}.Debug|x86.Build.0 = Debug|Any CPU - {9943384E-DA05-4C70-9773-613754102C5F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9943384E-DA05-4C70-9773-613754102C5F}.Release|Any CPU.Build.0 = Release|Any CPU - {9943384E-DA05-4C70-9773-613754102C5F}.Release|x64.ActiveCfg = Release|Any CPU - {9943384E-DA05-4C70-9773-613754102C5F}.Release|x64.Build.0 = Release|Any CPU - {9943384E-DA05-4C70-9773-613754102C5F}.Release|x86.ActiveCfg = Release|Any CPU - {9943384E-DA05-4C70-9773-613754102C5F}.Release|x86.Build.0 = Release|Any CPU + {849AD5B1-57E6-4C93-B26A-9DF703CAE67D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {849AD5B1-57E6-4C93-B26A-9DF703CAE67D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {849AD5B1-57E6-4C93-B26A-9DF703CAE67D}.Debug|x64.ActiveCfg = Debug|Any CPU + {849AD5B1-57E6-4C93-B26A-9DF703CAE67D}.Debug|x64.Build.0 = Debug|Any CPU + {849AD5B1-57E6-4C93-B26A-9DF703CAE67D}.Debug|x86.ActiveCfg = Debug|Any CPU + {849AD5B1-57E6-4C93-B26A-9DF703CAE67D}.Debug|x86.Build.0 = Debug|Any CPU + {849AD5B1-57E6-4C93-B26A-9DF703CAE67D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {849AD5B1-57E6-4C93-B26A-9DF703CAE67D}.Release|Any CPU.Build.0 = Release|Any CPU + {849AD5B1-57E6-4C93-B26A-9DF703CAE67D}.Release|x64.ActiveCfg = Release|Any CPU + {849AD5B1-57E6-4C93-B26A-9DF703CAE67D}.Release|x64.Build.0 = Release|Any CPU + {849AD5B1-57E6-4C93-B26A-9DF703CAE67D}.Release|x86.ActiveCfg = Release|Any CPU + {849AD5B1-57E6-4C93-B26A-9DF703CAE67D}.Release|x86.Build.0 = Release|Any CPU + {9C9C5EFB-941B-4B2C-B8AE-0486B79E3256}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C9C5EFB-941B-4B2C-B8AE-0486B79E3256}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C9C5EFB-941B-4B2C-B8AE-0486B79E3256}.Debug|x64.ActiveCfg = Debug|Any CPU + {9C9C5EFB-941B-4B2C-B8AE-0486B79E3256}.Debug|x64.Build.0 = Debug|Any CPU + {9C9C5EFB-941B-4B2C-B8AE-0486B79E3256}.Debug|x86.ActiveCfg = Debug|Any CPU + {9C9C5EFB-941B-4B2C-B8AE-0486B79E3256}.Debug|x86.Build.0 = Debug|Any CPU + {9C9C5EFB-941B-4B2C-B8AE-0486B79E3256}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C9C5EFB-941B-4B2C-B8AE-0486B79E3256}.Release|Any CPU.Build.0 = Release|Any CPU + {9C9C5EFB-941B-4B2C-B8AE-0486B79E3256}.Release|x64.ActiveCfg = Release|Any CPU + {9C9C5EFB-941B-4B2C-B8AE-0486B79E3256}.Release|x64.Build.0 = Release|Any CPU + {9C9C5EFB-941B-4B2C-B8AE-0486B79E3256}.Release|x86.ActiveCfg = Release|Any CPU + {9C9C5EFB-941B-4B2C-B8AE-0486B79E3256}.Release|x86.Build.0 = Release|Any CPU + {FA943FB1-F77B-4C7D-BFD2-26FDC6C62751}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA943FB1-F77B-4C7D-BFD2-26FDC6C62751}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA943FB1-F77B-4C7D-BFD2-26FDC6C62751}.Debug|x64.ActiveCfg = Debug|Any CPU + {FA943FB1-F77B-4C7D-BFD2-26FDC6C62751}.Debug|x64.Build.0 = Debug|Any CPU + {FA943FB1-F77B-4C7D-BFD2-26FDC6C62751}.Debug|x86.ActiveCfg = Debug|Any CPU + {FA943FB1-F77B-4C7D-BFD2-26FDC6C62751}.Debug|x86.Build.0 = Debug|Any CPU + {FA943FB1-F77B-4C7D-BFD2-26FDC6C62751}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA943FB1-F77B-4C7D-BFD2-26FDC6C62751}.Release|Any CPU.Build.0 = Release|Any CPU + {FA943FB1-F77B-4C7D-BFD2-26FDC6C62751}.Release|x64.ActiveCfg = Release|Any CPU + {FA943FB1-F77B-4C7D-BFD2-26FDC6C62751}.Release|x64.Build.0 = Release|Any CPU + {FA943FB1-F77B-4C7D-BFD2-26FDC6C62751}.Release|x86.ActiveCfg = Release|Any CPU + {FA943FB1-F77B-4C7D-BFD2-26FDC6C62751}.Release|x86.Build.0 = Release|Any CPU + {22A8AA93-D63F-41EC-B729-595FE817E525}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22A8AA93-D63F-41EC-B729-595FE817E525}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22A8AA93-D63F-41EC-B729-595FE817E525}.Debug|x64.ActiveCfg = Debug|Any CPU + {22A8AA93-D63F-41EC-B729-595FE817E525}.Debug|x64.Build.0 = Debug|Any CPU + {22A8AA93-D63F-41EC-B729-595FE817E525}.Debug|x86.ActiveCfg = Debug|Any CPU + {22A8AA93-D63F-41EC-B729-595FE817E525}.Debug|x86.Build.0 = Debug|Any CPU + {22A8AA93-D63F-41EC-B729-595FE817E525}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22A8AA93-D63F-41EC-B729-595FE817E525}.Release|Any CPU.Build.0 = Release|Any CPU + {22A8AA93-D63F-41EC-B729-595FE817E525}.Release|x64.ActiveCfg = Release|Any CPU + {22A8AA93-D63F-41EC-B729-595FE817E525}.Release|x64.Build.0 = Release|Any CPU + {22A8AA93-D63F-41EC-B729-595FE817E525}.Release|x86.ActiveCfg = Release|Any CPU + {22A8AA93-D63F-41EC-B729-595FE817E525}.Release|x86.Build.0 = Release|Any CPU + {BC106469-CD50-49E5-9CBB-FFC67BD8E2B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC106469-CD50-49E5-9CBB-FFC67BD8E2B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC106469-CD50-49E5-9CBB-FFC67BD8E2B7}.Debug|x64.ActiveCfg = Debug|Any CPU + {BC106469-CD50-49E5-9CBB-FFC67BD8E2B7}.Debug|x64.Build.0 = Debug|Any CPU + {BC106469-CD50-49E5-9CBB-FFC67BD8E2B7}.Debug|x86.ActiveCfg = Debug|Any CPU + {BC106469-CD50-49E5-9CBB-FFC67BD8E2B7}.Debug|x86.Build.0 = Debug|Any CPU + {BC106469-CD50-49E5-9CBB-FFC67BD8E2B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC106469-CD50-49E5-9CBB-FFC67BD8E2B7}.Release|Any CPU.Build.0 = Release|Any CPU + {BC106469-CD50-49E5-9CBB-FFC67BD8E2B7}.Release|x64.ActiveCfg = Release|Any CPU + {BC106469-CD50-49E5-9CBB-FFC67BD8E2B7}.Release|x64.Build.0 = Release|Any CPU + {BC106469-CD50-49E5-9CBB-FFC67BD8E2B7}.Release|x86.ActiveCfg = Release|Any CPU + {BC106469-CD50-49E5-9CBB-FFC67BD8E2B7}.Release|x86.Build.0 = Release|Any CPU + {40F03B32-AAA9-45F6-989E-2044CB61EB43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {40F03B32-AAA9-45F6-989E-2044CB61EB43}.Debug|Any CPU.Build.0 = Debug|Any CPU + {40F03B32-AAA9-45F6-989E-2044CB61EB43}.Debug|x64.ActiveCfg = Debug|Any CPU + {40F03B32-AAA9-45F6-989E-2044CB61EB43}.Debug|x64.Build.0 = Debug|Any CPU + {40F03B32-AAA9-45F6-989E-2044CB61EB43}.Debug|x86.ActiveCfg = Debug|Any CPU + {40F03B32-AAA9-45F6-989E-2044CB61EB43}.Debug|x86.Build.0 = Debug|Any CPU + {40F03B32-AAA9-45F6-989E-2044CB61EB43}.Release|Any CPU.ActiveCfg = Release|Any CPU + {40F03B32-AAA9-45F6-989E-2044CB61EB43}.Release|Any CPU.Build.0 = Release|Any CPU + {40F03B32-AAA9-45F6-989E-2044CB61EB43}.Release|x64.ActiveCfg = Release|Any CPU + {40F03B32-AAA9-45F6-989E-2044CB61EB43}.Release|x64.Build.0 = Release|Any CPU + {40F03B32-AAA9-45F6-989E-2044CB61EB43}.Release|x86.ActiveCfg = Release|Any CPU + {40F03B32-AAA9-45F6-989E-2044CB61EB43}.Release|x86.Build.0 = Release|Any CPU + {1913E4BD-9EBE-4032-9569-10FCFF7D5F0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1913E4BD-9EBE-4032-9569-10FCFF7D5F0A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1913E4BD-9EBE-4032-9569-10FCFF7D5F0A}.Debug|x64.ActiveCfg = Debug|Any CPU + {1913E4BD-9EBE-4032-9569-10FCFF7D5F0A}.Debug|x64.Build.0 = Debug|Any CPU + {1913E4BD-9EBE-4032-9569-10FCFF7D5F0A}.Debug|x86.ActiveCfg = Debug|Any CPU + {1913E4BD-9EBE-4032-9569-10FCFF7D5F0A}.Debug|x86.Build.0 = Debug|Any CPU + {1913E4BD-9EBE-4032-9569-10FCFF7D5F0A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1913E4BD-9EBE-4032-9569-10FCFF7D5F0A}.Release|Any CPU.Build.0 = Release|Any CPU + {1913E4BD-9EBE-4032-9569-10FCFF7D5F0A}.Release|x64.ActiveCfg = Release|Any CPU + {1913E4BD-9EBE-4032-9569-10FCFF7D5F0A}.Release|x64.Build.0 = Release|Any CPU + {1913E4BD-9EBE-4032-9569-10FCFF7D5F0A}.Release|x86.ActiveCfg = Release|Any CPU + {1913E4BD-9EBE-4032-9569-10FCFF7D5F0A}.Release|x86.Build.0 = Release|Any CPU + {30B21D7F-E10F-40E6-B356-F787BE3FD1B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30B21D7F-E10F-40E6-B356-F787BE3FD1B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30B21D7F-E10F-40E6-B356-F787BE3FD1B4}.Debug|x64.ActiveCfg = Debug|Any CPU + {30B21D7F-E10F-40E6-B356-F787BE3FD1B4}.Debug|x64.Build.0 = Debug|Any CPU + {30B21D7F-E10F-40E6-B356-F787BE3FD1B4}.Debug|x86.ActiveCfg = Debug|Any CPU + {30B21D7F-E10F-40E6-B356-F787BE3FD1B4}.Debug|x86.Build.0 = Debug|Any CPU + {30B21D7F-E10F-40E6-B356-F787BE3FD1B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30B21D7F-E10F-40E6-B356-F787BE3FD1B4}.Release|Any CPU.Build.0 = Release|Any CPU + {30B21D7F-E10F-40E6-B356-F787BE3FD1B4}.Release|x64.ActiveCfg = Release|Any CPU + {30B21D7F-E10F-40E6-B356-F787BE3FD1B4}.Release|x64.Build.0 = Release|Any CPU + {30B21D7F-E10F-40E6-B356-F787BE3FD1B4}.Release|x86.ActiveCfg = Release|Any CPU + {30B21D7F-E10F-40E6-B356-F787BE3FD1B4}.Release|x86.Build.0 = Release|Any CPU + {B5C65510-B04C-4F09-B15E-AB98E675F70C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5C65510-B04C-4F09-B15E-AB98E675F70C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5C65510-B04C-4F09-B15E-AB98E675F70C}.Debug|x64.ActiveCfg = Debug|Any CPU + {B5C65510-B04C-4F09-B15E-AB98E675F70C}.Debug|x64.Build.0 = Debug|Any CPU + {B5C65510-B04C-4F09-B15E-AB98E675F70C}.Debug|x86.ActiveCfg = Debug|Any CPU + {B5C65510-B04C-4F09-B15E-AB98E675F70C}.Debug|x86.Build.0 = Debug|Any CPU + {B5C65510-B04C-4F09-B15E-AB98E675F70C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5C65510-B04C-4F09-B15E-AB98E675F70C}.Release|Any CPU.Build.0 = Release|Any CPU + {B5C65510-B04C-4F09-B15E-AB98E675F70C}.Release|x64.ActiveCfg = Release|Any CPU + {B5C65510-B04C-4F09-B15E-AB98E675F70C}.Release|x64.Build.0 = Release|Any CPU + {B5C65510-B04C-4F09-B15E-AB98E675F70C}.Release|x86.ActiveCfg = Release|Any CPU + {B5C65510-B04C-4F09-B15E-AB98E675F70C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {04126612-3F71-434B-967B-5CADA3A27AA8} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {D0888052-747D-47E5-9C6F-0ECA52647781} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {4AEF0A7C-503A-44BB-BFE9-D0B83C2ED9D8} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {D8715588-C2E6-4B90-9B1D-0613649E228B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {86E04BA4-5837-4782-AD78-04FC3C414E0F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {8F61803F-6AC5-4F70-9124-C57C895A20AE} = {0AB3BF05-4346-4AA6-1389-037BE0695223} - {9943384E-DA05-4C70-9773-613754102C5F} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {849AD5B1-57E6-4C93-B26A-9DF703CAE67D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {9C9C5EFB-941B-4B2C-B8AE-0486B79E3256} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {FA943FB1-F77B-4C7D-BFD2-26FDC6C62751} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {22A8AA93-D63F-41EC-B729-595FE817E525} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {BC106469-CD50-49E5-9CBB-FFC67BD8E2B7} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {40F03B32-AAA9-45F6-989E-2044CB61EB43} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {1913E4BD-9EBE-4032-9569-10FCFF7D5F0A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {30B21D7F-E10F-40E6-B356-F787BE3FD1B4} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {B5C65510-B04C-4F09-B15E-AB98E675F70C} = {0AB3BF05-4346-4AA6-1389-037BE0695223} EndGlobalSection EndGlobal diff --git a/backend/README.md b/backend/README.md index 09a268a..ec6c920 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,21 +1,61 @@ -# E-Shop 后端 +# E-Shop 后端基础设施 -当前目录提供 .NET 10 模块化单体的共享分层骨架: +本目录是冻结需求、流程、接口和数据库设计的代码承载层。当前阶段只提供 +`.NET 10` 模块化单体的技术骨架,不包含任何业务实体、业务 DTO、业务端点、 +业务 Worker、Migration 或种子数据。 -- `Mall.Domain`:领域模型与领域规则; -- `Mall.Application`:应用用例与公开应用契约; -- `Mall.Infrastructure`:数据库及外部基础设施实现; -- `Mall.Api`:HTTP 组合根、ProblemDetails、OpenAPI 与健康检查; -- `Mall.Worker`:后台任务组合根; -- `Mall.UnitTests`、`Mall.IntegrationTests`:测试入口。 +## 项目边界 + +- `Mall.Domain`:领域模型归属层;当前仅有程序集标识。 +- `Mall.Application`:应用用例归属层;当前仅有程序集标识。 +- `Mall.Infrastructure`:PostgreSQL、Redis、RabbitMQ、S3 兼容对象存储、 + 日志、遥测和运行配置的统一接入点。 +- `Mall.Api`:HTTP 组合根,已实现公共管道及 A506/A507。 +- `Mall.Worker`:后台任务宿主;当前不注册业务任务。 +- `Mall.Migrator`:唯一允许执行 EF Core Migration 的一次性宿主。 +- `Mall.AppHost`:本地开发编排,不作为 C10 现场验收入口。 + +不为十个业务模块预建空项目或空目录。首个真实用例落地时,在既有四层中 +按模块纵向加入代码,并显式注册依赖与端点。 + +## 本地命令 ```powershell dotnet restore Mall.sln -dotnet build Mall.sln --no-restore -dotnet test Mall.sln --no-build -dotnet run --project src/Mall.Api +dotnet build Mall.sln -c Release --no-restore +dotnet test Mall.sln -c Release --no-build +dotnet format Mall.sln --verify-no-changes --no-restore +``` + +运行 API: + +```powershell +dotnet run --project src/Mall.Api/Mall.Api.csproj ``` -API 当前只提供 `/health/live` 和 `/health/ready`。业务端点、实体、EF Core 映射与 Migration 必须等对应接口和数据库设计评审后再加入。 +- OpenAPI:`/openapi/v1.json` +- Swagger UI(Development):`/swagger` +- 存活:`/health/live` +- 就绪:`/health/ready` + +当前尚未创建数据库设计规定的统一 `InitialEshopSchema`。因此 Migrator 会 +明确失败,A507 会返回 `503 notReady`;这属于真实门禁,不是故障占位。业务 +实体、Mapping 和完整初始 Migration 一次性准备完毕后,才能改变该状态。 + +## 配置与秘密 + +仓库只保存无秘密默认值。连接字符串、JWT 签名材料、对象存储凭据和预期认证 +摘要必须通过用户机环境变量或受控部署 Secret 注入,禁止写入 +`appsettings*.json`。关键环境变量使用 .NET 双下划线层级写法,例如: + +```text +ConnectionStrings__Postgres +Authentication__SigningKey +Authentication__ExpectedDigest +Deployment__Version +Deployment__ExpectedVersion +Deployment__InstanceId +``` -`Mall.AppHost` 将在 Aspire 项目模板可用并锁定版本后补充;当前不使用普通控制台项目冒充 AppHost。 +API 与 Worker 不调用 `Database.Migrate()`;只有 `Mall.Migrator` 可以执行 +Migration。 diff --git a/backend/global.json b/backend/global.json new file mode 100644 index 0000000..a0e9d0e --- /dev/null +++ b/backend/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.301", + "rollForward": "latestPatch", + "allowPrerelease": false + } +} diff --git a/backend/src/Mall.Api/Authentication/AuthorizationPolicies.cs b/backend/src/Mall.Api/Authentication/AuthorizationPolicies.cs new file mode 100644 index 0000000..fc3648b --- /dev/null +++ b/backend/src/Mall.Api/Authentication/AuthorizationPolicies.cs @@ -0,0 +1,8 @@ +namespace Mall.Api.Authentication; + +public static class AuthorizationPolicies +{ + public const string BuyerOnly = nameof(BuyerOnly); + public const string MerchantOnly = nameof(MerchantOnly); + public const string AdminOnly = nameof(AdminOnly); +} diff --git a/backend/src/Mall.Api/DependencyInjection.cs b/backend/src/Mall.Api/DependencyInjection.cs new file mode 100644 index 0000000..418b4e9 --- /dev/null +++ b/backend/src/Mall.Api/DependencyInjection.cs @@ -0,0 +1,147 @@ +using System.Diagnostics; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Mall.Api.Authentication; +using Mall.Api.Health; +using Mall.Infrastructure.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.IdentityModel.Tokens; + +namespace Mall.Api; + +public static class DependencyInjection +{ + public const string BrowserCorsPolicy = "BrowserCors"; + + public static IServiceCollection AddApiFoundation( + this IServiceCollection services, + IConfiguration configuration) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configuration); + + services.AddSingleton(TimeProvider.System); + services.AddScoped(); + + services.ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.SerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + options.SerializerOptions.Converters.Add( + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); + }); + + services.AddProblemDetails(options => + { + options.CustomizeProblemDetails = context => + { + context.ProblemDetails.Extensions.TryAdd( + "traceId", + Activity.Current?.Id ?? context.HttpContext.TraceIdentifier); + }; + }); + + services.Configure(options => + { + options.ForwardedHeaders = + ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + options.ForwardLimit = 1; + options.KnownIPNetworks.Clear(); + options.KnownProxies.Clear(); + }); + + ConfigureCors(services, configuration); + ConfigureAuthentication(services, configuration); + services.AddOpenApi("v1"); + + return services; + } + + private static void ConfigureCors( + IServiceCollection services, + IConfiguration configuration) + { + var allowedOrigins = configuration + .GetSection("Cors:AllowedOrigins") + .Get() + ?.Where(origin => Uri.TryCreate(origin, UriKind.Absolute, out _)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray() ?? []; + + services.AddCors(options => + { + options.AddPolicy( + BrowserCorsPolicy, + policy => + { + if (allowedOrigins.Length > 0) + { + policy + .WithOrigins(allowedOrigins) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + } + }); + }); + } + + private static void ConfigureAuthentication( + IServiceCollection services, + IConfiguration configuration) + { + var security = configuration + .GetSection(AuthenticationSecurityOptions.SectionName) + .Get() ?? new AuthenticationSecurityOptions(); + var http = configuration + .GetSection($"{AuthenticationSecurityOptions.SectionName}:HttpBearer") + .Get() ?? new AuthenticationContractOptions(); + SecurityKey? signingKey = string.IsNullOrEmpty(security.SigningKey) + ? null + : new SymmetricSecurityKey(Encoding.UTF8.GetBytes(security.SigningKey)); + + services + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.MapInboundClaims = false; + options.RequireHttpsMetadata = true; + options.SaveToken = false; + options.TokenValidationParameters = new TokenValidationParameters + { + RequireExpirationTime = true, + RequireSignedTokens = true, + ValidateAudience = true, + ValidAudience = http.Audience, + ValidateIssuer = true, + ValidIssuer = http.Issuer, + ValidateIssuerSigningKey = true, + IssuerSigningKey = signingKey, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero, + NameClaimType = "sub", + RoleClaimType = ClaimTypes.Role, + }; + }); + + var authenticatedFallback = new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .Build(); + services + .AddAuthorizationBuilder() + .SetDefaultPolicy(authenticatedFallback) + .AddPolicy( + AuthorizationPolicies.BuyerOnly, + policy => policy.RequireRole("Buyer")) + .AddPolicy( + AuthorizationPolicies.MerchantOnly, + policy => policy.RequireRole("Merchant")) + .AddPolicy( + AuthorizationPolicies.AdminOnly, + policy => policy.RequireRole("Admin")); + } +} diff --git a/backend/src/Mall.Api/Health/HealthContracts.cs b/backend/src/Mall.Api/Health/HealthContracts.cs new file mode 100644 index 0000000..12ba3cb --- /dev/null +++ b/backend/src/Mall.Api/Health/HealthContracts.cs @@ -0,0 +1,37 @@ +namespace Mall.Api.Health; + +public sealed record HealthStatusResponse( + string Status, + string Service, + string InstanceId, + DateTimeOffset CheckedAt); + +public sealed record ReadinessStatusResponse( + string Status, + string Service, + string InstanceId, + string Version, + DateTimeOffset CheckedAt, + GlobalGatesResponse GlobalGates, + AuthenticationConfigurationResponse AuthenticationConfiguration, + CapabilitiesResponse Capabilities); + +public sealed record GlobalGatesResponse( + string SecureConfiguration, + string RuntimeVersion, + string MigrationVersion, + string Postgres); + +public sealed record AuthenticationConfigurationResponse( + string DigestAlgorithm, + string? ExpectedDigest, + string? HttpDigest, + string? HubDigest, + bool MatchesExpected); + +public sealed record CapabilitiesResponse( + string CatalogCache, + string ProtectedAuthentication, + string RealTimeMessaging, + string OutboxDelivery, + string ObjectWrites); diff --git a/backend/src/Mall.Api/Health/HealthEndpointExtensions.cs b/backend/src/Mall.Api/Health/HealthEndpointExtensions.cs new file mode 100644 index 0000000..421bce6 --- /dev/null +++ b/backend/src/Mall.Api/Health/HealthEndpointExtensions.cs @@ -0,0 +1,63 @@ +using Mall.Infrastructure.Configuration; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.Extensions.Options; + +namespace Mall.Api.Health; + +public static class HealthEndpointExtensions +{ + public static IEndpointRouteBuilder MapFoundationHealthEndpoints( + this IEndpointRouteBuilder endpoints) + { + endpoints.MapGet( + "/health/live", + Ok ( + HttpContext context, + IOptions deploymentOptions, + TimeProvider timeProvider) => + { + context.Response.Headers.CacheControl = "no-store"; + var deployment = deploymentOptions.Value; + return TypedResults.Ok( + new HealthStatusResponse( + "healthy", + PublicValue(deployment.ServiceName, "mall-api"), + PublicValue(deployment.InstanceId, "unassigned"), + timeProvider.GetUtcNow())); + }) + .AllowAnonymous() + .WithName("Infrastructure_GetLiveness") + .WithTags("Infrastructure") + .WithSummary("API 存活检查") + .Produces(StatusCodes.Status200OK); + + endpoints.MapGet( + "/health/ready", + async Task ( + HttpContext context, + IReadinessSnapshotProvider snapshotProvider, + CancellationToken cancellationToken) => + { + context.Response.Headers.CacheControl = "no-store"; + var snapshot = await snapshotProvider.GetSnapshotAsync(cancellationToken); + var statusCode = snapshot.Status == "ready" + ? StatusCodes.Status200OK + : StatusCodes.Status503ServiceUnavailable; + return Results.Json(snapshot, statusCode: statusCode); + }) + .AllowAnonymous() + .WithName("Infrastructure_GetReadiness") + .WithTags("Infrastructure") + .WithSummary("API 就绪检查") + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status503ServiceUnavailable); + + return endpoints; + } + + private static string PublicValue(string value, string fallback) + { + var normalized = value.Trim(); + return normalized.Length is > 0 and <= 128 ? normalized : fallback; + } +} diff --git a/backend/src/Mall.Api/Health/IReadinessSnapshotProvider.cs b/backend/src/Mall.Api/Health/IReadinessSnapshotProvider.cs new file mode 100644 index 0000000..0874bc3 --- /dev/null +++ b/backend/src/Mall.Api/Health/IReadinessSnapshotProvider.cs @@ -0,0 +1,6 @@ +namespace Mall.Api.Health; + +public interface IReadinessSnapshotProvider +{ + Task GetSnapshotAsync(CancellationToken cancellationToken); +} diff --git a/backend/src/Mall.Api/Health/ReadinessSnapshotProvider.cs b/backend/src/Mall.Api/Health/ReadinessSnapshotProvider.cs new file mode 100644 index 0000000..7e26da5 --- /dev/null +++ b/backend/src/Mall.Api/Health/ReadinessSnapshotProvider.cs @@ -0,0 +1,213 @@ +using Amazon.S3; +using Amazon.S3.Model; +using Mall.Infrastructure.Authentication; +using Mall.Infrastructure.Caching; +using Mall.Infrastructure.Configuration; +using Mall.Infrastructure.Integration; +using Mall.Infrastructure.ObjectStorage; +using Mall.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using RabbitMQ.Client; +using StackExchange.Redis; + +namespace Mall.Api.Health; + +internal sealed class ReadinessSnapshotProvider( + IConfiguration configuration, + IServiceProvider serviceProvider, + IAuthenticationConfigurationProvider authenticationConfigurationProvider, + IOptions deploymentOptions, + IOptions redisOptions, + IOptions rabbitMqOptions, + IOptions objectStorageOptions, + TimeProvider timeProvider) + : IReadinessSnapshotProvider +{ + public async Task GetSnapshotAsync(CancellationToken cancellationToken) + { + var deployment = deploymentOptions.Value; + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeout.CancelAfter(TimeSpan.FromMilliseconds( + Math.Clamp(deployment.ProbeTimeoutMilliseconds, 250, 5000))); + + var authentication = authenticationConfigurationProvider.GetSnapshot(); + var runtimeHealthy = IsRuntimeVersionHealthy(deployment); + var (postgresHealthy, migrationHealthy) = + await ProbePostgresAsync(deployment.TargetMigration, timeout.Token); + + var redisHealthy = await ProbeRedisAsync(timeout.Token); + var rabbitMqHealthy = await ProbeRabbitMqAsync(timeout.Token); + var objectStorageHealthy = await ProbeObjectStorageAsync(timeout.Token); + + var secureConfigurationHealthy = + authentication.MatchesExpected && authentication.HasSecureSigningMaterial; + var ready = secureConfigurationHealthy + && runtimeHealthy + && migrationHealthy + && postgresHealthy; + + return new ReadinessStatusResponse( + ready ? "ready" : "notReady", + NormalizePublicValue(deployment.ServiceName, "mall-api"), + NormalizePublicValue(deployment.InstanceId, "unassigned"), + NormalizePublicValue(deployment.Version, "unknown"), + timeProvider.GetUtcNow(), + new GlobalGatesResponse( + HealthWord(secureConfigurationHealthy), + HealthWord(runtimeHealthy), + HealthWord(migrationHealthy), + HealthWord(postgresHealthy)), + new AuthenticationConfigurationResponse( + authentication.DigestAlgorithm, + authentication.ExpectedDigest, + authentication.HttpDigest, + authentication.HubDigest, + authentication.MatchesExpected), + new CapabilitiesResponse( + Capability(redisOptions.Value.Enabled, redisHealthy, "available", "fallback"), + redisOptions.Value.Enabled && redisHealthy ? "failClosed" : "disabled", + "disabled", + Capability(rabbitMqOptions.Value.Enabled, rabbitMqHealthy, "available", "paused"), + Capability(objectStorageOptions.Value.Enabled, objectStorageHealthy, "available", "disabled"))); + } + + private async Task<(bool PostgresHealthy, bool MigrationHealthy)> ProbePostgresAsync( + string targetMigration, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(configuration.GetConnectionString("Postgres")) + || string.IsNullOrWhiteSpace(targetMigration)) + { + return (false, false); + } + + try + { + await using var scope = serviceProvider.CreateAsyncScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + if (!await dbContext.Database.CanConnectAsync(cancellationToken)) + { + return (false, false); + } + + var appliedMigrations = await dbContext.Database + .GetAppliedMigrationsAsync(cancellationToken); + var migrationHealthy = appliedMigrations.Contains( + targetMigration.Trim(), + StringComparer.Ordinal); + return (true, migrationHealthy); + } + catch (Exception exception) when (IsDependencyFailure(exception)) + { + return (false, false); + } + } + + private async Task ProbeRedisAsync(CancellationToken cancellationToken) + { + if (!redisOptions.Value.Enabled) + { + return false; + } + + var connectionString = configuration.GetConnectionString("Redis"); + if (string.IsNullOrWhiteSpace(connectionString)) + { + return false; + } + + try + { + using var multiplexer = await ConnectionMultiplexer + .ConnectAsync(ConfigurationOptions.Parse(connectionString)) + .WaitAsync(cancellationToken); + await multiplexer.GetDatabase().PingAsync().WaitAsync(cancellationToken); + return true; + } + catch (Exception exception) when (IsDependencyFailure(exception)) + { + return false; + } + } + + private async Task ProbeRabbitMqAsync(CancellationToken cancellationToken) + { + if (!rabbitMqOptions.Value.Enabled) + { + return false; + } + + var connectionFactory = serviceProvider.GetService(); + if (connectionFactory is null) + { + return false; + } + + try + { + await using var connection = + await connectionFactory.CreateConnectionAsync(cancellationToken); + return connection.IsOpen; + } + catch (Exception exception) when (IsDependencyFailure(exception)) + { + return false; + } + } + + private async Task ProbeObjectStorageAsync(CancellationToken cancellationToken) + { + var options = objectStorageOptions.Value; + if (!options.Enabled || string.IsNullOrWhiteSpace(options.BucketName)) + { + return false; + } + + var client = serviceProvider.GetService(); + if (client is null) + { + return false; + } + + try + { + await client.GetBucketLocationAsync( + new GetBucketLocationRequest { BucketName = options.BucketName }, + cancellationToken); + return true; + } + catch (Exception exception) when (IsDependencyFailure(exception)) + { + return false; + } + } + + private static bool IsRuntimeVersionHealthy(DeploymentOptions deployment) => + !string.IsNullOrWhiteSpace(deployment.Version) + && !string.Equals(deployment.Version, "unknown", StringComparison.OrdinalIgnoreCase) + && string.Equals( + deployment.Version.Trim(), + deployment.ExpectedVersion.Trim(), + StringComparison.Ordinal); + + private static string HealthWord(bool healthy) => healthy ? "healthy" : "unhealthy"; + + private static string Capability( + bool enabled, + bool healthy, + string healthyValue, + string unhealthyValue) => + !enabled ? "disabled" : healthy ? healthyValue : unhealthyValue; + + private static string NormalizePublicValue(string value, string fallback) + { + var normalized = value.Trim(); + return normalized.Length is > 0 and <= 128 ? normalized : fallback; + } + + private static bool IsDependencyFailure(Exception exception) => + exception is not OutOfMemoryException + and not StackOverflowException + and not AccessViolationException; +} diff --git a/backend/src/Mall.Api/Mall.Api.csproj b/backend/src/Mall.Api/Mall.Api.csproj index 4b123b0..cfa1c4e 100644 --- a/backend/src/Mall.Api/Mall.Api.csproj +++ b/backend/src/Mall.Api/Mall.Api.csproj @@ -1,19 +1,13 @@ - - - net10.0 - enable - enable - - - - - - - + + + + + + diff --git a/backend/src/Mall.Api/Mall.Api.http b/backend/src/Mall.Api/Mall.Api.http index 9100c06..cc5ebed 100644 --- a/backend/src/Mall.Api/Mall.Api.http +++ b/backend/src/Mall.Api/Mall.Api.http @@ -1,11 +1,14 @@ -@Mall.Api_HostAddress = http://localhost:5210 +@Mall_Api_HostAddress = http://localhost:5000 -GET {{Mall.Api_HostAddress}}/health/live +GET {{Mall_Api_HostAddress}}/health/live Accept: application/json ### -GET {{Mall.Api_HostAddress}}/health/ready +GET {{Mall_Api_HostAddress}}/health/ready Accept: application/json ### + +GET {{Mall_Api_HostAddress}}/openapi/v1.json +Accept: application/json diff --git a/backend/src/Mall.Api/Program.cs b/backend/src/Mall.Api/Program.cs index 0eb4503..9a9d3dc 100644 --- a/backend/src/Mall.Api/Program.cs +++ b/backend/src/Mall.Api/Program.cs @@ -1,21 +1,79 @@ +using System.Diagnostics; +using Mall.Api; +using Mall.Api.Health; +using Mall.Infrastructure; +using Mall.Infrastructure.Observability; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.WebUtilities; +using Serilog; + var builder = WebApplication.CreateBuilder(args); -builder.Services.AddProblemDetails(); -builder.Services.AddOpenApi(); -builder.Services.AddHealthChecks(); +builder.AddFoundationObservability(includeAspNetCoreInstrumentation: true); +builder.Services + .AddFoundationInfrastructure(builder.Configuration) + .AddApiFoundation(builder.Configuration); var app = builder.Build(); +app.UseForwardedHeaders(); app.UseExceptionHandler(); +app.UseSerilogRequestLogging(options => +{ + options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => + { + diagnosticContext.Set( + "TraceId", + Activity.Current?.Id ?? httpContext.TraceIdentifier); + }; +}); -if (app.Environment.IsDevelopment()) +if (!app.Environment.IsDevelopment()) { - app.MapOpenApi(); + app.UseHsts(); + app.UseHttpsRedirection(); } -app.MapHealthChecks("/health/live"); -app.MapHealthChecks("/health/ready"); +app.UseStatusCodePages(async statusCodeContext => +{ + var response = statusCodeContext.HttpContext.Response; + if (response.HasStarted) + { + return; + } + + var problem = new ProblemDetails + { + Status = response.StatusCode, + Title = ReasonPhrases.GetReasonPhrase(response.StatusCode), + Type = $"https://httpstatuses.com/{response.StatusCode}", + }; + problem.Extensions["traceId"] = + Activity.Current?.Id ?? statusCodeContext.HttpContext.TraceIdentifier; + await response.WriteAsJsonAsync( + problem, + options: null, + contentType: "application/problem+json", + cancellationToken: statusCodeContext.HttpContext.RequestAborted); +}); + +app.UseCors(Mall.Api.DependencyInjection.BrowserCorsPolicy); +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapFoundationHealthEndpoints(); +app.MapOpenApi("/openapi/{documentName}.json").AllowAnonymous(); + +if (app.Configuration.GetValue("OpenApi:EnableSwaggerUi")) +{ + app.UseSwaggerUI(options => + { + options.DocumentTitle = "E-Shop API"; + options.SwaggerEndpoint("/openapi/v1.json", "E-Shop API v1"); + options.RoutePrefix = "swagger"; + }); +} -app.Run(); +await app.RunAsync(); public partial class Program; diff --git a/backend/src/Mall.Api/Properties/launchSettings.json b/backend/src/Mall.Api/Properties/launchSettings.json index 866b768..33ab10a 100644 --- a/backend/src/Mall.Api/Properties/launchSettings.json +++ b/backend/src/Mall.Api/Properties/launchSettings.json @@ -1,11 +1,22 @@ -{ +{ "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "http://localhost:5210", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7000;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/backend/src/Mall.Api/appsettings.Development.json b/backend/src/Mall.Api/appsettings.Development.json index 0c208ae..c539b68 100644 --- a/backend/src/Mall.Api/appsettings.Development.json +++ b/backend/src/Mall.Api/appsettings.Development.json @@ -1,8 +1,15 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } + "Cors": { + "AllowedOrigins": [ + "http://localhost:5173" + ] + }, + "Deployment": { + "InstanceId": "api-local", + "Version": "local-dev", + "ExpectedVersion": "local-dev" + }, + "OpenApi": { + "EnableSwaggerUi": true } } diff --git a/backend/src/Mall.Api/appsettings.json b/backend/src/Mall.Api/appsettings.json index 10f68b8..72d9065 100644 --- a/backend/src/Mall.Api/appsettings.json +++ b/backend/src/Mall.Api/appsettings.json @@ -1,9 +1,63 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "AllowedHosts": "*", + "Authentication": { + "SigningKey": "", + "ExpectedDigest": "", + "HttpBearer": { + "Issuer": "", + "Audience": "", + "KeyFingerprint": "", + "AccessTokenLifetimeSeconds": 7200, + "ClockSkewSeconds": 0, + "TokenVersionValidationRule": "" + }, + "Hub": { + "Issuer": "", + "Audience": "", + "KeyFingerprint": "", + "AccessTokenLifetimeSeconds": 7200, + "ClockSkewSeconds": 0, + "TokenVersionValidationRule": "" } }, - "AllowedHosts": "*" + "ConnectionStrings": { + "Postgres": "", + "Redis": "", + "RabbitMq": "" + }, + "Cors": { + "AllowedOrigins": [] + }, + "Deployment": { + "ServiceName": "mall-api", + "InstanceId": "unassigned", + "Version": "unknown", + "ExpectedVersion": "", + "TargetMigration": "InitialEshopSchema", + "ProbeTimeoutMilliseconds": 1500 + }, + "Infrastructure": { + "Redis": { + "Enabled": false, + "ProbeTimeoutMilliseconds": 1500 + }, + "RabbitMq": { + "Enabled": false, + "ProbeTimeoutMilliseconds": 1500 + }, + "ObjectStorage": { + "Enabled": false, + "ServiceUrl": "", + "AccessKey": "", + "SecretKey": "", + "BucketName": "", + "ProbeTimeoutMilliseconds": 1500 + } + }, + "Observability": { + "OtlpEndpoint": "" + }, + "OpenApi": { + "EnableSwaggerUi": false + } } diff --git a/backend/src/Mall.Api/packages.lock.json b/backend/src/Mall.Api/packages.lock.json new file mode 100644 index 0000000..2066960 --- /dev/null +++ b/backend/src/Mall.Api/packages.lock.json @@ -0,0 +1,363 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "type": "Direct", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "VAcqS42zb9WJd9DjPdkVTS5YrQENmNzPNJuRu8VAW7x3TEWUipc4d4hHzVJdFB0h/KLdr4XcXZzRHcUOKVanMQ==", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.19.2" + } + }, + "Microsoft.AspNetCore.OpenApi": { + "type": "Direct", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "d4Atx9IHq7JgX0F/h7Db+m9zAUzC+cKdI9k+OWnnyQIOUQtfvjIEuhvbjPigVMkAmPUgCbJ8Yp6M9ghUqHtJSQ==", + "dependencies": { + "Microsoft.OpenApi": "2.0.0" + } + }, + "Serilog.AspNetCore": { + "type": "Direct", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", + "dependencies": { + "Serilog": "4.3.0", + "Serilog.Extensions.Hosting": "10.0.0", + "Serilog.Formatting.Compact": "3.0.0", + "Serilog.Settings.Configuration": "10.0.0", + "Serilog.Sinks.Console": "6.1.1", + "Serilog.Sinks.Debug": "3.0.0", + "Serilog.Sinks.File": "7.0.0" + } + }, + "Swashbuckle.AspNetCore.SwaggerUI": { + "type": "Direct", + "requested": "[10.2.3, )", + "resolved": "10.2.3", + "contentHash": "nthWONRs/FJ4yyG206g1cC52WEG8EqrjuMWjGdR+5XG7lbjFto6NqcI9EMICgVFom/UivIjUVwI76ZHbHwTPfQ==" + }, + "AWSSDK.Core": { + "type": "Transitive", + "resolved": "4.0.100.8", + "contentHash": "xnuBVLQBmYQXsDZJ9mq2UDSFZm3xgO5oUb4/UR8p0UO7tG6heDhsLLI1NzZhIVAKyfW0tXg9hn9a/ek018TOVw==" + }, + "Microsoft.Bcl.Cryptography": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "LG9Yll3B5aNpxv0+D47g6LiOiKBIlodhcHdQwcYzo8VeexFLGqx5ymetmA2aBRyo9cCcWsQWrFsdbsr8LvmWDw==" + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "bOzrFCl6uZCjaSh2bG1ToRQRdx+iXvxosCg9hFyG9OWeAzOFI4xev9OqKeWfKf/kAHyox2JnbcvLVf2ceA7sqA==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "2gLDordUCGf3aNOOuqtTbP5mxhiP9nk6TnvGiE3RnqT891O+Zf/qKu1PIREubs1M16A0SImr4vULBfU5BTDs1Q==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "RFYJR7APio/BiqdQunRq6DB+nDB6nc2qhHr77mlvZ0q0BT8PubMXN7XicmfzCbrDE/dzhBnUKBRXLTcqUiZDGg==" + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.19.2", + "contentHash": "HJbo/lnSfNHUfphPRT910poQc4T2/9+8svFLvzuaYHGAOJ2Tu+oEDqpX0BVP3BJ4OuUM1kylEKyaiX2fCAK3Cw==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.19.2", + "contentHash": "ui3fuBT4fs8kdKfBthI4NzLYIBIneEVS8UrL1JVBzAn80UiKmngBBi0BEByE7n/9c+EElcfFlCMFFTqpkBSLNA==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.19.2" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.19.2", + "contentHash": "r5YLDIxGOnkVJHrqXv/iD1FM1CgGrQOdriXuvuWvTPmKbnGANhEysq9XKmN6IHjf2a+9bAGEpnoRBsAQlvJU5w==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.19.2" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "8.19.2", + "contentHash": "sGxSsSrZXNmca6D+jHH2rVRyo2nNRd/g4H9CFbPmLLq0xgoH1U0orLWE5minfijw7+zq49tBs7txenbfAErRoQ==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.19.2" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "8.19.2", + "contentHash": "1XOcyY36cVymzE3qKdzKaUEZ4Pzt7ZpSa14JZoPPK1NLFUkQDs85TCqpV6XDo0YjFXj6nVK00AfOHppjghjhtw==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "8.19.2", + "System.IdentityModel.Tokens.Jwt": "8.19.2" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.19.2", + "contentHash": "GtPC1S02uH1gOO4fQ+zRysIicKmEXaYFP8PIkdJYXqMyruYhopre4ozVHp0XiDSA0+GJvOZH9prxCPvgBMg4Ww==", + "dependencies": { + "Microsoft.Bcl.Cryptography": "10.0.2", + "Microsoft.IdentityModel.Logging": "8.19.2" + } + }, + "Npgsql": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "7nb5YzXuvWWJxB0J8DiyL3we+X4FOctZrt0fIBnucOIaIevFEEwGQVZKtiu9olXdlNAK1eNgqSral6r/jlhI4w==" + }, + "OpenTelemetry": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "rMLOTftlMlTm7+MSrvXDHnJRjVkROFNKXHZrYjOsX+LankaFG7QSflx7qRRGjoqZoirohnxmJQ7GEb9occO4Gg==", + "dependencies": { + "OpenTelemetry.Api.ProviderBuilderExtensions": "1.17.0" + } + }, + "OpenTelemetry.Api": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "mSBxzomZgHIJu9CyVNqyDu/n2JHEtqVgfcCD1Br0cV5iLYogjZOMqhlVLt99PEp+0KGBNUR3GXgeOdN2GR3F9g==" + }, + "OpenTelemetry.Api.ProviderBuilderExtensions": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "Xgc3Qf9B9TFMFpx6exTdGqMWuYIT2miNzkdMPutVvT9YuMFaEovXWke1Gb6z8NxYaQbbGF38vYLuSg1JCeui5Q==", + "dependencies": { + "OpenTelemetry.Api": "1.17.0" + } + }, + "RESPite": { + "type": "Transitive", + "resolved": "3.0.17", + "contentHash": "68slEMyRTUNLc75DruEDsEohFmFfNwkHLLtwQ46bobF+8Tl+UXMKM1kM87ihhKy6JdAqy1RrvAfvBWeLmcY9Gg==" + }, + "Serilog": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", + "dependencies": { + "Serilog": "4.2.0" + } + }, + "Serilog.Settings.Configuration": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", + "dependencies": { + "Microsoft.Extensions.DependencyModel": "10.0.0", + "Serilog": "4.3.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.File": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", + "dependencies": { + "Serilog": "4.2.0" + } + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "8.19.2", + "contentHash": "gqhDC/icByKEutygpr+OFgAmjwTVowyzFjWB8K0q1ww8uFj5a1BQNL+QvijUl9uhq4p8OdDwcAIrjVC+eC9FVA==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.19.2", + "Microsoft.IdentityModel.Tokens": "8.19.2" + } + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8IBJWcCT9+e4Bmevm4T7+fQEiAh133KGiz4oiVTgJckd3Q76OFdR1falgn9lpz7+C4HJvogCDJeAa2QmvbeVtg==" + }, + "mall.application": { + "type": "Project", + "dependencies": { + "Mall.Domain": "[0.1.0, )" + } + }, + "mall.domain": { + "type": "Project" + }, + "mall.infrastructure": { + "type": "Project", + "dependencies": { + "AWSSDK.S3": "[4.0.101.4, )", + "Mall.Application": "[0.1.0, )", + "Mall.Domain": "[0.1.0, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.3, )", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.17.0, )", + "OpenTelemetry.Extensions.Hosting": "[1.17.0, )", + "OpenTelemetry.Instrumentation.AspNetCore": "[1.17.0, )", + "OpenTelemetry.Instrumentation.Http": "[1.17.0, )", + "OpenTelemetry.Instrumentation.Runtime": "[1.17.0, )", + "RabbitMQ.Client": "[7.2.1, )", + "Serilog.Extensions.Hosting": "[10.0.0, )", + "Serilog.Formatting.Compact": "[3.0.0, )", + "Serilog.Sinks.Console": "[6.1.1, )", + "StackExchange.Redis": "[3.0.17, )" + } + }, + "AWSSDK.S3": { + "type": "CentralTransitive", + "requested": "[4.0.101.4, )", + "resolved": "4.0.101.4", + "contentHash": "TYFuatWECzCbj/Lu1SsANucgpU/Br5YJ5Padl3YEdNJGfZqAcx3QemJw2BUopAsYTJ3e7TFpWhpNWha3dB9/dw==", + "dependencies": { + "AWSSDK.Core": "[4.0.100.8, 5.0.0)" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "a0V7zj/VbYP6dTdWpUgE/r2PuLKtUGe2aJ0lVKkn/wP9ZhaxUz2kQydVfvOjCv2SKxlrqdBfHhPD4Cvlf+4ffA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.10", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.10" + } + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "wNonj40aZxia+GtuBiiD6ZqVh4h6y5Nje1bGdmzZ8/ui0QRsAN+S0SIrLHFCEGbG9cDbeaE40sh+Lr7o9rRs6g==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.10" + } + }, + "Microsoft.OpenApi": { + "type": "CentralTransitive", + "requested": "[2.7.5, )", + "resolved": "2.7.5", + "contentHash": "0FA67RSnRM4tcBKqiqVu/HPdZ9+QOKbmeRjxRUGTCjPU4C0bmUhd97Dso7Yild5P7nOV6GxJ2xrK0Kv/O9xp0w==" + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "IPGrrZnRkuW7OlHDhUESZz4G5DLkW7Nej/O3Cx+0iTsgyU5XJxBgpsvTHLloo3WWuAKKbDHXBvWPVkX1deRh1Q==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.3" + } + }, + "OpenTelemetry.Exporter.OpenTelemetryProtocol": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "R1omQOrQpGlS0Cp5UIr/TAiuEA48JrPlgr1NPV5gESiTU7HhWU+ILe2EBSYb1fKdsSavZ7nZkHcUxAzofPqr2A==", + "dependencies": { + "OpenTelemetry": "1.17.0" + } + }, + "OpenTelemetry.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "t1OwL/4qgboGMobYVT+UV5zgWnFqCp4Pw8lcsmzh8m2K8PQsTKkyxrC32tqYTMYny3GOW4q5cltE3dTVzLmRew==", + "dependencies": { + "OpenTelemetry": "1.17.0" + } + }, + "OpenTelemetry.Instrumentation.AspNetCore": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "rGbmk1vuy1kvgZmE0ps7Vb99YZvDap6AalrrF60FwnNit1uW/PbeFZj1cpb0T8MPkYmjhBrRJ1/JB6QqXkRjHA==", + "dependencies": { + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.17.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Http": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "uTwVtxIJ/xB96wGYTaDsbkJVeCFdUxTwvrlDUn2YJixy0UuKc8DvQMzwKNJMTzNFiiyYO9c40id6tUHTmWs33A==", + "dependencies": { + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.17.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Runtime": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "HyYenisDn/xdtyVXdjImsCl+RNC2gq01N0rvSR7tsYAylXR2sxX/YgMsyTajMXA27+r1vB7lNU8cWRhV0fwL+Q==", + "dependencies": { + "OpenTelemetry.Api": "[1.17.0, 2.0.0)" + } + }, + "RabbitMQ.Client": { + "type": "CentralTransitive", + "requested": "[7.2.1, )", + "resolved": "7.2.1", + "contentHash": "YKXEfg9fVQiTKgZlvIhAfPSFaamEgi8DsQmisCH0IAsU4FYLrtoguDrDj6JtJVGUt40QPnBLRH6fTQcAC4qsOg==" + }, + "Serilog.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", + "dependencies": { + "Serilog": "4.3.0", + "Serilog.Extensions.Logging": "10.0.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "CentralTransitive", + "requested": "[3.0.0, )", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Console": { + "type": "CentralTransitive", + "requested": "[6.1.1, )", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "StackExchange.Redis": { + "type": "CentralTransitive", + "requested": "[3.0.17, )", + "resolved": "3.0.17", + "contentHash": "ItAm9lokZ1mWsQLF3u4Yw4eb/gvkX6Rp9bLAe1KUKgKAjPkALoQwTc6jJLtBZoOv9LT7RwvSIXZZb9FF30QhOg==", + "dependencies": { + "RESPite": "3.0.17", + "System.IO.Hashing": "10.0.5" + } + } + } + } +} \ No newline at end of file diff --git a/backend/src/Mall.AppHost/Mall.AppHost.csproj b/backend/src/Mall.AppHost/Mall.AppHost.csproj new file mode 100644 index 0000000..78cc281 --- /dev/null +++ b/backend/src/Mall.AppHost/Mall.AppHost.csproj @@ -0,0 +1,21 @@ + + + + + Exe + true + + + + + + + + + + + + + + + diff --git a/backend/src/Mall.AppHost/Program.cs b/backend/src/Mall.AppHost/Program.cs new file mode 100644 index 0000000..bfc4214 --- /dev/null +++ b/backend/src/Mall.AppHost/Program.cs @@ -0,0 +1,49 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var postgresPassword = builder.AddParameter("postgres-password", secret: true); +var postgres = builder + .AddPostgres("postgres", password: postgresPassword) + .WithImageTag("18.4") + .WithDataVolume("eshop-postgres-data"); +var database = postgres.AddDatabase("Postgres", "eshop"); + +var redis = builder + .AddRedis("Redis") + .WithImageTag("8.2.7") + .WithDataVolume("eshop-redis-data"); + +var rabbitMq = builder + .AddRabbitMQ("RabbitMq") + .WithImageTag("4.3.4-management") + .WithDataVolume("eshop-rabbitmq-data") + .WithManagementPlugin(); + +var seaweedfs = builder + .AddContainer("seaweedfs", "chrislusf/seaweedfs", "4.29") + .WithArgs("server", "-s3", "-dir=/data", "-ip=seaweedfs") + .WithVolume("eshop-seaweedfs-data", "/data") + .WithHttpEndpoint(targetPort: 8333, name: "s3") + .WithHttpEndpoint(targetPort: 9333, name: "master"); + +var migrator = builder + .AddProject("migrator") + .WithReference(database) + .WaitFor(database); + +var api = builder + .AddProject("api") + .WithReference(database) + .WithReference(redis) + .WithReference(rabbitMq) + .WaitForCompletion(migrator); + +builder + .AddProject("worker") + .WithReference(database) + .WithReference(redis) + .WithReference(rabbitMq) + .WaitForCompletion(migrator); + +api.WithEnvironment("Infrastructure__ObjectStorage__ServiceUrl", seaweedfs.GetEndpoint("s3")); + +builder.Build().Run(); diff --git a/backend/src/Mall.AppHost/Properties/launchSettings.json b/backend/src/Mall.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000..d616a90 --- /dev/null +++ b/backend/src/Mall.AppHost/Properties/launchSettings.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15888", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:18889" + } + } + } +} diff --git a/backend/src/Mall.AppHost/appsettings.json b/backend/src/Mall.AppHost/appsettings.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/backend/src/Mall.AppHost/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/backend/src/Mall.AppHost/packages.lock.json b/backend/src/Mall.AppHost/packages.lock.json new file mode 100644 index 0000000..d1d9f3f --- /dev/null +++ b/backend/src/Mall.AppHost/packages.lock.json @@ -0,0 +1,863 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "Aspire.Dashboard.Sdk.win-x64": { + "type": "Direct", + "requested": "[13.4.4, )", + "resolved": "13.4.4", + "contentHash": "8nWMde1RhIox/kHbAHhRXtIRSliBKXnU8jBQndHnbCPN4wGjYg0e5mh02FbFmmICK0uMnmxvjHyx63oIaSqnTg==" + }, + "Aspire.Hosting.AppHost": { + "type": "Direct", + "requested": "[13.4.4, )", + "resolved": "13.4.4", + "contentHash": "cUSc3s0iZ8FTtgJpiBmugSchcQA2qdIQwa/b7GpxbDfBr2no3OANa3pi+FSgqNE3rOkc0IFe9X6cRGaXcDRaLQ==", + "dependencies": { + "AspNetCore.HealthChecks.Uris": "9.0.0", + "Aspire.Hosting": "13.4.4", + "Google.Protobuf": "3.34.1", + "Grpc.AspNetCore": "2.80.0", + "Grpc.Net.ClientFactory": "2.80.0", + "Grpc.Tools": "2.80.0", + "Humanizer.Core": "3.0.10", + "JsonPatch.Net": "3.3.0", + "KubernetesClient": "19.0.2", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.8", + "Microsoft.Extensions.Configuration.Binder": "10.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.Diagnostics.HealthChecks": "10.0.8", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.8", + "Microsoft.Extensions.Hosting": "10.0.8", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.8", + "Microsoft.Extensions.Http": "10.0.8", + "Microsoft.Extensions.Logging": "10.0.8", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8", + "Microsoft.Extensions.Primitives": "10.0.8", + "ModelContextProtocol": "1.3.0", + "Newtonsoft.Json": "13.0.4", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "1.15.3", + "OpenTelemetry.Extensions.Hosting": "1.15.3", + "Polly.Core": "8.6.6", + "Semver": "3.0.0", + "StreamJsonRpc": "2.22.23", + "System.IO.Hashing": "10.0.8" + } + }, + "Aspire.Hosting.Orchestration.win-x64": { + "type": "Direct", + "requested": "[13.4.4, )", + "resolved": "13.4.4", + "contentHash": "1YrNrKncqHwktBALr9EJgf0J94frT88BOjf+YEaufK5iukjV9d+idwGqMQql0utNs3lxErFyxx68/h/Jfj6QeA==" + }, + "Aspire.Hosting.PostgreSQL": { + "type": "Direct", + "requested": "[13.4.4, )", + "resolved": "13.4.4", + "contentHash": "AbX5179pyl40gwrf5bhnGkYEYnGZO71Y/c7b8pMIpEyI6mpt9157c/27tPWfkDOL7Qzz4d68h+cO2es6U5HHKg==", + "dependencies": { + "AspNetCore.HealthChecks.NpgSql": "9.0.0", + "AspNetCore.HealthChecks.Uris": "9.0.0", + "Aspire.Hosting": "13.4.4", + "Google.Protobuf": "3.34.1", + "Grpc.AspNetCore": "2.80.0", + "Grpc.Net.ClientFactory": "2.80.0", + "Grpc.Tools": "2.80.0", + "Humanizer.Core": "3.0.10", + "JsonPatch.Net": "3.3.0", + "KubernetesClient": "19.0.2", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.8", + "Microsoft.Extensions.Configuration.Binder": "10.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.27", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.8", + "Microsoft.Extensions.Hosting": "10.0.8", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.8", + "Microsoft.Extensions.Http": "10.0.8", + "Microsoft.Extensions.Logging": "10.0.8", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8", + "Microsoft.Extensions.Primitives": "10.0.8", + "ModelContextProtocol": "1.3.0", + "Newtonsoft.Json": "13.0.4", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "1.15.3", + "OpenTelemetry.Extensions.Hosting": "1.15.3", + "Polly.Core": "8.6.6", + "Semver": "3.0.0", + "StreamJsonRpc": "2.22.23", + "System.IO.Hashing": "10.0.8" + } + }, + "Aspire.Hosting.RabbitMQ": { + "type": "Direct", + "requested": "[13.4.4, )", + "resolved": "13.4.4", + "contentHash": "Es5gwaj5JcdSqeUdDCc/joCeQ9C7P2AUjkbsPdLsaocrVOSCZMux3fQvjmqJPMHGxjCl2m1eFgY4bKqimlfCMw==", + "dependencies": { + "AspNetCore.HealthChecks.Rabbitmq": "9.0.0", + "AspNetCore.HealthChecks.Uris": "9.0.0", + "Aspire.Hosting": "13.4.4", + "Google.Protobuf": "3.34.1", + "Grpc.AspNetCore": "2.80.0", + "Grpc.Net.ClientFactory": "2.80.0", + "Grpc.Tools": "2.80.0", + "Humanizer.Core": "3.0.10", + "JsonPatch.Net": "3.3.0", + "KubernetesClient": "19.0.2", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.8", + "Microsoft.Extensions.Configuration.Binder": "10.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.27", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.8", + "Microsoft.Extensions.Hosting": "10.0.8", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.8", + "Microsoft.Extensions.Http": "10.0.8", + "Microsoft.Extensions.Logging": "10.0.8", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8", + "Microsoft.Extensions.Primitives": "10.0.8", + "ModelContextProtocol": "1.3.0", + "Newtonsoft.Json": "13.0.4", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "1.15.3", + "OpenTelemetry.Extensions.Hosting": "1.15.3", + "Polly.Core": "8.6.6", + "RabbitMQ.Client": "7.2.1", + "Semver": "3.0.0", + "StreamJsonRpc": "2.22.23", + "System.IO.Hashing": "10.0.8" + } + }, + "Aspire.Hosting.Redis": { + "type": "Direct", + "requested": "[13.4.4, )", + "resolved": "13.4.4", + "contentHash": "zxYmucwVUdzhzWA2JyLkOTSmQzmz9yvliDOMM/GOhMFtrntVEq0+EFb8SlfzuI6KXdgGjCURa1Xf2HMDAe0PdA==", + "dependencies": { + "AspNetCore.HealthChecks.Redis": "9.0.0", + "AspNetCore.HealthChecks.Uris": "9.0.0", + "Aspire.Hosting": "13.4.4", + "Google.Protobuf": "3.34.1", + "Grpc.AspNetCore": "2.80.0", + "Grpc.Net.ClientFactory": "2.80.0", + "Grpc.Tools": "2.80.0", + "Humanizer.Core": "3.0.10", + "JsonPatch.Net": "3.3.0", + "KubernetesClient": "19.0.2", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.8", + "Microsoft.Extensions.Configuration.Binder": "10.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.27", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.8", + "Microsoft.Extensions.Hosting": "10.0.8", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.8", + "Microsoft.Extensions.Http": "10.0.8", + "Microsoft.Extensions.Logging": "10.0.8", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8", + "Microsoft.Extensions.Primitives": "10.0.8", + "ModelContextProtocol": "1.3.0", + "Newtonsoft.Json": "13.0.4", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "1.15.3", + "OpenTelemetry.Extensions.Hosting": "1.15.3", + "Polly.Core": "8.6.6", + "Semver": "3.0.0", + "StackExchange.Redis": "2.13.1", + "StreamJsonRpc": "2.22.23", + "System.IO.Hashing": "10.0.8" + } + }, + "Aspire.Hosting": { + "type": "Transitive", + "resolved": "13.4.4", + "contentHash": "LdE2fAaXU9scaUsSHVZb1DdvjNofZM0Yea9KDBlLUMf1x+6SddQf6A/AtzcMclethZunfhs/Rkj+WFXuehlIMA==", + "dependencies": { + "AspNetCore.HealthChecks.Uris": "9.0.0", + "Google.Protobuf": "3.34.1", + "Grpc.AspNetCore": "2.80.0", + "Grpc.Net.ClientFactory": "2.80.0", + "Humanizer.Core": "3.0.10", + "JsonPatch.Net": "3.3.0", + "KubernetesClient": "19.0.2", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.8", + "Microsoft.Extensions.Configuration.Binder": "10.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.27", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.8", + "Microsoft.Extensions.Hosting": "10.0.8", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.8", + "Microsoft.Extensions.Http": "10.0.8", + "Microsoft.Extensions.Logging": "10.0.8", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8", + "Microsoft.Extensions.Primitives": "10.0.8", + "ModelContextProtocol": "1.3.0", + "Newtonsoft.Json": "13.0.4", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "1.15.3", + "OpenTelemetry.Extensions.Hosting": "1.15.3", + "Polly.Core": "8.6.6", + "Semver": "3.0.0", + "StreamJsonRpc": "2.22.23", + "System.IO.Hashing": "10.0.8" + } + }, + "AspNetCore.HealthChecks.NpgSql": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "npc58/AD5zuVxERdhCl2Kb7WnL37mwX42SJcXIwvmEig0/dugOLg3SIwtfvvh3TnvTwR/sk5LYNkkPaBdks61A==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11", + "Npgsql": "8.0.3" + } + }, + "AspNetCore.HealthChecks.Rabbitmq": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "7WSQ7EwioA5niakzzLtGVcZMEOh+42fSwrI24vnNsT7gZuVGOViNekyz38G6wBPYKcpL/lUkMdg3ZaCiZTi/Dw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11", + "RabbitMQ.Client": "7.0.0" + } + }, + "AspNetCore.HealthChecks.Redis": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "yNH0h8GLRbAf+PU5HNVLZ5hNeyq9mDVmRKO9xuZsme/znUYoBJlQvI0gq45gaZNlLncCHkMhR4o90MuT+gxxPw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11", + "StackExchange.Redis": "2.7.4" + } + }, + "AspNetCore.HealthChecks.Uris": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "XYdNlA437KeF8p9qOpZFyNqAN+c0FXt/JjTvzH/Qans0q0O3pPE8KPnn39ucQQjR/Roum1vLTP3kXiUs8VHyuA==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11", + "Microsoft.Extensions.Http": "8.0.0" + } + }, + "Fractions": { + "type": "Transitive", + "resolved": "7.3.0", + "contentHash": "2bETFWLBc8b7Ut2SVi+bxhGVwiSpknHYGBh2PADyGWONLkTxT7bKyDRhF8ao+XUv90tq8Fl7GTPxSI5bacIRJw==" + }, + "Google.Protobuf": { + "type": "Transitive", + "resolved": "3.34.1", + "contentHash": "212vdYxRuVopGE5bess6Jg5oXWyizA6hcLPTI7G+qA4PthQEvfeof3njT+7VSY5v/+O0P22xTydiP5fSJJpGEA==" + }, + "Grpc.AspNetCore": { + "type": "Transitive", + "resolved": "2.80.0", + "contentHash": "ASbJbdtCUlGiKxe9NsN/QeG1PdibYoN0pEgP9U87cvS6HV0XsT+VdO/g2M6Ri8zZURfX5c7MBTaFVP/YCrC72A==", + "dependencies": { + "Google.Protobuf": "3.31.1", + "Grpc.AspNetCore.Server.ClientFactory": "2.80.0", + "Grpc.Tools": "2.80.0" + } + }, + "Grpc.AspNetCore.Server": { + "type": "Transitive", + "resolved": "2.80.0", + "contentHash": "lbK7z1sZP5imPdQ035f/CZ61iIaBWouY6A580E9JNo7vzXYX/Qo+GQtSjOzhP9VFbxk7mtLoMhn/v1fT2U7kTw==", + "dependencies": { + "Grpc.Net.Common": "2.80.0" + } + }, + "Grpc.AspNetCore.Server.ClientFactory": { + "type": "Transitive", + "resolved": "2.80.0", + "contentHash": "7JdfxQZv/pO9qU5Ild2mrkzYA7PmVGDKVmWqKVCZNRSDN/RluRN4S4utYgBSwAcYgUBWQDHNb1Ll7wm3BdHfqw==", + "dependencies": { + "Grpc.AspNetCore.Server": "2.80.0", + "Grpc.Net.ClientFactory": "2.80.0" + } + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.80.0", + "contentHash": "i/8s+MOrYa6n7BmZ5bilcbHk+EMJDQHm2MKLhwGAT+urQqlZ6cpjvSivYvuULjebuX+UKieLYZbLRCmVQxFRkw==" + }, + "Grpc.Net.Client": { + "type": "Transitive", + "resolved": "2.80.0", + "contentHash": "I1Aa24nTRMHqx0pmQfvthFsOpejquDjiJV6092KBqjw6EEr3wA9CMXlrdkoEgHartOiJrpKyZiQRl7n0NVlfBg==", + "dependencies": { + "Grpc.Net.Common": "2.80.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + } + }, + "Grpc.Net.ClientFactory": { + "type": "Transitive", + "resolved": "2.80.0", + "contentHash": "YtY1DWID2phwiGc8qBG7+wf00Do5jE7BkJgCc6nbu5b50OsD89mSd73oCE8fCnUo7IjtDAEYFOYI3NSzgn28gw==", + "dependencies": { + "Grpc.Net.Client": "2.80.0", + "Microsoft.Extensions.Http": "8.0.0" + } + }, + "Grpc.Net.Common": { + "type": "Transitive", + "resolved": "2.80.0", + "contentHash": "E2ERsx+9IXlry4yjBl8btx0XMIKzymGNSvX5jmBS/uSwYyYDoKIDcIREyLqFLvd8vcJdcoRlycpvn9YRXutFpQ==", + "dependencies": { + "Grpc.Core.Api": "2.80.0" + } + }, + "Grpc.Tools": { + "type": "Transitive", + "resolved": "2.80.0", + "contentHash": "NS1AxwVZnrdbBoMf5L5ruGjBjoLBej9avhqxWNsgfu2GU6FhpxEVJOwLJsdljlDCjvquJiAH2zf/WodIWMvf2w==" + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "3.0.10", + "contentHash": "yZIhtw8sYuvsONzQbZxWpR60tMWYHXoo0DL6nyOqSFiU5POjBTSEyWFpTQtJEZuy+oqiYTXKXY/Mjx7KnqIQFw==" + }, + "Json.More.Net": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "qtwsyAsL55y2vB2/sK4Pjg3ZyVzD5KKSpV3lOAMHlnjFfsjQ/86eHJfQT9aV1YysVXzF4+xyHOZbh7Iu3YQ7Lg==" + }, + "JsonPatch.Net": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "GIcMMDtzfzVfIpQgey8w7dhzcw6jG5nD4DDAdQCTmHfblkCvN7mI8K03to8YyUhKMl4PTR6D6nLSvWmyOGFNTg==", + "dependencies": { + "JsonPointer.Net": "5.2.0" + } + }, + "JsonPointer.Net": { + "type": "Transitive", + "resolved": "5.2.0", + "contentHash": "qe1F7Tr/p4mgwLPU9P60MbYkp+xnL2uCPnWXGgzfR/AZCunAZIC0RZ32dLGJJEhSuLEfm0YF/1R3u5C7mEVq+w==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Json.More.Net": "2.1.0" + } + }, + "KubernetesClient": { + "type": "Transitive", + "resolved": "19.0.2", + "contentHash": "0m8FuzCDBygaXMbsb7qVapNZzTm4ftM7ECp/waTRh/XOUQziRF2rKJCRmsvYbBXgJkQvU+FBbkoa40hexzDC+g==", + "dependencies": { + "Fractions": "7.3.0", + "YamlDotNet": "16.3.0" + } + }, + "MessagePack.Annotations": { + "type": "Transitive", + "resolved": "2.5.302", + "contentHash": "PTHQHbMJzx1VtUUCpnvPavD7o5X9s2dAJ4uQHAP2kBCwJKICpPTZq0qxzbB4/6iJJiIxem8zlxyfldGAx1SjSQ==" + }, + "Microsoft.Extensions.AI.Abstractions": { + "type": "Transitive", + "resolved": "10.5.2", + "contentHash": "Ei+YWV9Ybnps7pR1dgjlG29gelXEwZkhLVAcWmKe6HvXS6LNBYgSdWiY3Hk9OZXYtK34rv/NtLWBQYQGOBQYPQ==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.7", + "contentHash": "pUDgQKEqNUFlerDIFRg7zzoDVRPEWIG7nR40h8Gzg8RXza4Ry0lWZ7u91bmwu3iUDCxw3Dv6TLHVFoAgY0gy7Q==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.7" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "33cBeR2HRbzHUTtmcmLdNOApneNGcymwwL4arHuotgVK9Frba8kcDTrvVTj7cSCmF1R9OiSbZH0KxNOwab3HUg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "KRfFSSCV58vEdU7mPED/YMzeovIWF5P0g8s9K8n9HEfy0/WzMq37SrPdXdFN5/dFT/rPMHpF7AvpoXHckbcBFg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "1s1sKFTk/Foam64JY6+m/diH8drL3Wx6V3gtSd5v1IEZtszZYyc1pW8uRnMblzpNiR0l0t8gGk7tXj3xHzFgdg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Json": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "IlXZUZIA9b99yQURc8jOLmmS6GD42vj6t+LqWs6Dd+naggObSbEXDw8DU8DUu2tL8CnyG96F4pDLhjd7BmZPAQ==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "10.0.8", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.8", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "0j06qq5I1YGSIi4M86UaVITBprvn3bTWKmQiLDNEqnZ2d+UxaFb+tVqemxVzr5lkr6GTR83ExUfSIpCnBaTZFA==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5LugpYGHk+mkn0a8IZgcyfBca8PCTAU9RQFoMrTdtOOidq88M2SI5f3px6ugnzgxC+eTkvYYJi8pzlUnG5xdAQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Http": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "/9LU/KWJOrtZJB9ymPjcARDyjp679BvBA/aSncv2Kt84WlSKz767HtxHg8EFsu8n21BMLZi+5XxlkKbLwfn4iA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8", + "Microsoft.Extensions.Diagnostics": "10.0.8", + "Microsoft.Extensions.Logging": "10.0.8", + "Microsoft.Extensions.Logging.Abstractions": "10.0.8", + "Microsoft.Extensions.Options": "10.0.8" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "cLrqxkuEfcilZ8SjK+9KAnpLk9lOoMPaOokF+wRUYie+iUEcdX4/p/+gJkt0BYgWLthjpBUCkVTBI6Kxg0nsOw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "VIlNzPwPS0GeQVSmCqqo36ugryX3LpE9ul6gEkks5VLET3weH/XMLeWmclwfoGn4Nxi2mwVibB+OZBVJ9tDqvg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Configuration": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Debug": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "8+TZBnV5fgBXoVNJ5ROSErUwYogk4hOgV7c2HWK1u5cqKGmiUTUn7+KqZ35iQu8e/B7Ykccyz5OTjdXcidNZ9g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.EventLog": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "0RE4951AzQ+YD4gVrvbq0BhdsiBgSDo44yM7+QBZ2mrmMJeNjY+teCIYfUjqDPVYnKs0HR6SkkhgrX1YgXZq3Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "System.Diagnostics.EventLog": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.EventSource": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "85SAPwXhJtdBInzN2k7SChiFiBGh3KOWay5AfoY+GREF6P7oZA98+ST2p7Z9384iLKYjkZSKIZ/FqIO5aojtNw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==" + }, + "Microsoft.NET.StringTools": { + "type": "Transitive", + "resolved": "17.6.3", + "contentHash": "N0ZIanl1QCgvUumEL1laasU0a7sOE5ZwLZVTn0pAePnfhq8P7SvTjF8Axq+CnavuQkmdQpGNXQ1efZtu5kDFbA==" + }, + "Microsoft.VisualStudio.Threading.Only": { + "type": "Transitive", + "resolved": "17.13.61", + "contentHash": "vl5a2URJYCO5m+aZZtNlAXAMz28e2pUotRuoHD7RnCWOCeoyd8hWp5ZBaLNYq4iEj2oeJx5ZxiSboAjVmB20Qg==", + "dependencies": { + "Microsoft.VisualStudio.Validation": "17.8.8" + } + }, + "Microsoft.VisualStudio.Validation": { + "type": "Transitive", + "resolved": "17.8.8", + "contentHash": "rWXThIpyQd4YIXghNkiv2+VLvzS+MCMKVRDR0GAMlflsdo+YcAN2g2r5U1Ah98OFjQMRexTFtXQQ2LkajxZi3g==" + }, + "ModelContextProtocol": { + "type": "Transitive", + "resolved": "1.3.0", + "contentHash": "WDaD6z9KkkCUHSo15xK7tYBERHy8uqP+cIUp8uIxhR0yrlpJLXTRcJcUUVqpXlBkV7MK9Eo3mTrAnctLnJuHDQ==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.7", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.7", + "ModelContextProtocol.Core": "1.3.0" + } + }, + "ModelContextProtocol.Core": { + "type": "Transitive", + "resolved": "1.3.0", + "contentHash": "OWmdxDSwA7K9pNNg4t98MXNIssHG/wOQEr/G8pG5B7synDdw4MnmZ/IIVeb3yUdeznPqnDHvd3FBCK0jRk4IZQ==", + "dependencies": { + "Microsoft.Extensions.AI.Abstractions": "10.5.2", + "Microsoft.Extensions.Logging.Abstractions": "10.0.7" + } + }, + "Nerdbank.Streams": { + "type": "Transitive", + "resolved": "2.12.87", + "contentHash": "oDKOeKZ865I5X8qmU3IXMyrAnssYEiYWTobPGdrqubN3RtTzEHIv+D6fwhdcfrdhPJzHjCkK/ORztR/IsnmA6g==", + "dependencies": { + "Microsoft.VisualStudio.Threading.Only": "17.13.61", + "Microsoft.VisualStudio.Validation": "17.8.8" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.4", + "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" + }, + "Npgsql": { + "type": "Transitive", + "resolved": "8.0.3", + "contentHash": "6WEmzsQJCZAlUG1pThKg/RmeF6V+I0DmBBBE/8YzpRtEzhyZzKcK7ulMANDm5CkxrALBEC8H+5plxHWtIL7xnA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + } + }, + "OpenTelemetry": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "rMLOTftlMlTm7+MSrvXDHnJRjVkROFNKXHZrYjOsX+LankaFG7QSflx7qRRGjoqZoirohnxmJQ7GEb9occO4Gg==", + "dependencies": { + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "1.17.0" + } + }, + "OpenTelemetry.Api": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "mSBxzomZgHIJu9CyVNqyDu/n2JHEtqVgfcCD1Br0cV5iLYogjZOMqhlVLt99PEp+0KGBNUR3GXgeOdN2GR3F9g==" + }, + "OpenTelemetry.Api.ProviderBuilderExtensions": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "Xgc3Qf9B9TFMFpx6exTdGqMWuYIT2miNzkdMPutVvT9YuMFaEovXWke1Gb6z8NxYaQbbGF38vYLuSg1JCeui5Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "OpenTelemetry.Api": "1.17.0" + } + }, + "Polly.Core": { + "type": "Transitive", + "resolved": "8.6.6", + "contentHash": "lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==" + }, + "RESPite": { + "type": "Transitive", + "resolved": "3.0.17", + "contentHash": "68slEMyRTUNLc75DruEDsEohFmFfNwkHLLtwQ46bobF+8Tl+UXMKM1kM87ihhKy6JdAqy1RrvAfvBWeLmcY9Gg==" + }, + "Semver": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "9jZCicsVgTebqkAujRWtC9J1A5EQVlu0TVKHcgoCuv345ve5DYf4D1MjhKEnQjdRZo6x/vdv6QQrYFs7ilGzLA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "5.0.1" + } + }, + "StreamJsonRpc": { + "type": "Transitive", + "resolved": "2.22.23", + "contentHash": "Ahq6uUFPnU9alny5h4agyX74th3PRq3NQCRNaDOqWcx20WT06mH/wENSk5IbHDc8BmfreQVEIBx5IXLBbsLFIA==", + "dependencies": { + "MessagePack": "2.5.192", + "Microsoft.VisualStudio.Threading.Only": "17.13.61", + "Microsoft.VisualStudio.Validation": "17.8.8", + "Nerdbank.Streams": "2.12.87", + "Newtonsoft.Json": "13.0.3" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "OvGz3PrzuAI/Sj7LTcXcCe3FClRI1IyRMZjNONcZtFh+Ww7nAtSh4kh08r8KVe/xxkXJPjR0Y1jF7H+N42d4xQ==" + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "+dJsbPJ3FyUbTZNplFj0RCKePFizmv6ewDV46JE9q/IVH4c3xTCftHfHelLsAKf0jryIPqgMb5GpS0x7TAY3mg==" + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "YamlDotNet": { + "type": "Transitive", + "resolved": "16.3.0", + "contentHash": "SgMOdxbz8X65z8hraIs6hOEdnkH6hESTAIUa7viEngHOYaH+6q5XJmwr1+yb9vJpNQ19hCQY69xbFsLtXpobQA==" + }, + "MessagePack": { + "type": "CentralTransitive", + "requested": "[2.5.302, )", + "resolved": "2.5.302", + "contentHash": "5DZsKli4dMEpm/glcMAheuO8/IesfaTskbfsuvgnQwWFKS5FN7Z6yNx+5F+UW94E+9N2qt8Zs63/XVzpLsElgA==", + "dependencies": { + "MessagePack.Annotations": "2.5.302", + "Microsoft.NET.StringTools": "17.6.3" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "tL9FkfV64GPUDSPvwrgyw42LVzsnVAnyrqJEuZVJbODgrQ3eL63zmzEcVWoCHzfgqUhWggzbgAyUCnz/zfI3Pg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.Configuration.CommandLine": "10.0.10", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.10", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.10", + "Microsoft.Extensions.Configuration.Json": "10.0.10", + "Microsoft.Extensions.Configuration.UserSecrets": "10.0.10", + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Diagnostics": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Configuration": "10.0.10", + "Microsoft.Extensions.Logging.Console": "10.0.10", + "Microsoft.Extensions.Logging.Debug": "10.0.10", + "Microsoft.Extensions.Logging.EventLog": "10.0.10", + "Microsoft.Extensions.Logging.EventSource": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "OpenTelemetry.Exporter.OpenTelemetryProtocol": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "R1omQOrQpGlS0Cp5UIr/TAiuEA48JrPlgr1NPV5gESiTU7HhWU+ILe2EBSYb1fKdsSavZ7nZkHcUxAzofPqr2A==", + "dependencies": { + "OpenTelemetry": "1.17.0" + } + }, + "OpenTelemetry.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "t1OwL/4qgboGMobYVT+UV5zgWnFqCp4Pw8lcsmzh8m2K8PQsTKkyxrC32tqYTMYny3GOW4q5cltE3dTVzLmRew==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "OpenTelemetry": "1.17.0" + } + }, + "RabbitMQ.Client": { + "type": "CentralTransitive", + "requested": "[7.2.1, )", + "resolved": "7.2.1", + "contentHash": "YKXEfg9fVQiTKgZlvIhAfPSFaamEgi8DsQmisCH0IAsU4FYLrtoguDrDj6JtJVGUt40QPnBLRH6fTQcAC4qsOg==", + "dependencies": { + "System.Threading.RateLimiting": "8.0.0" + } + }, + "StackExchange.Redis": { + "type": "CentralTransitive", + "requested": "[3.0.17, )", + "resolved": "3.0.17", + "contentHash": "ItAm9lokZ1mWsQLF3u4Yw4eb/gvkX6Rp9bLAe1KUKgKAjPkALoQwTc6jJLtBZoOv9LT7RwvSIXZZb9FF30QhOg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "RESPite": "3.0.17", + "System.IO.Hashing": "10.0.5" + } + } + } + } +} \ No newline at end of file diff --git a/backend/src/Mall.Application/ApplicationAssembly.cs b/backend/src/Mall.Application/ApplicationAssembly.cs new file mode 100644 index 0000000..cdbc8da --- /dev/null +++ b/backend/src/Mall.Application/ApplicationAssembly.cs @@ -0,0 +1,6 @@ +namespace Mall.Application; + +/// +/// Identifies the application assembly without defining a business use case. +/// +public static class ApplicationAssembly; diff --git a/backend/src/Mall.Application/Mall.Application.csproj b/backend/src/Mall.Application/Mall.Application.csproj index c8b7d96..14e685e 100644 --- a/backend/src/Mall.Application/Mall.Application.csproj +++ b/backend/src/Mall.Application/Mall.Application.csproj @@ -1,13 +1,5 @@ - - + - - - net10.0 - enable - enable - - diff --git a/backend/src/Mall.Application/packages.lock.json b/backend/src/Mall.Application/packages.lock.json new file mode 100644 index 0000000..4485744 --- /dev/null +++ b/backend/src/Mall.Application/packages.lock.json @@ -0,0 +1,10 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "mall.domain": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/backend/src/Mall.Domain/DomainAssembly.cs b/backend/src/Mall.Domain/DomainAssembly.cs new file mode 100644 index 0000000..ad2a565 --- /dev/null +++ b/backend/src/Mall.Domain/DomainAssembly.cs @@ -0,0 +1,6 @@ +namespace Mall.Domain; + +/// +/// Identifies the domain assembly without introducing domain behavior. +/// +public static class DomainAssembly; diff --git a/backend/src/Mall.Domain/Mall.Domain.csproj b/backend/src/Mall.Domain/Mall.Domain.csproj index b760144..35e3d84 100644 --- a/backend/src/Mall.Domain/Mall.Domain.csproj +++ b/backend/src/Mall.Domain/Mall.Domain.csproj @@ -1,9 +1,2 @@ - - - - net10.0 - enable - enable - - + diff --git a/backend/src/Mall.Domain/packages.lock.json b/backend/src/Mall.Domain/packages.lock.json new file mode 100644 index 0000000..6afd678 --- /dev/null +++ b/backend/src/Mall.Domain/packages.lock.json @@ -0,0 +1,6 @@ +{ + "version": 2, + "dependencies": { + "net10.0": {} + } +} \ No newline at end of file diff --git a/backend/src/Mall.Infrastructure/Authentication/AuthenticationConfigurationProvider.cs b/backend/src/Mall.Infrastructure/Authentication/AuthenticationConfigurationProvider.cs new file mode 100644 index 0000000..07d74f4 --- /dev/null +++ b/backend/src/Mall.Infrastructure/Authentication/AuthenticationConfigurationProvider.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.Options; + +namespace Mall.Infrastructure.Authentication; + +internal sealed class AuthenticationConfigurationProvider( + IOptionsMonitor securityOptions, + IOptionsMonitor contractOptions) + : IAuthenticationConfigurationProvider +{ + public AuthenticationConfigurationSnapshot GetSnapshot() => + AuthenticationDigestCalculator.CreateSnapshot( + securityOptions.CurrentValue, + contractOptions.Get(AuthenticationContractOptions.HttpBearerName), + contractOptions.Get(AuthenticationContractOptions.HubName)); +} diff --git a/backend/src/Mall.Infrastructure/Authentication/AuthenticationConfigurationSnapshot.cs b/backend/src/Mall.Infrastructure/Authentication/AuthenticationConfigurationSnapshot.cs new file mode 100644 index 0000000..5829dfa --- /dev/null +++ b/backend/src/Mall.Infrastructure/Authentication/AuthenticationConfigurationSnapshot.cs @@ -0,0 +1,9 @@ +namespace Mall.Infrastructure.Authentication; + +public sealed record AuthenticationConfigurationSnapshot( + string DigestAlgorithm, + string? ExpectedDigest, + string? HttpDigest, + string? HubDigest, + bool MatchesExpected, + bool HasSecureSigningMaterial); diff --git a/backend/src/Mall.Infrastructure/Authentication/AuthenticationContractOptions.cs b/backend/src/Mall.Infrastructure/Authentication/AuthenticationContractOptions.cs new file mode 100644 index 0000000..e7b817b --- /dev/null +++ b/backend/src/Mall.Infrastructure/Authentication/AuthenticationContractOptions.cs @@ -0,0 +1,19 @@ +namespace Mall.Infrastructure.Authentication; + +public sealed class AuthenticationContractOptions +{ + public const string HttpBearerName = "HttpBearer"; + public const string HubName = "Hub"; + + public string Issuer { get; init; } = string.Empty; + + public string Audience { get; init; } = string.Empty; + + public string KeyFingerprint { get; init; } = string.Empty; + + public int AccessTokenLifetimeSeconds { get; init; } + + public int ClockSkewSeconds { get; init; } + + public string TokenVersionValidationRule { get; init; } = string.Empty; +} diff --git a/backend/src/Mall.Infrastructure/Authentication/AuthenticationDigestCalculator.cs b/backend/src/Mall.Infrastructure/Authentication/AuthenticationDigestCalculator.cs new file mode 100644 index 0000000..06d09ad --- /dev/null +++ b/backend/src/Mall.Infrastructure/Authentication/AuthenticationDigestCalculator.cs @@ -0,0 +1,82 @@ +using System.Buffers; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace Mall.Infrastructure.Authentication; + +public static partial class AuthenticationDigestCalculator +{ + private const int MinimumSigningKeyLength = 32; + + public static AuthenticationConfigurationSnapshot CreateSnapshot( + AuthenticationSecurityOptions security, + AuthenticationContractOptions http, + AuthenticationContractOptions hub) + { + var expectedDigest = NormalizeDigest(security.ExpectedDigest); + var httpDigest = TryComputeDigest(http); + var hubDigest = TryComputeDigest(hub); + var matchesExpected = expectedDigest is not null + && string.Equals(expectedDigest, httpDigest, StringComparison.Ordinal) + && string.Equals(expectedDigest, hubDigest, StringComparison.Ordinal); + + return new AuthenticationConfigurationSnapshot( + "SHA-256", + expectedDigest, + httpDigest, + hubDigest, + matchesExpected, + HasSecureSigningMaterial(security.SigningKey)); + } + + public static string? TryComputeDigest(AuthenticationContractOptions options) + { + var issuer = Normalize(options.Issuer); + var audience = Normalize(options.Audience); + var keyFingerprint = Normalize(options.KeyFingerprint); + var tokenVersionValidationRule = Normalize(options.TokenVersionValidationRule); + + if (issuer.Length == 0 + || audience.Length == 0 + || keyFingerprint.Length == 0 + || tokenVersionValidationRule.Length == 0 + || options.AccessTokenLifetimeSeconds <= 0 + || options.ClockSkewSeconds != 0) + { + return null; + } + + var buffer = new ArrayBufferWriter(); + using (var writer = new Utf8JsonWriter(buffer)) + { + writer.WriteStartObject(); + writer.WriteString("issuer", issuer); + writer.WriteString("audience", audience); + writer.WriteString("keyFingerprint", keyFingerprint); + writer.WriteNumber("accessTokenLifetimeSeconds", options.AccessTokenLifetimeSeconds); + writer.WriteNumber("clockSkewSeconds", options.ClockSkewSeconds); + writer.WriteString("tokenVersionValidationRule", tokenVersionValidationRule); + writer.WriteEndObject(); + } + + return Convert.ToHexStringLower(SHA256.HashData(buffer.WrittenSpan)); + } + + private static string Normalize(string? value) => + (value ?? string.Empty).Trim().Normalize(NormalizationForm.FormC); + + private static string? NormalizeDigest(string? digest) + { + var normalized = Normalize(digest).ToLowerInvariant(); + return DigestPattern().IsMatch(normalized) ? normalized : null; + } + + private static bool HasSecureSigningMaterial(string? signingKey) => + !string.IsNullOrWhiteSpace(signingKey) + && Encoding.UTF8.GetByteCount(signingKey) >= MinimumSigningKeyLength; + + [GeneratedRegex("^[0-9a-f]{64}$", RegexOptions.CultureInvariant)] + private static partial Regex DigestPattern(); +} diff --git a/backend/src/Mall.Infrastructure/Authentication/AuthenticationSecurityOptions.cs b/backend/src/Mall.Infrastructure/Authentication/AuthenticationSecurityOptions.cs new file mode 100644 index 0000000..8a0f512 --- /dev/null +++ b/backend/src/Mall.Infrastructure/Authentication/AuthenticationSecurityOptions.cs @@ -0,0 +1,10 @@ +namespace Mall.Infrastructure.Authentication; + +public sealed class AuthenticationSecurityOptions +{ + public const string SectionName = "Authentication"; + + public string SigningKey { get; init; } = string.Empty; + + public string ExpectedDigest { get; init; } = string.Empty; +} diff --git a/backend/src/Mall.Infrastructure/Authentication/IAuthenticationConfigurationProvider.cs b/backend/src/Mall.Infrastructure/Authentication/IAuthenticationConfigurationProvider.cs new file mode 100644 index 0000000..0af6bb6 --- /dev/null +++ b/backend/src/Mall.Infrastructure/Authentication/IAuthenticationConfigurationProvider.cs @@ -0,0 +1,6 @@ +namespace Mall.Infrastructure.Authentication; + +public interface IAuthenticationConfigurationProvider +{ + AuthenticationConfigurationSnapshot GetSnapshot(); +} diff --git a/backend/src/Mall.Infrastructure/Caching/RedisOptions.cs b/backend/src/Mall.Infrastructure/Caching/RedisOptions.cs new file mode 100644 index 0000000..234e84a --- /dev/null +++ b/backend/src/Mall.Infrastructure/Caching/RedisOptions.cs @@ -0,0 +1,10 @@ +namespace Mall.Infrastructure.Caching; + +public sealed class RedisOptions +{ + public const string SectionName = "Infrastructure:Redis"; + + public bool Enabled { get; init; } + + public int ProbeTimeoutMilliseconds { get; init; } = 1500; +} diff --git a/backend/src/Mall.Infrastructure/Configuration/DeploymentOptions.cs b/backend/src/Mall.Infrastructure/Configuration/DeploymentOptions.cs new file mode 100644 index 0000000..a79d727 --- /dev/null +++ b/backend/src/Mall.Infrastructure/Configuration/DeploymentOptions.cs @@ -0,0 +1,19 @@ +namespace Mall.Infrastructure.Configuration; + +public sealed class DeploymentOptions +{ + public const string SectionName = "Deployment"; + public const string DefaultTargetMigration = "InitialEshopSchema"; + + public string ServiceName { get; init; } = "mall-api"; + + public string InstanceId { get; init; } = "unassigned"; + + public string Version { get; init; } = "unknown"; + + public string ExpectedVersion { get; init; } = string.Empty; + + public string TargetMigration { get; init; } = DefaultTargetMigration; + + public int ProbeTimeoutMilliseconds { get; init; } = 1500; +} diff --git a/backend/src/Mall.Infrastructure/DependencyInjection.cs b/backend/src/Mall.Infrastructure/DependencyInjection.cs new file mode 100644 index 0000000..64c7649 --- /dev/null +++ b/backend/src/Mall.Infrastructure/DependencyInjection.cs @@ -0,0 +1,112 @@ +using Amazon.S3; +using Mall.Infrastructure.Authentication; +using Mall.Infrastructure.Caching; +using Mall.Infrastructure.Configuration; +using Mall.Infrastructure.Integration; +using Mall.Infrastructure.ObjectStorage; +using Mall.Infrastructure.Observability; +using Mall.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using RabbitMQ.Client; +using StackExchange.Redis; + +namespace Mall.Infrastructure; + +public static class DependencyInjection +{ + public static IServiceCollection AddFoundationInfrastructure( + this IServiceCollection services, + IConfiguration configuration) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configuration); + + services.AddOptions() + .Bind(configuration.GetSection(DeploymentOptions.SectionName)); + services.AddOptions() + .Bind(configuration.GetSection(AuthenticationSecurityOptions.SectionName)); + services.AddOptions(AuthenticationContractOptions.HttpBearerName) + .Bind(configuration.GetSection($"{AuthenticationSecurityOptions.SectionName}:HttpBearer")); + services.AddOptions(AuthenticationContractOptions.HubName) + .Bind(configuration.GetSection($"{AuthenticationSecurityOptions.SectionName}:Hub")); + services.AddOptions() + .Bind(configuration.GetSection(RedisOptions.SectionName)); + services.AddOptions() + .Bind(configuration.GetSection(RabbitMqOptions.SectionName)); + services.AddOptions() + .Bind(configuration.GetSection(ObjectStorageOptions.SectionName)); + services.AddOptions() + .Bind(configuration.GetSection(ObservabilityOptions.SectionName)); + + services.AddSingleton(); + + var postgresConnectionString = configuration.GetConnectionString("Postgres") ?? string.Empty; + services.AddDbContextPool(options => + options.UseNpgsql( + postgresConnectionString, + npgsql => npgsql.MigrationsAssembly(typeof(MallDbContext).Assembly.FullName))); + + RegisterRedis(services, configuration); + RegisterRabbitMq(services, configuration); + RegisterObjectStorage(services, configuration); + + return services; + } + + private static void RegisterRedis(IServiceCollection services, IConfiguration configuration) + { + var redisOptions = configuration.GetSection(RedisOptions.SectionName).Get(); + var connectionString = configuration.GetConnectionString("Redis"); + if (redisOptions?.Enabled != true || string.IsNullOrWhiteSpace(connectionString)) + { + return; + } + + services.AddSingleton(_ => + ConnectionMultiplexer.Connect(ConfigurationOptions.Parse(connectionString))); + } + + private static void RegisterRabbitMq(IServiceCollection services, IConfiguration configuration) + { + var rabbitMqOptions = configuration.GetSection(RabbitMqOptions.SectionName).Get(); + var connectionString = configuration.GetConnectionString("RabbitMq"); + if (rabbitMqOptions?.Enabled != true + || !Uri.TryCreate(connectionString, UriKind.Absolute, out var connectionUri)) + { + return; + } + + services.AddSingleton( + new ConnectionFactory + { + Uri = connectionUri, + AutomaticRecoveryEnabled = true, + TopologyRecoveryEnabled = true, + ClientProvidedName = "mall-foundation", + }); + } + + private static void RegisterObjectStorage(IServiceCollection services, IConfiguration configuration) + { + var options = configuration.GetSection(ObjectStorageOptions.SectionName).Get(); + if (options?.Enabled != true + || string.IsNullOrWhiteSpace(options.ServiceUrl) + || string.IsNullOrWhiteSpace(options.AccessKey) + || string.IsNullOrWhiteSpace(options.SecretKey)) + { + return; + } + + services.AddSingleton( + new AmazonS3Client( + options.AccessKey, + options.SecretKey, + new AmazonS3Config + { + ServiceURL = options.ServiceUrl, + ForcePathStyle = true, + })); + } +} diff --git a/backend/src/Mall.Infrastructure/Integration/RabbitMqOptions.cs b/backend/src/Mall.Infrastructure/Integration/RabbitMqOptions.cs new file mode 100644 index 0000000..e1b0e64 --- /dev/null +++ b/backend/src/Mall.Infrastructure/Integration/RabbitMqOptions.cs @@ -0,0 +1,10 @@ +namespace Mall.Infrastructure.Integration; + +public sealed class RabbitMqOptions +{ + public const string SectionName = "Infrastructure:RabbitMq"; + + public bool Enabled { get; init; } + + public int ProbeTimeoutMilliseconds { get; init; } = 1500; +} diff --git a/backend/src/Mall.Infrastructure/Mall.Infrastructure.csproj b/backend/src/Mall.Infrastructure/Mall.Infrastructure.csproj index 35173a6..307e86e 100644 --- a/backend/src/Mall.Infrastructure/Mall.Infrastructure.csproj +++ b/backend/src/Mall.Infrastructure/Mall.Infrastructure.csproj @@ -1,14 +1,28 @@ - - + - - net10.0 - enable - enable - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + diff --git a/backend/src/Mall.Infrastructure/ObjectStorage/ObjectStorageOptions.cs b/backend/src/Mall.Infrastructure/ObjectStorage/ObjectStorageOptions.cs new file mode 100644 index 0000000..7510498 --- /dev/null +++ b/backend/src/Mall.Infrastructure/ObjectStorage/ObjectStorageOptions.cs @@ -0,0 +1,18 @@ +namespace Mall.Infrastructure.ObjectStorage; + +public sealed class ObjectStorageOptions +{ + public const string SectionName = "Infrastructure:ObjectStorage"; + + public bool Enabled { get; init; } + + public string ServiceUrl { get; init; } = string.Empty; + + public string AccessKey { get; init; } = string.Empty; + + public string SecretKey { get; init; } = string.Empty; + + public string BucketName { get; init; } = string.Empty; + + public int ProbeTimeoutMilliseconds { get; init; } = 1500; +} diff --git a/backend/src/Mall.Infrastructure/Observability/ObservabilityExtensions.cs b/backend/src/Mall.Infrastructure/Observability/ObservabilityExtensions.cs new file mode 100644 index 0000000..237651e --- /dev/null +++ b/backend/src/Mall.Infrastructure/Observability/ObservabilityExtensions.cs @@ -0,0 +1,90 @@ +using Mall.Infrastructure.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using Serilog; +using Serilog.Events; +using Serilog.Formatting.Compact; + +namespace Mall.Infrastructure.Observability; + +public static class ObservabilityExtensions +{ + public static IHostApplicationBuilder AddFoundationObservability( + this IHostApplicationBuilder builder, + bool includeAspNetCoreInstrumentation) + { + ArgumentNullException.ThrowIfNull(builder); + + var deployment = builder.Configuration + .GetSection(DeploymentOptions.SectionName) + .Get() ?? new DeploymentOptions(); + var observability = builder.Configuration + .GetSection(ObservabilityOptions.SectionName) + .Get() ?? new ObservabilityOptions(); + + builder.Services.AddSerilog( + (_, logger) => logger + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information) + .Enrich.FromLogContext() + .Enrich.WithProperty("ServiceName", deployment.ServiceName) + .Enrich.WithProperty("ServiceVersion", deployment.Version) + .Enrich.WithProperty("ServiceInstanceId", deployment.InstanceId) + .WriteTo.Console(new RenderedCompactJsonFormatter())); + + var otlpEndpoint = Uri.TryCreate( + observability.OtlpEndpoint, + UriKind.Absolute, + out var parsedOtlpEndpoint) + ? parsedOtlpEndpoint + : null; + + builder.Services + .AddOpenTelemetry() + .ConfigureResource(resource => resource.AddService( + serviceName: deployment.ServiceName, + serviceVersion: deployment.Version, + serviceInstanceId: deployment.InstanceId)) + .WithMetrics(metrics => + { + metrics.AddRuntimeInstrumentation(); + if (otlpEndpoint is not null) + { + metrics.AddOtlpExporter(options => options.Endpoint = otlpEndpoint); + } + }) + .WithTracing(tracing => + { + tracing.AddHttpClientInstrumentation(options => + { + options.FilterHttpRequestMessage = request => + !request.RequestUri?.Query.Contains( + "access_token=", + StringComparison.OrdinalIgnoreCase) ?? true; + }); + + if (includeAspNetCoreInstrumentation) + { + tracing.AddAspNetCoreInstrumentation(options => + { + options.Filter = context => + !context.Request.QueryString.Value?.Contains( + "access_token=", + StringComparison.OrdinalIgnoreCase) ?? true; + }); + } + + if (otlpEndpoint is not null) + { + tracing.AddOtlpExporter(options => options.Endpoint = otlpEndpoint); + } + }); + + return builder; + } +} diff --git a/backend/src/Mall.Infrastructure/Observability/ObservabilityOptions.cs b/backend/src/Mall.Infrastructure/Observability/ObservabilityOptions.cs new file mode 100644 index 0000000..ed0f6c9 --- /dev/null +++ b/backend/src/Mall.Infrastructure/Observability/ObservabilityOptions.cs @@ -0,0 +1,8 @@ +namespace Mall.Infrastructure.Observability; + +public sealed class ObservabilityOptions +{ + public const string SectionName = "Observability"; + + public string OtlpEndpoint { get; init; } = string.Empty; +} diff --git a/backend/src/Mall.Infrastructure/Persistence/MallDbContext.cs b/backend/src/Mall.Infrastructure/Persistence/MallDbContext.cs new file mode 100644 index 0000000..4666bdf --- /dev/null +++ b/backend/src/Mall.Infrastructure/Persistence/MallDbContext.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore; + +namespace Mall.Infrastructure.Persistence; + +public sealed class MallDbContext(DbContextOptions options) : DbContext(options) +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + ArgumentNullException.ThrowIfNull(modelBuilder); + modelBuilder.ApplyConfigurationsFromAssembly(typeof(MallDbContext).Assembly); + } +} diff --git a/backend/src/Mall.Infrastructure/Persistence/MallDbContextFactory.cs b/backend/src/Mall.Infrastructure/Persistence/MallDbContextFactory.cs new file mode 100644 index 0000000..b786557 --- /dev/null +++ b/backend/src/Mall.Infrastructure/Persistence/MallDbContextFactory.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Mall.Infrastructure.Persistence; + +public sealed class MallDbContextFactory : IDesignTimeDbContextFactory +{ + public MallDbContext CreateDbContext(string[] args) + { + var connectionString = Environment.GetEnvironmentVariable("ConnectionStrings__Postgres"); + if (string.IsNullOrWhiteSpace(connectionString)) + { + throw new InvalidOperationException( + "Design-time database operations require ConnectionStrings__Postgres."); + } + + var options = new DbContextOptionsBuilder() + .UseNpgsql( + connectionString, + npgsql => npgsql.MigrationsAssembly(typeof(MallDbContext).Assembly.FullName)) + .Options; + + return new MallDbContext(options); + } +} diff --git a/backend/src/Mall.Infrastructure/packages.lock.json b/backend/src/Mall.Infrastructure/packages.lock.json new file mode 100644 index 0000000..9736a2c --- /dev/null +++ b/backend/src/Mall.Infrastructure/packages.lock.json @@ -0,0 +1,714 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "AWSSDK.S3": { + "type": "Direct", + "requested": "[4.0.101.4, )", + "resolved": "4.0.101.4", + "contentHash": "TYFuatWECzCbj/Lu1SsANucgpU/Br5YJ5Padl3YEdNJGfZqAcx3QemJw2BUopAsYTJ3e7TFpWhpNWha3dB9/dw==", + "dependencies": { + "AWSSDK.Core": "[4.0.100.8, 5.0.0)" + } + }, + "Microsoft.EntityFrameworkCore.Design": { + "type": "Direct", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "BsvxiKcy8k4/ijAPitmwKG1mlVsdC2lQtFLP28K2N8PlsGYbqPFOyfJ7p2kWil3gM6xXgQGf8Hz/pJB8ej+Dug==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "18.0.2", + "Microsoft.CodeAnalysis.CSharp": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", + "Microsoft.CodeAnalysis.Workspaces.MSBuild": "5.0.0", + "Microsoft.EntityFrameworkCore.Relational": "10.0.10", + "Microsoft.Extensions.Caching.Memory": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.DependencyModel": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Mono.TextTemplating": "3.0.0", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Direct", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Hosting": { + "type": "Direct", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "tL9FkfV64GPUDSPvwrgyw42LVzsnVAnyrqJEuZVJbODgrQ3eL63zmzEcVWoCHzfgqUhWggzbgAyUCnz/zfI3Pg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.Configuration.CommandLine": "10.0.10", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.10", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.10", + "Microsoft.Extensions.Configuration.Json": "10.0.10", + "Microsoft.Extensions.Configuration.UserSecrets": "10.0.10", + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Diagnostics": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Configuration": "10.0.10", + "Microsoft.Extensions.Logging.Console": "10.0.10", + "Microsoft.Extensions.Logging.Debug": "10.0.10", + "Microsoft.Extensions.Logging.EventLog": "10.0.10", + "Microsoft.Extensions.Logging.EventSource": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Direct", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "Direct", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "IPGrrZnRkuW7OlHDhUESZz4G5DLkW7Nej/O3Cx+0iTsgyU5XJxBgpsvTHLloo3WWuAKKbDHXBvWPVkX1deRh1Q==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.3" + } + }, + "OpenTelemetry.Exporter.OpenTelemetryProtocol": { + "type": "Direct", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "R1omQOrQpGlS0Cp5UIr/TAiuEA48JrPlgr1NPV5gESiTU7HhWU+ILe2EBSYb1fKdsSavZ7nZkHcUxAzofPqr2A==", + "dependencies": { + "OpenTelemetry": "1.17.0" + } + }, + "OpenTelemetry.Extensions.Hosting": { + "type": "Direct", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "t1OwL/4qgboGMobYVT+UV5zgWnFqCp4Pw8lcsmzh8m2K8PQsTKkyxrC32tqYTMYny3GOW4q5cltE3dTVzLmRew==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "OpenTelemetry": "1.17.0" + } + }, + "OpenTelemetry.Instrumentation.AspNetCore": { + "type": "Direct", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "rGbmk1vuy1kvgZmE0ps7Vb99YZvDap6AalrrF60FwnNit1uW/PbeFZj1cpb0T8MPkYmjhBrRJ1/JB6QqXkRjHA==", + "dependencies": { + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.17.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Http": { + "type": "Direct", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "uTwVtxIJ/xB96wGYTaDsbkJVeCFdUxTwvrlDUn2YJixy0UuKc8DvQMzwKNJMTzNFiiyYO9c40id6tUHTmWs33A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.17.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Runtime": { + "type": "Direct", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "HyYenisDn/xdtyVXdjImsCl+RNC2gq01N0rvSR7tsYAylXR2sxX/YgMsyTajMXA27+r1vB7lNU8cWRhV0fwL+Q==", + "dependencies": { + "OpenTelemetry.Api": "[1.17.0, 2.0.0)" + } + }, + "RabbitMQ.Client": { + "type": "Direct", + "requested": "[7.2.1, )", + "resolved": "7.2.1", + "contentHash": "YKXEfg9fVQiTKgZlvIhAfPSFaamEgi8DsQmisCH0IAsU4FYLrtoguDrDj6JtJVGUt40QPnBLRH6fTQcAC4qsOg==", + "dependencies": { + "System.Threading.RateLimiting": "8.0.0" + } + }, + "Serilog.Extensions.Hosting": { + "type": "Direct", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Serilog": "4.3.0", + "Serilog.Extensions.Logging": "10.0.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "Direct", + "requested": "[3.0.0, )", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Console": { + "type": "Direct", + "requested": "[6.1.1, )", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "StackExchange.Redis": { + "type": "Direct", + "requested": "[3.0.17, )", + "resolved": "3.0.17", + "contentHash": "ItAm9lokZ1mWsQLF3u4Yw4eb/gvkX6Rp9bLAe1KUKgKAjPkALoQwTc6jJLtBZoOv9LT7RwvSIXZZb9FF30QhOg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "RESPite": "3.0.17", + "System.IO.Hashing": "10.0.5" + } + }, + "AWSSDK.Core": { + "type": "Transitive", + "resolved": "4.0.100.8", + "contentHash": "xnuBVLQBmYQXsDZJ9mq2UDSFZm3xgO5oUb4/UR8p0UO7tG6heDhsLLI1NzZhIVAKyfW0tXg9hn9a/ek018TOVw==" + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Microsoft.Build.Framework": { + "type": "Transitive", + "resolved": "18.0.2", + "contentHash": "sOSb+0J4G/jCBW/YqmRuL0eOMXgfw1KQLdC9TkbvfA5xs7uNm+PBQXJCOzSJGXtZcZrtXozcwxPmUiRUbmd7FA==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZXRAdvH6GiDeHRyd3q/km8Z44RoM6FBWHd+gen/la81mVnAdHTEsEkO5J0TCNXBymAcx5UYKt5TvgKBhaLJEow==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5DSyJ9bk+ATuDy7fp2Zt0mJStDVKbBoiz1DyfAwSa+k4H4IwykAUcV3URelw5b8/iVbfSaOwkwmPUZH6opZKCw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]" + } + }, + "Microsoft.CodeAnalysis.CSharp.Workspaces": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "Al/Q8B+yO8odSqGVpSvrShMFDvlQdIBU//F3E6Rb0YdiLSALE9wh/pvozPNnfmh5HDnvU+mkmSjpz4hQO++jaA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.CSharp": "[5.0.0]", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZbUmIvT6lqTNKiv06Jl5wf0MTMi1vQ1oH7ou4CLcs2C/no/L7EhP3T8y3XXvn9VbqMcJaJnEsNA1jwYUMgc5jg==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.MSBuild": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "/G+LVoAGMz6Ae8nm+PGLxSw+F5RjYx/J7irbTO5uKAPw1bxHyQJLc/YOnpDxt+EpPtYxvC9wvBsg/kETZp1F9Q==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "17.11.31", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "Microsoft.Extensions.DependencyInjection": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0", + "Microsoft.VisualStudio.SolutionPersistence": "1.0.52", + "Newtonsoft.Json": "13.0.3", + "System.Composition": "9.0.0" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "bOzrFCl6uZCjaSh2bG1ToRQRdx+iXvxosCg9hFyG9OWeAzOFI4xev9OqKeWfKf/kAHyox2JnbcvLVf2ceA7sqA==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "2gLDordUCGf3aNOOuqtTbP5mxhiP9nk6TnvGiE3RnqT891O+Zf/qKu1PIREubs1M16A0SImr4vULBfU5BTDs1Q==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "33cBeR2HRbzHUTtmcmLdNOApneNGcymwwL4arHuotgVK9Frba8kcDTrvVTj7cSCmF1R9OiSbZH0KxNOwab3HUg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "KRfFSSCV58vEdU7mPED/YMzeovIWF5P0g8s9K8n9HEfy0/WzMq37SrPdXdFN5/dFT/rPMHpF7AvpoXHckbcBFg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "1s1sKFTk/Foam64JY6+m/diH8drL3Wx6V3gtSd5v1IEZtszZYyc1pW8uRnMblzpNiR0l0t8gGk7tXj3xHzFgdg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Json": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "rfZA1RjR021RPqSmIPovfz2aOd79TGqJ9BengbjnzIISOVwjLmuSDnhCMmiY/1c6iYvGolQ1iNGzkav0u11XEA==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5LugpYGHk+mkn0a8IZgcyfBca8PCTAU9RQFoMrTdtOOidq88M2SI5f3px6ugnzgxC+eTkvYYJi8pzlUnG5xdAQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "cLrqxkuEfcilZ8SjK+9KAnpLk9lOoMPaOokF+wRUYie+iUEcdX4/p/+gJkt0BYgWLthjpBUCkVTBI6Kxg0nsOw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "VIlNzPwPS0GeQVSmCqqo36ugryX3LpE9ul6gEkks5VLET3weH/XMLeWmclwfoGn4Nxi2mwVibB+OZBVJ9tDqvg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Configuration": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Debug": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "8+TZBnV5fgBXoVNJ5ROSErUwYogk4hOgV7c2HWK1u5cqKGmiUTUn7+KqZ35iQu8e/B7Ykccyz5OTjdXcidNZ9g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.EventLog": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "0RE4951AzQ+YD4gVrvbq0BhdsiBgSDo44yM7+QBZ2mrmMJeNjY+teCIYfUjqDPVYnKs0HR6SkkhgrX1YgXZq3Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "System.Diagnostics.EventLog": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.EventSource": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "85SAPwXhJtdBInzN2k7SChiFiBGh3KOWay5AfoY+GREF6P7oZA98+ST2p7Z9384iLKYjkZSKIZ/FqIO5aojtNw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==" + }, + "Microsoft.VisualStudio.SolutionPersistence": { + "type": "Transitive", + "resolved": "1.0.52", + "contentHash": "oNv2JtYXhpdJrX63nibx1JT3uCESOBQ1LAk7Dtz/sr0+laW0KRM6eKp4CZ3MHDR2siIkKsY8MmUkeP5DKkQQ5w==" + }, + "Mono.TextTemplating": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "Npgsql": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "7nb5YzXuvWWJxB0J8DiyL3we+X4FOctZrt0fIBnucOIaIevFEEwGQVZKtiu9olXdlNAK1eNgqSral6r/jlhI4w==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "OpenTelemetry": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "rMLOTftlMlTm7+MSrvXDHnJRjVkROFNKXHZrYjOsX+LankaFG7QSflx7qRRGjoqZoirohnxmJQ7GEb9occO4Gg==", + "dependencies": { + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "1.17.0" + } + }, + "OpenTelemetry.Api": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "mSBxzomZgHIJu9CyVNqyDu/n2JHEtqVgfcCD1Br0cV5iLYogjZOMqhlVLt99PEp+0KGBNUR3GXgeOdN2GR3F9g==" + }, + "OpenTelemetry.Api.ProviderBuilderExtensions": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "Xgc3Qf9B9TFMFpx6exTdGqMWuYIT2miNzkdMPutVvT9YuMFaEovXWke1Gb6z8NxYaQbbGF38vYLuSg1JCeui5Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "OpenTelemetry.Api": "1.17.0" + } + }, + "RESPite": { + "type": "Transitive", + "resolved": "3.0.17", + "contentHash": "68slEMyRTUNLc75DruEDsEohFmFfNwkHLLtwQ46bobF+8Tl+UXMKM1kM87ihhKy6JdAqy1RrvAfvBWeLmcY9Gg==" + }, + "Serilog": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.0", + "Serilog": "4.2.0" + } + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Composition": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "3Djj70fFTraOarSKmRnmRy/zm4YurICm+kiCtI0dYRqGJnLX6nJ+G3WYuFJ173cAPax/gh96REcbNiVqcrypFQ==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Convention": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0", + "System.Composition.TypedParts": "9.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "iri00l/zIX9g4lHMY+Nz0qV1n40+jFYAmgsaiNn16xvt2RDwlqByNG4wgblagnDYxm3YSQQ0jLlC/7Xlk9CzyA==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "+vuqVP6xpi582XIjJi6OCsIxuoTZfR0M7WWufk3uGDeCl3wGW6KnpylUJ3iiXdPByPE0vR5TjJgR6hDLez4FQg==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "OFqSeFeJYr7kHxDfaViGM1ymk7d4JxK//VSoNF9Ux0gpqkLsauDZpu89kTHHNdCWfSljbFcvAafGyBoY094btQ==", + "dependencies": { + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "w1HOlQY1zsOWYussjFGZCEYF2UZXgvoYnS94NIu2CBnAGMbXFAX8PY8c92KwUItPmowal68jnVLBCzdrWLeEKA==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "aRZlojCCGEHDKqh43jaDgaVpYETsgd7Nx4g1zwLKMtv4iTo0627715ajEFNpEEBTgLmvZuv8K0EVxc3sM4NWJA==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "OvGz3PrzuAI/Sj7LTcXcCe3FClRI1IyRMZjNONcZtFh+Ww7nAtSh4kh08r8KVe/xxkXJPjR0Y1jF7H+N42d4xQ==" + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8IBJWcCT9+e4Bmevm4T7+fQEiAh133KGiz4oiVTgJckd3Q76OFdR1falgn9lpz7+C4HJvogCDJeAa2QmvbeVtg==" + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "mall.application": { + "type": "Project", + "dependencies": { + "Mall.Domain": "[0.1.0, )" + } + }, + "mall.domain": { + "type": "Project" + }, + "Microsoft.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "a0V7zj/VbYP6dTdWpUgE/r2PuLKtUGe2aJ0lVKkn/wP9ZhaxUz2kQydVfvOjCv2SKxlrqdBfHhPD4Cvlf+4ffA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.10", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.10", + "Microsoft.Extensions.Caching.Memory": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10" + } + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "wNonj40aZxia+GtuBiiD6ZqVh4h6y5Nje1bGdmzZ8/ui0QRsAN+S0SIrLHFCEGbG9cDbeaE40sh+Lr7o9rRs6g==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.10", + "Microsoft.Extensions.Caching.Memory": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10" + } + } + } + } +} \ No newline at end of file diff --git a/backend/src/Mall.Migrator/Mall.Migrator.csproj b/backend/src/Mall.Migrator/Mall.Migrator.csproj new file mode 100644 index 0000000..1e9bd98 --- /dev/null +++ b/backend/src/Mall.Migrator/Mall.Migrator.csproj @@ -0,0 +1,14 @@ + + + Exe + + + + + + + + + + + diff --git a/backend/src/Mall.Migrator/MigrationRunner.cs b/backend/src/Mall.Migrator/MigrationRunner.cs new file mode 100644 index 0000000..ba241f2 --- /dev/null +++ b/backend/src/Mall.Migrator/MigrationRunner.cs @@ -0,0 +1,59 @@ +using Mall.Infrastructure.Configuration; +using Mall.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Mall.Migrator; + +public sealed partial class MigrationRunner( + MallDbContext dbContext, + IOptions deploymentOptions, + ILogger logger) +{ + public async Task RunAsync(CancellationToken cancellationToken) + { + var targetMigration = deploymentOptions.Value.TargetMigration.Trim(); + if (targetMigration.Length == 0) + { + throw new InvalidOperationException("Deployment:TargetMigration must be configured."); + } + + var knownMigrations = dbContext.Database.GetMigrations().ToArray(); + if (!knownMigrations.Contains(targetMigration, StringComparer.Ordinal)) + { + throw new InvalidOperationException( + $"Target migration '{targetMigration}' is not compiled into Mall.Infrastructure."); + } + + LogApplyingMigration(logger, targetMigration); + await dbContext.Database.MigrateAsync(cancellationToken); + + var appliedMigrations = (await dbContext.Database + .GetAppliedMigrationsAsync(cancellationToken)) + .ToArray(); + if (!appliedMigrations.Contains(targetMigration, StringComparer.Ordinal)) + { + throw new InvalidOperationException( + $"Target migration '{targetMigration}' was not applied."); + } + + LogMigrationApplied(logger, targetMigration); + } + + [LoggerMessage( + EventId = 1001, + Level = LogLevel.Information, + Message = "Applying database migrations through target {TargetMigration}.")] + private static partial void LogApplyingMigration( + ILogger logger, + string targetMigration); + + [LoggerMessage( + EventId = 1002, + Level = LogLevel.Information, + Message = "Database migration target {TargetMigration} is applied.")] + private static partial void LogMigrationApplied( + ILogger logger, + string targetMigration); +} diff --git a/backend/src/Mall.Migrator/Program.cs b/backend/src/Mall.Migrator/Program.cs new file mode 100644 index 0000000..5d4c4a6 --- /dev/null +++ b/backend/src/Mall.Migrator/Program.cs @@ -0,0 +1,33 @@ +using Mall.Infrastructure; +using Mall.Infrastructure.Observability; +using Mall.Migrator; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; + +var builder = Host.CreateApplicationBuilder(args); + +builder.AddFoundationObservability(includeAspNetCoreInstrumentation: false); +builder.Services.AddFoundationInfrastructure(builder.Configuration); +builder.Services.AddScoped(); + +var host = builder.Build(); +try +{ + await host.StartAsync(); + await using var scope = host.Services.CreateAsyncScope(); + var runner = scope.ServiceProvider.GetRequiredService(); + await runner.RunAsync(CancellationToken.None); + await host.StopAsync(); +} +catch (Exception exception) when (exception is not OutOfMemoryException) +{ + Log.Fatal( + "Migration host stopped with failure type {FailureType}.", + exception.GetType().FullName); + Environment.ExitCode = 1; +} +finally +{ + await Log.CloseAndFlushAsync(); +} diff --git a/backend/src/Mall.Migrator/appsettings.json b/backend/src/Mall.Migrator/appsettings.json new file mode 100644 index 0000000..b53f33b --- /dev/null +++ b/backend/src/Mall.Migrator/appsettings.json @@ -0,0 +1,26 @@ +{ + "ConnectionStrings": { + "Postgres": "" + }, + "Deployment": { + "ServiceName": "mall-migrator", + "InstanceId": "migrator-unassigned", + "Version": "unknown", + "ExpectedVersion": "", + "TargetMigration": "InitialEshopSchema" + }, + "Infrastructure": { + "Redis": { + "Enabled": false + }, + "RabbitMq": { + "Enabled": false + }, + "ObjectStorage": { + "Enabled": false + } + }, + "Observability": { + "OtlpEndpoint": "" + } +} diff --git a/backend/src/Mall.Migrator/packages.lock.json b/backend/src/Mall.Migrator/packages.lock.json new file mode 100644 index 0000000..de8054f --- /dev/null +++ b/backend/src/Mall.Migrator/packages.lock.json @@ -0,0 +1,565 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "Microsoft.Extensions.Hosting": { + "type": "Direct", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "tL9FkfV64GPUDSPvwrgyw42LVzsnVAnyrqJEuZVJbODgrQ3eL63zmzEcVWoCHzfgqUhWggzbgAyUCnz/zfI3Pg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.Configuration.CommandLine": "10.0.10", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.10", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.10", + "Microsoft.Extensions.Configuration.Json": "10.0.10", + "Microsoft.Extensions.Configuration.UserSecrets": "10.0.10", + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Diagnostics": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Configuration": "10.0.10", + "Microsoft.Extensions.Logging.Console": "10.0.10", + "Microsoft.Extensions.Logging.Debug": "10.0.10", + "Microsoft.Extensions.Logging.EventLog": "10.0.10", + "Microsoft.Extensions.Logging.EventSource": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Serilog.Extensions.Hosting": { + "type": "Direct", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Serilog": "4.3.0", + "Serilog.Extensions.Logging": "10.0.0" + } + }, + "AWSSDK.Core": { + "type": "Transitive", + "resolved": "4.0.100.8", + "contentHash": "xnuBVLQBmYQXsDZJ9mq2UDSFZm3xgO5oUb4/UR8p0UO7tG6heDhsLLI1NzZhIVAKyfW0tXg9hn9a/ek018TOVw==" + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "bOzrFCl6uZCjaSh2bG1ToRQRdx+iXvxosCg9hFyG9OWeAzOFI4xev9OqKeWfKf/kAHyox2JnbcvLVf2ceA7sqA==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "2gLDordUCGf3aNOOuqtTbP5mxhiP9nk6TnvGiE3RnqT891O+Zf/qKu1PIREubs1M16A0SImr4vULBfU5BTDs1Q==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "33cBeR2HRbzHUTtmcmLdNOApneNGcymwwL4arHuotgVK9Frba8kcDTrvVTj7cSCmF1R9OiSbZH0KxNOwab3HUg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "KRfFSSCV58vEdU7mPED/YMzeovIWF5P0g8s9K8n9HEfy0/WzMq37SrPdXdFN5/dFT/rPMHpF7AvpoXHckbcBFg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "1s1sKFTk/Foam64JY6+m/diH8drL3Wx6V3gtSd5v1IEZtszZYyc1pW8uRnMblzpNiR0l0t8gGk7tXj3xHzFgdg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Json": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5LugpYGHk+mkn0a8IZgcyfBca8PCTAU9RQFoMrTdtOOidq88M2SI5f3px6ugnzgxC+eTkvYYJi8pzlUnG5xdAQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "cLrqxkuEfcilZ8SjK+9KAnpLk9lOoMPaOokF+wRUYie+iUEcdX4/p/+gJkt0BYgWLthjpBUCkVTBI6Kxg0nsOw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "VIlNzPwPS0GeQVSmCqqo36ugryX3LpE9ul6gEkks5VLET3weH/XMLeWmclwfoGn4Nxi2mwVibB+OZBVJ9tDqvg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Configuration": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Debug": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "8+TZBnV5fgBXoVNJ5ROSErUwYogk4hOgV7c2HWK1u5cqKGmiUTUn7+KqZ35iQu8e/B7Ykccyz5OTjdXcidNZ9g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.EventLog": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "0RE4951AzQ+YD4gVrvbq0BhdsiBgSDo44yM7+QBZ2mrmMJeNjY+teCIYfUjqDPVYnKs0HR6SkkhgrX1YgXZq3Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "System.Diagnostics.EventLog": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.EventSource": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "85SAPwXhJtdBInzN2k7SChiFiBGh3KOWay5AfoY+GREF6P7oZA98+ST2p7Z9384iLKYjkZSKIZ/FqIO5aojtNw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==" + }, + "Npgsql": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "7nb5YzXuvWWJxB0J8DiyL3we+X4FOctZrt0fIBnucOIaIevFEEwGQVZKtiu9olXdlNAK1eNgqSral6r/jlhI4w==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "OpenTelemetry": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "rMLOTftlMlTm7+MSrvXDHnJRjVkROFNKXHZrYjOsX+LankaFG7QSflx7qRRGjoqZoirohnxmJQ7GEb9occO4Gg==", + "dependencies": { + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "1.17.0" + } + }, + "OpenTelemetry.Api": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "mSBxzomZgHIJu9CyVNqyDu/n2JHEtqVgfcCD1Br0cV5iLYogjZOMqhlVLt99PEp+0KGBNUR3GXgeOdN2GR3F9g==" + }, + "OpenTelemetry.Api.ProviderBuilderExtensions": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "Xgc3Qf9B9TFMFpx6exTdGqMWuYIT2miNzkdMPutVvT9YuMFaEovXWke1Gb6z8NxYaQbbGF38vYLuSg1JCeui5Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "OpenTelemetry.Api": "1.17.0" + } + }, + "RESPite": { + "type": "Transitive", + "resolved": "3.0.17", + "contentHash": "68slEMyRTUNLc75DruEDsEohFmFfNwkHLLtwQ46bobF+8Tl+UXMKM1kM87ihhKy6JdAqy1RrvAfvBWeLmcY9Gg==" + }, + "Serilog": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.0", + "Serilog": "4.2.0" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "OvGz3PrzuAI/Sj7LTcXcCe3FClRI1IyRMZjNONcZtFh+Ww7nAtSh4kh08r8KVe/xxkXJPjR0Y1jF7H+N42d4xQ==" + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8IBJWcCT9+e4Bmevm4T7+fQEiAh133KGiz4oiVTgJckd3Q76OFdR1falgn9lpz7+C4HJvogCDJeAa2QmvbeVtg==" + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "mall.application": { + "type": "Project", + "dependencies": { + "Mall.Domain": "[0.1.0, )" + } + }, + "mall.domain": { + "type": "Project" + }, + "mall.infrastructure": { + "type": "Project", + "dependencies": { + "AWSSDK.S3": "[4.0.101.4, )", + "Mall.Application": "[0.1.0, )", + "Mall.Domain": "[0.1.0, )", + "Microsoft.Extensions.Configuration.Binder": "[10.0.10, )", + "Microsoft.Extensions.Hosting": "[10.0.10, )", + "Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.10, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.3, )", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.17.0, )", + "OpenTelemetry.Extensions.Hosting": "[1.17.0, )", + "OpenTelemetry.Instrumentation.AspNetCore": "[1.17.0, )", + "OpenTelemetry.Instrumentation.Http": "[1.17.0, )", + "OpenTelemetry.Instrumentation.Runtime": "[1.17.0, )", + "RabbitMQ.Client": "[7.2.1, )", + "Serilog.Extensions.Hosting": "[10.0.0, )", + "Serilog.Formatting.Compact": "[3.0.0, )", + "Serilog.Sinks.Console": "[6.1.1, )", + "StackExchange.Redis": "[3.0.17, )" + } + }, + "AWSSDK.S3": { + "type": "CentralTransitive", + "requested": "[4.0.101.4, )", + "resolved": "4.0.101.4", + "contentHash": "TYFuatWECzCbj/Lu1SsANucgpU/Br5YJ5Padl3YEdNJGfZqAcx3QemJw2BUopAsYTJ3e7TFpWhpNWha3dB9/dw==", + "dependencies": { + "AWSSDK.Core": "[4.0.100.8, 5.0.0)" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "a0V7zj/VbYP6dTdWpUgE/r2PuLKtUGe2aJ0lVKkn/wP9ZhaxUz2kQydVfvOjCv2SKxlrqdBfHhPD4Cvlf+4ffA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.10", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.10", + "Microsoft.Extensions.Caching.Memory": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10" + } + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "wNonj40aZxia+GtuBiiD6ZqVh4h6y5Nje1bGdmzZ8/ui0QRsAN+S0SIrLHFCEGbG9cDbeaE40sh+Lr7o9rRs6g==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.10", + "Microsoft.Extensions.Caching.Memory": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "IPGrrZnRkuW7OlHDhUESZz4G5DLkW7Nej/O3Cx+0iTsgyU5XJxBgpsvTHLloo3WWuAKKbDHXBvWPVkX1deRh1Q==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.3" + } + }, + "OpenTelemetry.Exporter.OpenTelemetryProtocol": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "R1omQOrQpGlS0Cp5UIr/TAiuEA48JrPlgr1NPV5gESiTU7HhWU+ILe2EBSYb1fKdsSavZ7nZkHcUxAzofPqr2A==", + "dependencies": { + "OpenTelemetry": "1.17.0" + } + }, + "OpenTelemetry.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "t1OwL/4qgboGMobYVT+UV5zgWnFqCp4Pw8lcsmzh8m2K8PQsTKkyxrC32tqYTMYny3GOW4q5cltE3dTVzLmRew==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "OpenTelemetry": "1.17.0" + } + }, + "OpenTelemetry.Instrumentation.AspNetCore": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "rGbmk1vuy1kvgZmE0ps7Vb99YZvDap6AalrrF60FwnNit1uW/PbeFZj1cpb0T8MPkYmjhBrRJ1/JB6QqXkRjHA==", + "dependencies": { + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.17.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Http": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "uTwVtxIJ/xB96wGYTaDsbkJVeCFdUxTwvrlDUn2YJixy0UuKc8DvQMzwKNJMTzNFiiyYO9c40id6tUHTmWs33A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.17.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Runtime": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "HyYenisDn/xdtyVXdjImsCl+RNC2gq01N0rvSR7tsYAylXR2sxX/YgMsyTajMXA27+r1vB7lNU8cWRhV0fwL+Q==", + "dependencies": { + "OpenTelemetry.Api": "[1.17.0, 2.0.0)" + } + }, + "RabbitMQ.Client": { + "type": "CentralTransitive", + "requested": "[7.2.1, )", + "resolved": "7.2.1", + "contentHash": "YKXEfg9fVQiTKgZlvIhAfPSFaamEgi8DsQmisCH0IAsU4FYLrtoguDrDj6JtJVGUt40QPnBLRH6fTQcAC4qsOg==", + "dependencies": { + "System.Threading.RateLimiting": "8.0.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "CentralTransitive", + "requested": "[3.0.0, )", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Console": { + "type": "CentralTransitive", + "requested": "[6.1.1, )", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "StackExchange.Redis": { + "type": "CentralTransitive", + "requested": "[3.0.17, )", + "resolved": "3.0.17", + "contentHash": "ItAm9lokZ1mWsQLF3u4Yw4eb/gvkX6Rp9bLAe1KUKgKAjPkALoQwTc6jJLtBZoOv9LT7RwvSIXZZb9FF30QhOg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "RESPite": "3.0.17", + "System.IO.Hashing": "10.0.5" + } + } + } + } +} \ No newline at end of file diff --git a/backend/src/Mall.Worker/Mall.Worker.csproj b/backend/src/Mall.Worker/Mall.Worker.csproj index 9af1011..927ad9f 100644 --- a/backend/src/Mall.Worker/Mall.Worker.csproj +++ b/backend/src/Mall.Worker/Mall.Worker.csproj @@ -1,18 +1,11 @@ - - - net10.0 - enable - enable - dotnet-Mall.Worker-af946f70-9035-4c0a-bad3-0722d032850e - - - + + - - + + diff --git a/backend/src/Mall.Worker/Program.cs b/backend/src/Mall.Worker/Program.cs index e4d8b4e..8b8ec59 100644 --- a/backend/src/Mall.Worker/Program.cs +++ b/backend/src/Mall.Worker/Program.cs @@ -1,4 +1,15 @@ +using Mall.Infrastructure; +using Mall.Infrastructure.Observability; +using Microsoft.Extensions.Hosting; + var builder = Host.CreateApplicationBuilder(args); +builder.AddFoundationObservability(includeAspNetCoreInstrumentation: false); +builder.Services.AddFoundationInfrastructure(builder.Configuration); +builder.Services.Configure(options => +{ + options.ShutdownTimeout = TimeSpan.FromSeconds(30); +}); + var host = builder.Build(); -host.Run(); +await host.RunAsync(); diff --git a/backend/src/Mall.Worker/Properties/launchSettings.json b/backend/src/Mall.Worker/Properties/launchSettings.json index e7af724..7837628 100644 --- a/backend/src/Mall.Worker/Properties/launchSettings.json +++ b/backend/src/Mall.Worker/Properties/launchSettings.json @@ -1,4 +1,4 @@ -{ +{ "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { "Mall.Worker": { diff --git a/backend/src/Mall.Worker/appsettings.Development.json b/backend/src/Mall.Worker/appsettings.Development.json index b2dcdb6..51d2579 100644 --- a/backend/src/Mall.Worker/appsettings.Development.json +++ b/backend/src/Mall.Worker/appsettings.Development.json @@ -1,8 +1,7 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.Hosting.Lifetime": "Information" - } + "Deployment": { + "InstanceId": "worker-local", + "Version": "local-dev", + "ExpectedVersion": "local-dev" } } diff --git a/backend/src/Mall.Worker/appsettings.json b/backend/src/Mall.Worker/appsettings.json index b2dcdb6..503dff7 100644 --- a/backend/src/Mall.Worker/appsettings.json +++ b/backend/src/Mall.Worker/appsettings.json @@ -1,8 +1,33 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.Hosting.Lifetime": "Information" + "ConnectionStrings": { + "Postgres": "", + "Redis": "", + "RabbitMq": "" + }, + "Deployment": { + "ServiceName": "mall-worker", + "InstanceId": "worker-unassigned", + "Version": "unknown", + "ExpectedVersion": "", + "TargetMigration": "InitialEshopSchema", + "ProbeTimeoutMilliseconds": 1500 + }, + "Infrastructure": { + "Redis": { + "Enabled": false + }, + "RabbitMq": { + "Enabled": false + }, + "ObjectStorage": { + "Enabled": false, + "ServiceUrl": "", + "AccessKey": "", + "SecretKey": "", + "BucketName": "" } + }, + "Observability": { + "OtlpEndpoint": "" } } diff --git a/backend/src/Mall.Worker/packages.lock.json b/backend/src/Mall.Worker/packages.lock.json new file mode 100644 index 0000000..de8054f --- /dev/null +++ b/backend/src/Mall.Worker/packages.lock.json @@ -0,0 +1,565 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "Microsoft.Extensions.Hosting": { + "type": "Direct", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "tL9FkfV64GPUDSPvwrgyw42LVzsnVAnyrqJEuZVJbODgrQ3eL63zmzEcVWoCHzfgqUhWggzbgAyUCnz/zfI3Pg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.Configuration.CommandLine": "10.0.10", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.10", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.10", + "Microsoft.Extensions.Configuration.Json": "10.0.10", + "Microsoft.Extensions.Configuration.UserSecrets": "10.0.10", + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Diagnostics": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Configuration": "10.0.10", + "Microsoft.Extensions.Logging.Console": "10.0.10", + "Microsoft.Extensions.Logging.Debug": "10.0.10", + "Microsoft.Extensions.Logging.EventLog": "10.0.10", + "Microsoft.Extensions.Logging.EventSource": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Serilog.Extensions.Hosting": { + "type": "Direct", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Serilog": "4.3.0", + "Serilog.Extensions.Logging": "10.0.0" + } + }, + "AWSSDK.Core": { + "type": "Transitive", + "resolved": "4.0.100.8", + "contentHash": "xnuBVLQBmYQXsDZJ9mq2UDSFZm3xgO5oUb4/UR8p0UO7tG6heDhsLLI1NzZhIVAKyfW0tXg9hn9a/ek018TOVw==" + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "bOzrFCl6uZCjaSh2bG1ToRQRdx+iXvxosCg9hFyG9OWeAzOFI4xev9OqKeWfKf/kAHyox2JnbcvLVf2ceA7sqA==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "2gLDordUCGf3aNOOuqtTbP5mxhiP9nk6TnvGiE3RnqT891O+Zf/qKu1PIREubs1M16A0SImr4vULBfU5BTDs1Q==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "33cBeR2HRbzHUTtmcmLdNOApneNGcymwwL4arHuotgVK9Frba8kcDTrvVTj7cSCmF1R9OiSbZH0KxNOwab3HUg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "KRfFSSCV58vEdU7mPED/YMzeovIWF5P0g8s9K8n9HEfy0/WzMq37SrPdXdFN5/dFT/rPMHpF7AvpoXHckbcBFg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "1s1sKFTk/Foam64JY6+m/diH8drL3Wx6V3gtSd5v1IEZtszZYyc1pW8uRnMblzpNiR0l0t8gGk7tXj3xHzFgdg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Json": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5LugpYGHk+mkn0a8IZgcyfBca8PCTAU9RQFoMrTdtOOidq88M2SI5f3px6ugnzgxC+eTkvYYJi8pzlUnG5xdAQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "cLrqxkuEfcilZ8SjK+9KAnpLk9lOoMPaOokF+wRUYie+iUEcdX4/p/+gJkt0BYgWLthjpBUCkVTBI6Kxg0nsOw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "VIlNzPwPS0GeQVSmCqqo36ugryX3LpE9ul6gEkks5VLET3weH/XMLeWmclwfoGn4Nxi2mwVibB+OZBVJ9tDqvg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Configuration": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Debug": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "8+TZBnV5fgBXoVNJ5ROSErUwYogk4hOgV7c2HWK1u5cqKGmiUTUn7+KqZ35iQu8e/B7Ykccyz5OTjdXcidNZ9g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.EventLog": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "0RE4951AzQ+YD4gVrvbq0BhdsiBgSDo44yM7+QBZ2mrmMJeNjY+teCIYfUjqDPVYnKs0HR6SkkhgrX1YgXZq3Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "System.Diagnostics.EventLog": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.EventSource": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "85SAPwXhJtdBInzN2k7SChiFiBGh3KOWay5AfoY+GREF6P7oZA98+ST2p7Z9384iLKYjkZSKIZ/FqIO5aojtNw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==" + }, + "Npgsql": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "7nb5YzXuvWWJxB0J8DiyL3we+X4FOctZrt0fIBnucOIaIevFEEwGQVZKtiu9olXdlNAK1eNgqSral6r/jlhI4w==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "OpenTelemetry": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "rMLOTftlMlTm7+MSrvXDHnJRjVkROFNKXHZrYjOsX+LankaFG7QSflx7qRRGjoqZoirohnxmJQ7GEb9occO4Gg==", + "dependencies": { + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "1.17.0" + } + }, + "OpenTelemetry.Api": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "mSBxzomZgHIJu9CyVNqyDu/n2JHEtqVgfcCD1Br0cV5iLYogjZOMqhlVLt99PEp+0KGBNUR3GXgeOdN2GR3F9g==" + }, + "OpenTelemetry.Api.ProviderBuilderExtensions": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "Xgc3Qf9B9TFMFpx6exTdGqMWuYIT2miNzkdMPutVvT9YuMFaEovXWke1Gb6z8NxYaQbbGF38vYLuSg1JCeui5Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "OpenTelemetry.Api": "1.17.0" + } + }, + "RESPite": { + "type": "Transitive", + "resolved": "3.0.17", + "contentHash": "68slEMyRTUNLc75DruEDsEohFmFfNwkHLLtwQ46bobF+8Tl+UXMKM1kM87ihhKy6JdAqy1RrvAfvBWeLmcY9Gg==" + }, + "Serilog": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.0", + "Serilog": "4.2.0" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "OvGz3PrzuAI/Sj7LTcXcCe3FClRI1IyRMZjNONcZtFh+Ww7nAtSh4kh08r8KVe/xxkXJPjR0Y1jF7H+N42d4xQ==" + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8IBJWcCT9+e4Bmevm4T7+fQEiAh133KGiz4oiVTgJckd3Q76OFdR1falgn9lpz7+C4HJvogCDJeAa2QmvbeVtg==" + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "mall.application": { + "type": "Project", + "dependencies": { + "Mall.Domain": "[0.1.0, )" + } + }, + "mall.domain": { + "type": "Project" + }, + "mall.infrastructure": { + "type": "Project", + "dependencies": { + "AWSSDK.S3": "[4.0.101.4, )", + "Mall.Application": "[0.1.0, )", + "Mall.Domain": "[0.1.0, )", + "Microsoft.Extensions.Configuration.Binder": "[10.0.10, )", + "Microsoft.Extensions.Hosting": "[10.0.10, )", + "Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.10, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.3, )", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.17.0, )", + "OpenTelemetry.Extensions.Hosting": "[1.17.0, )", + "OpenTelemetry.Instrumentation.AspNetCore": "[1.17.0, )", + "OpenTelemetry.Instrumentation.Http": "[1.17.0, )", + "OpenTelemetry.Instrumentation.Runtime": "[1.17.0, )", + "RabbitMQ.Client": "[7.2.1, )", + "Serilog.Extensions.Hosting": "[10.0.0, )", + "Serilog.Formatting.Compact": "[3.0.0, )", + "Serilog.Sinks.Console": "[6.1.1, )", + "StackExchange.Redis": "[3.0.17, )" + } + }, + "AWSSDK.S3": { + "type": "CentralTransitive", + "requested": "[4.0.101.4, )", + "resolved": "4.0.101.4", + "contentHash": "TYFuatWECzCbj/Lu1SsANucgpU/Br5YJ5Padl3YEdNJGfZqAcx3QemJw2BUopAsYTJ3e7TFpWhpNWha3dB9/dw==", + "dependencies": { + "AWSSDK.Core": "[4.0.100.8, 5.0.0)" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "a0V7zj/VbYP6dTdWpUgE/r2PuLKtUGe2aJ0lVKkn/wP9ZhaxUz2kQydVfvOjCv2SKxlrqdBfHhPD4Cvlf+4ffA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.10", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.10", + "Microsoft.Extensions.Caching.Memory": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10" + } + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "wNonj40aZxia+GtuBiiD6ZqVh4h6y5Nje1bGdmzZ8/ui0QRsAN+S0SIrLHFCEGbG9cDbeaE40sh+Lr7o9rRs6g==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.10", + "Microsoft.Extensions.Caching.Memory": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "IPGrrZnRkuW7OlHDhUESZz4G5DLkW7Nej/O3Cx+0iTsgyU5XJxBgpsvTHLloo3WWuAKKbDHXBvWPVkX1deRh1Q==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.3" + } + }, + "OpenTelemetry.Exporter.OpenTelemetryProtocol": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "R1omQOrQpGlS0Cp5UIr/TAiuEA48JrPlgr1NPV5gESiTU7HhWU+ILe2EBSYb1fKdsSavZ7nZkHcUxAzofPqr2A==", + "dependencies": { + "OpenTelemetry": "1.17.0" + } + }, + "OpenTelemetry.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "t1OwL/4qgboGMobYVT+UV5zgWnFqCp4Pw8lcsmzh8m2K8PQsTKkyxrC32tqYTMYny3GOW4q5cltE3dTVzLmRew==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "OpenTelemetry": "1.17.0" + } + }, + "OpenTelemetry.Instrumentation.AspNetCore": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "rGbmk1vuy1kvgZmE0ps7Vb99YZvDap6AalrrF60FwnNit1uW/PbeFZj1cpb0T8MPkYmjhBrRJ1/JB6QqXkRjHA==", + "dependencies": { + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.17.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Http": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "uTwVtxIJ/xB96wGYTaDsbkJVeCFdUxTwvrlDUn2YJixy0UuKc8DvQMzwKNJMTzNFiiyYO9c40id6tUHTmWs33A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.17.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Runtime": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "HyYenisDn/xdtyVXdjImsCl+RNC2gq01N0rvSR7tsYAylXR2sxX/YgMsyTajMXA27+r1vB7lNU8cWRhV0fwL+Q==", + "dependencies": { + "OpenTelemetry.Api": "[1.17.0, 2.0.0)" + } + }, + "RabbitMQ.Client": { + "type": "CentralTransitive", + "requested": "[7.2.1, )", + "resolved": "7.2.1", + "contentHash": "YKXEfg9fVQiTKgZlvIhAfPSFaamEgi8DsQmisCH0IAsU4FYLrtoguDrDj6JtJVGUt40QPnBLRH6fTQcAC4qsOg==", + "dependencies": { + "System.Threading.RateLimiting": "8.0.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "CentralTransitive", + "requested": "[3.0.0, )", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Console": { + "type": "CentralTransitive", + "requested": "[6.1.1, )", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "StackExchange.Redis": { + "type": "CentralTransitive", + "requested": "[3.0.17, )", + "resolved": "3.0.17", + "contentHash": "ItAm9lokZ1mWsQLF3u4Yw4eb/gvkX6Rp9bLAe1KUKgKAjPkALoQwTc6jJLtBZoOv9LT7RwvSIXZZb9FF30QhOg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "RESPite": "3.0.17", + "System.IO.Hashing": "10.0.5" + } + } + } + } +} \ No newline at end of file diff --git a/backend/tests/Mall.IntegrationTests/GlobalUsings.cs b/backend/tests/Mall.IntegrationTests/GlobalUsings.cs new file mode 100644 index 0000000..c802f44 --- /dev/null +++ b/backend/tests/Mall.IntegrationTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/backend/tests/Mall.IntegrationTests/HealthEndpointsTests.cs b/backend/tests/Mall.IntegrationTests/HealthEndpointsTests.cs index 34b4fcf..4ab0236 100644 --- a/backend/tests/Mall.IntegrationTests/HealthEndpointsTests.cs +++ b/backend/tests/Mall.IntegrationTests/HealthEndpointsTests.cs @@ -1,24 +1,60 @@ using System.Net; -using Microsoft.AspNetCore.Mvc.Testing; +using System.Net.Http.Json; +using System.Text.Json; namespace Mall.IntegrationTests; -public class HealthEndpointsTests : IClassFixture> +public sealed class HealthEndpointsTests(MallApiFactory factory) + : IClassFixture { - private readonly HttpClient _client; + private readonly HttpClient client = factory.CreateClient(); - public HealthEndpointsTests(WebApplicationFactory factory) + [Fact] + public async Task LivenessReturnsFrozenContractWithoutDependencies() { - _client = factory.CreateClient(); + var cancellationToken = TestContext.Current.CancellationToken; + using var response = await client.GetAsync("/health/live", cancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("no-store", response.Headers.CacheControl?.ToString()); + using var body = JsonDocument.Parse( + await response.Content.ReadAsStreamAsync(cancellationToken)); + Assert.Equal("healthy", body.RootElement.GetProperty("status").GetString()); + Assert.Equal("mall-api", body.RootElement.GetProperty("service").GetString()); + Assert.Equal("integration-api", body.RootElement.GetProperty("instanceId").GetString()); + Assert.True(body.RootElement.TryGetProperty("checkedAt", out _)); + Assert.Equal(4, body.RootElement.EnumerateObject().Count()); } - [Theory] - [InlineData("/health/live")] - [InlineData("/health/ready")] - public async Task GetHealthEndpoint_WhenApplicationStarts_ReturnsOk(string path) + [Fact] + public async Task ReadinessFailsClosedUntilSecurityDatabaseAndMigrationExist() { - var response = await _client.GetAsync(path); + var cancellationToken = TestContext.Current.CancellationToken; + using var response = await client.GetAsync("/health/ready", cancellationToken); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); + Assert.Contains("no-store", response.Headers.CacheControl?.ToString()); + using var body = JsonDocument.Parse( + await response.Content.ReadAsStreamAsync(cancellationToken)); + Assert.Equal("notReady", body.RootElement.GetProperty("status").GetString()); + Assert.Equal( + "unhealthy", + body.RootElement.GetProperty("globalGates") + .GetProperty("secureConfiguration") + .GetString()); + Assert.Equal( + "unhealthy", + body.RootElement.GetProperty("globalGates") + .GetProperty("migrationVersion") + .GetString()); + Assert.False( + body.RootElement.GetProperty("authenticationConfiguration") + .GetProperty("matchesExpected") + .GetBoolean()); + Assert.Equal( + "disabled", + body.RootElement.GetProperty("capabilities") + .GetProperty("catalogCache") + .GetString()); } } diff --git a/backend/tests/Mall.IntegrationTests/Mall.IntegrationTests.csproj b/backend/tests/Mall.IntegrationTests/Mall.IntegrationTests.csproj index d1343f4..20a0bde 100644 --- a/backend/tests/Mall.IntegrationTests/Mall.IntegrationTests.csproj +++ b/backend/tests/Mall.IntegrationTests/Mall.IntegrationTests.csproj @@ -1,27 +1,24 @@ - - + - net10.0 - enable - enable false + true - - - - - - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - diff --git a/backend/tests/Mall.IntegrationTests/MallApiFactory.cs b/backend/tests/Mall.IntegrationTests/MallApiFactory.cs new file mode 100644 index 0000000..a7541ae --- /dev/null +++ b/backend/tests/Mall.IntegrationTests/MallApiFactory.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; + +namespace Mall.IntegrationTests; + +public sealed class MallApiFactory : WebApplicationFactory +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseEnvironment("Development"); + builder.ConfigureAppConfiguration((_, configuration) => + { + configuration.AddInMemoryCollection( + new Dictionary + { + ["ConnectionStrings:Postgres"] = string.Empty, + ["Deployment:InstanceId"] = "integration-api", + ["Deployment:Version"] = "test-version", + ["Deployment:ExpectedVersion"] = "test-version", + ["OpenApi:EnableSwaggerUi"] = "false", + }); + }); + } +} diff --git a/backend/tests/Mall.IntegrationTests/OpenApiEndpointsTests.cs b/backend/tests/Mall.IntegrationTests/OpenApiEndpointsTests.cs new file mode 100644 index 0000000..14d2eff --- /dev/null +++ b/backend/tests/Mall.IntegrationTests/OpenApiEndpointsTests.cs @@ -0,0 +1,25 @@ +using System.Net; +using System.Text.Json; + +namespace Mall.IntegrationTests; + +public sealed class OpenApiEndpointsTests(MallApiFactory factory) + : IClassFixture +{ + private readonly HttpClient client = factory.CreateClient(); + + [Fact] + public async Task OpenApiContainsOnlyImplementedInfrastructureEndpoints() + { + var cancellationToken = TestContext.Current.CancellationToken; + using var response = await client.GetAsync("/openapi/v1.json", cancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + using var document = JsonDocument.Parse( + await response.Content.ReadAsStreamAsync(cancellationToken)); + var paths = document.RootElement.GetProperty("paths"); + Assert.True(paths.TryGetProperty("/health/live", out _)); + Assert.True(paths.TryGetProperty("/health/ready", out _)); + Assert.Equal(2, paths.EnumerateObject().Count()); + } +} diff --git a/backend/tests/Mall.IntegrationTests/ProblemDetailsTests.cs b/backend/tests/Mall.IntegrationTests/ProblemDetailsTests.cs new file mode 100644 index 0000000..5dfc29d --- /dev/null +++ b/backend/tests/Mall.IntegrationTests/ProblemDetailsTests.cs @@ -0,0 +1,27 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; + +namespace Mall.IntegrationTests; + +public sealed class ProblemDetailsTests(MallApiFactory factory) + : IClassFixture +{ + private readonly HttpClient client = factory.CreateClient(); + + [Fact] + public async Task UnknownRouteReturnsProblemDetailsWithTraceId() + { + var cancellationToken = TestContext.Current.CancellationToken; + using var response = await client.GetAsync("/api/not-implemented", cancellationToken); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + Assert.Equal( + "application/problem+json", + response.Content.Headers.ContentType?.MediaType); + using var body = JsonDocument.Parse( + await response.Content.ReadAsStreamAsync(cancellationToken)); + Assert.Equal(404, body.RootElement.GetProperty("status").GetInt32()); + Assert.True(body.RootElement.TryGetProperty("traceId", out _)); + } +} diff --git a/backend/tests/Mall.IntegrationTests/packages.lock.json b/backend/tests/Mall.IntegrationTests/packages.lock.json new file mode 100644 index 0000000..dd638b5 --- /dev/null +++ b/backend/tests/Mall.IntegrationTests/packages.lock.json @@ -0,0 +1,891 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "27jXSV/0DbVqF5jDrAxuQFZ9oaz6gmG03p8ttxAFk+X0M4woFYj7MoWDLCna5EGLb0CE6OE7X6ZH3Wt5smTtaA==" + }, + "Microsoft.AspNetCore.Mvc.Testing": { + "type": "Direct", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "pTWE4RtRbb9sl/U9QjZA5oapEZ01ZEMfRilZvvh55ZW97caTQ2XuAF6sgc+7ojKWBbR2qrcWVo5P80gMPuW/tQ==", + "dependencies": { + "Microsoft.AspNetCore.TestHost": "10.0.10", + "Microsoft.Extensions.DependencyModel": "10.0.10", + "Microsoft.Extensions.Hosting": "10.0.10" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[18.8.1, )", + "resolved": "18.8.1", + "contentHash": "dknJL3/9Y3t4XuCBqnc0PevPxgLsUMmVhjwup/b1HNovA8zWcj3XsfIf7c6p05363DWcqL7X/YhDL9B+Zymv1w==", + "dependencies": { + "Microsoft.CodeCoverage": "18.8.1", + "Microsoft.TestPlatform.TestHost": "18.8.1" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[3.1.5, )", + "resolved": "3.1.5", + "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" + }, + "xunit.v3": { + "type": "Direct", + "requested": "[3.2.2, )", + "resolved": "3.2.2", + "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==", + "dependencies": { + "xunit.v3.mtp-v1": "[3.2.2]" + } + }, + "AWSSDK.Core": { + "type": "Transitive", + "resolved": "4.0.100.8", + "contentHash": "xnuBVLQBmYQXsDZJ9mq2UDSFZm3xgO5oUb4/UR8p0UO7tG6heDhsLLI1NzZhIVAKyfW0tXg9hn9a/ek018TOVw==" + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.AspNetCore.TestHost": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Kks+OpQlP/eWQhnTjkiv0H9kc9Uqa7ieAzNV62yJpZ8Ips/WwpfpwrwZUKzV83CBJaBl5OI7J2XxVVTC8vah/Q==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.Bcl.Cryptography": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "LG9Yll3B5aNpxv0+D47g6LiOiKBIlodhcHdQwcYzo8VeexFLGqx5ymetmA2aBRyo9cCcWsQWrFsdbsr8LvmWDw==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.8.1", + "contentHash": "Eclse/ZZjr4lmWzZFNN9h/OluhKL+SK/QbUyKUewgX139aGeyMEO/DkMPwuFs2MixvanTnz6891rF8UHDg+W4Q==" + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "bOzrFCl6uZCjaSh2bG1ToRQRdx+iXvxosCg9hFyG9OWeAzOFI4xev9OqKeWfKf/kAHyox2JnbcvLVf2ceA7sqA==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "2gLDordUCGf3aNOOuqtTbP5mxhiP9nk6TnvGiE3RnqT891O+Zf/qKu1PIREubs1M16A0SImr4vULBfU5BTDs1Q==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "33cBeR2HRbzHUTtmcmLdNOApneNGcymwwL4arHuotgVK9Frba8kcDTrvVTj7cSCmF1R9OiSbZH0KxNOwab3HUg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "KRfFSSCV58vEdU7mPED/YMzeovIWF5P0g8s9K8n9HEfy0/WzMq37SrPdXdFN5/dFT/rPMHpF7AvpoXHckbcBFg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "1s1sKFTk/Foam64JY6+m/diH8drL3Wx6V3gtSd5v1IEZtszZYyc1pW8uRnMblzpNiR0l0t8gGk7tXj3xHzFgdg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Json": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "rfZA1RjR021RPqSmIPovfz2aOd79TGqJ9BengbjnzIISOVwjLmuSDnhCMmiY/1c6iYvGolQ1iNGzkav0u11XEA==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5LugpYGHk+mkn0a8IZgcyfBca8PCTAU9RQFoMrTdtOOidq88M2SI5f3px6ugnzgxC+eTkvYYJi8pzlUnG5xdAQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "cLrqxkuEfcilZ8SjK+9KAnpLk9lOoMPaOokF+wRUYie+iUEcdX4/p/+gJkt0BYgWLthjpBUCkVTBI6Kxg0nsOw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "VIlNzPwPS0GeQVSmCqqo36ugryX3LpE9ul6gEkks5VLET3weH/XMLeWmclwfoGn4Nxi2mwVibB+OZBVJ9tDqvg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Configuration": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Debug": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "8+TZBnV5fgBXoVNJ5ROSErUwYogk4hOgV7c2HWK1u5cqKGmiUTUn7+KqZ35iQu8e/B7Ykccyz5OTjdXcidNZ9g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.EventLog": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "0RE4951AzQ+YD4gVrvbq0BhdsiBgSDo44yM7+QBZ2mrmMJeNjY+teCIYfUjqDPVYnKs0HR6SkkhgrX1YgXZq3Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "System.Diagnostics.EventLog": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.EventSource": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "85SAPwXhJtdBInzN2k7SChiFiBGh3KOWay5AfoY+GREF6P7oZA98+ST2p7Z9384iLKYjkZSKIZ/FqIO5aojtNw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==" + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.19.2", + "contentHash": "HJbo/lnSfNHUfphPRT910poQc4T2/9+8svFLvzuaYHGAOJ2Tu+oEDqpX0BVP3BJ4OuUM1kylEKyaiX2fCAK3Cw==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.19.2", + "contentHash": "ui3fuBT4fs8kdKfBthI4NzLYIBIneEVS8UrL1JVBzAn80UiKmngBBi0BEByE7n/9c+EElcfFlCMFFTqpkBSLNA==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.19.2" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.19.2", + "contentHash": "r5YLDIxGOnkVJHrqXv/iD1FM1CgGrQOdriXuvuWvTPmKbnGANhEysq9XKmN6IHjf2a+9bAGEpnoRBsAQlvJU5w==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.19.2" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "8.19.2", + "contentHash": "sGxSsSrZXNmca6D+jHH2rVRyo2nNRd/g4H9CFbPmLLq0xgoH1U0orLWE5minfijw7+zq49tBs7txenbfAErRoQ==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.19.2" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "8.19.2", + "contentHash": "1XOcyY36cVymzE3qKdzKaUEZ4Pzt7ZpSa14JZoPPK1NLFUkQDs85TCqpV6XDo0YjFXj6nVK00AfOHppjghjhtw==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "8.19.2", + "System.IdentityModel.Tokens.Jwt": "8.19.2" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.19.2", + "contentHash": "GtPC1S02uH1gOO4fQ+zRysIicKmEXaYFP8PIkdJYXqMyruYhopre4ozVHp0XiDSA0+GJvOZH9prxCPvgBMg4Ww==", + "dependencies": { + "Microsoft.Bcl.Cryptography": "10.0.2", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.19.2" + } + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", + "dependencies": { + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", + "dependencies": { + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.8.1", + "contentHash": "qLbktNB1+b1XZLNJBTzaWVVJAd6PEzD7cgD406geMb6PcFZhp3EDNa1tctWx1+mtMU6MP/6ozVvFPC9vs2a9rw==" + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "18.8.1", + "contentHash": "FaQHPDTUOcE+SFTjssNPfrub2lT9Zyon4J2W/KLHt/efLJACb1TCeWXyOgh0D/4Q1e4n+S3E6mOKud+9nLZlEA==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.8.1" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" + }, + "Npgsql": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "7nb5YzXuvWWJxB0J8DiyL3we+X4FOctZrt0fIBnucOIaIevFEEwGQVZKtiu9olXdlNAK1eNgqSral6r/jlhI4w==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "OpenTelemetry": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "rMLOTftlMlTm7+MSrvXDHnJRjVkROFNKXHZrYjOsX+LankaFG7QSflx7qRRGjoqZoirohnxmJQ7GEb9occO4Gg==", + "dependencies": { + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "1.17.0" + } + }, + "OpenTelemetry.Api": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "mSBxzomZgHIJu9CyVNqyDu/n2JHEtqVgfcCD1Br0cV5iLYogjZOMqhlVLt99PEp+0KGBNUR3GXgeOdN2GR3F9g==" + }, + "OpenTelemetry.Api.ProviderBuilderExtensions": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "Xgc3Qf9B9TFMFpx6exTdGqMWuYIT2miNzkdMPutVvT9YuMFaEovXWke1Gb6z8NxYaQbbGF38vYLuSg1JCeui5Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "OpenTelemetry.Api": "1.17.0" + } + }, + "RESPite": { + "type": "Transitive", + "resolved": "3.0.17", + "contentHash": "68slEMyRTUNLc75DruEDsEohFmFfNwkHLLtwQ46bobF+8Tl+UXMKM1kM87ihhKy6JdAqy1RrvAfvBWeLmcY9Gg==" + }, + "Serilog": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.0", + "Serilog": "4.2.0" + } + }, + "Serilog.Settings.Configuration": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "10.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.0", + "Serilog": "4.3.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.File": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", + "dependencies": { + "Serilog": "4.2.0" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "OvGz3PrzuAI/Sj7LTcXcCe3FClRI1IyRMZjNONcZtFh+Ww7nAtSh4kh08r8KVe/xxkXJPjR0Y1jF7H+N42d4xQ==" + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "8.19.2", + "contentHash": "gqhDC/icByKEutygpr+OFgAmjwTVowyzFjWB8K0q1ww8uFj5a1BQNL+QvijUl9uhq4p8OdDwcAIrjVC+eC9FVA==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.19.2", + "Microsoft.IdentityModel.Tokens": "8.19.2" + } + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8IBJWcCT9+e4Bmevm4T7+fQEiAh133KGiz4oiVTgJckd3Q76OFdR1falgn9lpz7+C4HJvogCDJeAa2QmvbeVtg==" + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.27.0", + "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==" + }, + "xunit.v3.assert": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==" + }, + "xunit.v3.common": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0" + } + }, + "xunit.v3.core.mtp-v1": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==", + "dependencies": { + "Microsoft.Testing.Extensions.Telemetry": "1.9.1", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", + "Microsoft.Testing.Platform": "1.9.1", + "Microsoft.Testing.Platform.MSBuild": "1.9.1", + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.inproc.console": "[3.2.2]" + } + }, + "xunit.v3.extensibility.core": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==", + "dependencies": { + "xunit.v3.common": "[3.2.2]" + } + }, + "xunit.v3.mtp-v1": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==", + "dependencies": { + "xunit.analyzers": "1.27.0", + "xunit.v3.assert": "[3.2.2]", + "xunit.v3.core.mtp-v1": "[3.2.2]" + } + }, + "xunit.v3.runner.common": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==", + "dependencies": { + "Microsoft.Win32.Registry": "[5.0.0]", + "xunit.v3.common": "[3.2.2]" + } + }, + "xunit.v3.runner.inproc.console": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==", + "dependencies": { + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.common": "[3.2.2]" + } + }, + "mall.api": { + "type": "Project", + "dependencies": { + "Mall.Application": "[0.1.0, )", + "Mall.Infrastructure": "[0.1.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[10.0.10, )", + "Microsoft.AspNetCore.OpenApi": "[10.0.10, )", + "Serilog.AspNetCore": "[10.0.0, )", + "Swashbuckle.AspNetCore.SwaggerUI": "[10.2.3, )" + } + }, + "mall.application": { + "type": "Project", + "dependencies": { + "Mall.Domain": "[0.1.0, )" + } + }, + "mall.domain": { + "type": "Project" + }, + "mall.infrastructure": { + "type": "Project", + "dependencies": { + "AWSSDK.S3": "[4.0.101.4, )", + "Mall.Application": "[0.1.0, )", + "Mall.Domain": "[0.1.0, )", + "Microsoft.Extensions.Configuration.Binder": "[10.0.10, )", + "Microsoft.Extensions.Hosting": "[10.0.10, )", + "Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.10, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.3, )", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.17.0, )", + "OpenTelemetry.Extensions.Hosting": "[1.17.0, )", + "OpenTelemetry.Instrumentation.AspNetCore": "[1.17.0, )", + "OpenTelemetry.Instrumentation.Http": "[1.17.0, )", + "OpenTelemetry.Instrumentation.Runtime": "[1.17.0, )", + "RabbitMQ.Client": "[7.2.1, )", + "Serilog.Extensions.Hosting": "[10.0.0, )", + "Serilog.Formatting.Compact": "[3.0.0, )", + "Serilog.Sinks.Console": "[6.1.1, )", + "StackExchange.Redis": "[3.0.17, )" + } + }, + "AWSSDK.S3": { + "type": "CentralTransitive", + "requested": "[4.0.101.4, )", + "resolved": "4.0.101.4", + "contentHash": "TYFuatWECzCbj/Lu1SsANucgpU/Br5YJ5Padl3YEdNJGfZqAcx3QemJw2BUopAsYTJ3e7TFpWhpNWha3dB9/dw==", + "dependencies": { + "AWSSDK.Core": "[4.0.100.8, 5.0.0)" + } + }, + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "VAcqS42zb9WJd9DjPdkVTS5YrQENmNzPNJuRu8VAW7x3TEWUipc4d4hHzVJdFB0h/KLdr4XcXZzRHcUOKVanMQ==", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.19.2" + } + }, + "Microsoft.AspNetCore.OpenApi": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "d4Atx9IHq7JgX0F/h7Db+m9zAUzC+cKdI9k+OWnnyQIOUQtfvjIEuhvbjPigVMkAmPUgCbJ8Yp6M9ghUqHtJSQ==", + "dependencies": { + "Microsoft.OpenApi": "2.0.0" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "a0V7zj/VbYP6dTdWpUgE/r2PuLKtUGe2aJ0lVKkn/wP9ZhaxUz2kQydVfvOjCv2SKxlrqdBfHhPD4Cvlf+4ffA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.10", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.10", + "Microsoft.Extensions.Caching.Memory": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10" + } + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "wNonj40aZxia+GtuBiiD6ZqVh4h6y5Nje1bGdmzZ8/ui0QRsAN+S0SIrLHFCEGbG9cDbeaE40sh+Lr7o9rRs6g==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.10", + "Microsoft.Extensions.Caching.Memory": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "tL9FkfV64GPUDSPvwrgyw42LVzsnVAnyrqJEuZVJbODgrQ3eL63zmzEcVWoCHzfgqUhWggzbgAyUCnz/zfI3Pg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.Configuration.CommandLine": "10.0.10", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.10", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.10", + "Microsoft.Extensions.Configuration.Json": "10.0.10", + "Microsoft.Extensions.Configuration.UserSecrets": "10.0.10", + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Diagnostics": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Configuration": "10.0.10", + "Microsoft.Extensions.Logging.Console": "10.0.10", + "Microsoft.Extensions.Logging.Debug": "10.0.10", + "Microsoft.Extensions.Logging.EventLog": "10.0.10", + "Microsoft.Extensions.Logging.EventSource": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.OpenApi": { + "type": "CentralTransitive", + "requested": "[2.7.5, )", + "resolved": "2.7.5", + "contentHash": "0FA67RSnRM4tcBKqiqVu/HPdZ9+QOKbmeRjxRUGTCjPU4C0bmUhd97Dso7Yild5P7nOV6GxJ2xrK0Kv/O9xp0w==" + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "IPGrrZnRkuW7OlHDhUESZz4G5DLkW7Nej/O3Cx+0iTsgyU5XJxBgpsvTHLloo3WWuAKKbDHXBvWPVkX1deRh1Q==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.3" + } + }, + "OpenTelemetry.Exporter.OpenTelemetryProtocol": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "R1omQOrQpGlS0Cp5UIr/TAiuEA48JrPlgr1NPV5gESiTU7HhWU+ILe2EBSYb1fKdsSavZ7nZkHcUxAzofPqr2A==", + "dependencies": { + "OpenTelemetry": "1.17.0" + } + }, + "OpenTelemetry.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "t1OwL/4qgboGMobYVT+UV5zgWnFqCp4Pw8lcsmzh8m2K8PQsTKkyxrC32tqYTMYny3GOW4q5cltE3dTVzLmRew==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "OpenTelemetry": "1.17.0" + } + }, + "OpenTelemetry.Instrumentation.AspNetCore": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "rGbmk1vuy1kvgZmE0ps7Vb99YZvDap6AalrrF60FwnNit1uW/PbeFZj1cpb0T8MPkYmjhBrRJ1/JB6QqXkRjHA==", + "dependencies": { + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.17.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Http": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "uTwVtxIJ/xB96wGYTaDsbkJVeCFdUxTwvrlDUn2YJixy0UuKc8DvQMzwKNJMTzNFiiyYO9c40id6tUHTmWs33A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.17.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Runtime": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "HyYenisDn/xdtyVXdjImsCl+RNC2gq01N0rvSR7tsYAylXR2sxX/YgMsyTajMXA27+r1vB7lNU8cWRhV0fwL+Q==", + "dependencies": { + "OpenTelemetry.Api": "[1.17.0, 2.0.0)" + } + }, + "RabbitMQ.Client": { + "type": "CentralTransitive", + "requested": "[7.2.1, )", + "resolved": "7.2.1", + "contentHash": "YKXEfg9fVQiTKgZlvIhAfPSFaamEgi8DsQmisCH0IAsU4FYLrtoguDrDj6JtJVGUt40QPnBLRH6fTQcAC4qsOg==", + "dependencies": { + "System.Threading.RateLimiting": "8.0.0" + } + }, + "Serilog.AspNetCore": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", + "dependencies": { + "Serilog": "4.3.0", + "Serilog.Extensions.Hosting": "10.0.0", + "Serilog.Formatting.Compact": "3.0.0", + "Serilog.Settings.Configuration": "10.0.0", + "Serilog.Sinks.Console": "6.1.1", + "Serilog.Sinks.Debug": "3.0.0", + "Serilog.Sinks.File": "7.0.0" + } + }, + "Serilog.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Serilog": "4.3.0", + "Serilog.Extensions.Logging": "10.0.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "CentralTransitive", + "requested": "[3.0.0, )", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Console": { + "type": "CentralTransitive", + "requested": "[6.1.1, )", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "StackExchange.Redis": { + "type": "CentralTransitive", + "requested": "[3.0.17, )", + "resolved": "3.0.17", + "contentHash": "ItAm9lokZ1mWsQLF3u4Yw4eb/gvkX6Rp9bLAe1KUKgKAjPkALoQwTc6jJLtBZoOv9LT7RwvSIXZZb9FF30QhOg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "RESPite": "3.0.17", + "System.IO.Hashing": "10.0.5" + } + }, + "Swashbuckle.AspNetCore.SwaggerUI": { + "type": "CentralTransitive", + "requested": "[10.2.3, )", + "resolved": "10.2.3", + "contentHash": "nthWONRs/FJ4yyG206g1cC52WEG8EqrjuMWjGdR+5XG7lbjFto6NqcI9EMICgVFom/UivIjUVwI76ZHbHwTPfQ==" + } + } + } +} \ No newline at end of file diff --git a/backend/tests/Mall.UnitTests/ArchitectureDependencyTests.cs b/backend/tests/Mall.UnitTests/ArchitectureDependencyTests.cs index 677e0bd..b5945d4 100644 --- a/backend/tests/Mall.UnitTests/ArchitectureDependencyTests.cs +++ b/backend/tests/Mall.UnitTests/ArchitectureDependencyTests.cs @@ -1,18 +1,46 @@ -using System.Reflection; +using Mall.Application; +using Mall.Domain; +using Mall.Infrastructure.Persistence; namespace Mall.UnitTests; -public class ArchitectureDependencyTests +public sealed class ArchitectureDependencyTests { [Fact] - public void DomainReferences_WhenInspected_DoNotIncludeOuterLayers() + public void DomainDoesNotReferenceOtherMallLayers() { - var referencedAssemblies = Assembly - .Load("Mall.Domain") - .GetReferencedAssemblies() - .Select(assembly => assembly.Name); + var references = MallReferences(typeof(DomainAssembly).Assembly); - Assert.DoesNotContain("Mall.Application", referencedAssemblies); - Assert.DoesNotContain("Mall.Infrastructure", referencedAssemblies); + Assert.Empty(references); } + + [Fact] + public void ApplicationDoesNotReferenceInfrastructureOrHosts() + { + var references = MallReferences(typeof(ApplicationAssembly).Assembly); + + Assert.DoesNotContain("Mall.Infrastructure", references); + Assert.DoesNotContain("Mall.Api", references); + Assert.DoesNotContain("Mall.Worker", references); + Assert.DoesNotContain("Mall.Migrator", references); + } + + [Fact] + public void InfrastructureDoesNotReferenceApiOrWorkerHosts() + { + var references = MallReferences(typeof(MallDbContext).Assembly); + + Assert.DoesNotContain("Mall.Api", references); + Assert.DoesNotContain("Mall.Worker", references); + Assert.DoesNotContain("Mall.Migrator", references); + } + + private static string[] MallReferences(System.Reflection.Assembly assembly) => + assembly + .GetReferencedAssemblies() + .Select(reference => reference.Name) + .Where(name => name?.StartsWith("Mall.", StringComparison.Ordinal) == true) + .Cast() + .Order(StringComparer.Ordinal) + .ToArray(); } diff --git a/backend/tests/Mall.UnitTests/AuthenticationDigestCalculatorTests.cs b/backend/tests/Mall.UnitTests/AuthenticationDigestCalculatorTests.cs new file mode 100644 index 0000000..7896e66 --- /dev/null +++ b/backend/tests/Mall.UnitTests/AuthenticationDigestCalculatorTests.cs @@ -0,0 +1,60 @@ +using Mall.Infrastructure.Authentication; + +namespace Mall.UnitTests; + +public sealed class AuthenticationDigestCalculatorTests +{ + [Fact] + public void TryComputeDigestUsesFrozenCanonicalFieldOrder() + { + var options = ValidContract(); + + var digest = AuthenticationDigestCalculator.TryComputeDigest(options); + + Assert.Equal( + "4a69b01c4f8ec1947e11c307d3bb1c12c01f83b51a99bed15260ff8d998e9b73", + digest); + } + + [Fact] + public void CreateSnapshotRequiresExpectedHttpAndHubDigestsToMatch() + { + var contract = ValidContract(); + var expected = AuthenticationDigestCalculator.TryComputeDigest(contract); + var security = new AuthenticationSecurityOptions + { + SigningKey = new string('x', 32), + ExpectedDigest = expected!, + }; + + var snapshot = AuthenticationDigestCalculator.CreateSnapshot( + security, + contract, + contract); + + Assert.True(snapshot.MatchesExpected); + Assert.True(snapshot.HasSecureSigningMaterial); + Assert.Equal("SHA-256", snapshot.DigestAlgorithm); + } + + [Theory] + [InlineData(1)] + [InlineData(30)] + public void TryComputeDigestRejectsClockSkew(int clockSkewSeconds) + { + var options = ValidContract(clockSkewSeconds); + + Assert.Null(AuthenticationDigestCalculator.TryComputeDigest(options)); + } + + private static AuthenticationContractOptions ValidContract(int clockSkewSeconds = 0) => + new() + { + Issuer = "https://identity.example.test", + Audience = "eshop-web", + KeyFingerprint = "kid-2026-07", + AccessTokenLifetimeSeconds = 7200, + ClockSkewSeconds = clockSkewSeconds, + TokenVersionValidationRule = "user.tokenVersion==jwt.tokenVersion", + }; +} diff --git a/backend/tests/Mall.UnitTests/GlobalUsings.cs b/backend/tests/Mall.UnitTests/GlobalUsings.cs new file mode 100644 index 0000000..c802f44 --- /dev/null +++ b/backend/tests/Mall.UnitTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/backend/tests/Mall.UnitTests/Mall.UnitTests.csproj b/backend/tests/Mall.UnitTests/Mall.UnitTests.csproj index dbecac3..545f10a 100644 --- a/backend/tests/Mall.UnitTests/Mall.UnitTests.csproj +++ b/backend/tests/Mall.UnitTests/Mall.UnitTests.csproj @@ -1,26 +1,25 @@ - - + - net10.0 - enable - enable false + true - - - - - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + + - - \ No newline at end of file + diff --git a/backend/tests/Mall.UnitTests/packages.lock.json b/backend/tests/Mall.UnitTests/packages.lock.json new file mode 100644 index 0000000..c304e8b --- /dev/null +++ b/backend/tests/Mall.UnitTests/packages.lock.json @@ -0,0 +1,726 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "27jXSV/0DbVqF5jDrAxuQFZ9oaz6gmG03p8ttxAFk+X0M4woFYj7MoWDLCna5EGLb0CE6OE7X6ZH3Wt5smTtaA==" + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[18.8.1, )", + "resolved": "18.8.1", + "contentHash": "dknJL3/9Y3t4XuCBqnc0PevPxgLsUMmVhjwup/b1HNovA8zWcj3XsfIf7c6p05363DWcqL7X/YhDL9B+Zymv1w==", + "dependencies": { + "Microsoft.CodeCoverage": "18.8.1", + "Microsoft.TestPlatform.TestHost": "18.8.1" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[3.1.5, )", + "resolved": "3.1.5", + "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" + }, + "xunit.v3": { + "type": "Direct", + "requested": "[3.2.2, )", + "resolved": "3.2.2", + "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==", + "dependencies": { + "xunit.v3.mtp-v1": "[3.2.2]" + } + }, + "AWSSDK.Core": { + "type": "Transitive", + "resolved": "4.0.100.8", + "contentHash": "xnuBVLQBmYQXsDZJ9mq2UDSFZm3xgO5oUb4/UR8p0UO7tG6heDhsLLI1NzZhIVAKyfW0tXg9hn9a/ek018TOVw==" + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.8.1", + "contentHash": "Eclse/ZZjr4lmWzZFNN9h/OluhKL+SK/QbUyKUewgX139aGeyMEO/DkMPwuFs2MixvanTnz6891rF8UHDg+W4Q==" + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "bOzrFCl6uZCjaSh2bG1ToRQRdx+iXvxosCg9hFyG9OWeAzOFI4xev9OqKeWfKf/kAHyox2JnbcvLVf2ceA7sqA==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "2gLDordUCGf3aNOOuqtTbP5mxhiP9nk6TnvGiE3RnqT891O+Zf/qKu1PIREubs1M16A0SImr4vULBfU5BTDs1Q==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "33cBeR2HRbzHUTtmcmLdNOApneNGcymwwL4arHuotgVK9Frba8kcDTrvVTj7cSCmF1R9OiSbZH0KxNOwab3HUg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "KRfFSSCV58vEdU7mPED/YMzeovIWF5P0g8s9K8n9HEfy0/WzMq37SrPdXdFN5/dFT/rPMHpF7AvpoXHckbcBFg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "1s1sKFTk/Foam64JY6+m/diH8drL3Wx6V3gtSd5v1IEZtszZYyc1pW8uRnMblzpNiR0l0t8gGk7tXj3xHzFgdg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Json": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5LugpYGHk+mkn0a8IZgcyfBca8PCTAU9RQFoMrTdtOOidq88M2SI5f3px6ugnzgxC+eTkvYYJi8pzlUnG5xdAQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "cLrqxkuEfcilZ8SjK+9KAnpLk9lOoMPaOokF+wRUYie+iUEcdX4/p/+gJkt0BYgWLthjpBUCkVTBI6Kxg0nsOw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "VIlNzPwPS0GeQVSmCqqo36ugryX3LpE9ul6gEkks5VLET3weH/XMLeWmclwfoGn4Nxi2mwVibB+OZBVJ9tDqvg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Configuration": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Debug": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "8+TZBnV5fgBXoVNJ5ROSErUwYogk4hOgV7c2HWK1u5cqKGmiUTUn7+KqZ35iQu8e/B7Ykccyz5OTjdXcidNZ9g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.EventLog": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "0RE4951AzQ+YD4gVrvbq0BhdsiBgSDo44yM7+QBZ2mrmMJeNjY+teCIYfUjqDPVYnKs0HR6SkkhgrX1YgXZq3Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "System.Diagnostics.EventLog": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.EventSource": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "85SAPwXhJtdBInzN2k7SChiFiBGh3KOWay5AfoY+GREF6P7oZA98+ST2p7Z9384iLKYjkZSKIZ/FqIO5aojtNw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==" + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", + "dependencies": { + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", + "dependencies": { + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.8.1", + "contentHash": "qLbktNB1+b1XZLNJBTzaWVVJAd6PEzD7cgD406geMb6PcFZhp3EDNa1tctWx1+mtMU6MP/6ozVvFPC9vs2a9rw==" + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "18.8.1", + "contentHash": "FaQHPDTUOcE+SFTjssNPfrub2lT9Zyon4J2W/KLHt/efLJACb1TCeWXyOgh0D/4Q1e4n+S3E6mOKud+9nLZlEA==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.8.1" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" + }, + "Npgsql": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "7nb5YzXuvWWJxB0J8DiyL3we+X4FOctZrt0fIBnucOIaIevFEEwGQVZKtiu9olXdlNAK1eNgqSral6r/jlhI4w==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "OpenTelemetry": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "rMLOTftlMlTm7+MSrvXDHnJRjVkROFNKXHZrYjOsX+LankaFG7QSflx7qRRGjoqZoirohnxmJQ7GEb9occO4Gg==", + "dependencies": { + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "1.17.0" + } + }, + "OpenTelemetry.Api": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "mSBxzomZgHIJu9CyVNqyDu/n2JHEtqVgfcCD1Br0cV5iLYogjZOMqhlVLt99PEp+0KGBNUR3GXgeOdN2GR3F9g==" + }, + "OpenTelemetry.Api.ProviderBuilderExtensions": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "Xgc3Qf9B9TFMFpx6exTdGqMWuYIT2miNzkdMPutVvT9YuMFaEovXWke1Gb6z8NxYaQbbGF38vYLuSg1JCeui5Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "OpenTelemetry.Api": "1.17.0" + } + }, + "RESPite": { + "type": "Transitive", + "resolved": "3.0.17", + "contentHash": "68slEMyRTUNLc75DruEDsEohFmFfNwkHLLtwQ46bobF+8Tl+UXMKM1kM87ihhKy6JdAqy1RrvAfvBWeLmcY9Gg==" + }, + "Serilog": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.0", + "Serilog": "4.2.0" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "OvGz3PrzuAI/Sj7LTcXcCe3FClRI1IyRMZjNONcZtFh+Ww7nAtSh4kh08r8KVe/xxkXJPjR0Y1jF7H+N42d4xQ==" + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8IBJWcCT9+e4Bmevm4T7+fQEiAh133KGiz4oiVTgJckd3Q76OFdR1falgn9lpz7+C4HJvogCDJeAa2QmvbeVtg==" + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.27.0", + "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==" + }, + "xunit.v3.assert": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==" + }, + "xunit.v3.common": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0" + } + }, + "xunit.v3.core.mtp-v1": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==", + "dependencies": { + "Microsoft.Testing.Extensions.Telemetry": "1.9.1", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", + "Microsoft.Testing.Platform": "1.9.1", + "Microsoft.Testing.Platform.MSBuild": "1.9.1", + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.inproc.console": "[3.2.2]" + } + }, + "xunit.v3.extensibility.core": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==", + "dependencies": { + "xunit.v3.common": "[3.2.2]" + } + }, + "xunit.v3.mtp-v1": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==", + "dependencies": { + "xunit.analyzers": "1.27.0", + "xunit.v3.assert": "[3.2.2]", + "xunit.v3.core.mtp-v1": "[3.2.2]" + } + }, + "xunit.v3.runner.common": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==", + "dependencies": { + "Microsoft.Win32.Registry": "[5.0.0]", + "xunit.v3.common": "[3.2.2]" + } + }, + "xunit.v3.runner.inproc.console": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==", + "dependencies": { + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.common": "[3.2.2]" + } + }, + "mall.application": { + "type": "Project", + "dependencies": { + "Mall.Domain": "[0.1.0, )" + } + }, + "mall.domain": { + "type": "Project" + }, + "mall.infrastructure": { + "type": "Project", + "dependencies": { + "AWSSDK.S3": "[4.0.101.4, )", + "Mall.Application": "[0.1.0, )", + "Mall.Domain": "[0.1.0, )", + "Microsoft.Extensions.Configuration.Binder": "[10.0.10, )", + "Microsoft.Extensions.Hosting": "[10.0.10, )", + "Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.10, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.3, )", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.17.0, )", + "OpenTelemetry.Extensions.Hosting": "[1.17.0, )", + "OpenTelemetry.Instrumentation.AspNetCore": "[1.17.0, )", + "OpenTelemetry.Instrumentation.Http": "[1.17.0, )", + "OpenTelemetry.Instrumentation.Runtime": "[1.17.0, )", + "RabbitMQ.Client": "[7.2.1, )", + "Serilog.Extensions.Hosting": "[10.0.0, )", + "Serilog.Formatting.Compact": "[3.0.0, )", + "Serilog.Sinks.Console": "[6.1.1, )", + "StackExchange.Redis": "[3.0.17, )" + } + }, + "AWSSDK.S3": { + "type": "CentralTransitive", + "requested": "[4.0.101.4, )", + "resolved": "4.0.101.4", + "contentHash": "TYFuatWECzCbj/Lu1SsANucgpU/Br5YJ5Padl3YEdNJGfZqAcx3QemJw2BUopAsYTJ3e7TFpWhpNWha3dB9/dw==", + "dependencies": { + "AWSSDK.Core": "[4.0.100.8, 5.0.0)" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "a0V7zj/VbYP6dTdWpUgE/r2PuLKtUGe2aJ0lVKkn/wP9ZhaxUz2kQydVfvOjCv2SKxlrqdBfHhPD4Cvlf+4ffA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.10", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.10", + "Microsoft.Extensions.Caching.Memory": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10" + } + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "wNonj40aZxia+GtuBiiD6ZqVh4h6y5Nje1bGdmzZ8/ui0QRsAN+S0SIrLHFCEGbG9cDbeaE40sh+Lr7o9rRs6g==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.10", + "Microsoft.Extensions.Caching.Memory": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "tL9FkfV64GPUDSPvwrgyw42LVzsnVAnyrqJEuZVJbODgrQ3eL63zmzEcVWoCHzfgqUhWggzbgAyUCnz/zfI3Pg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.Configuration.CommandLine": "10.0.10", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.10", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.10", + "Microsoft.Extensions.Configuration.Json": "10.0.10", + "Microsoft.Extensions.Configuration.UserSecrets": "10.0.10", + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Diagnostics": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Configuration": "10.0.10", + "Microsoft.Extensions.Logging.Console": "10.0.10", + "Microsoft.Extensions.Logging.Debug": "10.0.10", + "Microsoft.Extensions.Logging.EventLog": "10.0.10", + "Microsoft.Extensions.Logging.EventSource": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.3", + "contentHash": "IPGrrZnRkuW7OlHDhUESZz4G5DLkW7Nej/O3Cx+0iTsgyU5XJxBgpsvTHLloo3WWuAKKbDHXBvWPVkX1deRh1Q==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.3" + } + }, + "OpenTelemetry.Exporter.OpenTelemetryProtocol": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "R1omQOrQpGlS0Cp5UIr/TAiuEA48JrPlgr1NPV5gESiTU7HhWU+ILe2EBSYb1fKdsSavZ7nZkHcUxAzofPqr2A==", + "dependencies": { + "OpenTelemetry": "1.17.0" + } + }, + "OpenTelemetry.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "t1OwL/4qgboGMobYVT+UV5zgWnFqCp4Pw8lcsmzh8m2K8PQsTKkyxrC32tqYTMYny3GOW4q5cltE3dTVzLmRew==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "OpenTelemetry": "1.17.0" + } + }, + "OpenTelemetry.Instrumentation.AspNetCore": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "rGbmk1vuy1kvgZmE0ps7Vb99YZvDap6AalrrF60FwnNit1uW/PbeFZj1cpb0T8MPkYmjhBrRJ1/JB6QqXkRjHA==", + "dependencies": { + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.17.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Http": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "uTwVtxIJ/xB96wGYTaDsbkJVeCFdUxTwvrlDUn2YJixy0UuKc8DvQMzwKNJMTzNFiiyYO9c40id6tUHTmWs33A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.17.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Runtime": { + "type": "CentralTransitive", + "requested": "[1.17.0, )", + "resolved": "1.17.0", + "contentHash": "HyYenisDn/xdtyVXdjImsCl+RNC2gq01N0rvSR7tsYAylXR2sxX/YgMsyTajMXA27+r1vB7lNU8cWRhV0fwL+Q==", + "dependencies": { + "OpenTelemetry.Api": "[1.17.0, 2.0.0)" + } + }, + "RabbitMQ.Client": { + "type": "CentralTransitive", + "requested": "[7.2.1, )", + "resolved": "7.2.1", + "contentHash": "YKXEfg9fVQiTKgZlvIhAfPSFaamEgi8DsQmisCH0IAsU4FYLrtoguDrDj6JtJVGUt40QPnBLRH6fTQcAC4qsOg==", + "dependencies": { + "System.Threading.RateLimiting": "8.0.0" + } + }, + "Serilog.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Serilog": "4.3.0", + "Serilog.Extensions.Logging": "10.0.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "CentralTransitive", + "requested": "[3.0.0, )", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Console": { + "type": "CentralTransitive", + "requested": "[6.1.1, )", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "StackExchange.Redis": { + "type": "CentralTransitive", + "requested": "[3.0.17, )", + "resolved": "3.0.17", + "contentHash": "ItAm9lokZ1mWsQLF3u4Yw4eb/gvkX6Rp9bLAe1KUKgKAjPkALoQwTc6jJLtBZoOv9LT7RwvSIXZZb9FF30QhOg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "RESPite": "3.0.17", + "System.IO.Hashing": "10.0.5" + } + } + } + } +} \ No newline at end of file -- Gitee From 6293634cc7e09b5e196df8f4c3839adcc967d1cc Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Sat, 25 Jul 2026 20:15:09 +0800 Subject: [PATCH 117/118] =?UTF-8?q?feat(frontend):=20=E9=87=8D=E5=BB=BAPC?= =?UTF-8?q?=20Web=E5=B7=A5=E7=A8=8B=E5=BA=95=E5=BA=A7=EF=BC=9B=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E6=8E=A5=E5=8F=A3=E3=80=81=E7=8A=B6=E6=80=81=E4=B8=8E?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.env.example | 3 + frontend/.gitignore | 31 +- frontend/.prettierignore | 6 + frontend/.prettierrc.json | 7 + frontend/README.md | 46 +- frontend/eslint.config.js | 30 + frontend/index.html | 4 + frontend/package-lock.json | 4990 ++++++++++++++++-- frontend/package.json | 54 +- frontend/playwright.config.ts | 41 + frontend/public/favicon.svg | 4 + frontend/src/App.vue | 15 +- frontend/src/api/apiClient.ts | 53 + frontend/src/api/apiError.ts | 95 + frontend/src/appConfig.ts | 43 + frontend/src/components/AppErrorBoundary.vue | 27 + frontend/src/components/AppStateFeedback.vue | 140 + frontend/src/components/FoundationStatus.vue | 100 + frontend/src/env.d.ts | 11 + frontend/src/main.ts | 12 +- frontend/src/router/index.ts | 12 + frontend/src/router/routes.ts | 13 + frontend/src/style.css | 53 - frontend/src/styles/base.css | 42 + frontend/src/styles/index.css | 2 + frontend/src/styles/tokens.css | 18 + frontend/src/types/api.ts | 16 + frontend/src/types/router.d.ts | 9 + frontend/tests/e2e/app-shell.spec.ts | 17 + frontend/tests/setup.ts | 4 + frontend/tests/unit/AppStateFeedback.spec.ts | 30 + frontend/tests/unit/apiClient.spec.ts | 63 + frontend/tests/unit/apiError.spec.ts | 37 + frontend/tests/unit/appConfig.spec.ts | 28 + frontend/tsconfig.app.json | 22 +- frontend/tsconfig.json | 11 +- frontend/tsconfig.node.json | 26 +- frontend/tsconfig.vitest.json | 8 + frontend/vite.config.ts | 59 +- 39 files changed, 5492 insertions(+), 690 deletions(-) create mode 100644 frontend/.env.example create mode 100644 frontend/.prettierignore create mode 100644 frontend/.prettierrc.json create mode 100644 frontend/eslint.config.js create mode 100644 frontend/playwright.config.ts create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/src/api/apiClient.ts create mode 100644 frontend/src/api/apiError.ts create mode 100644 frontend/src/appConfig.ts create mode 100644 frontend/src/components/AppErrorBoundary.vue create mode 100644 frontend/src/components/AppStateFeedback.vue create mode 100644 frontend/src/components/FoundationStatus.vue create mode 100644 frontend/src/env.d.ts create mode 100644 frontend/src/router/index.ts create mode 100644 frontend/src/router/routes.ts delete mode 100644 frontend/src/style.css create mode 100644 frontend/src/styles/base.css create mode 100644 frontend/src/styles/index.css create mode 100644 frontend/src/styles/tokens.css create mode 100644 frontend/src/types/api.ts create mode 100644 frontend/src/types/router.d.ts create mode 100644 frontend/tests/e2e/app-shell.spec.ts create mode 100644 frontend/tests/setup.ts create mode 100644 frontend/tests/unit/AppStateFeedback.spec.ts create mode 100644 frontend/tests/unit/apiClient.spec.ts create mode 100644 frontend/tests/unit/apiError.spec.ts create mode 100644 frontend/tests/unit/appConfig.spec.ts create mode 100644 frontend/tsconfig.vitest.json diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..8279d20 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,3 @@ +VITE_API_BASE_URL=/api +VITE_API_TIMEOUT_MS=15000 +VITE_DEV_PROXY_TARGET=http://localhost:5000 diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf3..74dd56b 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,24 +1,9 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr +node_modules/ +dist/ +coverage/ +playwright-report/ +test-results/ *.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? +.env +.env.* +!.env.example diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..330ff1f --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +coverage/ +playwright-report/ +test-results/ +package-lock.json diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json new file mode 100644 index 0000000..9fa8fe9 --- /dev/null +++ b/frontend/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "semi": false, + "singleQuote": true, + "printWidth": 100, + "trailingComma": "all", + "vueIndentScriptAndStyle": false +} diff --git a/frontend/README.md b/frontend/README.md index a42480c..2d8ef7f 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,11 +1,47 @@ -# E-Shop 前端 +# E-Shop PC Web 基础设施 -当前目录是 PC Web 的 Vue 3、TypeScript 与 Vite 基础工程,不包含尚未评审的业务页面、接口类型或状态管理。 +本目录是 Vue 3 + TypeScript 的 PC Web 工程底座。当前只提供跨业务模块共享的 +运行、接口、反馈和测试能力,不包含登录、商品、购物车、订单、售后、消息或 +管理端业务实现。 + +## 已装配能力 + +- Vite、Vue Router、Pinia、Element Plus 显式按需组件入口; +- Axios 客户端、ProblemDetails 转换和可注入的认证桥接; +- 加载、空数据、失败、无权限、离线反馈与根级错误边界; +- 严格 TypeScript、ESLint、Prettier; +- Vitest 单元测试、V8 覆盖率与 Playwright 浏览器测试。 + +`src/router/routes.ts` 中只有一个技术状态页,用于证明工程可挂载;它不代表 +任何业务功能已经完成。业务模块落地时再创建真实 `modules/`、`layouts/` 和 +`stores/` 内容,不提交空目录或模拟业务数据。 + +## 环境要求 + +- Node.js `24.15.x` +- npm `11.12.x` + +复制 `.env.example` 为个人 `.env.local` 后按需调整。所有 `VITE_*` 变量都会 +进入浏览器产物,因此不得存放密码、Token、密钥或数据库连接信息。 + +## 常用命令 ```powershell -npm install +npm ci npm run dev -npm run build +npm run check +npm run test:coverage +npx playwright install chromium +npm run test:e2e +``` + +Edge 兼容验证: + +```powershell +npm run test:e2e:edge ``` -业务模块后续按 `src/modules//` 接入;只有出现真实公共复用需求时,才向 `components/`、`stores/` 或 `types/` 增加共享内容。 +接口客户端不会自动重试写请求,不会擅自拆掉统一响应包装,也不会把 403、 +字段校验或网络错误当成登录失效。未来 Identity 模块通过 +`configureApiAuthentication` 注入 Token 读取和 401 处理,不在公共层硬编码 +存储方式。 diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..2bbef82 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,30 @@ +import pluginVue from 'eslint-plugin-vue' +import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' +import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' + +export default defineConfigWithVueTs( + { + name: 'eshop/files-to-lint', + files: ['**/*.{ts,vue}'], + }, + { + name: 'eshop/files-to-ignore', + ignores: [ + 'dist/**', + 'coverage/**', + 'node_modules/**', + 'playwright-report/**', + 'test-results/**', + ], + }, + pluginVue.configs['flat/recommended'], + vueTsConfigs.recommended, + { + rules: { + 'vue/multi-word-component-names': 'off', + 'vue/no-v-html': 'error', + 'vue/require-default-prop': 'off', + }, + }, + skipFormatting, +) diff --git a/frontend/index.html b/frontend/index.html index 06bd303..a0e2d18 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,9 +3,13 @@ + + + E-Shop +
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9ced7a4..c6b9112 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,28 +1,107 @@ { - "name": "eshop-frontend", - "version": "0.0.0", + "name": "eshop-pc-web", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "eshop-frontend", - "version": "0.0.0", + "name": "eshop-pc-web", + "version": "0.1.0", "dependencies": { - "vue": "^3.5.39" + "axios": "1.18.1", + "element-plus": "2.14.3", + "pinia": "4.0.2", + "vue": "3.5.40", + "vue-router": "5.2.0" }, "devDependencies": { - "@types/node": "^24.13.2", - "@vitejs/plugin-vue": "^6.0.7", - "@vue/tsconfig": "^0.9.1", - "typescript": "~6.0.2", - "vite": "^8.1.1", - "vue-tsc": "^3.3.5" + "@playwright/test": "1.62.0", + "@types/node": "24.13.3", + "@vitejs/plugin-vue": "6.0.8", + "@vitest/coverage-v8": "4.1.10", + "@vue/eslint-config-prettier": "10.2.0", + "@vue/eslint-config-typescript": "14.9.0", + "@vue/test-utils": "2.4.11", + "@vue/tsconfig": "0.9.1", + "eslint": "10.8.0", + "eslint-plugin-vue": "10.10.0", + "happy-dom": "20.11.1", + "prettier": "3.9.6", + "typescript": "6.0.3", + "vite": "8.1.5", + "vitest": "4.1.10", + "vue-tsc": "3.3.8" + }, + "engines": { + "node": ">=24.15.0 <25", + "npm": ">=11.12.1 <12" + } + }, + "node_modules/@babel/generator": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-8.0.0.tgz", + "integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^8.0.0", + "@babel/types": "^8.0.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "@types/jsesc": "^2.5.0", + "jsesc": "^3.0.2" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/helper-string-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", + "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": { + "version": "8.0.4", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/parser": { + "version": "8.0.4", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-8.0.4.tgz", + "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==", + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/types": { + "version": "8.0.4", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-8.0.4.tgz", + "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^8.0.0", + "@babel/helper-validator-identifier": "^8.0.4" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -30,8 +109,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -39,8 +116,6 @@ }, "node_modules/@babel/parser": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.7" @@ -54,8 +129,6 @@ }, "node_modules/@babel/types": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -65,11 +138,38 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, "node_modules/@emnapi/core": { "version": "1.11.1", "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.11.1.tgz", "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -81,7 +181,6 @@ "version": "1.11.1", "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.11.1.tgz", "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -92,24 +191,262 @@ "version": "1.2.2", "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmmirror.com/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -124,16 +461,110 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, "node_modules/@oxc-project/types": { "version": "0.139.0", - "resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmmirror.com/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@playwright/test": { + "version": "1.62.0", + "resolved": "https://registry.npmmirror.com/@playwright/test/-/test-1.62.0.tgz", + "integrity": "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.1.5", "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", @@ -141,7 +572,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -158,7 +588,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -175,7 +604,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -192,7 +620,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -209,7 +636,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -226,7 +652,6 @@ "cpu": [ "arm64" ], - "dev": true, "libc": [ "glibc" ], @@ -246,7 +671,6 @@ "cpu": [ "arm64" ], - "dev": true, "libc": [ "musl" ], @@ -266,7 +690,6 @@ "cpu": [ "ppc64" ], - "dev": true, "libc": [ "glibc" ], @@ -286,7 +709,6 @@ "cpu": [ "s390x" ], - "dev": true, "libc": [ "glibc" ], @@ -306,7 +728,6 @@ "cpu": [ "x64" ], - "dev": true, "libc": [ "glibc" ], @@ -326,7 +747,6 @@ "cpu": [ "x64" ], - "dev": true, "libc": [ "musl" ], @@ -346,7 +766,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -363,7 +782,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -382,7 +800,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -394,12 +811,9 @@ }, "node_modules/@rolldown/binding-win32-x64-msvc": { "version": "1.1.5", - "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -411,8 +825,13 @@ }, "node_modules/@rolldown/pluginutils": { "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, @@ -420,642 +839,3283 @@ "version": "0.10.3", "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, - "node_modules/@types/node": { - "version": "24.13.3", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.13.3.tgz", - "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/@vitejs/plugin-vue": { - "version": "6.0.8", - "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", - "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "vue": "^3.2.25" - } + "license": "MIT" }, - "node_modules/@volar/language-core": { - "version": "2.4.28", - "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.28.tgz", - "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", "dev": true, - "license": "MIT", - "dependencies": { - "@volar/source-map": "2.4.28" - } + "license": "MIT" }, - "node_modules/@volar/source-map": { - "version": "2.4.28", - "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.28.tgz", - "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, - "node_modules/@volar/typescript": { - "version": "2.4.28", - "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.28.tgz", - "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "node_modules/@types/jsesc": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@types/jsesc/-/jsesc-2.5.1.tgz", + "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, - "license": "MIT", - "dependencies": { - "@volar/language-core": "2.4.28", - "path-browserify": "^1.0.1", - "vscode-uri": "^3.0.8" - } + "license": "MIT" }, - "node_modules/@vue/compiler-core": { - "version": "3.5.40", - "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.40.tgz", - "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@vue/shared": "3.5.40", - "entities": "^7.0.1", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.40", - "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz", - "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==", + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.40", - "@vue/shared": "3.5.40" + "@types/lodash": "*" } }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.40", - "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz", - "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==", + "node_modules/@types/node": { + "version": "24.13.3", + "devOptional": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.7", - "@vue/compiler-core": "3.5.40", - "@vue/compiler-dom": "3.5.40", - "@vue/compiler-ssr": "3.5.40", - "@vue/shared": "3.5.40", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.21", - "postcss": "^8.5.19", - "source-map-js": "^1.2.1" + "undici-types": "~7.18.0" } }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.40", - "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz", - "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.40", - "@vue/shared": "3.5.40" - } + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" }, - "node_modules/@vue/language-core": { - "version": "3.3.8", - "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.3.8.tgz", - "integrity": "sha512-ieGT8jJdhhy0mGzStZhsg/qPw5bQZJg5yF+3+XU6saf4sM7yo9ZXy3h+nCwrm2+b4qS/SypkNdR2jAF3uei9tA==", + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", "dev": true, - "license": "MIT", - "dependencies": { - "@volar/language-core": "2.4.28", - "@vue/compiler-dom": "^3.5.0", - "@vue/shared": "^3.5.0", - "alien-signals": "^3.2.1", - "muggle-string": "^0.4.1", - "path-browserify": "^1.0.1", - "picomatch": "^4.0.4" - } + "license": "MIT" }, - "node_modules/@vue/reactivity": { - "version": "3.5.40", - "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.40.tgz", - "integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==", + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, "license": "MIT", "dependencies": { - "@vue/shared": "3.5.40" + "@types/node": "*" } }, - "node_modules/@vue/runtime-core": { - "version": "3.5.40", - "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.40.tgz", - "integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.65.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", + "dev": true, "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.40", - "@vue/shared": "3.5.40" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.65.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@vue/runtime-dom": { - "version": "3.5.40", - "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz", - "integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.40", - "@vue/runtime-core": "3.5.40", - "@vue/shared": "3.5.40", - "csstype": "^3.2.3" + "engines": { + "node": ">= 4" } }, - "node_modules/@vue/server-renderer": { - "version": "3.5.40", - "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.40.tgz", - "integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==", + "node_modules/@typescript-eslint/parser": { + "version": "8.65.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.40", - "@vue/runtime-dom": "3.5.40", - "@vue/shared": "3.5.40" + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@vue/shared": { - "version": "3.5.40", - "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.40.tgz", - "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", - "license": "MIT" - }, - "node_modules/@vue/tsconfig": { - "version": "0.9.1", - "resolved": "https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.9.1.tgz", - "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.65.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", "dev": true, "license": "MIT", - "peerDependencies": { - "typescript": ">= 5.8", - "vue": "^3.4.0" + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "debug": "^4.4.3" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "vue": { - "optional": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/alien-signals": { - "version": "3.2.1", - "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.2.1.tgz", - "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", - "dev": true, - "license": "MIT" - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.65.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" + }, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "license": "BSD-2-Clause", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=0.12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, "engines": { - "node": ">=12.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.65.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", "dev": true, - "license": "MPL-2.0", + "license": "MIT", "dependencies": { - "detect-libc": "^2.0.3" + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": ">= 12.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/typescript-eslint" }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/utils": { + "version": "8.65.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" + }, "engines": { - "node": ">= 12.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.65.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "eslint-visitor-keys": "^5.0.0" + }, "engines": { - "node": ">= 12.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", "engines": { - "node": ">= 12.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/eslint" } }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", - "cpu": [ - "x64" - ], + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.8", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, "engines": { - "node": ">= 12.0.0" + "node": "^20.19.0 || >=22.12.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" } }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", - "cpu": [ - "arm" - ], + "node_modules/@vitest/coverage-v8": { + "version": "4.1.10", + "resolved": "https://registry.npmmirror.com/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", + "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.10", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.10", + "vitest": "4.1.10" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } } }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/vitest" } }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/vitest" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/vitest" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/vitest" } }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, + "license": "MIT", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/vitest" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/muggle-string": { - "version": "0.4.1", - "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", - "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "node_modules/@volar/language-core": { + "version": "2.4.28", "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "dependencies": { + "@volar/source-map": "2.4.28" } }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "node_modules/@volar/source-map": { + "version": "2.4.28", "dev": true, "license": "MIT" }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "node_modules/@volar/typescript": { + "version": "2.4.28", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" } }, - "node_modules/postcss": { - "version": "8.5.22", - "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.22.tgz", - "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/@vue-macros/common": { + "version": "3.1.4", + "resolved": "https://registry.npmmirror.com/@vue-macros/common/-/common-3.1.4.tgz", + "integrity": "sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==", "license": "MIT", "dependencies": { - "nanoid": "^3.3.16", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "@vue/compiler-sfc": "^3.5.22", + "ast-kit": "^2.1.2", + "local-pkg": "^1.1.2", + "magic-string-ast": "^1.0.2", + "unplugin-utils": "^0.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/vue-macros" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.2.25" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.40", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.40", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.40", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.40", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.40", + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-ssr": "3.5.40", + "@vue/shared": "3.5.40", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.40", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/devtools-api": { + "version": "8.2.1", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-8.2.1.tgz", + "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.2.1" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.2.1", + "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz", + "integrity": "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.2.1", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.2.1", + "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz", + "integrity": "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==", + "license": "MIT" + }, + "node_modules/@vue/eslint-config-prettier": { + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/@vue/eslint-config-prettier/-/eslint-config-prettier-10.2.0.tgz", + "integrity": "sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2" + }, + "peerDependencies": { + "eslint": ">= 8.21.0", + "prettier": ">= 3.0.0" + } + }, + "node_modules/@vue/eslint-config-typescript": { + "version": "14.9.0", + "resolved": "https://registry.npmmirror.com/@vue/eslint-config-typescript/-/eslint-config-typescript-14.9.0.tgz", + "integrity": "sha512-E3j9hDlfVf10F30MRcLTPY2IIhWIx1nsvkVukk14kTcuA+oBVot9zsP1hzsO+PAMDxV3Fd9FimBJtUBNBL5KFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.60.0", + "fast-glob": "^3.3.3", + "typescript-eslint": "^8.60.0", + "vue-eslint-parser": "^10.4.0" + }, + "bin": { + "vue-eslint-config-typescript": "dist/bin.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.10.0 || ^10.0.0", + "eslint-plugin-vue": "^9.28.0 || ^10.0.0", + "typescript": ">=4.8.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core": { + "version": "3.3.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.2.1", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.40", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.40", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.40", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/runtime-core": "3.5.40", + "@vue/shared": "3.5.40", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.40", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.40", + "license": "MIT" + }, + "node_modules/@vue/test-utils": { + "version": "2.4.11", + "resolved": "https://registry.npmmirror.com/@vue/test-utils/-/test-utils-2.4.11.tgz", + "integrity": "sha512-GDqaqZsA6m2E5vNzej0aYiIb6BX8xV9pNSbbbXKOfEYwg7ZNblVX8suyqmUBThq8VIrgAJNxn+z72hVtUeiWHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^3.0.0" + }, + "peerDependencies": { + "@vue/compiler-dom": "3.x", + "@vue/server-renderer": "3.x", + "vue": "3.x" + }, + "peerDependenciesMeta": { + "@vue/server-renderer": { + "optional": true + } + } + }, + "node_modules/@vue/tsconfig": { + "version": "0.9.1", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 5.8", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/core": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/alien-signals": { + "version": "3.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-kit": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/ast-kit/-/ast-kit-2.2.0.tgz", + "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz", + "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-walker-scope": { + "version": "0.9.0", + "resolved": "https://registry.npmmirror.com/ast-walker-scope/-/ast-walker-scope-0.9.0.tgz", + "integrity": "sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@babel/types": "^7.29.0", + "ast-kit": "^2.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-image-size": { + "version": "0.6.4", + "resolved": "https://registry.npmmirror.com/buffer-image-size/-/buffer-image-size-0.6.4.tgz", + "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmmirror.com/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/element-plus": { + "version": "2.14.3", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.3.tgz", + "integrity": "sha512-pJcvxcpZjYruNzuJhAeVwnbYjfNgzBKnWHwSVEhwzM2/kcLI3brzmtIBxtPqd4hQWJfD1PRnjoc1WipLw2eBGg==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.7.6", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "14.3.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.3.5" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.8.0", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.6", + "resolved": "https://registry.npmmirror.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", + "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.13" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "10.10.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-10.10.0.tgz", + "integrity": "sha512-dL9x9rBHqqNcByWiLOHK6L0SB97V82/NC0cZRn9cXPjM7pCuWlpQQP9bFH4vjBv80ej1ZpzAkuD8zWH1o9bZbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^7.1.4", + "semver": "^7.8.5", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "vue-eslint-parser": "^10.3.0" + }, + "peerDependenciesMeta": { + "@stylistic/eslint-plugin": { + "optional": true + }, + "@typescript-eslint/parser": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exsolve": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.1.0.tgz", + "integrity": "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.4.3.tgz", + "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/happy-dom": { + "version": "20.11.1", + "resolved": "https://registry.npmmirror.com/happy-dom/-/happy-dom-20.11.1.tgz", + "integrity": "sha512-XSt8tMzbW9ymE7687xztkO1ckR7qJNQ3LywY9vlYGhGi3zXrGBHuUo2Cl1ztZaICW+1eAGdkLbj6iwVqDT33kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "buffer-image-size": "^0.6.4", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmmirror.com/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.8", + "resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.8.tgz", + "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "devOptional": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/local-pkg": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-1.2.1.tgz", + "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magic-string-ast": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/magic-string-ast/-/magic-string-ast-1.0.3.tgz", + "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==", + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.19" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmmirror.com/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmmirror.com/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/nostics": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/nostics/-/nostics-1.2.0.tgz", + "integrity": "sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==", + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-4.0.2.tgz", + "integrity": "sha512-yKVVA7bSj5oRZFp/Ab9wLlmyb5gPUYEiIm4ryiWTe/xe7PtkRdMVOp1X1ggvq0c6Uj7Q0Du1HnV2mtAwM0Ks1g==", + "license": "MIT", + "dependencies": { + "nostics": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/devtools-api": "^8.1.5", + "typescript": ">=5.6.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "@vue/devtools-api": { + "optional": false + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/playwright": { + "version": "1.62.0", + "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.62.0.tgz", + "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.0", + "resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.62.0.tgz", + "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/postcss": { + "version": "8.5.22", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rolldown": { "version": "1.1.5", - "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.139.0", @@ -1085,66 +4145,470 @@ "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmmirror.com/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.65.0", + "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.18.2", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unplugin": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-3.3.0.tgz", + "integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.4", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@farmfe/core": "*", + "@rspack/core": "*", + "bun-types-no-globals": "*", + "esbuild": "*", + "rolldown": "*", + "rollup": "*", + "unloader": "*", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "@farmfe/core": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "bun-types-no-globals": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "unloader": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.3.2.tgz", + "integrity": "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==", "license": "MIT", "dependencies": { - "fdir": "^6.5.0", + "pathe": "^2.0.3", "picomatch": "^4.0.4" }, "engines": { - "node": ">=12.0.0" + "node": ">=20.19.0" }, "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "url": "https://github.com/sponsors/sxzz" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "license": "0BSD", - "optional": true - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" } }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, "license": "MIT" }, "node_modules/vite": { "version": "8.1.5", - "resolved": "https://registry.npmmirror.com/vite/-/vite-8.1.5.tgz", - "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", @@ -1218,17 +4682,117 @@ } } }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "node_modules/vscode-uri": { "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", "dev": true, "license": "MIT" }, "node_modules/vue": { "version": "3.5.40", - "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.40.tgz", - "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", "license": "MIT", "dependencies": { "@vue/compiler-dom": "3.5.40", @@ -1246,10 +4810,101 @@ } } }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.8", + "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.8.tgz", + "integrity": "sha512-troqCMmQodQDqUqn63NQaFi+CDSclSe7sc8VEBFqf5GFLqmGR2Ph3P2WEC7qwpRVyEWsTi/aAr4vyOe/B1hU3g==", + "license": "MIT" + }, + "node_modules/vue-eslint-parser": { + "version": "10.4.1", + "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-10.4.1.tgz", + "integrity": "sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", + "esquery": "^1.6.0", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-router": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-5.2.0.tgz", + "integrity": "sha512-QAC5i0LEb1GLG0LXDQmHu8L7FX12j0KwU/JTKmLQUJMrn04gQdKP6Du+p0QwpHb3iy71vBlqnHQ8WAfOSAWhqw==", + "license": "MIT", + "dependencies": { + "@babel/generator": "^8.0.0", + "@vue-macros/common": "^3.1.3", + "@vue/devtools-api": "^8.1.5", + "ast-walker-scope": "^0.9.0", + "chokidar": "^5.0.0", + "json5": "^2.2.3", + "local-pkg": "^1.2.1", + "magic-string": "^0.30.21", + "mlly": "^1.8.2", + "muggle-string": "^0.4.1", + "nostics": "^1.1.4", + "pathe": "^2.0.3", + "picomatch": "^4.0.5", + "scule": "^1.3.0", + "tinyglobby": "^0.2.17", + "unplugin": "^3.3.0", + "unplugin-utils": "^0.3.2", + "yaml": "^2.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@pinia/colada": ">=0.21.2", + "@vue/compiler-sfc": "^3.5.34 || ^4.0.0", + "pinia": "^3.0.4 || ^4.0.2", + "vite": "^7.3.0 || ^8.0.0", + "vue": "^3.5.34 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@pinia/colada": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "pinia": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, "node_modules/vue-tsc": { "version": "3.3.8", - "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.3.8.tgz", - "integrity": "sha512-xXmYlVQpcwJDWyGlqbHrGVOl1h3UOsASymRibrHc+iy9j/UNnOrOn4u+fntHz4D6Cs74RtapeqVV6CzJeg+UlA==", "dev": true, "license": "MIT", "dependencies": { @@ -1262,6 +4917,223 @@ "peerDependencies": { "typescript": ">=5.0.0" } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "license": "MIT" + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 5f81d88..35f455f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,22 +1,56 @@ { - "name": "eshop-frontend", + "name": "eshop-pc-web", + "version": "0.1.0", "private": true, - "version": "0.0.0", "type": "module", + "engines": { + "node": ">=24.15.0 <25", + "npm": ">=11.12.1 <12" + }, + "packageManager": "npm@11.12.1", "scripts": { "dev": "vite", + "typecheck": "vue-tsc -b", "build": "vue-tsc -b && vite build", - "preview": "vite preview" + "preview": "vite preview", + "lint": "eslint . --max-warnings=0", + "lint:fix": "eslint . --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "test:unit": "vitest run", + "test:unit:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:e2e": "playwright test --project=chromium", + "test:e2e:edge": "playwright test --project=msedge", + "audit": "npm audit --registry=https://registry.npmjs.org", + "check": "npm run format:check && npm run lint && npm run typecheck && npm run test:unit && npm run build" }, "dependencies": { - "vue": "^3.5.39" + "axios": "1.18.1", + "element-plus": "2.14.3", + "pinia": "4.0.2", + "vue": "3.5.40", + "vue-router": "5.2.0" }, "devDependencies": { - "@types/node": "^24.13.2", - "@vitejs/plugin-vue": "^6.0.7", - "@vue/tsconfig": "^0.9.1", - "typescript": "~6.0.2", - "vite": "^8.1.1", - "vue-tsc": "^3.3.5" + "@playwright/test": "1.62.0", + "@types/node": "24.13.3", + "@vitejs/plugin-vue": "6.0.8", + "@vitest/coverage-v8": "4.1.10", + "@vue/eslint-config-prettier": "10.2.0", + "@vue/eslint-config-typescript": "14.9.0", + "@vue/test-utils": "2.4.11", + "@vue/tsconfig": "0.9.1", + "eslint": "10.8.0", + "eslint-plugin-vue": "10.10.0", + "happy-dom": "20.11.1", + "prettier": "3.9.6", + "typescript": "6.0.3", + "vite": "8.1.5", + "vitest": "4.1.10", + "vue-tsc": "3.3.8" + }, + "overrides": { + "brace-expansion@<=5.0.7": "5.0.8" } } diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..d32e6b4 --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,41 @@ +import { defineConfig, devices } from '@playwright/test' + +const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://127.0.0.1:4173' + +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 2 : 0, + ...(process.env.CI ? { workers: 1 } : {}), + reporter: [['list'], ['html', { open: 'never' }]], + use: { + baseURL, + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + ...(process.env.PLAYWRIGHT_BASE_URL + ? {} + : { + webServer: { + command: 'npm run dev -- --host 127.0.0.1 --port 4173', + url: baseURL, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, + }), + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'msedge', + use: { + ...devices['Desktop Edge'], + channel: 'msedge', + }, + }, + ], +}) diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..eff179f --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue index a7eec63..25da618 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,9 +1,10 @@ + + diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts new file mode 100644 index 0000000..2f4744a --- /dev/null +++ b/frontend/src/api/apiClient.ts @@ -0,0 +1,53 @@ +import axios from 'axios' +import { appConfig } from '@/appConfig' +import { toApiError, type ApiError } from '@/api/apiError' + +export interface ApiAuthenticationBridge { + getAccessToken: () => string | null | Promise + onUnauthorized?: (error: ApiError) => void | Promise +} + +let authenticationBridge: ApiAuthenticationBridge | undefined + +export function configureApiAuthentication(bridge: ApiAuthenticationBridge): () => void { + authenticationBridge = bridge + return () => { + if (authenticationBridge === bridge) { + authenticationBridge = undefined + } + } +} + +export const apiClient = axios.create({ + baseURL: appConfig.apiBaseUrl, + timeout: appConfig.apiTimeoutMs, + headers: { + Accept: 'application/json', + }, +}) + +apiClient.interceptors.request.use(async (request) => { + const accessToken = await authenticationBridge?.getAccessToken() + if (accessToken) { + request.headers.set('Authorization', `Bearer ${accessToken}`) + } + + const requestId = globalThis.crypto?.randomUUID?.() + if (requestId) { + request.headers.set('X-Request-Id', requestId) + } + + return request +}) + +apiClient.interceptors.response.use( + (response) => response, + async (error: unknown) => { + const apiError = toApiError(error) + if (apiError.status === 401 && authenticationBridge?.onUnauthorized) { + await authenticationBridge.onUnauthorized(apiError) + } + + return Promise.reject(apiError) + }, +) diff --git a/frontend/src/api/apiError.ts b/frontend/src/api/apiError.ts new file mode 100644 index 0000000..8730581 --- /dev/null +++ b/frontend/src/api/apiError.ts @@ -0,0 +1,95 @@ +import axios from 'axios' +import type { ProblemDetails } from '@/types/api' + +export class ApiError extends Error { + readonly code: string + readonly status: number | undefined + readonly traceId: string | undefined + readonly errors: Readonly> + + constructor( + message: string, + options: { + code: string + status?: number | undefined + traceId?: string | undefined + errors?: Record | undefined + }, + ) { + super(message) + this.name = 'ApiError' + this.code = options.code + this.status = options.status + this.traceId = options.traceId + this.errors = Object.freeze(options.errors ?? {}) + } +} + +export function toApiError(error: unknown): ApiError { + if (error instanceof ApiError) { + return error + } + + if (!axios.isAxiosError(error)) { + return new ApiError('系统暂时无法处理请求,请稍后重试。', { + code: 'COMMON.UNKNOWN_ERROR', + }) + } + + if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') { + return new ApiError('请求超时,请检查网络后重试。', { + code: 'NETWORK.TIMEOUT', + }) + } + + if (!error.response) { + const offline = typeof navigator !== 'undefined' && navigator.onLine === false + return new ApiError( + offline ? '当前设备已离线,请恢复网络后重试。' : '暂时无法连接服务器,请稍后重试。', + { + code: offline ? 'NETWORK.OFFLINE' : 'NETWORK.UNAVAILABLE', + }, + ) + } + + const problem = isProblemDetails(error.response.data) ? error.response.data : undefined + return new ApiError( + problem?.detail ?? problem?.title ?? safeStatusMessage(error.response.status), + { + code: problem?.code ?? `HTTP.${error.response.status}`, + status: error.response.status, + traceId: problem?.traceId, + errors: problem?.errors, + }, + ) +} + +function isProblemDetails(value: unknown): value is ProblemDetails { + return ( + typeof value === 'object' && + value !== null && + ('status' in value || 'title' in value || 'detail' in value || 'code' in value) + ) +} + +function safeStatusMessage(status: number): string { + if (status === 401) { + return '登录凭证无效或已过期。' + } + if (status === 403) { + return '当前账号无权执行此操作。' + } + if (status === 404) { + return '请求的资源不存在。' + } + if (status === 409) { + return '数据状态已变化,请刷新后重试。' + } + if (status === 422) { + return '提交内容未通过校验。' + } + if (status >= 500) { + return '服务暂时不可用,请稍后重试。' + } + return '请求未能完成。' +} diff --git a/frontend/src/appConfig.ts b/frontend/src/appConfig.ts new file mode 100644 index 0000000..a3970eb --- /dev/null +++ b/frontend/src/appConfig.ts @@ -0,0 +1,43 @@ +const DEFAULT_API_BASE_URL = '/api' +const DEFAULT_API_TIMEOUT_MS = 15_000 +const MINIMUM_API_TIMEOUT_MS = 1_000 +const MAXIMUM_API_TIMEOUT_MS = 120_000 + +function parseApiBaseUrl(rawValue: string | undefined): string { + const value = rawValue?.trim() || DEFAULT_API_BASE_URL + if (!value.startsWith('/') && !URL.canParse(value)) { + throw new Error('VITE_API_BASE_URL 必须是站内绝对路径或有效 URL。') + } + + return value.length > 1 ? value.replace(/\/+$/, '') : value +} + +function parseTimeout(rawValue: string | undefined): number { + if (!rawValue?.trim()) { + return DEFAULT_API_TIMEOUT_MS + } + + const value = Number(rawValue) + if ( + !Number.isSafeInteger(value) || + value < MINIMUM_API_TIMEOUT_MS || + value > MAXIMUM_API_TIMEOUT_MS + ) { + throw new Error( + `VITE_API_TIMEOUT_MS 必须是 ${MINIMUM_API_TIMEOUT_MS}~${MAXIMUM_API_TIMEOUT_MS} 的整数。`, + ) + } + + return value +} + +export function createAppConfig( + env: Pick, +) { + return Object.freeze({ + apiBaseUrl: parseApiBaseUrl(env.VITE_API_BASE_URL), + apiTimeoutMs: parseTimeout(env.VITE_API_TIMEOUT_MS), + }) +} + +export const appConfig = createAppConfig(import.meta.env) diff --git a/frontend/src/components/AppErrorBoundary.vue b/frontend/src/components/AppErrorBoundary.vue new file mode 100644 index 0000000..c1bb6de --- /dev/null +++ b/frontend/src/components/AppErrorBoundary.vue @@ -0,0 +1,27 @@ + + + diff --git a/frontend/src/components/AppStateFeedback.vue b/frontend/src/components/AppStateFeedback.vue new file mode 100644 index 0000000..d101d22 --- /dev/null +++ b/frontend/src/components/AppStateFeedback.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/frontend/src/components/FoundationStatus.vue b/frontend/src/components/FoundationStatus.vue new file mode 100644 index 0000000..eb2da44 --- /dev/null +++ b/frontend/src/components/FoundationStatus.vue @@ -0,0 +1,100 @@ + + + diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts new file mode 100644 index 0000000..72f7ad0 --- /dev/null +++ b/frontend/src/env.d.ts @@ -0,0 +1,11 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL?: string + readonly VITE_API_TIMEOUT_MS?: string + readonly VITE_DEV_PROXY_TARGET?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 2425c0f..5e636f2 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,5 +1,11 @@ +import { createPinia } from 'pinia' import { createApp } from 'vue' -import './style.css' -import App from './App.vue' +import App from '@/App.vue' +import { router } from '@/router' +import '@/styles/index.css' -createApp(App).mount('#app') +const app = createApp(App) + +app.use(createPinia()) +app.use(router) +app.mount('#app') diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..66b9ab7 --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,12 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { routes } from '@/router/routes' + +export const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [...routes], + scrollBehavior: () => ({ top: 0 }), +}) + +router.afterEach((route) => { + document.title = route.meta.pageTitle ? `${route.meta.pageTitle} · E-Shop` : 'E-Shop' +}) diff --git a/frontend/src/router/routes.ts b/frontend/src/router/routes.ts new file mode 100644 index 0000000..253ef16 --- /dev/null +++ b/frontend/src/router/routes.ts @@ -0,0 +1,13 @@ +import type { RouteRecordRaw } from 'vue-router' +import FoundationStatus from '@/components/FoundationStatus.vue' + +export const routes = [ + { + path: '/', + name: 'foundation-status', + component: FoundationStatus, + meta: { + pageTitle: '工程底座', + }, + }, +] satisfies readonly RouteRecordRaw[] diff --git a/frontend/src/style.css b/frontend/src/style.css deleted file mode 100644 index 631b647..0000000 --- a/frontend/src/style.css +++ /dev/null @@ -1,53 +0,0 @@ -:root { - font-family: Inter, "Segoe UI", sans-serif; - color: #1f2937; - background: #f5f7fb; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - margin: 0; - min-width: 320px; - min-height: 100vh; -} - -#app { - min-height: 100vh; - display: grid; - place-items: center; -} - -.shell { - width: min(680px, calc(100% - 48px)); - box-sizing: border-box; - padding: 48px; - border: 1px solid #e5e7eb; - border-radius: 20px; - background: #ffffff; - box-shadow: 0 20px 50px rgb(15 23 42 / 8%); -} - -.eyebrow { - margin: 0 0 16px; - color: #2563eb; - font-size: 14px; - font-weight: 700; - letter-spacing: 0.16em; -} - -h1 { - margin: 0; - color: #111827; - font-size: clamp(32px, 6vw, 52px); - line-height: 1.1; -} - -.summary { - margin: 24px 0 0; - color: #4b5563; - font-size: 18px; - line-height: 1.75; -} diff --git a/frontend/src/styles/base.css b/frontend/src/styles/base.css new file mode 100644 index 0000000..9aa340d --- /dev/null +++ b/frontend/src/styles/base.css @@ -0,0 +1,42 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + min-width: 20rem; + color-scheme: light; + font-family: var(--eshop-font-sans); + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; +} + +body { + min-width: 20rem; + min-height: 100dvh; + margin: 0; + background: var(--eshop-color-surface-subtle); + color: var(--eshop-color-text); +} + +button, +input, +textarea, +select { + font: inherit; +} + +button, +a { + -webkit-tap-highlight-color: transparent; +} + +:focus-visible { + outline: 3px solid color-mix(in srgb, var(--eshop-color-focus), transparent 30%); + outline-offset: 2px; +} + +#app { + min-height: 100dvh; +} diff --git a/frontend/src/styles/index.css b/frontend/src/styles/index.css new file mode 100644 index 0000000..833e0ff --- /dev/null +++ b/frontend/src/styles/index.css @@ -0,0 +1,2 @@ +@import './tokens.css'; +@import './base.css'; diff --git a/frontend/src/styles/tokens.css b/frontend/src/styles/tokens.css new file mode 100644 index 0000000..b95cef0 --- /dev/null +++ b/frontend/src/styles/tokens.css @@ -0,0 +1,18 @@ +:root { + --eshop-color-primary: #2563eb; + --eshop-color-primary-hover: #1d4ed8; + --eshop-color-text: #172033; + --eshop-color-text-secondary: #536078; + --eshop-color-text-tertiary: #7d879a; + --eshop-color-border: #dce2ea; + --eshop-color-surface: #ffffff; + --eshop-color-surface-subtle: #f5f7fa; + --eshop-color-focus: #60a5fa; + --eshop-radius-md: 0.5rem; + --eshop-radius-lg: 0.75rem; + --eshop-radius-xl: 1rem; + --eshop-shadow-lg: 0 1.5rem 4rem rgb(15 23 42 / 10%); + --eshop-font-sans: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', + 'Microsoft YaHei', sans-serif; +} diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts new file mode 100644 index 0000000..3fd5481 --- /dev/null +++ b/frontend/src/types/api.ts @@ -0,0 +1,16 @@ +export interface ApiResponse { + code: string + message: string + data: T +} + +export interface ProblemDetails { + type?: string + title?: string + status?: number + detail?: string + instance?: string + code?: string + traceId?: string + errors?: Record +} diff --git a/frontend/src/types/router.d.ts b/frontend/src/types/router.d.ts new file mode 100644 index 0000000..5310c8f --- /dev/null +++ b/frontend/src/types/router.d.ts @@ -0,0 +1,9 @@ +import 'vue-router' + +export {} + +declare module 'vue-router' { + interface RouteMeta { + pageTitle?: string + } +} diff --git a/frontend/tests/e2e/app-shell.spec.ts b/frontend/tests/e2e/app-shell.spec.ts new file mode 100644 index 0000000..aef3218 --- /dev/null +++ b/frontend/tests/e2e/app-shell.spec.ts @@ -0,0 +1,17 @@ +import { expect, test } from '@playwright/test' + +test('应用根壳可访问且无控制台错误', async ({ page }) => { + const consoleErrors: string[] = [] + page.on('console', (message) => { + if (message.type() === 'error') { + consoleErrors.push(message.text()) + } + }) + + await page.goto('/') + + await expect(page).toHaveTitle('工程底座 · E-Shop') + await expect(page.getByRole('heading', { name: '前端工程底座已就绪' })).toBeVisible() + await expect(page.getByText('当前没有接入任何业务页面')).toBeVisible() + expect(consoleErrors).toEqual([]) +}) diff --git a/frontend/tests/setup.ts b/frontend/tests/setup.ts new file mode 100644 index 0000000..0d1f926 --- /dev/null +++ b/frontend/tests/setup.ts @@ -0,0 +1,4 @@ +import { enableAutoUnmount } from '@vue/test-utils' +import { afterEach } from 'vitest' + +enableAutoUnmount(afterEach) diff --git a/frontend/tests/unit/AppStateFeedback.spec.ts b/frontend/tests/unit/AppStateFeedback.spec.ts new file mode 100644 index 0000000..74ebc6e --- /dev/null +++ b/frontend/tests/unit/AppStateFeedback.spec.ts @@ -0,0 +1,30 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import AppStateFeedback from '@/components/AppStateFeedback.vue' + +describe('AppStateFeedback', () => { + it('在可重试错误状态发出 retry 事件', async () => { + const wrapper = mount(AppStateFeedback, { + props: { + state: 'error', + retryable: true, + }, + }) + + expect(wrapper.get('[role="alert"]').text()).toContain('加载失败') + await wrapper.get('button').trigger('click') + expect(wrapper.emitted('retry')).toHaveLength(1) + }) + + it('加载状态暴露 aria-busy 且不显示重试按钮', () => { + const wrapper = mount(AppStateFeedback, { + props: { + state: 'loading', + retryable: true, + }, + }) + + expect(wrapper.get('[role="status"]').attributes('aria-busy')).toBe('true') + expect(wrapper.find('button').exists()).toBe(false) + }) +}) diff --git a/frontend/tests/unit/apiClient.spec.ts b/frontend/tests/unit/apiClient.spec.ts new file mode 100644 index 0000000..4ebad9f --- /dev/null +++ b/frontend/tests/unit/apiClient.spec.ts @@ -0,0 +1,63 @@ +import { AxiosError, AxiosHeaders, type AxiosResponse } from 'axios' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { apiClient, configureApiAuthentication } from '@/api/apiClient' + +let resetAuthentication: (() => void) | undefined + +afterEach(() => { + resetAuthentication?.() + resetAuthentication = undefined +}) + +describe('apiClient', () => { + it('通过认证桥接注入 Bearer 且保留完整响应', async () => { + resetAuthentication = configureApiAuthentication({ + getAccessToken: () => 'token-for-test', + }) + + const response = await apiClient.get('/test', { + adapter: async (config): Promise => ({ + config, + data: { code: 'OK', message: 'ok', data: { value: 1 } }, + headers: new AxiosHeaders(), + status: 200, + statusText: 'OK', + }), + }) + + expect(response.config.headers.Authorization).toBe('Bearer token-for-test') + expect(response.status).toBe(200) + expect(response.data.data.value).toBe(1) + }) + + it('只在 401 时通知认证桥接', async () => { + const onUnauthorized = vi.fn() + resetAuthentication = configureApiAuthentication({ + getAccessToken: () => null, + onUnauthorized, + }) + + await expect( + apiClient.get('/test', { + adapter: async (config) => { + const response: AxiosResponse = { + config, + data: { + title: '凭证失效', + status: 401, + code: 'AUTH.INVALID_TOKEN', + }, + headers: new AxiosHeaders(), + status: 401, + statusText: 'Unauthorized', + } + throw new AxiosError('unauthorized', 'ERR_BAD_REQUEST', config, undefined, response) + }, + }), + ).rejects.toMatchObject({ + code: 'AUTH.INVALID_TOKEN', + status: 401, + }) + expect(onUnauthorized).toHaveBeenCalledOnce() + }) +}) diff --git a/frontend/tests/unit/apiError.spec.ts b/frontend/tests/unit/apiError.spec.ts new file mode 100644 index 0000000..29ef518 --- /dev/null +++ b/frontend/tests/unit/apiError.spec.ts @@ -0,0 +1,37 @@ +import { AxiosError, type AxiosResponse } from 'axios' +import { describe, expect, it } from 'vitest' +import { toApiError } from '@/api/apiError' + +describe('toApiError', () => { + it('保留 ProblemDetails 的安全诊断字段', () => { + const error = new AxiosError('validation failed') + error.response = { + status: 422, + statusText: 'Unprocessable Entity', + headers: {}, + config: error.config!, + data: { + title: '参数校验失败', + status: 422, + code: 'COMMON.VALIDATION_FAILED', + traceId: 'trace-1', + errors: { + mobile: ['手机号格式不正确'], + }, + }, + } satisfies AxiosResponse + + const result = toApiError(error) + + expect(result.code).toBe('COMMON.VALIDATION_FAILED') + expect(result.status).toBe(422) + expect(result.traceId).toBe('trace-1') + expect(result.errors.mobile).toEqual(['手机号格式不正确']) + }) + + it('将超时转换为稳定错误码', () => { + const error = new AxiosError('timeout', 'ECONNABORTED') + + expect(toApiError(error).code).toBe('NETWORK.TIMEOUT') + }) +}) diff --git a/frontend/tests/unit/appConfig.spec.ts b/frontend/tests/unit/appConfig.spec.ts new file mode 100644 index 0000000..43eaf2e --- /dev/null +++ b/frontend/tests/unit/appConfig.spec.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest' +import { createAppConfig } from '@/appConfig' + +describe('createAppConfig', () => { + it('提供安全默认值并清理 API 尾斜杠', () => { + expect(createAppConfig({})).toEqual({ + apiBaseUrl: '/api', + apiTimeoutMs: 15_000, + }) + expect( + createAppConfig({ + VITE_API_BASE_URL: 'https://api.example.test/', + VITE_API_TIMEOUT_MS: '30000', + }), + ).toEqual({ + apiBaseUrl: 'https://api.example.test', + apiTimeoutMs: 30_000, + }) + }) + + it.each([ + [{ VITE_API_BASE_URL: 'relative-path' }, 'VITE_API_BASE_URL'], + [{ VITE_API_TIMEOUT_MS: '999' }, 'VITE_API_TIMEOUT_MS'], + [{ VITE_API_TIMEOUT_MS: 'not-a-number' }, 'VITE_API_TIMEOUT_MS'], + ])('拒绝不安全配置 %#', (env, expectedMessage) => { + expect(() => createAppConfig(env)).toThrow(expectedMessage) + }) +}) diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index d72aa75..3a9adda 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -1,15 +1,17 @@ { "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "types": ["vite/client"], - "allowArbitraryExtensions": true, - - /* Linting */ - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] + "composite": true, + "noEmit": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "useUnknownInCatchVariables": true, + "paths": { + "@/*": ["./src/*"] + } + } } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 1ffef60..4b55668 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,7 +1,14 @@ { "files": [], "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.vitest.json" + } ] } diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json index 8455dcb..508ba70 100644 --- a/frontend/tsconfig.node.json +++ b/frontend/tsconfig.node.json @@ -1,23 +1,21 @@ { "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "es2023", + "target": "ES2023", "lib": ["ES2023"], - "types": ["node"], - "skipLibCheck": true, - - /* Bundler mode */ - "module": "nodenext", + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node", "@playwright/test"], + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, "verbatimModuleSyntax": true, "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true + "skipLibCheck": true, + "composite": true, + "noEmit": true }, - "include": ["vite.config.ts"] + "include": ["eslint.config.js", "playwright.config.ts", "vite.config.ts", "tests/e2e/**/*.ts"] } diff --git a/frontend/tsconfig.vitest.json b/frontend/tsconfig.vitest.json new file mode 100644 index 0000000..23e3df6 --- /dev/null +++ b/frontend/tsconfig.vitest.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo", + "types": ["vite/client", "vitest/globals"] + }, + "include": ["src/**/*.ts", "src/**/*.vue", "tests/unit/**/*.ts", "tests/setup.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index bbcf80c..787b24e 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,7 +1,58 @@ -import { defineConfig } from 'vite' +import { fileURLToPath, URL } from 'node:url' import vue from '@vitejs/plugin-vue' +import { loadEnv } from 'vite' +import { defineConfig } from 'vitest/config' -// https://vite.dev/config/ -export default defineConfig({ - plugins: [vue()], +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + const proxyTarget = env.VITE_DEV_PROXY_TARGET?.trim() + + return { + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + server: { + host: '127.0.0.1', + port: 5173, + strictPort: true, + ...(proxyTarget + ? { + proxy: { + '/api': { + target: proxyTarget, + changeOrigin: true, + }, + '/hubs': { + target: proxyTarget, + changeOrigin: true, + ws: true, + }, + }, + } + : {}), + }, + preview: { + host: '127.0.0.1', + port: 4173, + strictPort: true, + }, + build: { + target: 'es2022', + sourcemap: false, + }, + test: { + environment: 'happy-dom', + setupFiles: ['./tests/setup.ts'], + include: ['tests/unit/**/*.spec.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'html', 'lcov'], + include: ['src/**/*.{ts,vue}'], + exclude: ['src/env.d.ts', 'src/main.ts', 'src/types/router.d.ts'], + }, + }, + } }) -- Gitee From f373f334479494c8af69bb4deb73b2ed33a42da3 Mon Sep 17 00:00:00 2001 From: Lhchen <2244349522@qq.com> Date: Sat, 25 Jul 2026 21:17:57 +0800 Subject: [PATCH 118/118] =?UTF-8?q?feat(platform):=20=E5=BB=BA=E7=AB=8B?= =?UTF-8?q?=E5=AE=B9=E5=99=A8=E4=B8=8E=E8=B4=A8=E9=87=8F=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E8=AE=BE=E6=96=BD=EF=BC=9B=E8=90=BD=E5=AE=9E=E7=BD=91=E7=BB=9C?= =?UTF-8?q?=E9=9A=94=E7=A6=BB=E3=80=81=E8=BF=81=E7=A7=BB=E9=97=A8=E7=A6=81?= =?UTF-8?q?=E5=92=8C=E5=AE=89=E5=85=A8=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 28 ++ .env.example | 26 ++ .gitattributes | 37 ++ .gitignore | 6 + .node-version | 1 + README.md | 216 ++++++++---- backend/.dockerignore | 8 + backend/Dockerfile | 60 ++++ backend/README.md | 5 +- backend/src/Mall.Api/Program.cs | 1 - backend/src/Mall.Migrator/MigrationRunner.cs | 18 + compose.yaml | 346 +++++++++++++++++++ deploy/README.md | 71 ++++ deploy/nginx/Dockerfile | 11 + deploy/nginx/html/service-unavailable.html | 54 +++ deploy/nginx/nginx.conf | 149 ++++++++ deploy/postgres/init/01-create-roles.sh | 54 +++ frontend/.dockerignore | 8 + frontend/Dockerfile | 18 + frontend/nginx.conf | 54 +++ scripts/Initialize-LocalEnvironment.ps1 | 116 +++++++ scripts/Start-Infrastructure.ps1 | 22 ++ scripts/Stop-Infrastructure.ps1 | 43 +++ scripts/Test-ComposeConfiguration.ps1 | 153 ++++++++ scripts/Test-ContainerImages.ps1 | 145 ++++++++ scripts/Test-FrozenBaseline.ps1 | 49 +++ scripts/Verify-Repository.ps1 | 68 ++++ 27 files changed, 1698 insertions(+), 69 deletions(-) create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 .node-version create mode 100644 backend/.dockerignore create mode 100644 backend/Dockerfile create mode 100644 compose.yaml create mode 100644 deploy/README.md create mode 100644 deploy/nginx/Dockerfile create mode 100644 deploy/nginx/html/service-unavailable.html create mode 100644 deploy/nginx/nginx.conf create mode 100644 deploy/postgres/init/01-create-roles.sh create mode 100644 frontend/.dockerignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/nginx.conf create mode 100644 scripts/Initialize-LocalEnvironment.ps1 create mode 100644 scripts/Start-Infrastructure.ps1 create mode 100644 scripts/Stop-Infrastructure.ps1 create mode 100644 scripts/Test-ComposeConfiguration.ps1 create mode 100644 scripts/Test-ContainerImages.ps1 create mode 100644 scripts/Test-FrozenBaseline.ps1 create mode 100644 scripts/Verify-Repository.ps1 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c7f8a1e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,28 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{cs,csx}] +indent_style = space +indent_size = 4 + +[*.{csproj,props,targets,json,jsonc,yaml,yml,xml}] +indent_style = space +indent_size = 2 + +[*.{js,ts,vue,css,scss,html}] +indent_style = space +indent_size = 2 + +[*.{ps1,psm1,psd1}] +charset = utf-8-bom +end_of_line = crlf +indent_style = space +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..58d520c --- /dev/null +++ b/.env.example @@ -0,0 +1,26 @@ +ESHOP_VERSION=local-dev +ESHOP_BIND_ADDRESS=127.0.0.1 +ESHOP_HTTP_PORT=8080 + +POSTGRES_BOOTSTRAP_PASSWORD= +POSTGRES_MIGRATOR_PASSWORD= +POSTGRES_APP_PASSWORD= +REDIS_PASSWORD= +RABBITMQ_USER=eshop +RABBITMQ_PASSWORD= + +AUTH_SIGNING_KEY= +AUTH_KEY_FINGERPRINT= +AUTH_EXPECTED_DIGEST= +AUTH_ISSUER=https://eshop.local +AUTH_AUDIENCE=eshop-web +AUTH_ACCESS_TOKEN_LIFETIME_SECONDS=7200 +AUTH_TOKEN_VERSION_RULE=user.tokenVersion==jwt.tokenVersion + +INFRASTRUCTURE_REDIS_ENABLED=false +INFRASTRUCTURE_RABBITMQ_ENABLED=false +INFRASTRUCTURE_OBJECT_STORAGE_ENABLED=false + +OBJECT_STORAGE_ACCESS_KEY= +OBJECT_STORAGE_SECRET_KEY= +OBJECT_STORAGE_BUCKET=eshop diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4fcb162 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,37 @@ +* text=auto + +.editorconfig text eol=lf +.env.example text eol=lf +.gitattributes text eol=lf +.gitignore text eol=lf +.node-version text eol=lf + +*.cs text eol=lf +*.csproj text eol=lf +*.props text eol=lf +*.targets text eol=lf +*.json text eol=lf +*.js text eol=lf +*.ts text eol=lf +*.vue text eol=lf +*.css text eol=lf +*.html text eol=lf +*.md text eol=lf +*.ps1 text eol=crlf +*.sh text eol=lf +*.yaml text eol=lf +*.yml text eol=lf +Dockerfile text eol=lf +*.conf text eol=lf +*.svg text eol=lf + +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.webp binary +*.ico binary +*.pdf binary +*.docx binary +*.pptx binary +*.xlsx binary diff --git a/.gitignore b/.gitignore index 8e2cc6c..9c9bc7c 100644 --- a/.gitignore +++ b/.gitignore @@ -171,6 +171,12 @@ appsettings.*.local.json secrets.json *.pfx *.p12 +*.pem +*.key +*.jks +*.keystore +compose.override.yml +compose.override.yaml # Local archives *.zip diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..5bf4400 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24.15.0 diff --git a/README.md b/README.md index 518ddaa..0a821e7 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,172 @@ # 电子商城(E-Shop)暑期企业级综合项目实战 -> 班级:____ 组号:____ 组名:____ +E-Shop 是一个单店 B2C 教学项目,正式交付边界为 **PC Web + 后端 Web API**。 +购物端、商家端和管理端均通过浏览器接入同一套业务模块、接口契约和 +PostgreSQL 事实源。Electron 与 Android 只属于后续扩展,不在当前骨架中创建。 + +当前仓库已经从旧模板重新建立前后端与运行基础设施,但**尚未实现业务模块**: +没有业务实体、业务接口、业务页面、业务 Worker、数据库 Migration 或种子数据, +也不以模拟成功响应冒充完成度。 + +## 唯一实施基线 + +需求、流程、接口和数据库必须按以下事实源向代码派生,不能让流程反向追随现有 +接口或旧骨架: + +- [需求规格说明书](docs/01-需求文档/需求规格说明书.md) +- [业务流程](docs/02-设计文档/process/) +- [接口设计](docs/02-设计文档/interface/接口设计.md) +- [数据库设计](docs/02-设计文档/数据库设计.md) + +跨技术栈名称统一遵守 +[命名规范](docs/02-设计文档/命名规范.md)。`docs/00-项目要求/` 是教师发布的 +只读基线。 + +## 技术与版本基线 + +| 范围 | 技术 | 当前锁定版本 | +|---|---|---| +| PC Web | Vue、Vue Router、Pinia、Axios、Element Plus | `3.5.40`、`5.2.0`、`4.0.2`、`1.18.1`、`2.14.3` | +| 前端构建与测试 | Node.js、npm、TypeScript、Vite、Vitest、Playwright | `24.15.0`、`11.12.1`、`6.0.3`、`8.1.5`、`4.1.10`、`1.62.0` | +| 后端 | .NET SDK、ASP.NET Core、EF Core、Npgsql | `10.0.301`、`10.0.10`、`10.0.10`、`10.0.3` | +| 本地编排 | .NET Aspire | `13.4.4` | +| 运行依赖 | PostgreSQL、Redis、RabbitMQ、SeaweedFS | `18.4`、`8.2.7`、`4.3.4`、`4.29` | +| 入口与部署 | Docker Compose、Nginx | Compose v2+、Nginx `1.28.0` | + +NuGet 使用中央版本和每项目锁文件;npm 使用精确版本和 +`package-lock.json`。不得改回浮动版本或 `latest` 镜像。 + +## 仓库结构 + +```text +. +├─ backend/ +│ ├─ src/ +│ │ ├─ Mall.Domain/ +│ │ ├─ Mall.Application/ +│ │ ├─ Mall.Infrastructure/ +│ │ ├─ Mall.Api/ +│ │ ├─ Mall.Worker/ +│ │ ├─ Mall.Migrator/ +│ │ └─ Mall.AppHost/ +│ └─ tests/ +│ ├─ Mall.UnitTests/ +│ └─ Mall.IntegrationTests/ +├─ frontend/ +│ ├─ src/ +│ │ ├─ api/ +│ │ ├─ components/ +│ │ ├─ router/ +│ │ ├─ styles/ +│ │ └─ types/ +│ └─ tests/ +│ ├─ unit/ +│ └─ e2e/ +├─ deploy/ +│ ├─ nginx/ +│ └─ postgres/ +├─ scripts/ +├─ compose.yaml +├─ docs/ +└─ reports/ +``` + +不提前创建十个空业务模块项目或目录。首个真实用例出现时,再按模块纵向加入 +数据库映射、应用用例、API、前端和测试,并使用显式依赖与端点注册。 + +## 本机开发 + +### 后端 -## 一、项目简介 +```powershell +cd backend +dotnet restore Mall.sln --locked-mode +dotnet build Mall.sln -c Release --no-restore +dotnet test Mall.sln -c Release --no-build +dotnet run --project src/Mall.Api/Mall.Api.csproj +``` -本项目为软件技术专业暑期企业级综合项目实战,目标是以小组协作方式,在 **4 周** 内完成一个功能完整、可演示、可部署的 **电子商城系统**,全程模拟企业真实研发流程:需求分析 → 系统设计 → 编码实现 → 测试 → 部署 → 验收答辩。 +Development 环境提供: -当前阶段以 **PC Web + 后端 Web API** 为正式交付边界,购物端、商家端和管理端均通过浏览器使用。后续可在同一套业务模块和 Web API 之上扩展 Electron 桌面端与 Android 客户端,不为不同客户端复制后端、数据库或业务规则。 +- OpenAPI:`http://localhost:5000/openapi/v1.json` +- Swagger UI:`http://localhost:5000/swagger` +- A506:`http://localhost:5000/health/live` +- A507:`http://localhost:5000/health/ready` -## 二、小组成员与分工(模块负责制) +A507 当前应返回 `503 notReady`,因为完整 `InitialEshopSchema` 尚不存在。这是 +真实流量门禁,不得改成固定 200。 -本项目采用 **模块负责制**:不按前后端分工,每人认领业务模块,独立负责该模块的 **数据库表 + 后端接口 + 前端页面** 完整链路。 +### 前端 -| 姓名 | 学号 | 角色 | 负责模块(全栈) | -|------|------|------|------------------| -| | | 组长/架构员 | 项目脚手架、登录鉴权、公共组件、集成联调 + 1 个小模块 | -| | | 模块负责人 | 商品模块(分类/列表/搜索/详情 + 后台商品管理) | -| | | 模块负责人 | 订单 + 支付模块(含后台订单管理) | -| | | 模块负责人 | 用户 + 购物车模块(含后台用户管理) | -| | | 模块负责人 | 测试 + 部署 + 选做功能 | +```powershell +cd frontend +npm ci +npm run dev +``` -> 分工规则详见 `docs/00-项目要求/项目要求.md` 第三节,模块难度权重与考核挂钩。 -> 4 人组时,测试与部署职责分摊到各模块负责人,选做功能由认领者负责。 +前端状态页只证明 Vue 工程可挂载,不代表任何商城功能已经完成。业务模块不得 +依赖该页面或模拟数据。 -## 三、技术栈(本组已确认) +## 质量门禁 -| 分层 | 技术选型 | 版本 | -|------|----------|------| -| 前端 | Vue 3、TypeScript、Vite、Pinia、Axios | 依赖创建时锁定 | -| 前端测试 | Vitest、Playwright | 依赖创建时锁定 | -| PC 桌面端(后续规划) | Electron,复用 PC Web 页面与业务逻辑 | 技术验证时锁定 | -| Android 端(后续规划) | 复用前端业务逻辑与统一 Web API,容器方案待技术验证 | 技术验证时锁定 | -| 后端 | .NET 10、ASP.NET Core Web API | .NET 10 | -| 接口契约与文档 | OpenAPI、Swagger | 依赖创建时锁定 | -| 数据访问 | EF Core 10、Npgsql | EF Core 10 | -| 数据库 | PostgreSQL | 镜像创建时锁定 | -| 架构 | 核心领域使用 DDD、Clean Architecture、简化 CQRS | — | -| 事件与可靠性 | 领域事件、RabbitMQ 集成事件、Outbox | — | -| 分布式组件 | Redis、RabbitMQ、Worker Service、Aspire | 依赖创建时锁定 | -| 部署 | Docker Compose、Nginx | 镜像创建时锁定 | -| 对象存储 | S3 Compatible Object Storage;开发环境使用 SeaweedFS | 镜像创建时锁定 | -| 日志与可观测性 | Serilog、OpenTelemetry | 依赖创建时锁定 | +一键验证: -客户端交付顺序为:先完成 PC Web,再评估 Electron 和 Android。三个客户端必须使用同一套后端 Web API、鉴权、业务状态和数据事实;后续客户端不得演变为独立后端。 +```powershell +./scripts/Verify-Repository.ps1 +``` -## 四、仓库目录结构 +已安装依赖且不运行浏览器时: +```powershell +./scripts/Verify-Repository.ps1 -SkipInstall -SkipBrowser -SkipContainers ``` -├── README.md # 项目说明(本文件) -├── docs/ # 项目文档 -│ ├── 00-项目要求/ # 项目要求、验收标准、评分标准(教师发布,勿改) -│ ├── 01-需求文档/ # 需求规格说明书 -│ ├── 02-设计文档/ # 业务流程、架构、数据库、接口与跨技术栈命名规范 -│ ├── 03-测试文档/ # 测试计划、测试报告 -│ ├── 04-会议记录/ # 小组会议纪要 -│ └── 05-总结答辩/ # 项目总结报告、答辩材料 -├── reports/ # 日报周报 -│ ├── daily/ # 日报(每人每天一份) -│ └── weekly/ # 周报(每组每周一份,组长汇总) -├── frontend/ # 前端代码(自行创建) -└── backend/ # 后端代码(自行创建) + +门禁包含: + +- 冻结需求、流程、接口和数据库基线防改; +- NuGet 锁定还原、格式、Release 构建与测试; +- npm 锁定安装、Prettier、ESLint、类型检查、单测、构建和官方安全审计; +- Chromium 冒烟; +- Compose 必需服务、同版本双 API、端口暴露和非 `latest` 检查; +- PostgreSQL、Redis、RabbitMQ、SeaweedFS 精确标签与摘要拉取,以及五个交付 + 镜像的实际构建和非 root 检查;API 及两层 Nginx 另做只读根文件系统运行检查; +- Git 空白与冲突标记检查。 + +Gitee Go 启用后会由平台生成 `.workflow` 文件;流水线应直接调用上述脚本。 +在平台尚未生成工作流前,不在仓库中编造无法验证的 CI 配置。 + +## 容器基础设施 + +首次生成仅保存在本机、不会输出到终端的随机 Secret: + +```powershell +./scripts/Initialize-LocalEnvironment.ps1 ``` -## 五、关键时间节点(4 周) +本地入口默认只监听 `127.0.0.1:8080`。Redis、RabbitMQ 和对象写入能力开关 +默认保持 `false`,因为对应业务缓存、可靠事件与对象用例尚未实现;以后只能由 +相应纵向模块在实现与验证完成后通过 `.env` 开启,不能靠修改 A507 伪装可用。 + +只启动 PostgreSQL、Redis、RabbitMQ 与 SeaweedFS: -| 阶段 | 时间 | 交付物 | -|------|------|--------| -| 第 1 周 | 需求与设计 | 需求规格说明书、架构/数据库/接口设计 | -| 第 2 周 | 核心功能开发 | 用户、商品模块可演示 | -| 第 3 周 | 完整功能开发 | 购物车、订单、支付(模拟)模块可演示;选做功能与挑战模块开发 | -| 第 4 周 | 测试、部署与答辩 | 测试报告、挑战模块压测/边界验证、部署上线、总结报告、答辩 | +```powershell +./scripts/Start-Infrastructure.ps1 +``` -## 六、Git 协作规范 +停止全部 Compose 服务并保留数据卷: -1. `master` 是稳定发布分支,`dev` 是日常集成分支;两个长期分支均禁止直接 push。 -2. 采用短生命周期任务分支:格式为 `<类型>/<模块>-<任务>-<姓名拼音首字母>`,例如 `feature/auth-login-lhc`;从最新 `dev` 创建,通过 PR 合入 `dev`,合并后立即删除。 -3. 提交信息格式:`: <描述>`,type 取值:`feat` `fix` `refactor` `docs` `test` `chore`。 -4. **每人每天至少一次有效提交**,提交记录将作为个人考核依据。 -5. **交叉 Code Review**:每人的功能分支由相邻模块负责人审查后方可合并(审查人在合并说明中留名)。 -6. 阶段验收或发布时,由 `dev` 向 `master` 创建发布 PR,通过完整验证和审查后合并。 +```powershell +./scripts/Stop-Infrastructure.ps1 +``` -完整流程见 [`docs/02-设计文档/Git团队协作流程.md`](docs/02-设计文档/Git团队协作流程.md)。 +`compose.yaml` 已定义 Nginx、前端、双 API、Worker、Migrator 和四个共享依赖。 +但是,在完整数据库映射与 `InitialEshopSchema` 到位前,Migrator 会失败并阻止 +API/Worker/Nginx 开放,这符合 C10 冻结流程。更多边界见 +[部署说明](deploy/README.md)。 -## 七、如何开始 +## Git 协作 -1. 全组阅读 `docs/00-项目要求/` 下的全部文档。 -2. 完成分工表、确定技术栈并填入本文件。 -3. 按 `docs/01-需求文档/` 模板开始编写需求文档。 -4. 每天下班前提交个人日报到 `reports/daily/`。 +`master` 是稳定发布分支,`dev` 是日常集成分支,均禁止直接开发和直接 Push。 +普通任务从最新 `dev` 创建短生命周期分支,经真实验证与至少一名成员交叉 +Review 后通过 PR 合入。完整规则见 +[Git 团队协作流程](docs/02-设计文档/Git团队协作流程.md)。 diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..a9ff1ea --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,8 @@ +**/bin/ +**/obj/ +**/TestResults/ +**/*.user +**/*.suo +.aspire/ +.vs/ +.vscode/ diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..4f5256f --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,60 @@ +# syntax=docker/dockerfile:1.7@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e + +FROM mcr.microsoft.com/dotnet/sdk:10.0.301-alpine3.24@sha256:fc785a84b314fce6b7adb29ecc18542d7416f2e05c03ec1ebdb222eadaeafa50 AS restore +WORKDIR /src +COPY global.json Directory.Build.props Directory.Packages.props ./ +COPY src/Mall.Domain/Mall.Domain.csproj src/Mall.Domain/packages.lock.json src/Mall.Domain/ +COPY src/Mall.Application/Mall.Application.csproj src/Mall.Application/packages.lock.json src/Mall.Application/ +COPY src/Mall.Infrastructure/Mall.Infrastructure.csproj src/Mall.Infrastructure/packages.lock.json src/Mall.Infrastructure/ +COPY src/Mall.Api/Mall.Api.csproj src/Mall.Api/packages.lock.json src/Mall.Api/ +COPY src/Mall.Worker/Mall.Worker.csproj src/Mall.Worker/packages.lock.json src/Mall.Worker/ +COPY src/Mall.Migrator/Mall.Migrator.csproj src/Mall.Migrator/packages.lock.json src/Mall.Migrator/ +RUN --mount=type=cache,target=/root/.nuget/packages \ + dotnet restore src/Mall.Api/Mall.Api.csproj --locked-mode \ + && dotnet restore src/Mall.Worker/Mall.Worker.csproj --locked-mode \ + && dotnet restore src/Mall.Migrator/Mall.Migrator.csproj --locked-mode +COPY . . + +FROM restore AS publish-api +RUN --mount=type=cache,target=/root/.nuget/packages \ + dotnet publish src/Mall.Api/Mall.Api.csproj \ + --configuration Release \ + --no-restore \ + --output /app \ + /p:UseAppHost=false + +FROM restore AS publish-worker +RUN --mount=type=cache,target=/root/.nuget/packages \ + dotnet publish src/Mall.Worker/Mall.Worker.csproj \ + --configuration Release \ + --no-restore \ + --output /app \ + /p:UseAppHost=false + +FROM restore AS publish-migrator +RUN --mount=type=cache,target=/root/.nuget/packages \ + dotnet publish src/Mall.Migrator/Mall.Migrator.csproj \ + --configuration Release \ + --no-restore \ + --output /app \ + /p:UseAppHost=false + +FROM mcr.microsoft.com/dotnet/aspnet:10.0.10-alpine3.24@sha256:eb7c0c9ef04479bfff191036f6b8959a7d6bac983bd7160c6b8b84b20d3ad0e7 AS runtime +WORKDIR /app +ENV DOTNET_EnableDiagnostics=0 \ + DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false +RUN apk add --no-cache icu-libs tzdata +USER $APP_UID + +FROM runtime AS api +COPY --from=publish-api --chown=$APP_UID:$APP_UID /app . +EXPOSE 8080 +ENTRYPOINT ["dotnet", "Mall.Api.dll"] + +FROM runtime AS worker +COPY --from=publish-worker --chown=$APP_UID:$APP_UID /app . +ENTRYPOINT ["dotnet", "Mall.Worker.dll"] + +FROM runtime AS migrator +COPY --from=publish-migrator --chown=$APP_UID:$APP_UID /app . +ENTRYPOINT ["dotnet", "Mall.Migrator.dll"] diff --git a/backend/README.md b/backend/README.md index ec6c920..8ca9095 100644 --- a/backend/README.md +++ b/backend/README.md @@ -58,4 +58,7 @@ Deployment__InstanceId ``` API 与 Worker 不调用 `Database.Migrate()`;只有 `Mall.Migrator` 可以执行 -Migration。 +Migration。统一初始 Migration 必须对每张业务表、序列和函数显式授予 +`eshop_app` 所需的最小权限;角色初始化脚本不会把全部未来对象默认授权给运行 +账号。Migrator 成功后只授予 `eshop_app` 读取 `__EFMigrationsHistory` 的权限, +并明确撤销其写权限,供 A507 校验目标版本。 diff --git a/backend/src/Mall.Api/Program.cs b/backend/src/Mall.Api/Program.cs index 9a9d3dc..be88377 100644 --- a/backend/src/Mall.Api/Program.cs +++ b/backend/src/Mall.Api/Program.cs @@ -31,7 +31,6 @@ app.UseSerilogRequestLogging(options => if (!app.Environment.IsDevelopment()) { app.UseHsts(); - app.UseHttpsRedirection(); } app.UseStatusCodePages(async statusCodeContext => diff --git a/backend/src/Mall.Migrator/MigrationRunner.cs b/backend/src/Mall.Migrator/MigrationRunner.cs index ba241f2..4c47a39 100644 --- a/backend/src/Mall.Migrator/MigrationRunner.cs +++ b/backend/src/Mall.Migrator/MigrationRunner.cs @@ -38,6 +38,18 @@ public sealed partial class MigrationRunner( $"Target migration '{targetMigration}' was not applied."); } + await dbContext.Database.ExecuteSqlRawAsync( + """ + REVOKE ALL PRIVILEGES + ON TABLE "__EFMigrationsHistory" + FROM eshop_app; + GRANT SELECT + ON TABLE "__EFMigrationsHistory" + TO eshop_app; + """, + cancellationToken); + + LogRuntimePermissionsFinalized(logger); LogMigrationApplied(logger, targetMigration); } @@ -56,4 +68,10 @@ public sealed partial class MigrationRunner( private static partial void LogMigrationApplied( ILogger logger, string targetMigration); + + [LoggerMessage( + EventId = 1003, + Level = LogLevel.Information, + Message = "Runtime access to migration history is restricted to read-only.")] + private static partial void LogRuntimePermissionsFinalized(ILogger logger); } diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..f944003 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,346 @@ +x-default-logging: &default-logging + driver: json-file + options: + max-size: 10m + max-file: "3" + +x-api-environment: &api-environment + ASPNETCORE_URLS: http://+:8080 + DOTNET_ENVIRONMENT: Production + HOME: /tmp + Deployment__ServiceName: mall-api + Deployment__Version: ${ESHOP_VERSION:-local-dev} + Deployment__ExpectedVersion: ${ESHOP_VERSION:-local-dev} + Deployment__TargetMigration: InitialEshopSchema + Deployment__ProbeTimeoutMilliseconds: "1500" + Authentication__SigningKey: ${AUTH_SIGNING_KEY:?run scripts/Initialize-LocalEnvironment.ps1} + Authentication__ExpectedDigest: ${AUTH_EXPECTED_DIGEST:?run scripts/Initialize-LocalEnvironment.ps1} + Authentication__HttpBearer__Issuer: ${AUTH_ISSUER:-https://eshop.local} + Authentication__HttpBearer__Audience: ${AUTH_AUDIENCE:-eshop-web} + Authentication__HttpBearer__KeyFingerprint: ${AUTH_KEY_FINGERPRINT:?run scripts/Initialize-LocalEnvironment.ps1} + Authentication__HttpBearer__AccessTokenLifetimeSeconds: ${AUTH_ACCESS_TOKEN_LIFETIME_SECONDS:-7200} + Authentication__HttpBearer__ClockSkewSeconds: "0" + Authentication__HttpBearer__TokenVersionValidationRule: ${AUTH_TOKEN_VERSION_RULE:-user.tokenVersion==jwt.tokenVersion} + Authentication__Hub__Issuer: ${AUTH_ISSUER:-https://eshop.local} + Authentication__Hub__Audience: ${AUTH_AUDIENCE:-eshop-web} + Authentication__Hub__KeyFingerprint: ${AUTH_KEY_FINGERPRINT:?run scripts/Initialize-LocalEnvironment.ps1} + Authentication__Hub__AccessTokenLifetimeSeconds: ${AUTH_ACCESS_TOKEN_LIFETIME_SECONDS:-7200} + Authentication__Hub__ClockSkewSeconds: "0" + Authentication__Hub__TokenVersionValidationRule: ${AUTH_TOKEN_VERSION_RULE:-user.tokenVersion==jwt.tokenVersion} + ConnectionStrings__Postgres: Host=postgres;Port=5432;Database=eshop;Username=eshop_app;Password=${POSTGRES_APP_PASSWORD:?run scripts/Initialize-LocalEnvironment.ps1};Pooling=true;Maximum Pool Size=100 + ConnectionStrings__Redis: redis:6379,password=${REDIS_PASSWORD:?run scripts/Initialize-LocalEnvironment.ps1},abortConnect=false + ConnectionStrings__RabbitMq: amqp://${RABBITMQ_USER:-eshop}:${RABBITMQ_PASSWORD:?run scripts/Initialize-LocalEnvironment.ps1}@rabbitmq:5672/%2f + Cors__AllowedOrigins__0: http://localhost:${ESHOP_HTTP_PORT:-8080} + Infrastructure__Redis__Enabled: ${INFRASTRUCTURE_REDIS_ENABLED:-false} + Infrastructure__RabbitMq__Enabled: ${INFRASTRUCTURE_RABBITMQ_ENABLED:-false} + Infrastructure__ObjectStorage__Enabled: ${INFRASTRUCTURE_OBJECT_STORAGE_ENABLED:-false} + Infrastructure__ObjectStorage__ServiceUrl: http://seaweedfs:8333 + Infrastructure__ObjectStorage__AccessKey: ${OBJECT_STORAGE_ACCESS_KEY:?run scripts/Initialize-LocalEnvironment.ps1} + Infrastructure__ObjectStorage__SecretKey: ${OBJECT_STORAGE_SECRET_KEY:?run scripts/Initialize-LocalEnvironment.ps1} + Infrastructure__ObjectStorage__BucketName: ${OBJECT_STORAGE_BUCKET:-eshop} + OpenApi__EnableSwaggerUi: "false" + Observability__OtlpEndpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} + +x-api-service: &api-service + image: eshop-api:${ESHOP_VERSION:-local-dev} + build: + context: ./backend + dockerfile: Dockerfile + target: api + init: true + restart: unless-stopped + read_only: true + tmpfs: + - /tmp:size=64m,mode=1777 + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + pids_limit: 256 + stop_grace_period: 30s + networks: + - backend + depends_on: + migrator: + condition: service_completed_successfully + healthcheck: + test: + [ + "CMD-SHELL", + "wget -q -O /dev/null http://127.0.0.1:8080/health/ready", + ] + interval: 10s + timeout: 3s + retries: 5 + start_period: 15s + logging: *default-logging + +services: + postgres: + image: postgres:18.4@sha256:3a82e1f56c8f0f5616a11103ac3d47e632c3938698946a7ad26da0df1334744a + restart: unless-stopped + environment: + POSTGRES_DB: eshop + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_BOOTSTRAP_PASSWORD:?run scripts/Initialize-LocalEnvironment.ps1} + POSTGRES_INITDB_ARGS: --encoding=UTF8 --data-checksums + ESHOP_MIGRATOR_PASSWORD: ${POSTGRES_MIGRATOR_PASSWORD:?run scripts/Initialize-LocalEnvironment.ps1} + ESHOP_APP_PASSWORD: ${POSTGRES_APP_PASSWORD:?run scripts/Initialize-LocalEnvironment.ps1} + volumes: + - postgres-data:/var/lib/postgresql + - ./deploy/postgres/init/01-create-roles.sh:/docker-entrypoint-initdb.d/01-create-roles.sh:ro + networks: + - backend + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d eshop"] + interval: 5s + timeout: 3s + retries: 12 + start_period: 10s + security_opt: + - no-new-privileges:true + stop_grace_period: 60s + logging: *default-logging + + redis: + image: redis:8.2.7@sha256:d30960f73a599496d8b2802c97758bab6b1cd421fd06337f837779c47a57e1f3 + restart: unless-stopped + environment: + REDIS_PASSWORD: ${REDIS_PASSWORD:?run scripts/Initialize-LocalEnvironment.ps1} + command: + - sh + - -ec + - exec redis-server --appendonly yes --appendfsync everysec --requirepass "$$REDIS_PASSWORD" + volumes: + - redis-data:/data + networks: + - backend + healthcheck: + test: + [ + "CMD-SHELL", + "REDISCLI_AUTH=$$REDIS_PASSWORD redis-cli ping | grep -q PONG", + ] + interval: 5s + timeout: 3s + retries: 12 + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + stop_grace_period: 30s + logging: *default-logging + + rabbitmq: + image: rabbitmq:4.3.4-management@sha256:656e8ab6b06fb4f84a8a3d90fe80c4f151c4e731bf3e87a50477774b3d7c08b3 + restart: unless-stopped + environment: + RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-eshop} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:?run scripts/Initialize-LocalEnvironment.ps1} + volumes: + - rabbitmq-data:/var/lib/rabbitmq + networks: + - backend + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 20s + security_opt: + - no-new-privileges:true + stop_grace_period: 60s + logging: *default-logging + + seaweedfs: + image: chrislusf/seaweedfs:4.29@sha256:d47c7ee99fcb951351d7194915f4e3a5ea604a8e8871183d713907dec4fb9bf5 + restart: unless-stopped + environment: + AWS_ACCESS_KEY_ID: ${OBJECT_STORAGE_ACCESS_KEY:?run scripts/Initialize-LocalEnvironment.ps1} + AWS_SECRET_ACCESS_KEY: ${OBJECT_STORAGE_SECRET_KEY:?run scripts/Initialize-LocalEnvironment.ps1} + S3_BUCKET: ${OBJECT_STORAGE_BUCKET:-eshop} + command: ["mini", "-dir=/data"] + volumes: + - seaweedfs-data:/data + networks: + - backend + healthcheck: + test: + [ + "CMD-SHELL", + "test -n \"$$S3_BUCKET\" && wget -q -O /dev/null http://127.0.0.1:9333/cluster/status && wget -q -O /dev/null \"http://127.0.0.1:8888/buckets/$$S3_BUCKET/?pretty=y\" && nc -z 127.0.0.1 8333", + ] + interval: 10s + timeout: 5s + retries: 12 + start_period: 20s + security_opt: + - no-new-privileges:true + stop_grace_period: 60s + logging: *default-logging + + migrator: + image: eshop-migrator:${ESHOP_VERSION:-local-dev} + build: + context: ./backend + dockerfile: Dockerfile + target: migrator + init: true + restart: "no" + read_only: true + tmpfs: + - /tmp:size=32m,mode=1777 + environment: + DOTNET_ENVIRONMENT: Production + HOME: /tmp + Deployment__ServiceName: mall-migrator + Deployment__InstanceId: migrator + Deployment__Version: ${ESHOP_VERSION:-local-dev} + Deployment__ExpectedVersion: ${ESHOP_VERSION:-local-dev} + Deployment__TargetMigration: InitialEshopSchema + ConnectionStrings__Postgres: "Host=postgres;Port=5432;Database=eshop;Username=eshop_migrator;Password=${POSTGRES_MIGRATOR_PASSWORD:?run scripts/Initialize-LocalEnvironment.ps1};Options=-c role=eshop_owner;Pooling=false" + Observability__OtlpEndpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} + depends_on: + postgres: + condition: service_healthy + networks: + - backend + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + pids_limit: 128 + stop_grace_period: 30s + logging: *default-logging + + api-1: + <<: *api-service + environment: + <<: *api-environment + Deployment__InstanceId: api-1 + + api-2: + <<: *api-service + environment: + <<: *api-environment + Deployment__InstanceId: api-2 + + worker: + image: eshop-worker:${ESHOP_VERSION:-local-dev} + build: + context: ./backend + dockerfile: Dockerfile + target: worker + init: true + restart: unless-stopped + read_only: true + tmpfs: + - /tmp:size=64m,mode=1777 + environment: + DOTNET_ENVIRONMENT: Production + HOME: /tmp + Deployment__ServiceName: mall-worker + Deployment__InstanceId: worker-1 + Deployment__Version: ${ESHOP_VERSION:-local-dev} + Deployment__ExpectedVersion: ${ESHOP_VERSION:-local-dev} + Deployment__TargetMigration: InitialEshopSchema + ConnectionStrings__Postgres: Host=postgres;Port=5432;Database=eshop;Username=eshop_app;Password=${POSTGRES_APP_PASSWORD:?run scripts/Initialize-LocalEnvironment.ps1};Pooling=true;Maximum Pool Size=50 + ConnectionStrings__Redis: redis:6379,password=${REDIS_PASSWORD:?run scripts/Initialize-LocalEnvironment.ps1},abortConnect=false + ConnectionStrings__RabbitMq: amqp://${RABBITMQ_USER:-eshop}:${RABBITMQ_PASSWORD:?run scripts/Initialize-LocalEnvironment.ps1}@rabbitmq:5672/%2f + Infrastructure__Redis__Enabled: ${INFRASTRUCTURE_REDIS_ENABLED:-false} + Infrastructure__RabbitMq__Enabled: ${INFRASTRUCTURE_RABBITMQ_ENABLED:-false} + Infrastructure__ObjectStorage__Enabled: ${INFRASTRUCTURE_OBJECT_STORAGE_ENABLED:-false} + Infrastructure__ObjectStorage__ServiceUrl: http://seaweedfs:8333 + Infrastructure__ObjectStorage__AccessKey: ${OBJECT_STORAGE_ACCESS_KEY:?run scripts/Initialize-LocalEnvironment.ps1} + Infrastructure__ObjectStorage__SecretKey: ${OBJECT_STORAGE_SECRET_KEY:?run scripts/Initialize-LocalEnvironment.ps1} + Infrastructure__ObjectStorage__BucketName: ${OBJECT_STORAGE_BUCKET:-eshop} + Observability__OtlpEndpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} + depends_on: + migrator: + condition: service_completed_successfully + networks: + - backend + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + pids_limit: 256 + stop_grace_period: 60s + logging: *default-logging + + frontend: + image: eshop-frontend:${ESHOP_VERSION:-local-dev} + build: + context: ./frontend + dockerfile: Dockerfile + restart: unless-stopped + read_only: true + tmpfs: + - /tmp:size=32m,mode=1777 + networks: + - edge + healthcheck: + test: + [ + "CMD-SHELL", + "wget -q -O /dev/null http://127.0.0.1:8080/", + ] + interval: 10s + timeout: 3s + retries: 5 + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + pids_limit: 128 + stop_grace_period: 30s + logging: *default-logging + + nginx: + image: eshop-gateway:${ESHOP_VERSION:-local-dev} + build: + context: ./deploy/nginx + dockerfile: Dockerfile + restart: unless-stopped + read_only: true + tmpfs: + - /tmp:size=64m,mode=1777 + ports: + - ${ESHOP_BIND_ADDRESS:-127.0.0.1}:${ESHOP_HTTP_PORT:-8080}:8080 + depends_on: + frontend: + condition: service_healthy + api-1: + condition: service_healthy + api-2: + condition: service_healthy + networks: + - edge + - backend + healthcheck: + test: + [ + "CMD-SHELL", + "wget -q -O /dev/null http://127.0.0.1:8080/gateway-health", + ] + interval: 10s + timeout: 3s + retries: 5 + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + pids_limit: 128 + stop_grace_period: 30s + logging: *default-logging + +networks: + edge: + backend: + internal: true + +volumes: + postgres-data: + redis-data: + rabbitmq-data: + seaweedfs-data: diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..d5b80ae --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,71 @@ +# 部署基础设施 + +`compose.yaml` 是 C10 的单机多容器承载骨架,包含统一 Nginx 入口、独立前端、 +两个同镜像 API、Worker、一次性 Migrator、PostgreSQL、Redis、RabbitMQ 和 +SeaweedFS。只有 Nginx 发布宿主机端口,其余服务只在内部网络通信。 + +## 当前真实状态 + +容器、门禁与运行边界已经定义,但数据库完整实体映射和统一 +`InitialEshopSchema` 尚未实现。因此: + +- `Mall.Migrator` 会明确失败,不会空跑成功; +- API/Worker 不会自行执行 Migration; +- 完整 Compose 会停在 Migrator 门禁,API/Worker 不会启动; +- 单独启动 API 时,A507 会如实返回 `503 notReady`; +- Nginx 不会在门禁失败时开放业务流量。 + +这是冻结流程要求的失败关闭,不代表完整 C10 已验收。不得为了启动 Compose +创建空 Migration、伪造 A507 或绕过 `service_completed_successfully`。 + +## 使用 + +首次在本机生成未跟踪的 `.env`: + +```powershell +./scripts/Initialize-LocalEnvironment.ps1 +``` + +生成器拒绝覆盖既有 `.env`,避免只更换文件却没有同步轮换持久 PostgreSQL、 +Redis、RabbitMQ 和对象存储凭据。Secret 轮换必须作为独立运维任务执行。 + +只启动数据与中间件依赖: + +```powershell +./scripts/Start-Infrastructure.ps1 +``` + +停止并保留数据卷: + +```powershell +./scripts/Stop-Infrastructure.ps1 +``` + +完整 Migration 到位后,才使用: + +```powershell +docker compose up --build +``` + +日常停止固定使用 `docker compose down`,不得默认附加 `-v`。需要清理数据卷 +属于破坏性操作,必须单独确认。 + +当前 Compose 是本地/集成入口,默认 HTTP。演示和发布环境还必须由部署环境 +提供受控 TLS 证书与 HTTPS/WSS 终止;证书私钥不得提交仓库。 + +本地入口默认绑定 `127.0.0.1`,不会直接暴露到局域网;部署环境需要对外监听时 +通过受控 override 或 `ESHOP_BIND_ADDRESS` 明确放开。Compose 不固定项目名, +不同 Worktree 默认使用各自目录名隔离容器、网络和数据卷。 + +Redis、RabbitMQ、对象存储三个能力开关默认关闭。基础依赖可以先启动,但只有 +对应缓存、可靠事件和对象写入实现及测试完成后才能在 `.env` 中显式开启。 +SeaweedFS 使用生成的 S3 凭据并幂等准备配置的 Bucket;未启用对象能力时, +A507 仍应报告 `disabled`。 + +PostgreSQL 初始化只建立固定角色、连接和 schema 边界,不给 `eshop_app` 或 +`eshop_readonly` 批量授予全部未来表。统一初始 Migration 必须按设计对业务表、 +序列和函数显式授权;Migrator 只额外保证应用账号可只读 Migration 历史。 + +开源 Nginx 使用启动门禁和有界被动故障摘除,不具备 Nginx Plus 主动健康检查。 +因此后续 C10 现场验收仍须证明实例停止、A507 恢复、认证摘要跨实例一致与流量 +重新入池,不能只以容器 `running` 作为证据。 diff --git a/deploy/nginx/Dockerfile b/deploy/nginx/Dockerfile new file mode 100644 index 0000000..b0139e3 --- /dev/null +++ b/deploy/nginx/Dockerfile @@ -0,0 +1,11 @@ +FROM nginx:1.28.0-alpine@sha256:30f1c0d78e0ad60901648be663a710bdadf19e4c10ac6782c235200619158284 + +COPY nginx.conf /etc/nginx/nginx.conf +COPY html/service-unavailable.html /usr/share/nginx/html/service-unavailable.html + +RUN chown -R nginx:nginx /usr/share/nginx/html + +USER nginx +EXPOSE 8080 +ENTRYPOINT ["nginx"] +CMD ["-g", "daemon off;"] diff --git a/deploy/nginx/html/service-unavailable.html b/deploy/nginx/html/service-unavailable.html new file mode 100644 index 0000000..af8b666 --- /dev/null +++ b/deploy/nginx/html/service-unavailable.html @@ -0,0 +1,54 @@ + + + + + + + 服务暂时不可用 · E-Shop + + + +
+

服务暂时不可用

+

系统正在启动或短暂维护,请稍后重试。已提交操作请先查询结果,不要连续重复提交。

+ +
+ + diff --git a/deploy/nginx/nginx.conf b/deploy/nginx/nginx.conf new file mode 100644 index 0000000..726541b --- /dev/null +++ b/deploy/nginx/nginx.conf @@ -0,0 +1,149 @@ +worker_processes auto; +pid /tmp/nginx.pid; +error_log /dev/stderr crit; + +events { + worker_connections 2048; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + server_tokens off; + sendfile on; + keepalive_timeout 65; + + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + map $http_x_request_id $safe_request_id { + "~^[A-Za-z0-9._:-]{1,128}$" $http_x_request_id; + default $request_id; + } + + log_format sanitized + '$remote_addr - $safe_request_id [$time_local] ' + '"$request_method $uri $server_protocol" $status $body_bytes_sent ' + 'upstream=$upstream_addr'; + access_log /dev/stdout sanitized; + + upstream mall_api { + zone mall_api 64k; + least_conn; + server api-1:8080 max_fails=2 fail_timeout=10s; + server api-2:8080 max_fails=2 fail_timeout=10s; + keepalive 32; + } + + upstream mall_frontend { + zone mall_frontend 32k; + server frontend:8080; + keepalive 8; + } + + server { + listen 8080; + client_max_body_size 12m; + + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header X-Frame-Options "DENY" always; + proxy_hide_header X-Content-Type-Options; + proxy_hide_header Referrer-Policy; + proxy_hide_header X-Frame-Options; + + location = /gateway-health { + access_log off; + add_header Cache-Control "no-store" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header X-Frame-Options "DENY" always; + return 204; + } + + location /api/ { + proxy_pass http://mall_api; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Request-Id $safe_request_id; + proxy_set_header Connection ""; + proxy_connect_timeout 3s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + proxy_next_upstream error timeout http_502 http_503 http_504; + proxy_next_upstream_tries 2; + proxy_buffering off; + } + + location /health/ { + proxy_pass http://mall_api; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Request-Id $safe_request_id; + proxy_set_header Connection ""; + proxy_connect_timeout 2s; + proxy_read_timeout 5s; + proxy_next_upstream error timeout http_502 http_503 http_504; + proxy_next_upstream_tries 2; + add_header Cache-Control "no-store" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header X-Frame-Options "DENY" always; + } + + location /hubs/ { + if ($http_upgrade = "") { + return 426; + } + + proxy_pass http://mall_api; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Request-Id $safe_request_id; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_connect_timeout 3s; + proxy_send_timeout 75s; + proxy_read_timeout 75s; + proxy_next_upstream off; + proxy_buffering off; + } + + location / { + proxy_pass http://mall_frontend; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Request-Id $safe_request_id; + proxy_set_header Connection ""; + proxy_connect_timeout 3s; + proxy_read_timeout 30s; + } + + error_page 502 503 504 /service-unavailable.html; + location = /service-unavailable.html { + internal; + root /usr/share/nginx/html; + add_header Cache-Control "no-store" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header X-Frame-Options "DENY" always; + } + } +} diff --git a/deploy/postgres/init/01-create-roles.sh b/deploy/postgres/init/01-create-roles.sh new file mode 100644 index 0000000..8736bce --- /dev/null +++ b/deploy/postgres/init/01-create-roles.sh @@ -0,0 +1,54 @@ +#!/bin/sh +set -eu + +: "${POSTGRES_DB:?POSTGRES_DB is required}" +: "${POSTGRES_USER:?POSTGRES_USER is required}" +: "${ESHOP_MIGRATOR_PASSWORD:?ESHOP_MIGRATOR_PASSWORD is required}" +: "${ESHOP_APP_PASSWORD:?ESHOP_APP_PASSWORD is required}" + +psql \ + --set=ON_ERROR_STOP=1 \ + --username "$POSTGRES_USER" \ + --dbname "$POSTGRES_DB" \ + --set=db_name="$POSTGRES_DB" \ + --set=migrator_password="$ESHOP_MIGRATOR_PASSWORD" \ + --set=app_password="$ESHOP_APP_PASSWORD" <<'SQL' +SELECT 'CREATE ROLE eshop_owner NOLOGIN' +WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'eshop_owner') +\gexec + +SELECT format( + 'CREATE ROLE eshop_migrator LOGIN PASSWORD %L IN ROLE eshop_owner', + :'migrator_password' +) +WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'eshop_migrator') +\gexec + +SELECT format( + 'CREATE ROLE eshop_app LOGIN PASSWORD %L', + :'app_password' +) +WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'eshop_app') +\gexec + +SELECT 'CREATE ROLE eshop_readonly NOLOGIN' +WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'eshop_readonly') +\gexec + +ALTER ROLE eshop_migrator PASSWORD :'migrator_password'; +ALTER ROLE eshop_app PASSWORD :'app_password'; +ALTER DATABASE :"db_name" OWNER TO eshop_owner; +ALTER SCHEMA public OWNER TO eshop_owner; + +REVOKE ALL ON DATABASE :"db_name" FROM PUBLIC; +REVOKE ALL ON SCHEMA public FROM PUBLIC; +GRANT CONNECT ON DATABASE :"db_name" TO eshop_migrator, eshop_app, eshop_readonly; +GRANT USAGE ON SCHEMA public TO eshop_app, eshop_readonly; + +ALTER DEFAULT PRIVILEGES FOR ROLE eshop_owner IN SCHEMA public + REVOKE ALL ON TABLES FROM PUBLIC; +ALTER DEFAULT PRIVILEGES FOR ROLE eshop_owner IN SCHEMA public + REVOKE ALL ON SEQUENCES FROM PUBLIC; +ALTER DEFAULT PRIVILEGES FOR ROLE eshop_owner + REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; +SQL diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..c712b8c --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +coverage/ +playwright-report/ +test-results/ +.env +.env.* +!.env.example diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..a2f28c5 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,18 @@ +# syntax=docker/dockerfile:1.7@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e + +FROM node:24.15.0-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN --mount=type=cache,target=/root/.npm \ + npm ci --no-audit --no-fund +COPY . . +RUN npm run build + +FROM nginx:1.28.0-alpine@sha256:30f1c0d78e0ad60901648be663a710bdadf19e4c10ac6782c235200619158284 AS runtime +COPY nginx.conf /etc/nginx/nginx.conf +COPY --from=build /app/dist /usr/share/nginx/html +RUN chown -R nginx:nginx /usr/share/nginx/html +USER nginx +EXPOSE 8080 +ENTRYPOINT ["nginx"] +CMD ["-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..9b61807 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,54 @@ +worker_processes auto; +pid /tmp/nginx.pid; +error_log /dev/stderr crit; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + server_tokens off; + + log_format sanitized + '$remote_addr [$time_local] "$request_method $uri $server_protocol" ' + '$status $body_bytes_sent'; + access_log /dev/stdout sanitized; + + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + server { + listen 8080; + root /usr/share/nginx/html; + index index.html; + + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header X-Frame-Options "DENY" always; + + location = /index.html { + add_header Cache-Control "no-store" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header X-Frame-Options "DENY" always; + } + + location /assets/ { + try_files $uri =404; + add_header Cache-Control "public, max-age=31536000, immutable"; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header X-Frame-Options "DENY" always; + } + + location / { + try_files $uri $uri/ /index.html; + } + } +} diff --git a/scripts/Initialize-LocalEnvironment.ps1 b/scripts/Initialize-LocalEnvironment.ps1 new file mode 100644 index 0000000..c04b4d7 --- /dev/null +++ b/scripts/Initialize-LocalEnvironment.ps1 @@ -0,0 +1,116 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) +$environmentPath = Join-Path $repositoryRoot '.env' + +if (Test-Path -LiteralPath $environmentPath) { + throw '.env 已存在,已拒绝覆盖。持久数据环境的 Secret 轮换必须作为独立运维任务执行。' +} + +function New-RandomSecret { + param( + [ValidateRange(32, 128)] + [int]$ByteCount = 48 + ) + + $bytes = New-Object byte[] $ByteCount + $generator = [Security.Cryptography.RandomNumberGenerator]::Create() + try { + $generator.GetBytes($bytes) + } + finally { + $generator.Dispose() + } + + $secret = [Convert]::ToBase64String($bytes) + return $secret.TrimEnd('=').Replace('+', '-').Replace('/', '_') +} + +function Get-AuthenticationDigest { + param( + [Parameter(Mandatory)] + [string]$Issuer, + [Parameter(Mandatory)] + [string]$Audience, + [Parameter(Mandatory)] + [string]$KeyFingerprint, + [Parameter(Mandatory)] + [int]$AccessTokenLifetimeSeconds, + [Parameter(Mandatory)] + [string]$TokenVersionValidationRule + ) + + $source = [ordered]@{ + issuer = $Issuer.Trim().Normalize() + audience = $Audience.Trim().Normalize() + keyFingerprint = $KeyFingerprint.Trim().Normalize() + accessTokenLifetimeSeconds = $AccessTokenLifetimeSeconds + clockSkewSeconds = 0 + tokenVersionValidationRule = $TokenVersionValidationRule.Trim().Normalize() + } + $json = $source | ConvertTo-Json -Compress + $sha256 = [Security.Cryptography.SHA256]::Create() + try { + $hash = $sha256.ComputeHash([Text.Encoding]::UTF8.GetBytes($json)) + } + finally { + $sha256.Dispose() + } + + return ([BitConverter]::ToString($hash)).Replace('-', '').ToLowerInvariant() +} + +$issuer = 'https://eshop.local' +$audience = 'eshop-web' +$keyFingerprint = "local-key-$([Guid]::NewGuid().ToString('N'))" +$accessTokenLifetimeSeconds = 7200 +$tokenVersionRule = 'user.tokenVersion==jwt.tokenVersion' +$expectedDigest = Get-AuthenticationDigest ` + -Issuer $issuer ` + -Audience $audience ` + -KeyFingerprint $keyFingerprint ` + -AccessTokenLifetimeSeconds $accessTokenLifetimeSeconds ` + -TokenVersionValidationRule $tokenVersionRule + +$lines = @( + 'ESHOP_VERSION=local-dev' + 'ESHOP_BIND_ADDRESS=127.0.0.1' + 'ESHOP_HTTP_PORT=8080' + '' + "POSTGRES_BOOTSTRAP_PASSWORD=$(New-RandomSecret)" + "POSTGRES_MIGRATOR_PASSWORD=$(New-RandomSecret)" + "POSTGRES_APP_PASSWORD=$(New-RandomSecret)" + "REDIS_PASSWORD=$(New-RandomSecret)" + 'RABBITMQ_USER=eshop' + "RABBITMQ_PASSWORD=$(New-RandomSecret)" + '' + "AUTH_SIGNING_KEY=$(New-RandomSecret -ByteCount 64)" + "AUTH_KEY_FINGERPRINT=$keyFingerprint" + "AUTH_EXPECTED_DIGEST=$expectedDigest" + "AUTH_ISSUER=$issuer" + "AUTH_AUDIENCE=$audience" + "AUTH_ACCESS_TOKEN_LIFETIME_SECONDS=$accessTokenLifetimeSeconds" + "AUTH_TOKEN_VERSION_RULE=$tokenVersionRule" + '' + 'INFRASTRUCTURE_REDIS_ENABLED=false' + 'INFRASTRUCTURE_RABBITMQ_ENABLED=false' + 'INFRASTRUCTURE_OBJECT_STORAGE_ENABLED=false' + '' + "OBJECT_STORAGE_ACCESS_KEY=$(New-RandomSecret -ByteCount 32)" + "OBJECT_STORAGE_SECRET_KEY=$(New-RandomSecret)" + 'OBJECT_STORAGE_BUCKET=eshop' +) + +$utf8WithoutBom = New-Object Text.UTF8Encoding $false +[IO.File]::WriteAllText( + $environmentPath, + ($lines -join [Environment]::NewLine) + [Environment]::NewLine, + $utf8WithoutBom +) + +Write-Host "已生成未跟踪的本地环境文件:$environmentPath" +Write-Host 'Secret 未输出到终端。请勿提交 .env。' diff --git a/scripts/Start-Infrastructure.ps1 b/scripts/Start-Infrastructure.ps1 new file mode 100644 index 0000000..679405c --- /dev/null +++ b/scripts/Start-Infrastructure.ps1 @@ -0,0 +1,22 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) +$environmentPath = Join-Path $repositoryRoot '.env' +if (-not (Test-Path -LiteralPath $environmentPath)) { + throw '未找到 .env。请先运行 scripts/Initialize-LocalEnvironment.ps1。' +} + +Push-Location $repositoryRoot +try { + docker compose --env-file .env config --quiet + if ($LASTEXITCODE -ne 0) { throw 'Compose 配置无效。' } + docker compose --env-file .env up -d --wait --wait-timeout 180 postgres redis rabbitmq seaweedfs + if ($LASTEXITCODE -ne 0) { throw '基础依赖启动失败。' } +} +finally { + Pop-Location +} diff --git a/scripts/Stop-Infrastructure.ps1 b/scripts/Stop-Infrastructure.ps1 new file mode 100644 index 0000000..ea5bc5e --- /dev/null +++ b/scripts/Stop-Infrastructure.ps1 @@ -0,0 +1,43 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) +$placeholderVariables = [ordered]@{ + POSTGRES_BOOTSTRAP_PASSWORD = 'unused-for-compose-down' + POSTGRES_MIGRATOR_PASSWORD = 'unused-for-compose-down' + POSTGRES_APP_PASSWORD = 'unused-for-compose-down' + REDIS_PASSWORD = 'unused-for-compose-down' + RABBITMQ_PASSWORD = 'unused-for-compose-down' + AUTH_SIGNING_KEY = 'unused-for-compose-down' + AUTH_KEY_FINGERPRINT = 'unused-for-compose-down' + AUTH_EXPECTED_DIGEST = ('0' * 64) + OBJECT_STORAGE_ACCESS_KEY = 'unused-for-compose-down' + OBJECT_STORAGE_SECRET_KEY = 'unused-for-compose-down' +} + +$originalValues = @{} +foreach ($entry in $placeholderVariables.GetEnumerator()) { + $originalValues[$entry.Key] = [Environment]::GetEnvironmentVariable($entry.Key, 'Process') + [Environment]::SetEnvironmentVariable($entry.Key, $entry.Value, 'Process') +} + +$locationPushed = $false +try { + Push-Location $repositoryRoot + $locationPushed = $true + docker compose down --remove-orphans + if ($LASTEXITCODE -ne 0) { throw 'Compose 停止失败。' } +} +finally { + if ($locationPushed) { + Pop-Location + } + foreach ($entry in $originalValues.GetEnumerator()) { + [Environment]::SetEnvironmentVariable($entry.Key, $entry.Value, 'Process') + } +} + +Write-Host '服务已停止,命名数据卷已保留。' diff --git a/scripts/Test-ComposeConfiguration.ps1 b/scripts/Test-ComposeConfiguration.ps1 new file mode 100644 index 0000000..daa4fa8 --- /dev/null +++ b/scripts/Test-ComposeConfiguration.ps1 @@ -0,0 +1,153 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) +$validationVariables = [ordered]@{ + ESHOP_VERSION = 'configuration-check' + ESHOP_BIND_ADDRESS = '127.0.0.1' + ESHOP_HTTP_PORT = '18080' + POSTGRES_BOOTSTRAP_PASSWORD = 'configuration-check-bootstrap-password' + POSTGRES_MIGRATOR_PASSWORD = 'configuration-check-migrator-password' + POSTGRES_APP_PASSWORD = 'configuration-check-app-password' + REDIS_PASSWORD = 'configuration-check-redis-password' + RABBITMQ_USER = 'eshop' + RABBITMQ_PASSWORD = 'configuration-check-rabbit-password' + AUTH_SIGNING_KEY = 'configuration-check-signing-key-that-is-long-enough' + AUTH_KEY_FINGERPRINT = 'configuration-check-key' + AUTH_EXPECTED_DIGEST = ('1' * 64) + AUTH_ISSUER = 'https://eshop.local' + AUTH_AUDIENCE = 'eshop-web' + AUTH_ACCESS_TOKEN_LIFETIME_SECONDS = '7200' + AUTH_TOKEN_VERSION_RULE = 'user.tokenVersion==jwt.tokenVersion' + OBJECT_STORAGE_ACCESS_KEY = 'configuration-check-access-key' + OBJECT_STORAGE_SECRET_KEY = 'configuration-check-secret-key' + OBJECT_STORAGE_BUCKET = 'eshop' +} + +$originalValues = @{} +foreach ($entry in $validationVariables.GetEnumerator()) { + $originalValues[$entry.Key] = [Environment]::GetEnvironmentVariable($entry.Key, 'Process') + [Environment]::SetEnvironmentVariable($entry.Key, $entry.Value, 'Process') +} + +Push-Location $repositoryRoot +try { + $json = docker compose -f compose.yaml config --format json + if ($LASTEXITCODE -ne 0) { + throw 'docker compose config 失败。' + } + + $configuration = $json | ConvertFrom-Json + $requiredServices = @( + 'nginx' + 'frontend' + 'api-1' + 'api-2' + 'worker' + 'migrator' + 'postgres' + 'redis' + 'rabbitmq' + 'seaweedfs' + ) + $serviceNames = @($configuration.services.PSObject.Properties.Name) + $missingServices = $requiredServices | Where-Object { $_ -notin $serviceNames } + if ($missingServices) { + throw "Compose 缺少服务:$($missingServices -join ', ')" + } + + foreach ($serviceProperty in $configuration.services.PSObject.Properties) { + $service = $serviceProperty.Value + if ($service.PSObject.Properties['container_name']) { + throw "服务 $($serviceProperty.Name) 不得固定 container_name。" + } + $imageProperty = $service.PSObject.Properties['image'] + if ($imageProperty -and $imageProperty.Value -match '(^|:)latest$') { + throw "服务 $($serviceProperty.Name) 不得使用 latest 镜像。" + } + $portsProperty = $service.PSObject.Properties['ports'] + if ($serviceProperty.Name -ne 'nginx' -and $portsProperty -and $portsProperty.Value) { + throw "只有 nginx 可以发布宿主机端口,发现:$($serviceProperty.Name)" + } + } + + foreach ($serviceName in @('postgres', 'redis', 'rabbitmq', 'seaweedfs')) { + $image = $configuration.services.$serviceName.image + if ($image -notmatch '^[^@]+@sha256:[0-9a-f]{64}$') { + throw "外部依赖镜像必须同时锁定版本标签和 SHA-256 摘要:$serviceName" + } + } + + $nginxPorts = @($configuration.services.nginx.ports) + if ($nginxPorts.Count -ne 1 -or $nginxPorts[0].host_ip -ne '127.0.0.1') { + throw '本地 Compose 入口必须只发布一个 127.0.0.1 监听端口。' + } + + $apiOneImage = $configuration.services.'api-1'.image + $apiTwoImage = $configuration.services.'api-2'.image + if ($apiOneImage -ne $apiTwoImage) { + throw '两个 API 必须使用同一个镜像版本。' + } + + $apiOneInstanceId = $configuration.services.'api-1'.environment.Deployment__InstanceId + $apiTwoInstanceId = $configuration.services.'api-2'.environment.Deployment__InstanceId + if ($apiOneInstanceId -ne 'api-1' -or $apiTwoInstanceId -ne 'api-2') { + throw '两个 API 必须使用不同且稳定的实例标识 api-1/api-2。' + } + + foreach ($serviceName in @('api-1', 'api-2', 'worker')) { + $dependencyProperty = $configuration.services.$serviceName.PSObject.Properties['depends_on'] + if (-not $dependencyProperty) { + throw "服务 $serviceName 缺少 Migrator 成功门禁。" + } + + $dependencyNames = @($dependencyProperty.Value.PSObject.Properties.Name) + if ('migrator' -notin $dependencyNames) { + throw "服务 $serviceName 必须等待 Migrator 成功完成。" + } + + $blockingCapabilities = @('redis', 'rabbitmq', 'seaweedfs') | + Where-Object { $_ -in $dependencyNames } + if ($blockingCapabilities) { + throw "服务 $serviceName 不得把外围能力作为全局启动门:$($blockingCapabilities -join ', ')" + } + } + + $backendNetwork = $configuration.networks.PSObject.Properties['backend'] + if (-not $backendNetwork -or -not $backendNetwork.Value.internal) { + throw 'backend 网络必须保持 internal,数据库和中间件不得直接访问外网。' + } + + foreach ($serviceName in @('api-1', 'api-2')) { + $networkNames = @($configuration.services.$serviceName.networks.PSObject.Properties.Name) + if ($networkNames.Count -ne 1 -or 'backend' -notin $networkNames) { + throw "服务 $serviceName 只能加入 backend 网络,禁止绕过网关暴露到 edge。" + } + } + $gatewayNetworks = @($configuration.services.nginx.networks.PSObject.Properties.Name) + if ('edge' -notin $gatewayNetworks -or 'backend' -notin $gatewayNetworks) { + throw 'nginx 必须同时加入 edge 与 backend,作为唯一跨网络入口。' + } + $frontendNetworks = @($configuration.services.frontend.networks.PSObject.Properties.Name) + if ($frontendNetworks.Count -ne 1 -or 'edge' -notin $frontendNetworks) { + throw 'frontend 只能加入 edge 网络。' + } + + $requiredVolumes = @('postgres-data', 'redis-data', 'rabbitmq-data', 'seaweedfs-data') + $volumeNames = @($configuration.volumes.PSObject.Properties.Name) + $missingVolumes = $requiredVolumes | Where-Object { $_ -notin $volumeNames } + if ($missingVolumes) { + throw "Compose 缺少命名数据卷:$($missingVolumes -join ', ')" + } + + Write-Host 'Compose 结构检查通过。' +} +finally { + Pop-Location + foreach ($entry in $originalValues.GetEnumerator()) { + [Environment]::SetEnvironmentVariable($entry.Key, $entry.Value, 'Process') + } +} diff --git a/scripts/Test-ContainerImages.ps1 b/scripts/Test-ContainerImages.ps1 new file mode 100644 index 0000000..3daf528 --- /dev/null +++ b/scripts/Test-ContainerImages.ps1 @@ -0,0 +1,145 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) +$validationVariables = [ordered]@{ + ESHOP_VERSION = 'container-check' + ESHOP_BIND_ADDRESS = '127.0.0.1' + ESHOP_HTTP_PORT = '18080' + POSTGRES_BOOTSTRAP_PASSWORD = 'container-check-bootstrap-password' + POSTGRES_MIGRATOR_PASSWORD = 'container-check-migrator-password' + POSTGRES_APP_PASSWORD = 'container-check-app-password' + REDIS_PASSWORD = 'container-check-redis-password' + RABBITMQ_USER = 'eshop' + RABBITMQ_PASSWORD = 'container-check-rabbit-password' + AUTH_SIGNING_KEY = 'container-check-signing-key-that-is-long-enough' + AUTH_KEY_FINGERPRINT = 'container-check-key' + AUTH_EXPECTED_DIGEST = ('1' * 64) + AUTH_ISSUER = 'https://eshop.local' + AUTH_AUDIENCE = 'eshop-web' + AUTH_ACCESS_TOKEN_LIFETIME_SECONDS = '7200' + AUTH_TOKEN_VERSION_RULE = 'user.tokenVersion==jwt.tokenVersion' + OBJECT_STORAGE_ACCESS_KEY = 'container-check-access-key' + OBJECT_STORAGE_SECRET_KEY = 'container-check-secret-key' + OBJECT_STORAGE_BUCKET = 'eshop' +} + +$originalValues = @{} +foreach ($entry in $validationVariables.GetEnumerator()) { + $originalValues[$entry.Key] = [Environment]::GetEnvironmentVariable($entry.Key, 'Process') + [Environment]::SetEnvironmentVariable($entry.Key, $entry.Value, 'Process') +} + +Push-Location $repositoryRoot +try { + foreach ($dockerfile in @( + 'backend/Dockerfile' + 'frontend/Dockerfile' + 'deploy/nginx/Dockerfile' + )) { + $dockerfileLines = @(Get-Content -LiteralPath $dockerfile) + $syntaxLines = $dockerfileLines | + Where-Object { $_ -match '^#\s*syntax=' } + $unpinnedSyntaxLines = $syntaxLines | + Where-Object { $_ -notmatch '@sha256:[0-9a-f]{64}$' } + if ($unpinnedSyntaxLines) { + throw "Dockerfile syntax 解析器镜像必须锁定 SHA-256 摘要:$dockerfile" + } + + $stageAliases = @{} + foreach ($fromLine in ($dockerfileLines | Where-Object { $_ -match '^FROM\s+' })) { + if ($fromLine -notmatch + '^FROM\s+(?:--platform=\S+\s+)?(?\S+)(?:\s+AS\s+(?\S+))?$') { + throw "无法解析 Dockerfile FROM 指令:$dockerfile" + } + + $image = $Matches['image'] + $alias = if ($Matches.ContainsKey('alias')) { $Matches['alias'] } else { $null } + if ($image -ne 'scratch' -and + -not $stageAliases.ContainsKey($image) -and + $image -notmatch '@sha256:[0-9a-f]{64}$') { + throw "Dockerfile 基础镜像必须锁定 SHA-256 摘要:$dockerfile" + } + if ($alias) { + $stageAliases[$alias] = $true + } + } + } + + docker compose pull --policy always --ignore-buildable postgres redis rabbitmq seaweedfs + if ($LASTEXITCODE -ne 0) { throw '基础依赖镜像拉取失败。' } + + docker compose build --pull api-1 worker migrator frontend nginx + if ($LASTEXITCODE -ne 0) { throw '交付镜像构建失败。' } + + $expectedImages = @( + 'eshop-api:container-check' + 'eshop-worker:container-check' + 'eshop-migrator:container-check' + 'eshop-frontend:container-check' + 'eshop-gateway:container-check' + ) + foreach ($image in $expectedImages) { + $user = docker image inspect $image --format '{{.Config.User}}' + if ($LASTEXITCODE -ne 0) { throw "无法检查镜像:$image" } + if ([string]::IsNullOrWhiteSpace($user) -or $user -in @('0', 'root')) { + throw "镜像必须声明非 root 运行用户:$image" + } + } + + $apiProbeContainer = 'eshop-api-container-check' + if (docker ps -a --quiet --filter "name=^/$apiProbeContainer$") { + throw "API 探针容器名已被占用:$apiProbeContainer" + } + try { + docker run -d --rm --name $apiProbeContainer ` + --read-only --tmpfs /tmp:size=64m,mode=1777 ` + --cap-drop ALL --security-opt no-new-privileges:true ` + --env ASPNETCORE_URLS=http://+:8080 ` + --env DOTNET_ENVIRONMENT=Production ` + --env HOME=/tmp ` + eshop-api:container-check | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'API 只读探针容器启动失败。' } + + $apiLive = $false + foreach ($attempt in 1..20) { + docker exec $apiProbeContainer sh -c ` + 'wget -q -O /dev/null http://127.0.0.1:8080/health/live >/dev/null 2>&1' + if ($LASTEXITCODE -eq 0) { + $apiLive = $true + break + } + Start-Sleep -Milliseconds 500 + } + if (-not $apiLive) { throw 'API 只读容器未通过 A506 存活检查。' } + } + finally { + if (docker ps -a --quiet --filter "name=^/$apiProbeContainer$") { + docker rm --force $apiProbeContainer | Out-Null + } + } + + docker run --rm --read-only --tmpfs /tmp:size=32m,mode=1777 ` + --cap-drop ALL --security-opt no-new-privileges:true ` + eshop-frontend:container-check -t + if ($LASTEXITCODE -ne 0) { throw '前端 Nginx 运行约束检查失败。' } + + docker run --rm --read-only --tmpfs /tmp:size=64m,mode=1777 ` + --cap-drop ALL --security-opt no-new-privileges:true ` + --add-host api-1:127.0.0.1 ` + --add-host api-2:127.0.0.1 ` + --add-host frontend:127.0.0.1 ` + eshop-gateway:container-check -t + if ($LASTEXITCODE -ne 0) { throw '网关 Nginx 运行约束检查失败。' } +} +finally { + Pop-Location + foreach ($entry in $originalValues.GetEnumerator()) { + [Environment]::SetEnvironmentVariable($entry.Key, $entry.Value, 'Process') + } +} + +Write-Host '容器镜像、非 root 用户与只读运行约束检查通过。' diff --git a/scripts/Test-FrozenBaseline.ps1 b/scripts/Test-FrozenBaseline.ps1 new file mode 100644 index 0000000..3a80864 --- /dev/null +++ b/scripts/Test-FrozenBaseline.ps1 @@ -0,0 +1,49 @@ +[CmdletBinding()] +param( + [string]$BaseRef = 'origin/dev' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) +Push-Location $repositoryRoot +try { + git rev-parse --verify --quiet $BaseRef | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "无法解析基线引用:$BaseRef" + } + + $changedPaths = @( + git -c core.quotePath=false diff --name-only "$BaseRef...HEAD" + git -c core.quotePath=false diff --name-only + git -c core.quotePath=false diff --cached --name-only + git -c core.quotePath=false ls-files --others --exclude-standard + ) | Where-Object { $_ } | Sort-Object -Unique + + $frozenPaths = @( + 'docs/01-需求文档/需求规格说明书.md' + 'docs/02-设计文档/process/' + 'docs/02-设计文档/interface/接口设计.md' + 'docs/02-设计文档/数据库设计.md' + ) + + $violations = foreach ($path in $changedPaths) { + foreach ($frozenPath in $frozenPaths) { + if ($path -eq $frozenPath -or $path.StartsWith($frozenPath, [StringComparison]::Ordinal)) { + $path + break + } + } + } + + if ($violations) { + $joined = ($violations | Sort-Object -Unique) -join [Environment]::NewLine + throw "检测到冻结基线改动:$([Environment]::NewLine)$joined" + } + + Write-Host "冻结基线检查通过(相对 $BaseRef)。" +} +finally { + Pop-Location +} diff --git a/scripts/Verify-Repository.ps1 b/scripts/Verify-Repository.ps1 new file mode 100644 index 0000000..bae6ce3 --- /dev/null +++ b/scripts/Verify-Repository.ps1 @@ -0,0 +1,68 @@ +[CmdletBinding()] +param( + [switch]$SkipInstall, + [switch]$SkipBrowser, + [switch]$SkipContainers, + [string]$BaseRef = 'origin/dev' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) + +& (Join-Path $PSScriptRoot 'Test-FrozenBaseline.ps1') -BaseRef $BaseRef +& (Join-Path $PSScriptRoot 'Test-ComposeConfiguration.ps1') + +Push-Location (Join-Path $repositoryRoot 'backend') +try { + dotnet restore Mall.sln --locked-mode + if ($LASTEXITCODE -ne 0) { throw '后端锁定还原失败。' } + dotnet format Mall.sln --verify-no-changes --no-restore + if ($LASTEXITCODE -ne 0) { throw '后端格式检查失败。' } + dotnet build Mall.sln --configuration Release --no-restore + if ($LASTEXITCODE -ne 0) { throw '后端 Release 构建失败。' } + dotnet test Mall.sln --configuration Release --no-build + if ($LASTEXITCODE -ne 0) { throw '后端测试失败。' } +} +finally { + Pop-Location +} + +Push-Location (Join-Path $repositoryRoot 'frontend') +try { + if (-not $SkipInstall) { + npm ci --no-audit --no-fund + if ($LASTEXITCODE -ne 0) { throw '前端锁定安装失败。' } + } + npm run check + if ($LASTEXITCODE -ne 0) { throw '前端检查失败。' } + npm run audit + if ($LASTEXITCODE -ne 0) { throw '前端依赖审计失败。' } + if (-not $SkipBrowser) { + npm run test:e2e + if ($LASTEXITCODE -ne 0) { throw 'Chromium 浏览器测试失败。' } + } +} +finally { + Pop-Location +} + +if (-not $SkipContainers) { + & (Join-Path $PSScriptRoot 'Test-ContainerImages.ps1') +} + +Push-Location $repositoryRoot +try { + git diff --check "$BaseRef...HEAD" + if ($LASTEXITCODE -ne 0) { throw '已提交差异的 Git 空白与冲突标记检查失败。' } + git diff --check + if ($LASTEXITCODE -ne 0) { throw '未暂存差异的 Git 空白与冲突标记检查失败。' } + git diff --cached --check + if ($LASTEXITCODE -ne 0) { throw '已暂存差异的 Git 空白与冲突标记检查失败。' } +} +finally { + Pop-Location +} + +Write-Host '仓库基础质量门禁全部通过。' -- Gitee