Browse Source

编写商家分管账号功能规格

明确分管账号身份、门店多对多授权、商家端数据隔离、平台端归属展示与推送路由方案。
补充数据模型、接口契约、实施计划和任务清单。
qmj 1 month ago
parent
commit
817c78d5c8

+ 1 - 1
.specify/feature.json

@@ -1,3 +1,3 @@
 {
-  "feature_directory": "specs/019-line-pay"
+  "feature_directory": "specs/022-merchant-store-subaccounts"
 }

+ 42 - 0
specs/022-merchant-store-subaccounts/checklists/requirements.md

@@ -0,0 +1,42 @@
+# 规格质量检查清单:商家门店分管账号与通知路由
+
+**目的**:在进入计划阶段前验证规格完整性与可验收性
+**创建日期**:2026-08-28
+**规格**:[spec.md](../spec.md)
+
+## 内容质量
+
+- [x] 不包含具体框架、类名、表名或接口实现方案
+- [x] 聚焦用户价值、业务规则和数据权限结果
+- [x] 主账号、分管账号、店铺授权和通知接收者定义清晰
+- [x] 所有必填章节均已完成
+
+## 需求完整性
+
+- [x] 不包含未解决的 `[NEEDS CLARIFICATION]`
+- [x] 每条功能需求均可测试且表述明确
+- [x] 成功标准可衡量并与具体技术实现无关
+- [x] 验收场景覆盖正常流程、越权流程和失败流程
+- [x] 明确分管账号可操作范围以及主账号专属能力
+- [x] 明确多账号、多店铺关系和不使用当前店铺状态
+- [x] 明确在线分管账号全量通知和主账号兜底规则
+- [x] 明确消息入库、设备去重及单接收者失败隔离
+- [x] 明确权限撤销和账号停用立即生效
+- [x] 明确分管账号不进入平台商家列表、审核、导出和统计
+- [x] 明确平台按主商家查看分管账号并可独立强制停用
+- [x] 明确主账号不能解除平台停用,平台不能修改授权或密码
+- [x] 明确假设、兼容性要求和非目标
+
+## 功能就绪度
+
+- [x] 每个用户故事都有独立测试方式
+- [x] 功能需求可映射到用户故事和验收场景
+- [x] 数据边界以实际门店归属为准,避免信任客户端参数
+- [x] 现有未配置分管账号的商家兼容行为已定义
+- [x] 规格已具备进入 `/speckit.plan` 阶段的条件
+
+## 备注
+
+- 计划阶段需要盘点所有商家端店铺、商品、订单、退款、财务和通知入口,建立接口权限矩阵。
+- 计划阶段需要确定“有效登录状态”的现有实现能否可靠支持退出、停用和 Token 失效;规格不改变已确认的业务语义。
+- 计划阶段需要明确历史订单缺失门店归属时的兼容查询与通知兜底实现。

+ 141 - 0
specs/022-merchant-store-subaccounts/contracts/api.md

@@ -0,0 +1,141 @@
+# API 契约:商家门店分管账号
+
+所有商家端接口使用 `token` 请求头;平台接口使用现有若依后台认证和权限注解。业务错误沿用 `AjaxResult` 与国际化消息。
+
+## 商家端分管账号接口
+
+基础路径:`/merchant/subaccounts`
+
+### `GET /merchant/subaccounts`
+
+仅主账号可调用。返回所属分管账号及授权店铺。
+
+```json
+{
+  "code": 200,
+  "data": [
+    {
+      "userId": 501,
+      "name": "张三",
+      "phone": "+886900000001",
+      "ownerEnabled": true,
+      "platformEnabled": true,
+      "online": true,
+      "lastLoginAt": "2026-08-28T10:30:00",
+      "stores": [
+        { "storeId": 101, "storeName": "店铺 A" },
+        { "storeId": 102, "storeName": "店铺 B" }
+      ]
+    }
+  ]
+}
+```
+
+### `POST /merchant/subaccounts`
+
+仅主账号可调用。
+
+```json
+{
+  "phone": "+886900000001",
+  "name": "张三",
+  "password": "前端RSA加密值",
+  "storeIds": [101, 102]
+}
+```
+
+规则:手机号和登录名使用规范化后的同一手机号;手机号全局唯一;`storeIds` 非空且全部属于当前主账号;账号创建和授权写入同一事务;不创建钱包。
+
+### `PUT /merchant/subaccounts/{subaccountUserId}`
+
+仅所属主账号可调用,用于修改姓名和授权店铺。
+
+```json
+{
+  "name": "张三",
+  "storeIds": [102, 103]
+}
+```
+
+### `PUT /merchant/subaccounts/{subaccountUserId}/password`
+
+仅所属主账号可调用。
+
+```json
+{ "password": "前端RSA加密值" }
+```
+
+成功后撤销该分管账号现有商家 App/PC 会话,要求重新登录。
+
+### `PUT /merchant/subaccounts/{subaccountUserId}/status`
+
+仅所属主账号可调用,只修改主账号控制状态。
+
+```json
+{ "enabled": false }
+```
+
+停用时立即撤销该账号商家 App/PC 会话;启用不能覆盖平台停用状态。
+
+## 平台端分管账号接口
+
+基础路径:`/infouser/merchant-subaccounts`
+
+### `GET /infouser/merchant-subaccounts?merchantUserId={id}`
+
+需要 `infouser:user:list`。`merchantUserId` 必须对应类型 1 主账号。响应结构与商家端列表一致,不返回密码、CID、Token 或商户敏感资料。
+
+### `PUT /infouser/merchant-subaccounts/{subaccountUserId}/platform-status`
+
+需要 `infouser:user:edit`。
+
+```json
+{ "enabled": false }
+```
+
+只修改平台控制状态。停用时撤销会话;恢复后如果主账号控制状态仍为停用,账号仍不可用。
+
+平台不提供新增、密码重置或店铺授权修改接口。
+
+## 登录与退出
+
+### `POST /infouser/user/shanglodeing`
+
+现有接口扩展:允许 `user_type=5`。签发 Token 前校验平台状态、主账号控制状态和所属主账号状态;成功后更新 CID 与 `lastLoginAt`。类型 5 不执行商家资料审核或钱包逻辑。
+
+### `POST /infouser/user/merchantLogout`
+
+请求头:`token`。删除当前 JWT 对应 Redis 会话。前端在清除本地 token 前调用;删除失败不返回 Token 内容。
+
+## 店铺权限契约
+
+以下有效商家端能力必须统一使用登录身份和实际门店归属校验:
+
+| 能力 | 主账号 | 分管账号 |
+|---|---|---|
+| 店铺列表 | 全部自有店铺 | 全部授权店铺 |
+| 店铺详情、基础资料、营业状态、营业时间 | 自有店铺 | 授权店铺 |
+| 新增、删除店铺 | 允许 | 拒绝 |
+| 商品、分类、规格查询与维护 | 自有店铺 | 授权店铺 |
+| 订单列表、详情、商家状态操作、退款 | 自有店铺 | 授权店铺 |
+| 分管账号管理 | 允许 | 拒绝 |
+| 收款配置、提现、结算、跨店财务 | 允许 | 拒绝 |
+
+所有写接口不得使用请求体中的 `userId`、`shId` 作为授权依据;订单、商品等先按 ID 查询实际 `mdId` 再校验。
+
+## 通知路由契约
+
+内部服务接口:
+
+```java
+void sendStoreNotification(
+    Long storeId,
+    Long ownerUserId,
+    String titleKey,
+    String contentKey,
+    String body,
+    String orderNo
+);
+```
+
+返回值不影响原订单事务。每个选中账号先生成自己的 `push_message`,再尝试外部推送;单个账号失败只记录日志。店铺无法解析时只向 `ownerUserId` 兜底。

+ 83 - 0
specs/022-merchant-store-subaccounts/data-model.md

