## Context ### 当前状态 `tb_package` 有 `shelf_status` 字段(1-上架, 2-下架),由平台控制套餐的全局可见性。`tb_shop_package_allocation` 只有 `status` 字段(1-启用, 2-禁用),没有代理独立的上下架字段。 当代理调用 `PATCH /api/admin/packages/:id/shelf` 时,service 层直接修改 `tb_package.shelf_status`,导致该操作影响全平台——所有代理和平台的该套餐都被下架。 ### 核心矛盾 "上下架"在业务上有两个层次: - **平台层**:控制平台自营店面的客户侧可见性,与代理无关 - **代理层**:每个代理独立控制自己客户侧的可见性,互不影响 目前两个层次合并成一个字段,是 Bug 的根源。 ### 约束 - `tb_shop_package_allocation.status`(启用/禁用)语义为"分配者临时暂停该分配",不等同于上下架 - 平台若要停止某套餐全网销售,应回收分配而非修改 shelf_status - `Package.status`(全局禁用)是唯一影响全平台购买的开关 ## Goals / Non-Goals **Goals:** - 为 `tb_shop_package_allocation` 新增 `shelf_status` 字段,默认上架 - `PATCH /packages/:id/shelf` 接口按调用者角色路由到不同数据层(平台→package,代理→allocation) - 代理查看套餐列表/详情时,`shelf_status` 返回自己分配记录的值 - 购买校验按购买场景分流:代理场景检查 `allocation.shelf_status`,平台场景检查 `package.shelf_status` - 修复 `UpdateAllocationStatus` 接口缺少所有者校验的安全 Bug **Non-Goals:** - 不引入级联上下架(代理A下架不影响代理B) - 不修改 `Package.status`(全局禁用)的语义和操作权限 - 不在代理层增加"启用/禁用套餐"能力(status 仍只有分配者可改) - 不变更 URL 路径(同一接口服务不同角色) ## Decisions ### 决策 1:角色上下文路由在 Service 层实现 **选择**:在 `PackageService.UpdateShelfStatus()` 中通过 `middleware.GetUserTypeFromContext(ctx)` 判断角色,路由到不同的 store 操作。 **理由**:Handler 层保持薄,不含业务逻辑。角色判断属于业务规则,放 Service 层符合分层原则。URL 不变对前端友好,无需区分调用地址。 **备选方案**:为代理新增单独的 API 路由(如 `PATCH /shop-package-allocations/:id/shelf`)。问题是代理需要知道 allocation ID 而非 package ID,增加前端复杂度,且未来其他类似接口都要新增一套路由。 ### 决策 2:默认上架 **选择**:分配给代理时,`allocation.shelf_status` 默认为 1(上架)。 **理由**:分配行为本身代表分配者希望下级销售此套餐,默认上架减少操作步骤。代理可主动下架。 ### 决策 3:购买校验按场景分流 **选择**: - 客户直接从平台购买 → 检查 `Package.status == enabled AND Package.shelf_status == on` - 客户通过代理购买 → 检查 `Package.status == enabled AND allocation(seller).shelf_status == on` **理由**:平台 shelf_status 仅代表平台自营店面状态,与代理销售解耦。代理链路只检查最终销售代理的 allocation,不向上追溯(上级若不想让下级卖,应回收分配)。 ### 决策 4:分配 status 修改加所有者校验 **选择**:`UpdateAllocationStatus` 检查调用者是否为该 allocation 的 `allocator_shop_id`(平台用户则通过,代理用户需 allocator_shop_id == 调用者 shop_id)。 **理由**:当前无校验,任意代理可修改任意分配记录的 status,是安全漏洞。 ## Risks / Trade-offs **[风险] 存量数据的 shelf_status 默认值** → 迁移时 `ALTER TABLE` 设 `DEFAULT 1 NOT NULL`,存量记录全部视为上架,符合业务现状(原来没有这个字段时代理就是在卖)。 **[风险] 购买校验需要额外查询 allocation** → 校验路径需增加一次 `GetByShopAndPackage` 查询。订单创建场景已有多个查询,增加一次影响可接受;可加索引 `(shop_id, package_id)` 优化(已存在)。 **[风险] 前端展示的 shelf_status 语义变化** → 代理端看到的 shelf_status 从 package 级变为 allocation 级,前端无需改动(字段名相同,值含义不变),但平台管理员在查看"代理管理的套餐"时无法直接看到各代理的 shelf_status,这属于 Non-Goal。 ## Migration Plan 1. 生成数据库迁移文件:`tb_shop_package_allocation` 增加 `shelf_status INT NOT NULL DEFAULT 1` 2. 执行迁移(无停机,仅 ADD COLUMN) 3. 部署新代码(接口行为自动分流) 4. 无回滚风险:新增字段,原有字段语义不变 ## Open Questions 无。所有设计决策已在探索阶段与业务确认。