feat: 新增代理分配套餐上架状态(shelf_status)功能
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m56s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m56s
- 新增数据库迁移:为 shop_package_allocation 表添加 shelf_status 字段 - 更新模型/DTO:ShopPackageAllocation 增加 ShelfStatus 字段及相关枚举 - 更新套餐分配 Service:支持上架/下架状态管理逻辑 - 更新套餐 Store/Service:根据 shelf_status 过滤可售套餐 - 更新购买验证 Service:引入上架状态校验逻辑 - 归档 OpenSpec 变更:2026-03-02-agent-allocation-shelf-status - 同步更新主规范文档:allocation-shelf-status、package-management、purchase-validation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
## 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
|
||||
|
||||
无。所有设计决策已在探索阶段与业务确认。
|
||||
Reference in New Issue
Block a user