@@ -0,0 +1,83 @@
+# 数据模型:商家门店分管账号
+
+## `info_user` 扩展
+
+| 字段 | 类型 | 可空 | 默认值 | 说明 |
+|---|---|---:|---|---|
+| `merchant_owner_id` | `BIGINT` | 是 | `NULL` | 类型 5 分管账号所属的商家主账号 ID;其他类型为空 |
+| `subaccount_status` | `CHAR(1)` | 是 | `NULL` | 主账号控制状态:`0=启用`、`1=停用`;仅类型 5 使用 |
+| `last_login_at` | `DATETIME` | 是 | `NULL` | 商家端最近一次成功登录时间 |
+
+新增索引:
+
+```sql
+KEY idx_info_user_merchant_owner (merchant_owner_id, user_type, del_flag)
+```
+
+### 类型与状态约束
+
+| 账号 | `user_type` | `merchant_owner_id` | `status` | `subaccount_status` |
+|---|---:|---|---|---|
+| 普通商家主账号 | `1` | `NULL` | 平台控制 | `NULL` |
+| 夜市账号 | `3` | `NULL` | 平台控制 | `NULL` |
+| 摊位商家 | `4` | 现有规则 | 平台控制 | `NULL` |
+| 商家分管账号 | `5` | 必须为类型 1 主账号 | 平台控制 | 主账号控制 |
+
+类型 5 不创建独立商家钱包,不参与商家审核,不写入商户资质、收款配置或 `store_id` 单店字段。
+
+## `merchant_subaccount_store`
+
+| 字段 | 类型 | 可空 | 说明 |
+|---|---|---:|---|
+| `id` | `BIGINT AUTO_INCREMENT` | 否 | 主键 |
+| `subaccount_user_id` | `BIGINT` | 否 | `info_user.user_id`,必须是类型 5 |
+| `store_id` | `BIGINT` | 否 | `pos_store.id` |
+| `create_time` | `DATETIME` | 否 | 创建时间 |
+| `update_time` | `DATETIME` | 否 | 更新时间 |
+
+索引与唯一性:
+
+```sql
+PRIMARY KEY (id)
+UNIQUE KEY uk_subaccount_store (subaccount_user_id, store_id)
+KEY idx_subaccount_store_store (store_id, subaccount_user_id)
+```
+
+不使用软删除。取消授权时删除关联行;权限查询始终同时验证分管账号状态、所属主账号状态和店铺当前所有者。
+
+## 聚合视图
+
+### `MerchantSubaccountView`
+
+- `userId`
+- `name`
+- `phone`
+- `merchantOwnerId`
+- `ownerEnabled`:主账号是否启用该分管账号
+- `platformEnabled`:平台是否允许该分管账号
+- `online`:是否存在有效商家 App 或 PC 会话
+- `lastLoginAt`
+- `stores[]`:`storeId`、`storeName`
+
+### 可用性判定
+
+```text
+subaccount.del_flag = 0
+AND subaccount.user_type = 5
+AND subaccount.status = 0
+AND subaccount.subaccount_status = 0
+AND owner.user_type = 1
+AND owner.del_flag = 0
+AND owner.status = 0
+```
+
+通知接收还必须满足目标店铺授权有效、App 会话有效且 CID 非空。
+
+## 数据生命周期
+
+1. 主账号创建分管账号:插入 `info_user` 类型 5,再批量插入授权关系,单事务提交。
+2. 调整店铺:验证全部店铺归属后,以事务替换该账号授权集合。
+3. 主账号停用:只更新 `subaccount_status=1` 并撤销该账号商家会话。
+4. 平台停用:只更新 `status=1` 并撤销该账号商家会话。
+5. 删除店铺:同步删除该店铺授权关系;即使清理失败,权限服务也因店铺所有权或有效性不符而拒绝访问。
+6. 主商家停用:不批量改写子账号字段;可用性判定通过主账号状态统一阻断,恢复主商家后仍保留原分配关系。

+ 191 - 0
specs/022-merchant-store-subaccounts/plan.md

@@ -0,0 +1,191 @@
+# 商家门店分管账号 Implementation Plan
+
+**功能标识**:`022-merchant-store-subaccounts` | **日期**:2026-08-28 | **规格**:[spec.md](spec.md)
+
+**目标**:为商家主账号提供可分配多个店铺的独立分管账号,使其按授权门店操作订单、商品和退款,并将店铺通知发送给所有可接收分管账号,同时保持平台商家管理只统计主账号。
+
+**架构**:分管账号复用 `info_user` 认证主键并使用独立 `user_type=5`,通过 `merchant_subaccount_store` 表表达多店授权。后端用集中式门店访问服务校验真实数据归属,通知通过门店接收者路由服务选择在线分管账号或主账号兜底。平台以主商家下级视图呈现分管账号,商家端新增账号管理页。
+
+**技术栈**:Java 21、Spring Boot、MyBatis/MyBatis-Plus、MySQL、Redis、JUnit 5、Mockito、Vue 2、Element UI、vue-i18n。
+
+## 全局约束
+
+- 模块依赖保持 `ruoyi-admin -> ruoyi-system`,系统模块不得导入 `com.ruoyi.app.*`。
+- App Controller 使用明确 DTO、`@RequestBody`、`@RequestHeader String token` 和显式查询参数,不使用 `Map` 入参。
+- 数据库变更只追加到 `updatesql/sql.md`,不直接执行。
+- 商家端和平台端新增文案同步维护简中、繁中、英文、越南语,并保持原 CRLF。
+- 保留当前工作区已有订单状态及推送调整,不覆盖无关脏文件。
+- 密码、Token、CID 和支付资料不得写入日志或响应。
+
+## 技术上下文
+
+**存储**:`info_user` 增加所属主账号、主账号控制状态和最后登录时间;新增账号门店关联表。
+**认证**:沿用商家端 JWT 与 Redis 会话,类型 5 使用商家 App/PC token 前缀。
+**授权**:每次请求按数据库当前状态与目标实体真实 `mdId` 校验,不把店铺权限缓存进 Token。
+**推送**:沿用 iOS `msdstore` 和 `PushEvent -> push_message`,不新增 Android FCM。
+**兼容性**:类型 1、3、4 原有业务保持;无分管账号时通知继续发主账号。
+
+## Constitution Check
+
+- [通过] 新功能采用独立规格、计划、数据模型、接口契约和后续任务清单。
+- [通过] 权限校验集中到服务层,Controller 仍执行显式身份和参数边界。
+- [通过] 不引入反向模块依赖,不创建第二套认证系统。
+- [通过] 支付与退款只增加授权前置校验,不改变渠道状态机。
+- [通过] SQL 仅形成迁移文档,四语种与前端换行要求进入交付检查。
+
+## 项目结构与职责
+
+### 后端新增
+
+```text
+ruoyi-system/src/main/java/com/ruoyi/system/domain/MerchantSubaccountStore.java
+ruoyi-system/src/main/java/com/ruoyi/system/domain/constants/MerchantAccountConstants.java
+ruoyi-system/src/main/java/com/ruoyi/system/mapper/MerchantSubaccountStoreMapper.java
+ruoyi-system/src/main/resources/mapper/infouser/MerchantSubaccountStoreMapper.xml
+ruoyi-system/src/main/java/com/ruoyi/system/service/IMerchantSubaccountStoreService.java
+ruoyi-system/src/main/java/com/ruoyi/system/service/impl/MerchantSubaccountStoreServiceImpl.java
+ruoyi-system/src/main/java/com/ruoyi/system/service/MerchantStoreAccessService.java
+
+ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantSubaccountController.java
+ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantSubaccountAdminController.java
+ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantSubaccountApplicationService.java
+ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountCreateRequest.java
+ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountUpdateRequest.java
+ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountPasswordRequest.java
+ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountStatusRequest.java
+ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountView.java
+ruoyi-admin/src/main/java/com/ruoyi/app/order/MerchantNotificationRouter.java
+```
+
+`MerchantStoreAccessService` 只处理账号、店铺和实体归属授权;`MerchantSubaccountApplicationService` 负责创建、状态、密码、授权事务及 Redis 会话撤销;`MerchantNotificationRouter` 留在 admin 模块以使用 `PayPush` 和 `PushEventService`。
+
+### 后端修改
+
+```text
+ruoyi-system/src/main/java/com/ruoyi/system/domain/InfoUser.java
+ruoyi-system/src/main/resources/mapper/infouser/InfoUserMapper.xml
+ruoyi-system/src/main/java/com/ruoyi/system/mapper/InfoUserMapper.java
+ruoyi-admin/src/main/java/com/ruoyi/app/user/InfoUserController.java
+ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosStoreController.java
+ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosFoodController.java
+ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosFenleiController.java
+ruoyi-admin/src/main/java/com/ruoyi/app/mendian/FoodSpecController.java
+ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java
+ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderController.java
+ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java
+ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderQsOprateController.java
+ruoyi-admin/src/main/java/com/ruoyi/app/order/DeliveryOrderNotificationService.java
+ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties
+ruoyi-admin/src/main/resources/i18n/messages_zh_TW.properties
+ruoyi-admin/src/main/resources/i18n/messages_en_US.properties
+ruoyi-admin/src/main/resources/i18n/messages_vi.properties
+updatesql/sql.md
+```
+
+### 商家端
+
+```text
+E:/QtwCode/foodie/foodie-store/src/api/subaccount.js
+E:/QtwCode/foodie/foodie-store/src/views/MerchantSubaccount.vue
+E:/QtwCode/foodie/foodie-store/src/router/index.js
+E:/QtwCode/foodie/foodie-store/src/components/Aside.vue
+E:/QtwCode/foodie/foodie-store/src/components/Header.vue
+E:/QtwCode/foodie/foodie-store/src/views/AcidrollingCapacity.vue
+E:/QtwCode/foodie/foodie-store/src/api/store.js
+E:/QtwCode/foodie/foodie-store/src/api/food.js
+E:/QtwCode/foodie/foodie-store/src/lang/zh.js
+E:/QtwCode/foodie/foodie-store/src/lang/tw.js
+E:/QtwCode/foodie/foodie-store/src/lang/en.js
+E:/QtwCode/foodie/foodie-store/src/lang/vi.js
+```
+
+主账号显示分管账号菜单;类型 5 只显示订单、授权店铺和商品相关入口。店铺页面对类型 5 隐藏新增和删除,但后端仍强制校验。现有缺少 token 的店铺/商品请求补齐 `isToken: true`。
+
+### 平台端
+
+```text
+E:/QtwCode/foodie/foodie-admin-vue/src/views/infouser/user/sjuser.vue
+E:/QtwCode/foodie/foodie-admin-vue/src/api/infouser/user.js
+E:/QtwCode/foodie/foodie-admin-vue/src/api/language/language.zh_CN.js
+E:/QtwCode/foodie/foodie-admin-vue/src/api/language/language.zh_TW.js
+E:/QtwCode/foodie/foodie-admin-vue/src/api/language/language.en_US.js
+E:/QtwCode/foodie/foodie-admin-vue/src/api/language/language.vi.js
+```
+
+平台商家行新增“分管账号”按钮和对话框,不新增顶级菜单;只提供查看和平台启停。
+
+## 实施阶段
+
+### Phase 1:账号模型与授权基础
+
+1. 为 `InfoUser` 补齐三个字段和 MyBatis 映射,建立类型 5 常量,SQL 追加迁移语句。
+2. 建立关联实体、Mapper 和 Service,覆盖账号—店铺唯一关系与按店铺/账号查询。
+3. 实现 `MerchantStoreAccessService` 的 `requireOwner`、`getAccessibleStoreIds`、`requireStoreAccess`、`requireOrderAccess` 和 `requireFoodAccess`。
+4. 测试主账号全店权限、分管账号多店权限、跨商家授权拒绝、停用状态和授权撤销立即生效。
+
+### Phase 2:分管账号生命周期与登录
+
+1. 实现商家端创建、列表、编辑、重置密码和主账号启停接口,所有写入使用事务。
+2. 实现平台下级列表和平台启停接口,严格分离两个状态字段。
+3. 扩展 `shanglodeing` 支持类型 5,签发 Token 前检查自身与主账号状态,更新最后登录时间。
+4. 增加商家退出接口;停用或重置密码时删除 App/PC 会话。
+5. 回归平台 `userType=1` 列表、导出和统计,证明类型 5 不出现、不计数、不进入审核。
+
+### Phase 3:店铺、商品和订单数据权限
+
+1. `getmystorelist` 对主账号按所有权查询,对分管账号按授权集合查询。
+2. `addmendian` 增加 token 并强制主账号;店铺详情、修改、营业状态和营业时间校验实际店铺。
+3. 商品、分类和规格的商家端查询与写入补齐 token;修改和删除先读取目标实体的实际 `mdId` 再授权。
+4. 商家订单列表按可访问 `mdId` 集合查询;详情、接单后操作、取消和退款在状态流转前执行订单门店授权。
+5. 对收款配置、提现、结算、跨店财务及分管账号管理入口执行主账号校验;类型 5 默认拒绝未明确开放的商家能力。
+
+### Phase 4:通知路由
+
+1. 实现门店通知接收者查询:有效授权、双重启用、主账号正常、App 会话有效、CID 非空。
+2. 每个账号分别发布 `PushEvent`;CID 去重外部推送,单账号异常隔离。
+3. 替换当前有效商家推送调用点:支付成功/订单开放、骑手接单、订单取消等均传入 `mdId`。
+4. 无分管账号、无人具有 App 推送资格或门店异常时按规格向主账号兜底。
+
+### Phase 5:两个前端
+
+1. 商家端新增分管账号管理页,支持多店选择、创建、编辑、密码重置和主账号启停。
+2. 根据类型 1/5 调整菜单和店铺页面按钮,分管账号不显示新增/删除店铺及敏感模块。
+3. 平台商家列表新增下属账号对话框,显示账号、状态、在线/最近登录和负责店铺,只开放平台启停。
+4. 两端补齐四语种文案并保持 CRLF。
+
+### Phase 6:验证与交付
+
+1. 运行定向权限、登录、平台隔离、订单操作和通知路由测试。
+2. 使用 JDK 21 构建 `ruoyi-admin` 及依赖模块。
+3. 构建 `foodie-store` 与 `foodie-admin-vue`。
+4. 按 `quickstart.md` 执行越权、双重停用、通知全发和主账号兜底场景。
+5. 检查三个仓库的暂存范围、换行和差异;SQL 只保留在迁移文档。
+
+## 测试文件规划
+
+```text
+ruoyi-system/src/test/java/com/ruoyi/system/service/MerchantStoreAccessServiceTest.java
+ruoyi-system/src/test/java/com/ruoyi/system/mapper/MerchantSubaccountStoreMapperXmlTest.java
+ruoyi-admin/src/test/java/com/ruoyi/app/user/MerchantSubaccountControllerTest.java
+ruoyi-admin/src/test/java/com/ruoyi/app/user/MerchantSubaccountAdminControllerTest.java
+ruoyi-admin/src/test/java/com/ruoyi/app/order/MerchantNotificationRouterTest.java
+ruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosFoodControllerAccessTest.java
+```
+
+并扩展现有:
+
+```text
+ruoyi-admin/src/test/java/com/ruoyi/app/user/InfoUserControllerTest.java
+ruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosStoreControllerTest.java
+ruoyi-admin/src/test/java/com/ruoyi/app/order/PosOrderShOprateControllerTest.java
+ruoyi-admin/src/test/java/com/ruoyi/app/order/DeliveryOrderNotificationServiceTest.java
+```
+
+## 复杂度说明
+
+| 复杂度 | 必要性 | 未采用的简单方案 |
+|---|---|---|
+| `info_user` 类型 5 + 关联表 | 同时复用认证并隔离平台商家统计,支持账号与店铺多对多 | 类型 1 加角色容易污染所有商家查询;单独账号表复制认证体系 |
+| 双重状态 | 平台强制停用不能被主账号覆盖 | 共用一个状态无法区分操作者和恢复权限 |
+| 集中权限服务 | 多个旧 Controller 的归属规则不同,必须统一防止 IDOR | 仅前端隐藏或各 Controller 手写条件容易漏改 |
+| 通知路由服务 | 同店多接收者、消息逐账号归属、主账号兜底需要统一行为 | 在每个订单入口复制循环会产生规则漂移 |

