## 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

无。所有设计决策已在探索阶段与业务确认。