+ 49 - 0
specs/022-merchant-store-subaccounts/quickstart.md

@@ -0,0 +1,49 @@
+# 快速验证:商家门店分管账号
+
+## 前置条件
+
+1. 使用 JDK 21:`C:\Users\qmj\.jdks\graalvm-jdk-21.0.7`。
+2. 数据库迁移只从 `updatesql/sql.md` 人工执行;Codex 不直接执行迁移。
+3. 准备一个类型 1 主商家、三家归属门店和两个可接收推送的测试设备账号。
+
+## 自动化验证
+
+```powershell
+$env:JAVA_HOME='C:\Users\qmj\.jdks\graalvm-jdk-21.0.7'
+$env:Path="$env:JAVA_HOME\bin;$env:Path"
+mvn -pl ruoyi-admin -am -Dtest=MerchantStoreAccessServiceTest,MerchantSubaccountControllerTest,MerchantSubaccountAdminControllerTest,MerchantNotificationRouterTest,InfoUserControllerTest,PosStoreControllerTest,PosFoodControllerAccessTest,PosOrderShOprateControllerTest -Dsurefire.failIfNoSpecifiedTests=false test
+mvn -pl ruoyi-admin -am -DskipTests package
+```
+
+商家端:
+
+```powershell
+Set-Location E:\QtwCode\foodie\foodie-store
+npm run build
+```
+
+平台端:
+
+```powershell
+Set-Location E:\QtwCode\foodie\foodie-admin-vue
+npm run build:prod
+```
+
+## 手工验收路径
+
+1. 主账号创建分管账号甲并授权店铺 A、B;甲登录后只看到 A、B。
+2. 甲通过手工构造请求访问同主账号店铺 C 的订单、商品和退款,后端全部拒绝。
+3. 甲调用新增或删除店铺、支付配置和财务接口,后端拒绝。
+4. 主账号取消甲对 A 的授权;甲不重新登录,下一次访问 A 立即失败。
+5. A 同时配置甲、乙且两者 App 会话有效;触发 A 通知,甲乙各有一条消息,主账号没有重复消息。
+6. 甲乙均无有效 App 会话;再次触发 A 通知,主账号获得兜底消息。
+7. 平台商家列表、导出和统计只出现主账号;从主商家行进入分管账号视图可看到甲乙。
+8. 平台停用甲后,甲的现有会话失效;主账号尝试启用甲也不能覆盖平台状态。
+9. 平台恢复甲但主账号仍停用甲,甲仍不能登录;主账号再次启用后才恢复。
+10. 平台停用主商家,甲乙全部不能登录、操作或接收通知。
+
+## 安全检查
+
+- 请求日志、异常日志和测试输出不包含密码、RSA 密文、完整 Token 或 CID。
+- 所有越权失败在业务写入之前发生,不产生部分授权、订单状态或退款状态变更。
+- `git diff --check` 无空白错误;两个 Vue 仓库的原 CRLF 不被批量改写。

+ 90 - 0
specs/022-merchant-store-subaccounts/research.md

@@ -0,0 +1,90 @@
+# 技术调研:商家门店分管账号
+
+## 1. 账号存储方案
+
+### 决策
+
+分管账号继续存放在 `info_user`,使用新的 `user_type=5` 表示“商家分管账号”,并通过 `merchant_owner_id` 关联唯一的商家主账号。
+
+### 依据
+
+- 商家端登录、JWT、Redis 会话、CID、语言和消息归属都以 `InfoUser.userId` 为中心,复用 `info_user` 可以保持现有认证与推送链路。
+- 平台商家页固定传 `userType=1`,Mapper 使用 `user_type = #{userType}`;类型 5 天然不会进入商家列表、导出和商家数量统计。
+- `user_type=4` 已用于摊位商家,不能复用。
+
+### 否决方案
+
+- **分管账号仍使用 `user_type=1`,另加角色字段**:所有按 `user_type=1` 统计、审核和导出的查询都必须额外排除分管账号,漏改一个入口就会污染平台商家管理。
+- **单独建立完整账号表**:需要复制登录、Token、CID、语言和消息体系,认证链路复杂且容易产生两套用户主键。
+
+## 2. 双重停用状态
+
+### 决策
+
+- 复用 `info_user.status` 作为平台控制状态:`0=正常`、`1=平台停用`。
+- 新增 `subaccount_status` 作为主账号控制状态:`0=启用`、`1=主账号停用`,只对类型 5 生效。
+- 分管账号可用条件为:自身未删除、平台状态正常、主账号控制状态正常、所属主账号未删除且平台状态正常。
+
+### 原因
+
+如果主账号和平台共用一个可写状态,主账号可以重新启用平台强制停用的账号。两个状态必须独立,并由不同接口控制。
+
+## 3. 店铺授权模型
+
+### 决策
+
+新增 `merchant_subaccount_store` 多对多关联表。授权保存时同时校验:分管账号的 `merchant_owner_id` 等于 `pos_store.user_id`。
+
+### 原因
+
+一个分管账号可负责多个店铺,一个店铺也可配置多个分管账号;JSON、逗号字符串或 `info_user.store_id` 都不能可靠表达该关系。
+
+## 4. 权限校验边界
+
+### 决策
+
+在 `ruoyi-system` 建立统一的 `MerchantStoreAccessService`,每次业务请求从数据库读取当前账号和目标数据的真实门店归属:
+
+- 主账号可访问 `pos_store.user_id = 当前账号` 的全部门店。
+- 分管账号只可访问关联表中的有效门店。
+- 订单以 `pos_order.md_id`、商品以 `pos_food.md_id`、分类和规格以其实际关联商品或门店为准。
+- 新增店铺、分管账号管理、支付配置、结算和提现必须调用 `requireOwner`。
+
+不把店铺集合写入 Token,也不依赖前端传入的主账号 ID,因此撤销授权后下一次请求立即生效。
+
+## 5. 平台呈现
+
+### 决策
+
+不新增顶级“商家”行或独立商家审核流程。在平台 `sjuser.vue` 的主商家操作区增加“分管账号”入口,以对话框/抽屉展示该主账号的下属账号。
+
+平台可以修改 `info_user.status` 以强制停用或恢复,但不能修改 `subaccount_status`、密码或店铺授权。
+
+## 6. 在线状态与通知资格
+
+### 决策
+
+- 平台“在线”状态:Redis 中存在该账号的商家 App 或 PC 有效会话。
+- 外部推送资格:账号满足全部启用条件、拥有目标店铺授权、存在商家 App 有效会话且 CID 非空。
+- 任一可接收分管账号存在时,消息分别归属这些账号,主账号不重复接收。
+- 没有可接收分管账号时,消息归属主账号并尝试主账号推送。
+- 外部推送按 CID 去重;站内消息仍按账号分别入库。
+
+商家登录成功时更新 `last_login_at`。停用账号时删除其商家 App/PC Redis 会话;分管账号的每次业务请求仍重新检查数据库状态,不能只依赖会话存在。
+
+## 7. 现有代码风险与兼容措施
+
+- `InfoUserController.shanglodeing` 当前允许类型 1、3、4,且没有校验 `status`。实现时加入类型 5,并在签发 Token 前统一执行账号可用性检查。
+- `PosStoreController.addmendian`、`PosFoodController.setposfood` 等有效商家端写入口存在未声明 token 或信任请求体归属字段的情况。实现时按 Controller 规范补充 `@RequestHeader String token`,并从 Token 解析操作者。
+- 平台商家列表、导出、统计仍使用 `user_type=1`,新增回归测试证明类型 5 不会出现或计数。
+- `user_type=4` 的摊位逻辑、类型 3 的夜市逻辑保持不变;不能把分管账号塞入现有单店 `store_id` 规则。
+- 当前工作区已有订单状态和推送调整,实施时必须在其基础上接入通知路由,不能覆盖或回退这些未提交改动。
+
+## 8. 安全要求
+
+- 新接口使用明确 DTO 和 `@RequestBody`,App token 使用 `@RequestHeader`,不使用 `Map` 接收请求。
+- 手机号、名称、密码和店铺 ID 集合在 Controller/Service 中显式校验;错误使用现有国际化机制。
+- 密码沿用现有商家端 RSA 传输与存储兼容方式,禁止记录明文、密文或 Token。
+- 所有 Mapper 使用参数绑定,不拼接用户输入 SQL。
+- 批量授权在事务内完成;任一店铺越权时整体回滚。
+- 通过实体真实归属校验订单、商品和退款,防止 IDOR(越权直接对象访问)。

+ 198 - 0
specs/022-merchant-store-subaccounts/spec.md

@@ -0,0 +1,198 @@
+# 功能规格:商家门店分管账号与通知路由
+
+**功能标识**:`022-merchant-store-subaccounts`
+
+**创建日期**:2026-08-28
+
+**状态**:草案
+
+**输入**:商家主账号可为多个店铺创建和分配分管账号;分管账号登录商家端后,只能查看和操作被授权店铺的订单、商品及退款;店铺通知优先发送给该店所有已登录的分管账号,无可接收分管账号时发送给主账号。
+
+## 用户场景与测试
+
+### 用户故事 1:主账号管理分管账号及店铺授权(优先级:P1)
+
+商家主账号可以创建独立登录的分管账号,并为一个账号分配一个或多个属于自己的店铺。主账号可以调整授权店铺、重置密码、停用或重新启用分管账号。
+
+**优先级原因**:账号和店铺授权关系是后续数据隔离、业务操作和通知路由的基础。
+
+**独立测试**:主账号创建一个分管账号并分配两家店铺,分管账号使用独立凭据成功登录;主账号取消其中一家店铺授权后,该账号只剩另一家店铺的访问权限。
+
+**验收场景**:
+
+1. **假如** 主账号拥有店铺 A、店铺 B,**当** 主账号创建分管账号甲并同时分配 A、B,**那么** 账号甲可以使用独立账号登录,且同时拥有 A、B 的授权。
+2. **假如** 店铺 C 不属于当前主账号,**当** 主账号尝试将账号甲分配给店铺 C,**那么** 系统拒绝保存且不产生越权关系。
+3. **假如** 账号甲已经负责 A、B,**当** 主账号取消其店铺 A 授权,**那么** 账号甲立即失去 A 的数据和操作权限,但仍可访问 B。
+4. **假如** 账号甲被停用,**当** 账号甲尝试登录或继续调用商家业务接口,**那么** 系统拒绝访问。
+5. **假如** 当前登录者是分管账号,**当** 其尝试创建、编辑授权、停用或重置其他分管账号,**那么** 系统拒绝操作。
+
+---
+
+### 用户故事 2:分管账号按授权店铺开展经营(优先级:P1)
+
+分管账号登录现有商家端后,店铺列表直接展示其负责的全部店铺,无需切换或保存“当前店铺”。分管账号可以在这些店铺范围内管理商品、订单、营业状态、店铺基础资料和退款,但不能访问未授权店铺。
+
+**优先级原因**:这是分管账号承担门店日常经营工作的核心价值,并且必须保证店铺间数据隔离。
+
+**独立测试**:给账号甲授权店铺 A、B,保留同一主账号下未授权的店铺 C;验证账号甲的店铺列表及各业务列表只包含 A、B,对 C 的详情、修改、订单操作和退款请求均被后端拒绝。
+
+**验收场景**:
+
+1. **假如** 账号甲负责 A、B,**当** 其打开商家端店铺列表,**那么** 系统返回 A、B,不返回同一主账号的店铺 C。
+2. **假如** 账号甲负责 A、B,**当** 其同时查询两家店铺的订单或商品,**那么** 系统按授权集合返回结果,不要求先选择当前店铺。
+3. **假如** 账号甲负责 A,**当** 其操作 A 的订单、商品、营业状态、店铺基础资料或退款,**那么** 系统允许符合原业务规则的操作。
+4. **假如** 账号甲未负责 C,**当** 其通过伪造店铺 ID、订单 ID 或商品 ID 尝试读取或修改 C 的数据,**那么** 后端拒绝操作且不泄露 C 的业务数据。
+5. **假如** 当前登录者是分管账号,**当** 其调用新增店铺接口,**那么** 系统拒绝创建;前端是否隐藏入口不影响后端校验结果。
+6. **假如** 当前登录者是分管账号,**当** 其尝试删除店铺、修改收款配置、提现、结算或查看跨店财务汇总,**那么** 系统拒绝操作。
+
+---
+
+### 用户故事 3:通知发送给店铺在线分管账号(优先级:P1)
+
+订单或其他店铺业务产生商家通知时,系统根据通知对应的店铺查找所有已启用、已登录且仍有该店授权的分管账号,并向这些账号全部发送通知。如果没有符合条件的分管账号,则向店铺所属主账号发送通知。
+
+**优先级原因**:让实际负责门店的人及时处理业务,并在无人负责时通过主账号兜底,避免漏单或延迟处理。
+
+**独立测试**:店铺 A 同时配置账号甲、乙、丙;甲和乙已登录,丙未登录。触发 A 的新订单通知,验证甲、乙分别收到推送和消息记录,丙与主账号不收到;随后使甲、乙退出登录,再次触发通知,验证只发送给主账号。
+
+**验收场景**:
+
+1. **假如** 店铺 A 的账号甲、乙均已登录且可接收推送,**当** A 产生商家通知,**那么** 甲、乙都收到通知,并分别拥有自己的站内消息记录。
+2. **假如** 店铺 A 存在已登录分管账号,**当** A 产生商家通知,**那么** 主账号不再重复接收该通知。
+3. **假如** 店铺 A 没有已登录且可接收通知的分管账号,**当** A 产生商家通知,**那么** 通知发送给主账号作为兜底。
+4. **假如** 账号甲同时负责 A、B,**当** A 产生通知,**那么** 账号甲收到 A 的通知;未负责 A 的其他分管账号不接收。
+5. **假如** 多个接收账号意外登记了相同的设备标识,**当** 系统发送同一业务通知,**那么** 同一设备只收到一次外部推送,但每个应接收账号仍保留可追溯的消息归属。
+6. **假如** 通知无法解析出有效店铺,但可以确定原商家主账号,**当** 系统执行通知路由,**那么** 直接发送给该主账号并记录异常,不向无关分管账号广播。
+
+---
+
+### 用户故事 4:权限变更立即生效(优先级:P2)
+
+主账号调整分管账号状态或店铺授权后,新的查询、操作和通知路由立即使用最新权限,不能继续依赖登录时缓存的旧店铺集合。
+
+**优先级原因**:人员离职、调店或临时代班时需要及时收回或调整权限,防止越权操作和错误通知。
+
+**独立测试**:账号甲保持登录状态,主账号取消其店铺 A 授权;无需账号甲重新登录,随后查询 A、操作 A 订单及触发 A 通知,均不再将甲视为有效分管账号。
+
+**验收场景**:
+
+1. **假如** 账号甲仍处于登录状态,**当** 主账号取消其 A 的授权,**那么** 甲下一次访问 A 时立即被拒绝,A 的后续通知也不再发送给甲。
+2. **假如** 账号甲仍处于登录状态,**当** 主账号停用甲,**那么** 甲的后续业务请求被拒绝,且不再收到任何所分管店铺通知。
+3. **假如** 主账号重新启用甲但尚未重新分配任何店铺,**当** 甲登录商家端,**那么** 可以完成身份认证,但店铺列表为空且不能操作店铺业务。
+
+---
+
+### 用户故事 5:平台按主商家查看和管控分管账号(优先级:P2)
+
+平台管理员继续将商家主账号作为唯一商家管理对象。分管账号不进入现有商家列表、商家审核、商家导出或商家数量统计,而是在对应主商家的详情或下级入口中呈现。平台管理员可以查看分管账号及其负责店铺,并可出于安全或合规原因强制停用或重新启用账号。
+
+**优先级原因**:平台需要了解实际经营账号并具备风险处置能力,同时不能把一个商家的员工账号错误计算成多个商家。
+
+**独立测试**:主商家创建三个分管账号后,平台商家列表、导出和商家数量仍只增加一个主商家;从该主商家进入分管账号页面可看到三个账号及店铺授权,并可强制停用其中一个账号。
+
+**验收场景**:
+
+1. **假如** 一个主商家拥有三个分管账号,**当** 平台查询商家列表、导出商家或统计商家数量,**那么** 只把主账号计为一个商家,三个分管账号均不出现也不计数。
+2. **假如** 平台管理员打开主商家的分管账号入口,**当** 查询完成,**那么** 可以看到每个分管账号的姓名、手机号、账号状态、在线状态或最后登录时间、负责店铺和创建时间。
+3. **假如** 平台管理员强制停用分管账号甲,**当** 甲尝试登录、继续调用商家接口或等待店铺通知,**那么** 系统拒绝其访问且不再向其发送通知。
+4. **假如** 主账号尝试重新启用被平台强制停用的账号甲,**当** 提交启用操作,**那么** 系统仍保持平台停用状态,只有平台管理员可以解除。
+5. **假如** 平台管理员查看分管账号,**当** 尝试修改店铺授权或重置密码,**那么** 平台不提供此能力;店铺授权和密码管理仍由所属主账号负责。
+
+### 边界情况
+
+- 分管账号手机号已被现有用户、商家、骑手或其他分管账号使用时,不允许重复创建账号。
+- 一个分管账号只能隶属于一个商家主账号,但可以关联该主账号名下多个店铺。
+- 店铺转移所有权、删除或失效时,关联授权必须失效,不能继续用于查询、操作或通知。
+- 主账号不能通过分管账号管理功能被降级、停用或删除。
+- 分管账号提交的店铺 ID、订单 ID、商品 ID 和退款目标不一致时,按目标业务数据实际所属店铺校验,不能只校验请求参数。
+- 分管账号已登录但缺少有效推送标识时,不计入该次通知的可接收账号;如果因此没有任何可接收分管账号,则由主账号兜底。
+- 单个分管账号重复关联同一家店铺时,系统保持一条有效授权,不产生重复授权或重复通知。
+- 同一通知批量发送给多个账号时,单个账号发送失败不得阻止其他账号接收;失败结果需要被记录以便排查。
+- 主账号自身未登记有效推送标识时,系统仍需生成主账号的站内消息记录,并记录外部推送未发送原因。
+- 分管账号同时存在“主账号停用”和“平台强制停用”时,任一停用条件都必须阻止登录、业务访问和通知接收;主账号不能解除平台停用。
+- 主商家被平台停用时,其全部分管账号即使自身状态正常,也不能登录、操作店铺或接收通知。
+- 平台通过直接构造查询参数访问分管账号时,通用商家列表、商家审核和商家导出仍必须排除分管账号。
+
+## 需求
+
+### 功能需求
+
+- **FR-001**:系统必须区分商家主账号和分管账号,同时保持两者使用现有商家端身份认证体系。
+- **FR-002**:系统必须只允许商家主账号创建分管账号;创建时至少提供手机号、姓名和初始密码。
+- **FR-003**:系统必须保证分管账号登录标识在现有用户体系内唯一,不能因账号类型不同而重复。
+- **FR-004**:系统必须允许主账号重置分管账号密码、启用或停用分管账号。
+- **FR-005**:系统必须只允许主账号管理隶属于自己的分管账号,禁止跨商家读取或修改。
+- **FR-006**:系统必须支持一个分管账号关联同一主账号名下的一个或多个店铺,并保证相同账号与店铺的有效授权唯一。
+- **FR-007**:系统必须验证被分配店铺属于当前主账号,禁止建立跨商家店铺授权。
+- **FR-008**:分管账号不能自行注册为某个商家的分管账号,也不能自行变更所属主账号或授权店铺。
+- **FR-009**:主账号的店铺列表必须保持返回其拥有的全部店铺;分管账号的店铺列表必须只返回其当前有效授权店铺。
+- **FR-010**:系统不得要求分管账号选择或持久化“当前店铺”;每次查询和操作都必须依据实际目标门店进行授权校验。
+- **FR-011**:所有店铺范围的列表查询必须在后端按当前账号的可访问店铺集合过滤,不能只依赖前端隐藏数据。
+- **FR-012**:所有店铺范围的详情和修改操作必须从目标数据解析实际门店归属,并校验当前账号权限,不能仅信任客户端提交的门店 ID。
+- **FR-013**:分管账号必须可以在授权店铺范围内查看和操作订单、商品、营业状态、店铺基础资料及退款,且仍需满足各业务原有状态流转和校验规则。
+- **FR-014**:分管账号必须禁止新增或删除店铺、管理分管账号、修改收款配置、提现、结算及查看跨店财务汇总。
+- **FR-015**:新增店铺接口必须在后端明确校验当前登录者为主账号;不能将前端入口隐藏视为权限控制。
+- **FR-016**:账号停用或店铺授权移除后,新的查询、操作和通知路由必须立即使用最新权限,不得要求重新登录后才生效。
+- **FR-017**:店铺业务通知必须以实际店铺为路由依据;订单通知优先使用订单保存的门店归属。
+- **FR-018**:通知路由必须选择该店所有已启用、拥有有效授权、处于有效商家端登录状态且具有有效推送标识的分管账号。
+- **FR-019**:当存在一个或多个符合条件的分管账号时,系统必须向这些账号全部发送通知,并且不向主账号重复发送。
+- **FR-020**:当不存在符合条件的分管账号时,系统必须向店铺所属主账号发送通知作为兜底。
+- **FR-021**:系统必须为每个业务接收账号分别保存站内消息归属;外部推送缺少标识或发送失败不得导致站内消息丢失。
+- **FR-022**:同一次通知向多个账号发送时,系统必须按设备标识去重外部推送,并隔离单个接收者的发送失败。
+- **FR-023**:通知无法解析店铺但可以确定主账号时,系统必须直接发送给主账号、记录异常并禁止无范围广播。
+- **FR-024**:分管账号的退款权限必须限定在其有效授权店铺的订单内,且不得绕过现有支付渠道、订单状态和退款校验。
+- **FR-025**:系统必须保留主账号原有账号、店铺、订单和通知行为的兼容性;未创建分管账号的商家继续按原流程运行。
+- **FR-026**:商家端必须向主账号提供分管账号列表、创建、编辑、授权、停用和密码重置入口,并向分管账号隐藏或禁用主账号专属入口。
+- **FR-027**:新增用户可见文本和业务错误必须支持越南语、简体中文、繁体中文和英文。
+- **FR-028**:分管账号必须使用独立于商家主账号的账号类别,不能作为商家主账号参与平台商家列表、审核、导出或数量统计。
+- **FR-029**:平台必须在主商家的详情或下级入口中展示所属分管账号,不得将分管账号作为顶级商家行呈现。
+- **FR-030**:平台分管账号视图必须展示姓名、手机号、账号状态、在线状态或最后登录时间、负责店铺和创建时间。
+- **FR-031**:平台管理员必须可以强制停用或重新启用分管账号;主账号不能解除平台强制停用。
+- **FR-032**:主账号停用和平台强制停用必须作为相互独立的限制条件,任一条件生效时都禁止分管账号登录、访问业务和接收通知。
+- **FR-033**:平台不得修改分管账号的店铺授权或密码;这些能力只属于该账号的主账号。
+- **FR-034**:主商家被平台停用时,系统必须同时禁止其全部分管账号登录、访问商家业务和接收店铺通知。
+- **FR-035**:平台现有商家新增、修改和审核入口不得创建分管账号或把普通账号转换为分管账号。
+
+### 关键实体
+
+- **商家账号**:登录商家端的身份。角色分为主账号和分管账号;分管账号隶属于唯一主账号并具有独立登录凭据、启停状态和推送信息。
+- **店铺**:由主账号拥有的经营单元,是商品、订单、退款权限和通知路由的数据边界。
+- **店铺授权**:分管账号与店铺之间的有效关联;一个账号可以有多个店铺授权,一个店铺可以有多个分管账号。
+- **通知接收者**:某次店铺通知最终选出的账号,可能是所有符合条件的分管账号,也可能是在无人可接收时兜底的主账号。
+- **通知消息**:归属于具体接收账号的站内消息记录;外部推送是该消息的送达通道,不取代账号级消息记录。
+- **账号控制状态**:分管账号同时具有主账号控制状态和平台控制状态;只有两者都允许时账号才可登录、操作和接收通知。
+
+## 成功标准
+
+### 可衡量结果
+
+- **SC-001**:主账号能够在一次创建流程中完成分管账号建立并分配多个店铺,成功后该账号可立即登录。
+- **SC-002**:在自动化越权测试中,分管账号对未授权店铺的列表、详情、修改、订单操作、商品操作和退款请求拦截率达到 100%。
+- **SC-003**:店铺存在多个符合条件的在线分管账号时,所有符合条件账号均产生自己的消息记录,主账号不产生重复消息。
+- **SC-004**:店铺没有符合条件的分管账号时,主账号兜底消息产生率达到 100%。
+- **SC-005**:分管账号停用或授权移除后,无需重新登录,其下一次业务请求和下一次通知路由均使用新权限。
+- **SC-006**:未配置任何分管账号的现有商家,其新增店铺、店铺列表、订单、商品、退款和通知流程保持原有可用行为。
+- **SC-007**:同一通知的单个接收账号推送失败时,其他符合条件账号仍能完成消息入库和推送尝试。
+- **SC-008**:任意数量的分管账号都不会改变平台商家列表、导出和商家数量统计中的主商家数量。
+- **SC-009**:平台强制停用后,主账号无法绕过该状态重新启用分管账号,相关登录、业务访问和通知测试拦截率达到 100%。
+
+## 假设
+
+- 继续复用现有商家端账号登录、Token、设备标识和站内消息机制,不为分管账号建立第二套认证系统。
+- “已登录分管账号”指账号已启用、存在有效商家端登录状态且登记了有效推送标识;App 位于前台或后台不影响推送资格。
+- 第一阶段沿用现有单账号当前推送标识模型;同一账号多设备同时接收不在本功能范围内。
+- 一个分管账号只隶属于一个主账号;跨商家代运营账号不在本功能范围内。
+- 第一阶段采用固定的主账号/分管账号权限边界,不提供按按钮勾选的细粒度自定义权限。
+- 不引入排班或“当前值班账号”;同店多个符合条件的分管账号全部接收通知。
+- 订单和其他核心业务数据具有可解析的门店归属;历史异常数据无法解析门店时按主账号兜底并记录异常。
+
+## 非目标
+
+- 不实现一个分管账号同时隶属于多个商家主体。
+- 不实现分管账号自助注册、自助申请加入店铺或店铺间转移。
+- 不实现自定义角色、逐菜单权限配置或逐操作权限配置。
+- 不实现值班排班、只通知一个当前值班人员或抢单式门店通知。
+- 不实现同一账号多设备会话和多设备推送标识管理。
+- 不改变骑手、普通用户或平台管理后台的账号体系。
+- 不允许平台管理员修改分管账号店铺授权或重置分管账号密码。
+- 不改变订单、支付、退款或配送本身的业务状态流转,只增加门店范围授权和通知接收者路由。

+ 342 - 0
specs/022-merchant-store-subaccounts/tasks.md

@@ -0,0 +1,342 @@
+# 商家门店分管账号任务清单
+
+> **执行要求:** 按任务顺序实施;每个后端任务遵循测试先行,先确认定向测试失败,再写最小实现并复测。提交标题和正文使用中文。
+
+**输入**:[spec.md](spec.md)、[plan.md](plan.md)、[research.md](research.md)、[data-model.md](data-model.md)、[API 契约](contracts/api.md)
+
+## 全局约束
+
+- JDK 使用 `C:\Users\qmj\.jdks\graalvm-jdk-21.0.7`。
+- 模块依赖保持 `ruoyi-admin -> ruoyi-system`。
+- Controller 请求使用明确 DTO、`@RequestBody`、`@RequestHeader` 和显式查询参数,不使用 `Map`。
+- 数据库变更只写入 `updatesql/sql.md`,不直接执行。
+- 两个 Vue 仓库新增文案同步维护简中、繁中、英文、越南语并保持 CRLF。
+- 保留工作区已有订单状态和推送调整;暂存前逐文件核对,禁止混入 `.claude/homunculus/observations.jsonl`。
+- 密码、Token、CID 和支付资料不得出现在日志、错误响应或测试快照中。
+
+---
+
+## Phase 1:数据模型与授权基础
+
+### T001 [US1] 建立分管账号字段和账号—店铺关联模型
+
+**文件:**
+
+- 修改:`ruoyi-system/src/main/java/com/ruoyi/system/domain/InfoUser.java`
+- 创建:`ruoyi-system/src/main/java/com/ruoyi/system/domain/constants/MerchantAccountConstants.java`
+- 修改:`ruoyi-system/src/main/resources/mapper/infouser/InfoUserMapper.xml`
+- 创建:`ruoyi-system/src/main/java/com/ruoyi/system/domain/MerchantSubaccountStore.java`
+- 创建:`ruoyi-system/src/main/java/com/ruoyi/system/mapper/MerchantSubaccountStoreMapper.java`
+- 创建:`ruoyi-system/src/main/resources/mapper/infouser/MerchantSubaccountStoreMapper.xml`
+- 创建:`ruoyi-system/src/main/java/com/ruoyi/system/service/IMerchantSubaccountStoreService.java`
+- 创建:`ruoyi-system/src/main/java/com/ruoyi/system/service/impl/MerchantSubaccountStoreServiceImpl.java`
+- 创建:`ruoyi-system/src/test/java/com/ruoyi/system/mapper/MerchantSubaccountStoreMapperXmlTest.java`
+- 修改:`updatesql/sql.md`
+
+**产出接口:**
+
+```java
+public static final String MerchantAccountConstants.SUBACCOUNT_USER_TYPE = "5";
+
+Long InfoUser.getMerchantOwnerId();
+String InfoUser.getSubaccountStatus();
+Date InfoUser.getLastLoginAt();
+
+List<Long> MerchantSubaccountStoreMapper.selectStoreIdsBySubaccountUserId(Long subaccountUserId);
+List<InfoUser> MerchantSubaccountStoreMapper.selectEligibleSubaccountsByStoreId(Long storeId);
+int MerchantSubaccountStoreMapper.deleteBySubaccountUserId(Long subaccountUserId);
+int MerchantSubaccountStoreMapper.insertBatch(List<MerchantSubaccountStore> relations);
+```
+
+- [ ] 写 XML 结构测试,断言 `merchant_owner_id`、`subaccount_status`、`last_login_at` 映射存在,并断言关联表查询使用参数绑定和唯一字段。
+- [ ] 运行 `mvn -pl ruoyi-system -Dtest=MerchantSubaccountStoreMapperXmlTest test`,确认因实体、Mapper 或 XML 尚不存在而失败。
+- [ ] 为 `InfoUser` 增加三个字段、访问器和 `toString()` 映射;在 `MerchantAccountConstants` 定义类型 1/5 和双重状态常量,禁止在多个 Controller 重复魔法字符串。
+- [ ] 实现关联实体、Mapper XML 和 Service;批量插入为空集合时直接返回,不生成非法 SQL。
+- [ ] 在 `updatesql/sql.md` 追加 2026-08-28 迁移:三个 `info_user` 字段、所属主账号索引、关联表和两个索引,不执行 SQL。
+- [ ] 复跑定向测试并执行 `git diff --check -- ruoyi-system updatesql/sql.md`。
+- [ ] 暂存本任务文件并提交:`git commit -m "建立商家分管账号数据模型"`。
+
+### T002 [US2][US4] 实现集中式商家门店访问服务
+
+**文件:**
+
+- 创建:`ruoyi-system/src/main/java/com/ruoyi/system/service/MerchantStoreAccessService.java`
+- 创建:`ruoyi-system/src/test/java/com/ruoyi/system/service/MerchantStoreAccessServiceTest.java`
+- 按需修改:`ruoyi-system/src/main/java/com/ruoyi/system/mapper/InfoUserMapper.java`
+- 按需修改:`ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosStoreMapper.java`
+
+**产出接口:**
+
+```java
+public final class MerchantAccessContext {
+    Long loginUserId();
+    Long ownerUserId();
+    boolean owner();
+    Set<Long> storeIds();
+}
+
+MerchantAccessContext resolve(Long loginUserId);
+void requireOwner(Long loginUserId);
+Set<Long> getAccessibleStoreIds(Long loginUserId);
+void requireStoreAccess(Long loginUserId, Long storeId);
+PosOrder requireOrderAccess(Long loginUserId, Long orderId);
+PosFood requireFoodAccess(Long loginUserId, Long foodId);
+```
+
+- [ ] 写单元测试覆盖:类型 1 主账号获得全部自有店铺;类型 5 只获得授权店铺;类型 5 双重状态任一停用、所属主账号停用、跨商家关系、门店所有者变化均拒绝。
+- [ ] 增加订单和商品测试:请求参数中的 `mdId` 与实体真实归属不一致时,以数据库实体的 `mdId` 为准拒绝越权。
+- [ ] 运行 `mvn -pl ruoyi-system -Dtest=MerchantStoreAccessServiceTest test`,确认服务尚不存在而失败。
+- [ ] 实现服务并使用 `ServiceException` 返回国际化键;每次调用重新读取当前账号和授权,不把 `storeIds` 缓存进 Token。
+- [ ] 复跑测试,确认授权取消后无需重新登录即可生效。
+- [ ] 暂存本任务文件并提交:`git commit -m "集中商家门店数据权限校验"`。
+
+---
+
+## Phase 2:账号生命周期、平台隔离与登录
+
+### T003 [US1][US4] 实现商家端分管账号管理接口
+
+**文件:**
+
+- 创建:`ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantSubaccountController.java`
+- 创建:`ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantSubaccountApplicationService.java`
+- 创建:`ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountCreateRequest.java`
+- 创建:`ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountUpdateRequest.java`
+- 创建:`ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountPasswordRequest.java`
+- 创建:`ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountStatusRequest.java`
+- 创建:`ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/MerchantSubaccountView.java`
+- 创建:`ruoyi-admin/src/test/java/com/ruoyi/app/user/MerchantSubaccountControllerTest.java`
+- 修改:四个 `ruoyi-admin/src/main/resources/i18n/messages_*.properties`
+
+**接口:**
+
+```text
+GET  /merchant/subaccounts
+POST /merchant/subaccounts
+PUT  /merchant/subaccounts/{subaccountUserId}
+PUT  /merchant/subaccounts/{subaccountUserId}/password
+PUT  /merchant/subaccounts/{subaccountUserId}/status
+```
+
+- [ ] 写 Controller/Service 测试,覆盖手机号全局重复、空姓名、空密码、空店铺、跨商家店铺、非主账号调用、修改他人分管账号、事务中任一授权失败整体回滚。
+- [ ] 写状态测试,断言主账号启停只修改 `subaccountStatus`,不能修改平台 `status`;密码重置和停用必须撤销 App/PC Redis 会话。
+- [ ] 运行 `mvn -pl ruoyi-admin -am -Dtest=MerchantSubaccountControllerTest -Dsurefire.failIfNoSpecifiedTests=false test`,确认 RED。
+- [ ] 实现 DTO 和接口;手机号规范化后同时写入 `user_name` 与 `phone`,固定 `user_type=5`、`audit_status=1`、`status=0`、`subaccount_status=0`,不创建钱包和商户资质。
+- [ ] 账号插入与授权批量写入使用同一事务;更新授权先验证完整店铺集合,再替换关系。
+- [ ] 四语种补齐账号重复、无权管理、店铺越权、平台停用等业务错误。
+- [ ] 复跑测试并提交:`git commit -m "新增商家分管账号管理接口"`。
+
+### T004 [US5] 实现平台下属账号视图并隔离商家管理
+
+**文件:**
+
+- 创建:`ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantSubaccountAdminController.java`
+- 创建:`ruoyi-admin/src/test/java/com/ruoyi/app/user/MerchantSubaccountAdminControllerTest.java`
+- 修改:`ruoyi-admin/src/test/java/com/ruoyi/app/user/InfoUserControllerTest.java`
+- 按需修改:`ruoyi-admin/src/main/java/com/ruoyi/app/user/InfoUserController.java`
+
+**接口:**
+
+```text
+GET /infouser/merchant-subaccounts?merchantUserId={id}
+PUT /infouser/merchant-subaccounts/{subaccountUserId}/platform-status
+```
+
+- [ ] 写平台接口测试:列表只接受类型 1 主账号;只返回其类型 5 子账号;响应不含密码、CID、Token 和商户资质。
+- [ ] 写平台状态测试:只修改 `status`,停用撤销会话;平台恢复不能覆盖 `subaccountStatus=1`。
+- [ ] 扩展平台商家列表/导出服务测试,插入或模拟类型 5 后,`userType=1` 结果和总数不包含分管账号;平台通用新增/编辑不能把账号类型写成 5。
+- [ ] 运行两个定向测试类确认 RED,再实现 `@PreAuthorize` 列表/编辑权限和类型保护。
+- [ ] 复跑测试并提交:`git commit -m "隔离平台商家与分管账号管理"`。
+
+### T005 [US1][US4][US5] 扩展商家登录、退出和会话失效
+
+**文件:**
+
+- 修改:`ruoyi-admin/src/main/java/com/ruoyi/app/user/InfoUserController.java`
+- 修改:`ruoyi-admin/src/test/java/com/ruoyi/app/user/InfoUserControllerTest.java`
+
+**行为:**
+
+```java
+// shanglodeing 在签发 token 前必须满足
+user.getStatus().equals("0");
+// 类型 5 还必须满足 subaccountStatus=0 且 owner.status=0
+```
+
+- [ ] 写测试覆盖:类型 5 正常登录;自身平台停用、主账号停用、主账号控制停用分别拒绝;类型 1 被平台停用也拒绝;失败分支不能返回成功响应或 Token。
+- [ ] 写成功测试,断言更新 CID 和 `lastLoginAt`,使用商家 App/PC token 前缀;退出只删除当前 JTI 会话。
+- [ ] 运行 `InfoUserControllerTest` 确认 RED。
+- [ ] 将 `shanglodeing` 的类型集合扩展为 1、3、4、5,并在密码验证成功、Token 签发前调用账号可用性校验;修正当前捕获异常后落入成功响应的分支。
+- [ ] 新增 `POST /infouser/user/merchantLogout`,使用 `@RequestHeader String token`,验证并删除 JTI,不记录 token。
+- [ ] 复跑测试并提交:`git commit -m "完善商家账号登录与停用校验"`。
+
+---
+
+## Phase 3:店铺、商品、订单与退款权限
+
+### T006 [US2] 收口店铺和商品权限
+
+**文件:**
+
+- 修改:`ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosStoreController.java`
+- 修改:`ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosFoodController.java`
+- 修改:`ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosFenleiController.java`
+- 修改:`ruoyi-admin/src/main/java/com/ruoyi/app/mendian/FoodSpecController.java`
+- 修改:`ruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosStoreControllerTest.java`
+- 创建:`ruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosFoodControllerAccessTest.java`
+
+**必须保护的有效商家端能力:**
+
+```text
+店铺:getmystorelist、storelistlist、getstore、addmendian、delemendian、saveMdBusinessHours、营业状态/资料修改
+商品:getidlist、getFoodPageList、setposfood、delefood、changerecommend
+分类:getfenlei、addfenlei、delefenlei
+规格:foodSpecPageList、saveFoodSpec、deleteFoodSpec、changeOpen、changeSpecValueOpen
+```
+
+- [ ] 为主账号和类型 5 分别写列表测试:主账号返回全部自有店铺,分管账号返回所有授权店铺且不需要当前店铺。
+- [ ] 写越权测试:分管账号新增/删除店铺被拒;可编辑授权店铺;伪造 `userId`、`mdId`、商品 ID、分类 ID 或规格 ID 不能访问未授权店铺。
+- [ ] 运行 `PosStoreControllerTest,PosFoodControllerAccessTest` 确认 RED。
+- [ ] 为有效写接口补 `@RequestHeader String token` 和明确 DTO/参数;授权使用目标实体真实门店,忽略请求体中的所有者字段。
+- [ ] `addmendian` 调用 `requireOwner` 并由后端写入当前主账号 ID;分管账号前端隐藏入口不能替代该校验。
+- [ ] 复跑测试并提交:`git commit -m "限制店铺商品操作到授权门店"`。
+
+### T007 [US2] 收口商家订单操作和退款权限
+
+**文件:**
+
+- 修改:`ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java`
+- 修改:`ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderController.java`
+- 修改:`ruoyi-admin/src/test/java/com/ruoyi/app/order/PosOrderShOprateControllerTest.java`
+- 按实际有效退款调用修改对应支付 Controller/Service 的授权入口,不改变支付渠道状态机。
+
+**必须保护的能力:**
+
+```text
+orderList、getstoreorderlist、订单详情、商家确认/出餐/取消、现金确认、发票查看/作废、现有退款入口
+```
+
+- [ ] 写列表测试:类型 5 按授权 `md_id IN (...)` 查询;同主商家的未授权店铺订单不返回。
+- [ ] 写操作测试:订单的 `shId` 属于主账号但 `mdId` 未授权时仍拒绝;授权门店订单继续执行现有骑手先接单、支付成功后开放和状态流转规则。
+- [ ] 写退款测试:先通过 `requireOrderAccess`,再进入现有渠道退款校验;越权请求不得创建退款记录或改变订单状态。
+- [ ] 运行 `PosOrderShOprateControllerTest` 确认 RED。
+- [ ] 用集中权限服务替换仅按当前 `userId=shId` 的判断;主账号使用 owner ID,分管账号使用授权门店集合。
+- [ ] 复跑订单定向测试及现有支付退款测试,提交:`git commit -m "限制订单退款操作到授权门店"`。
+
+---
+
+## Phase 4:通知路由
+
+### T008 [US3] 将商家通知路由到在线分管账号
+
+**文件:**
+
+- 创建:`ruoyi-admin/src/main/java/com/ruoyi/app/order/MerchantNotificationRouter.java`
+- 创建:`ruoyi-admin/src/test/java/com/ruoyi/app/order/MerchantNotificationRouterTest.java`
+- 修改:`ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java`
+- 修改:`ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderQsOprateController.java`
+- 修改:`ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderController.java`
+- 修改:`ruoyi-admin/src/main/java/com/ruoyi/app/order/DeliveryOrderNotificationService.java`
+- 修改:`ruoyi-admin/src/test/java/com/ruoyi/app/order/DeliveryOrderNotificationServiceTest.java`
+
+**产出接口:**
+
+```java
+void sendStoreNotification(Long storeId, Long ownerUserId,
+                           String titleKey, String contentKey,
+                           String body, String orderNo);
+```
+
+- [ ] 写测试覆盖:两个符合条件分管账号全部收到各自消息;主账号不重复;无人具有 App 会话/CID 时主账号兜底;平台或主账号停用的分管账号排除;相同 CID 外部推送去重。
+- [ ] 写失败隔离测试:一个接收者推送抛异常,其他账号仍写消息并尝试推送;门店为空时只向 owner 兜底。
+- [ ] 运行 `MerchantNotificationRouterTest,DeliveryOrderNotificationServiceTest` 确认 RED。
+- [ ] 实现接收者解析、Redis App 会话检查、逐账号 `PushEvent` 入库和 CID 去重;不要新增 Android FCM。
+- [ ] 将支付成功/订单开放、骑手接单、订单取消等当前有效商家推送调用替换为路由服务,并保留现有订单状态调整。
+- [ ] 复跑通知和订单生命周期测试,提交:`git commit -m "按门店分管账号路由商家通知"`。
+
+---
+
+## Phase 5:商家端与平台端
+
+### T009 [US1][US2] 增加商家端分管账号管理与角色界面
+
+**文件:**
+
+- 创建:`E:/QtwCode/foodie/foodie-store/src/api/subaccount.js`
+- 创建:`E:/QtwCode/foodie/foodie-store/src/views/MerchantSubaccount.vue`
+- 修改:`E:/QtwCode/foodie/foodie-store/src/router/index.js`
+- 修改:`E:/QtwCode/foodie/foodie-store/src/components/Aside.vue`
+- 修改:`E:/QtwCode/foodie/foodie-store/src/components/Header.vue`
+- 修改:`E:/QtwCode/foodie/foodie-store/src/views/AcidrollingCapacity.vue`
+- 修改:`E:/QtwCode/foodie/foodie-store/src/api/store.js`
+- 修改:`E:/QtwCode/foodie/foodie-store/src/api/food.js`
+- 修改:四个 `E:/QtwCode/foodie/foodie-store/src/lang/*.js`
+
+- [ ] 新增 API 封装,所有分管账号、店铺和商品写请求设置 `isToken: true`;密码沿用现有 RSA 加密工具后提交。
+- [ ] 新增管理页:列表、创建、编辑、多店选择、主账号启停和密码重置;平台停用时展示状态且主账号启用按钮不能宣称已恢复。
+- [ ] 仅类型 1 显示“分管账号”菜单;类型 5 显示订单、授权店铺和商品入口,隐藏新增/删除店铺、分管账号、账单、支付配置及其他敏感模块。
+- [ ] 店铺列表类型 5 不显示新增/删除按钮,但允许编辑授权店铺;退出登录先调用后端退出接口,再清理本地 token。
+- [ ] 在 `zh.js/tw.js/en.js/vi.js` 同一嵌套对象增加语义化英文驼峰 key,不新增 `text1` 类编号。
+- [ ] 使用 `git diff --check` 和换行检查确认相关 Vue/JS 仍为 CRLF,运行 `npm run build`。
+- [ ] 在 `foodie-store` 仓库提交:`git commit -m "增加商家分管账号管理页面"`。
+
+### T010 [US5] 在平台主商家下呈现分管账号
+
+**文件:**
+
+- 修改:`E:/QtwCode/foodie/foodie-admin-vue/src/views/infouser/user/sjuser.vue`
+- 修改:`E:/QtwCode/foodie/foodie-admin-vue/src/api/infouser/user.js`
+- 修改:四个 `E:/QtwCode/foodie/foodie-admin-vue/src/api/language/language.*.js`
+
+- [ ] 在商家行增加“分管账号”按钮,打开抽屉或对话框并传当前类型 1 主账号 ID,不新增顶级商家行或商家审核入口。
+- [ ] 展示姓名、手机号、双重状态、在线/最近登录、负责店铺和创建时间;不展示密码、CID 或资质字段。
+- [ ] 平台只提供强制停用/恢复按钮;不提供新增、密码重置和店铺授权编辑。
+- [ ] 四语种补齐文案并保持 CRLF,运行 `npm run build:prod`。
+- [ ] 在 `foodie-admin-vue` 仓库提交:`git commit -m "在主商家下展示分管账号"`。
+
+---
+
+## Phase 6:集成验证与交付
+
+### T011 全量验证、规格回填与推送
+
+**文件:**
+
+- 修改:`specs/022-merchant-store-subaccounts/tasks.md`(完成状态)
+- 按实际结果修改:`specs/022-merchant-store-subaccounts/quickstart.md`
+
+- [ ] 使用 JDK 21 运行 `quickstart.md` 中全部定向测试,记录测试数和结果。
+- [ ] 运行 `mvn -pl ruoyi-admin -am -DskipTests package`,再运行项目约定完整回归;如有失败,区分本功能回归与既有失败并记录证据。
+- [ ] 构建 `foodie-store` 和 `foodie-admin-vue`,确认四语言文件解析成功。
+- [ ] 执行手工验收:多店授权、越权拒绝、授权即时撤销、双重停用、主商家停用、通知全发、CID 去重、主账号兜底、平台商家数量不变。
+- [ ] 检查三个仓库 `git diff --check`、换行、编码和暂存清单;后端不得混入 `.claude/homunculus/observations.jsonl` 或其他原有脏文件。
+- [ ] 更新任务勾选和验证结果,后端规格/SQL/源码/测试形成中文提交;分别推送三个仓库当前分支。
+
+## 依赖关系
+
+```text
+T001 -> T002 -> T003 -> T004 -> T005
+                  |       |
+                  +------>T006 -> T007 -> T008
+                          |             |
+                          +-----------> T009
+                                        |
+T004 --------------------------------> T010
+T001..T010 --------------------------> T011
+```
+
+- T001、T002 是所有业务故事的阻塞基础。
+- T003 完成后可开发商家端页面;T004 完成后可开发平台页面。
+- T006、T007 必须先于通知路由最终接入,确保通知目标与实际门店权限一致。
+- T009 和 T010 位于不同仓库,可独立构建,但不得在未完成后端契约前联调。
+
+## 需求覆盖
+
+| 需求 | 任务 |
+|---|---|
+| FR-001、FR-002、FR-003、FR-004、FR-005、FR-006、FR-007、FR-008 | T001、T003、T005 |
+| FR-009、FR-010、FR-011、FR-012、FR-013、FR-014、FR-015、FR-016 | T002、T006、T007、T009 |
+| FR-017、FR-018、FR-019、FR-020、FR-021、FR-022、FR-023 | T008 |
+| FR-024、FR-025、FR-026、FR-027 | T007、T009、T011 |
+| FR-028、FR-029、FR-030、FR-031、FR-032、FR-033、FR-034、FR-035 | T004、T005、T010 |
+| SC-001、SC-002、SC-003、SC-004、SC-005、SC-006、SC-007、SC-008、SC-009 | T003~T011 |