diff --git a/openspec/changes/update-package-management-api/proposal.md b/openspec/changes/update-package-management-api/proposal.md new file mode 100644 index 0000000..634c271 --- /dev/null +++ b/openspec/changes/update-package-management-api/proposal.md @@ -0,0 +1,82 @@ +# Change: 更新套餐管理API以支持新的佣金配置模型 + +## Why + +根据最新的API文档(`docs/修改原来的套餐管理.md`),后端API规范发生了重大变更,主要涉及: + +1. **套餐系列** - 新增一次性佣金配置支持(包括固定/梯度佣金、强制充值、时效类型等复杂配置) +2. **系列分配** - 佣金配置模型完全重构,从简单的base_commission改为更细粒度的配置 +3. **单套餐分配** - 新增系列关联字段和分配者信息字段 + +当前前端实现的类型定义(`src/types/api/packageManagement.ts`)和API服务与新规范不匹配,需要更新以支持新的数据结构。 + +## What Changes + +### 1. 类型定义重构 + +**修改**: `src/types/api/packageManagement.ts` + +**套餐系列相关类型**: +- `PackageSeriesResponse` - 新增 `enable_one_time_commission` 和 `one_time_commission_config` +- `CreatePackageSeriesRequest` - 新增一次性佣金配置字段 +- `UpdatePackageSeriesRequest` - 新增一次性佣金配置字段 +- 新增 `SeriesOneTimeCommissionConfig` - 一次性佣金配置(支持固定/梯度模式) +- 新增 `OneTimeCommissionTier` - 梯度佣金档位配置 + +**系列分配相关类型**: +- **BREAKING**: `ShopSeriesAllocationResponse` - 完全重构字段结构 + - 移除: `base_commission` (BaseCommissionConfig) + - 新增: `series_code`, `enable_force_recharge`, `enable_one_time_commission`, `force_recharge_amount`, `force_recharge_trigger_type`, `one_time_commission_amount`, `one_time_commission_threshold`, `one_time_commission_trigger` +- **BREAKING**: `CreateShopSeriesAllocationRequest` - 重构请求字段 +- **BREAKING**: `UpdateShopSeriesAllocationRequest` - 重构请求字段 +- 移除不再使用的类型: `BaseCommissionConfig`, `TierEntry`, `TierCommissionConfig`, `OneTimeCommissionTierEntry`, `OneTimeCommissionConfig`(旧版) + +**单套餐分配相关类型**: +- `ShopPackageAllocationResponse` - 字段调整 + - 新增: `series_id`, `series_name`, `series_allocation_id`, `allocator_shop_id`, `allocator_shop_name` + - 移除: `allocation_id` (重命名为 `series_allocation_id`), `calculated_cost_price` +- `ShopPackageAllocationQueryParams` - 新增 `series_allocation_id`, `allocator_shop_id` 筛选参数 + +### 2. API服务层无需修改 + +现有的API服务类(`PackageSeriesService`, `ShopSeriesAllocationService`, `ShopPackageAllocationService`)的方法签名保持不变,只是底层数据类型发生变化。 + +### 3. 页面/组件适配(实施阶段处理) + +以下文件需要适配新的数据结构(本提案阶段不编写代码): +- 套餐系列管理页面 - 需支持一次性佣金配置表单 +- 系列分配页面 - 需重构佣金配置表单UI +- 单套餐分配页面 - 需展示新增的关联字段 + +## Impact + +### 受影响的规范 + +- `package-series-management` - MODIFIED (新增一次性佣金配置能力) +- `shop-series-allocation` - MODIFIED (佣金配置模型重构) +- `shop-package-allocation` - MODIFIED (新增系列关联和分配者字段) + +### 受影响的代码 + +- `src/types/api/packageManagement.ts` - **BREAKING CHANGE** 类型定义重构 +- 所有使用 `ShopSeriesAllocationResponse` 的页面/组件 - 需适配新字段结构 +- 所有使用 `PackageSeriesResponse` 的页面/组件 - 需处理新增的一次性佣金配置 + +### 依赖关系 + +- 后端API已按新规范实现(`docs/修改原来的套餐管理.md`) +- 前端需要先完成类型定义更新,再更新UI组件 + +### 风险评估 + +- **高风险**: 这是一个BREAKING CHANGE,会影响所有使用系列分配的功能 +- **兼容性**: 需要确保后端API已更新,否则会导致运行时错误 +- **迁移成本**: 现有页面中的表单组件需要重写以支持新的数据结构 +- **测试需求**: 需要全面测试套餐系列、系列分配、单套餐分配的所有CRUD操作 + +### 建议迁移策略 + +1. 先更新类型定义,确保TypeScript编译通过 +2. 逐个页面/组件适配新数据结构 +3. 与后端联调确认新API规范工作正常 +4. 完成后删除旧的、不再使用的类型定义 diff --git a/openspec/changes/update-package-management-api/specs/package-series-management/spec.md b/openspec/changes/update-package-management-api/specs/package-series-management/spec.md new file mode 100644 index 0000000..f6199bc --- /dev/null +++ b/openspec/changes/update-package-management-api/specs/package-series-management/spec.md @@ -0,0 +1,140 @@ +# Package Series Management Specification Delta + +## MODIFIED Requirements + +### Requirement: Package Series Data Model + +套餐系列的数据模型SHALL支持一次性佣金配置,包括固定佣金、梯度佣金、强制充值、时效类型等高级功能。 + +**字段定义**: +- `id` (number) - 系列ID +- `series_code` (string) - 系列编码,唯一标识 +- `series_name` (string) - 系列名称 +- `description` (string, optional) - 系列描述 +- `enable_one_time_commission` (boolean) - 是否启用一次性佣金 +- `one_time_commission_config` (SeriesOneTimeCommissionConfig, optional) - 一次性佣金配置对象 +- `status` (number) - 状态 (1:启用, 2:禁用) +- `created_at` (string) - 创建时间 +- `updated_at` (string) - 更新时间 + +**一次性佣金配置结构** (`SeriesOneTimeCommissionConfig`): +- `enable` (boolean) - 是否启用一次性佣金 +- `commission_type` (string) - 佣金类型: "fixed"(固定) | "tiered"(梯度) +- `commission_amount` (number, optional) - 固定佣金金额(分),commission_type=fixed时使用 +- `threshold` (number) - 触发阈值(分) +- `trigger_type` (string) - 触发类型: "first_recharge"(首次充值) | "accumulated_recharge"(累计充值) +- `tiers` (OneTimeCommissionTier[], optional) - 梯度配置列表,commission_type=tiered时使用 +- `enable_force_recharge` (boolean) - 是否启用强充 +- `force_amount` (number, optional) - 强充金额(分) +- `force_calc_type` (string, optional) - 强充计算类型: "fixed"(固定) | "dynamic"(动态) +- `validity_type` (string) - 时效类型: "permanent"(永久) | "fixed_date"(固定日期) | "relative"(相对时长) +- `validity_value` (string, optional) - 时效值(日期字符串或月数) + +**梯度档位结构** (`OneTimeCommissionTier`): +- `threshold` (number) - 达标阈值 +- `dimension` (string) - 统计维度: "sales_count"(销量) | "sales_amount"(销售额) +- `amount` (number) - 佣金金额(分) +- `stat_scope` (string, optional) - 统计范围: "self"(仅自己) | "self_and_sub"(自己+下级) + +#### Scenario: 创建套餐系列并启用固定一次性佣金 + +- **GIVEN** 管理员登录系统 +- **WHEN** 创建套餐系列时提供以下配置: + - `series_code`: "SERIES001" + - `series_name`: "标准流量系列" + - `enable_one_time_commission`: true + - `one_time_commission_config`: + - `enable`: true + - `commission_type`: "fixed" + - `commission_amount`: 5000 (50元) + - `threshold`: 10000 (100元) + - `trigger_type`: "first_recharge" + - `validity_type`: "permanent" +- **THEN** 系统SHALL创建套餐系列,并保存一次性佣金配置 + +#### Scenario: 创建套餐系列并启用梯度一次性佣金 + +- **GIVEN** 管理员登录系统 +- **WHEN** 创建套餐系列时提供以下配置: + - `series_code`: "SERIES002" + - `series_name`: "高级流量系列" + - `enable_one_time_commission`: true + - `one_time_commission_config`: + - `enable`: true + - `commission_type`: "tiered" + - `threshold`: 5000 + - `trigger_type`: "accumulated_recharge" + - `tiers`: [ + { threshold: 10, dimension: "sales_count", amount: 3000 }, + { threshold: 50, dimension: "sales_count", amount: 8000 } + ] + - `validity_type`: "relative" + - `validity_value`: "12" (12个月) +- **THEN** 系统SHALL创建套餐系列,并保存梯度佣金配置 + +#### Scenario: 查询启用一次性佣金的套餐系列 + +- **GIVEN** 系统中存在多个套餐系列,部分启用了一次性佣金 +- **WHEN** 使用查询参数 `enable_one_time_commission=true` 查询列表 +- **THEN** 系统SHALL返回所有启用一次性佣金的套餐系列 + +#### Scenario: 更新套餐系列的一次性佣金配置 + +- **GIVEN** 系统中存在ID为1的套餐系列 +- **WHEN** 更新该系列时提供新的一次性佣金配置: + - `one_time_commission_config.commission_amount`: 8000 (从5000更新到8000) + - `one_time_commission_config.enable_force_recharge`: true + - `one_time_commission_config.force_amount`: 20000 + - `one_time_commission_config.force_calc_type`: "fixed" +- **THEN** 系统SHALL更新套餐系列的一次性佣金配置,并返回更新后的数据 + +#### Scenario: 获取套餐系列详情时包含完整的一次性佣金配置 + +- **GIVEN** 系统中存在ID为1的套餐系列,已配置一次性佣金 +- **WHEN** 请求获取该系列的详情 (GET /api/admin/package-series/1) +- **THEN** 系统SHALL返回包含完整 `one_time_commission_config` 对象的响应数据 + +## ADDED Requirements + +### Requirement: One-Time Commission Query Filter + +系统SHALL支持按一次性佣金启用状态筛选套餐系列列表。 + +#### Scenario: 按一次性佣金启用状态筛选 + +- **GIVEN** 系统中存在多个套餐系列 +- **WHEN** 使用查询参数 `enable_one_time_commission=true` +- **THEN** 系统SHALL只返回 `enable_one_time_commission` 为 true 的套餐系列 +- **AND** 分页信息正确反映筛选后的结果数量 + +### Requirement: One-Time Commission Configuration Validation + +系统SHALL验证一次性佣金配置的完整性和逻辑一致性。 + +#### Scenario: 固定佣金模式的验证 + +- **GIVEN** 管理员创建套餐系列 +- **WHEN** `commission_type` 为 "fixed" +- **THEN** 系统SHALL要求 `commission_amount` 字段必填 +- **AND** `tiers` 字段不应被使用 + +#### Scenario: 梯度佣金模式的验证 + +- **GIVEN** 管理员创建套餐系列 +- **WHEN** `commission_type` 为 "tiered" +- **THEN** 系统SHALL要求 `tiers` 数组不为空 +- **AND** 每个梯度档位的 `threshold`, `dimension`, `amount` 字段必填 + +#### Scenario: 强制充值配置的验证 + +- **GIVEN** 管理员创建套餐系列 +- **WHEN** `enable_force_recharge` 为 true +- **THEN** 系统SHALL要求 `force_amount` 和 `force_calc_type` 字段必填 + +#### Scenario: 时效配置的验证 + +- **GIVEN** 管理员创建套餐系列 +- **WHEN** `validity_type` 为 "fixed_date" +- **THEN** 系统SHALL要求 `validity_value` 字段必填,且格式为有效的日期字符串 +- **WHEN** `validity_type` 为 "relative" +- **THEN** 系统SHALL要求 `validity_value` 字段必填,且为表示月数的数字字符串 diff --git a/openspec/changes/update-package-management-api/specs/shop-package-allocation/spec.md b/openspec/changes/update-package-management-api/specs/shop-package-allocation/spec.md new file mode 100644 index 0000000..7e251b8 --- /dev/null +++ b/openspec/changes/update-package-management-api/specs/shop-package-allocation/spec.md @@ -0,0 +1,158 @@ +# Shop Package Allocation Specification Delta + +## MODIFIED Requirements + +### Requirement: Shop Package Allocation Data Model + +单套餐分配的数据模型SHALL新增系列关联字段和分配者信息字段,以支持更完整的分配关系追溯。 + +**字段定义**: +- `id` (number) - 分配ID +- `package_id` (number) - 套餐ID +- `package_code` (string) - 套餐编码 +- `package_name` (string) - 套餐名称 +- `series_id` (number) - 套餐系列ID +- `series_name` (string) - 套餐系列名称 +- `shop_id` (number) - 被分配的店铺ID +- `shop_name` (string) - 被分配的店铺名称 +- `allocator_shop_id` (number) - 分配者店铺ID,0表示平台分配 +- `allocator_shop_name` (string) - 分配者店铺名称 +- `series_allocation_id` (number, nullable) - 关联的系列分配ID(可选) +- `cost_price` (number) - 该代理的成本价(分) +- `status` (number) - 状态 (1:启用, 2:禁用) +- `created_at` (string) - 创建时间 +- `updated_at` (string) - 更新时间 + +**查询参数** (`ShopPackageAllocationQueryParams`): +- `page` (number, optional) - 页码 +- `page_size` (number, optional) - 每页数量 +- `shop_id` (number, optional) - 被分配的店铺ID +- `package_id` (number, optional) - 套餐ID +- `series_allocation_id` (number, optional) - 系列分配ID +- `allocator_shop_id` (number, optional) - 分配者店铺ID +- `status` (number, optional) - 状态 + +#### Scenario: 创建单套餐分配时包含系列信息 + +- **GIVEN** 平台管理员登录系统 +- **AND** 系统中存在ID为10的套餐,该套餐属于系列ID为2,系列名称为"标准流量系列" +- **AND** 系统中存在ID为100的店铺 +- **WHEN** 创建单套餐分配: + - `package_id`: 10 + - `shop_id`: 100 + - `cost_price`: 15000 +- **THEN** 系统SHALL创建单套餐分配记录 +- **AND** 响应数据中自动填充 `series_id` 为 2 +- **AND** 响应数据中自动填充 `series_name` 为 "标准流量系列" +- **AND** 响应数据中 `allocator_shop_id` 为 0(平台分配) + +#### Scenario: 创建单套餐分配并关联系列分配 + +- **GIVEN** 系统中存在ID为20的系列分配,关联系列ID为2,店铺ID为100 +- **AND** 系列2下存在套餐ID为10 +- **WHEN** 为店铺100创建套餐10的分配 +- **THEN** 系统MAY自动关联系列分配,设置 `series_allocation_id` 为 20 + +#### Scenario: 查询单套餐分配列表并显示完整关联信息 + +- **GIVEN** 系统中存在多个单套餐分配 +- **WHEN** 查询单套餐分配列表 (GET /api/admin/shop-package-allocations) +- **THEN** 系统SHALL返回所有分配记录 +- **AND** 每条记录包含 `series_id`, `series_name`, `allocator_shop_id`, `allocator_shop_name` +- **AND** 如果存在关联的系列分配,`series_allocation_id` 不为空 + +#### Scenario: 按系列分配ID筛选单套餐分配 + +- **GIVEN** 系统中存在多个单套餐分配,部分关联了系列分配ID为20的系列分配 +- **WHEN** 使用查询参数 `series_allocation_id=20` +- **THEN** 系统SHALL返回所有 `series_allocation_id` 为 20 的单套餐分配记录 + +#### Scenario: 按分配者店铺ID筛选单套餐分配 + +- **GIVEN** 系统中存在多个单套餐分配 +- **WHEN** 使用查询参数 `allocator_shop_id=50` +- **THEN** 系统SHALL返回所有由店铺ID为50的代理商创建的单套餐分配记录 + +#### Scenario: 获取单套餐分配详情时显示完整信息 + +- **GIVEN** 系统中存在ID为100的单套餐分配 +- **WHEN** 请求获取该分配的详情 (GET /api/admin/shop-package-allocations/100) +- **THEN** 响应数据SHALL包含所有字段,包括: + - `series_id`, `series_name` - 套餐所属系列信息 + - `allocator_shop_id`, `allocator_shop_name` - 分配者信息 + - `series_allocation_id` - 关联的系列分配ID(如果存在) + +## RENAMED Requirements + +- FROM: `allocation_id` +- TO: `series_allocation_id` + +**说明**: 将字段名从 `allocation_id` 重命名为 `series_allocation_id`,使其语义更加明确,表示关联的系列分配ID。 + +## REMOVED Requirements + +### Requirement: Calculated Cost Price Field + +旧版的 `calculated_cost_price` 字段已被移除。 + +**Reason**: 该字段仅用于参考,增加了数据模型的复杂性,且前端很少使用。成本价信息应直接使用 `cost_price` 字段。 + +**Migration**: 移除所有使用 `calculated_cost_price` 字段的代码。如果需要查看原始成本价信息,可以通过关联的系列分配或套餐系列获取。 + +## ADDED Requirements + +### Requirement: Series Information in Package Allocation + +系统SHALL在单套餐分配响应数据中包含套餐所属系列的ID和名称。 + +#### Scenario: 创建分配时自动关联系列信息 + +- **GIVEN** 系统中存在套餐,且该套餐属于某个系列 +- **WHEN** 创建单套餐分配 +- **THEN** 系统SHALL自动从套餐数据中获取 `series_id` 和 `series_name` +- **AND** 填充到分配响应数据中 + +### Requirement: Allocator Information Tracking in Package Allocation + +系统SHALL记录单套餐分配的创建者(分配者)信息,包括分配者店铺ID和名称。 + +#### Scenario: 平台创建的单套餐分配标记为平台分配 + +- **GIVEN** 平台管理员登录系统 +- **WHEN** 创建单套餐分配 +- **THEN** 系统SHALL自动设置 `allocator_shop_id` 为 0 +- **AND** `allocator_shop_name` 为 "平台" 或相应的平台标识 + +#### Scenario: 代理商创建的单套餐分配记录代理商信息 + +- **GIVEN** 代理商A登录系统,shop_id为50,shop_name为"代理商A" +- **WHEN** 创建单套餐分配给下级代理 +- **THEN** 系统SHALL自动设置 `allocator_shop_id` 为 50 +- **AND** `allocator_shop_name` 为 "代理商A" + +### Requirement: Query by Allocator Shop ID + +系统SHALL支持按分配者店铺ID筛选单套餐分配列表。 + +#### Scenario: 查看平台创建的所有分配 + +- **GIVEN** 系统中存在多个单套餐分配 +- **WHEN** 使用查询参数 `allocator_shop_id=0` +- **THEN** 系统SHALL返回所有由平台创建的单套餐分配记录 + +#### Scenario: 查看特定代理商创建的所有分配 + +- **GIVEN** 系统中存在多个单套餐分配 +- **WHEN** 使用查询参数 `allocator_shop_id=50` +- **THEN** 系统SHALL返回所有由店铺ID为50的代理商创建的单套餐分配记录 + +### Requirement: Query by Series Allocation ID + +系统SHALL支持按系列分配ID筛选单套餐分配列表,以便查找由特定系列分配派生的所有单套餐分配。 + +#### Scenario: 查找系列分配的所有派生单套餐分配 + +- **GIVEN** 系统中存在系列分配ID为20的系列分配 +- **AND** 该系列分配下有多个单套餐分配 +- **WHEN** 使用查询参数 `series_allocation_id=20` +- **THEN** 系统SHALL返回所有关联该系列分配的单套餐分配记录 diff --git a/openspec/changes/update-package-management-api/specs/shop-series-allocation/spec.md b/openspec/changes/update-package-management-api/specs/shop-series-allocation/spec.md new file mode 100644 index 0000000..4cb2d3f --- /dev/null +++ b/openspec/changes/update-package-management-api/specs/shop-series-allocation/spec.md @@ -0,0 +1,162 @@ +# Shop Series Allocation Specification Delta + +## MODIFIED Requirements + +### Requirement: Shop Series Allocation Data Model + +套餐系列分配的数据模型SHALL重构为更细粒度的佣金配置结构,移除旧的 `base_commission` 模式,采用独立的一次性佣金和强制充值配置字段。 + +**字段定义**: +- `id` (number) - 分配ID +- `series_id` (number) - 套餐系列ID +- `series_name` (string) - 套餐系列名称 +- `series_code` (string) - 套餐系列编码 +- `shop_id` (number) - 被分配的店铺ID +- `shop_name` (string) - 被分配的店铺名称 +- `allocator_shop_id` (number) - 分配者店铺ID,0表示平台分配 +- `allocator_shop_name` (string) - 分配者店铺名称 +- `enable_one_time_commission` (boolean) - 是否启用一次性佣金 +- `one_time_commission_amount` (number) - 该代理能拿的一次性佣金金额上限(分) +- `one_time_commission_threshold` (number, optional) - 一次性佣金触发阈值(分) +- `one_time_commission_trigger` (string, optional) - 一次性佣金触发类型: "first_recharge" | "accumulated_recharge" +- `enable_force_recharge` (boolean) - 是否启用强制充值 +- `force_recharge_amount` (number, optional) - 强制充值金额(分) +- `force_recharge_trigger_type` (number, optional) - 强充触发类型: 1(单次充值) | 2(累计充值) +- `status` (number) - 状态 (1:启用, 2:禁用) +- `created_at` (string) - 创建时间 +- `updated_at` (string) - 更新时间 + +**创建请求字段** (`CreateShopSeriesAllocationRequest`): +- `series_id` (number, required) - 套餐系列ID +- `shop_id` (number, required) - 被分配的店铺ID +- `one_time_commission_amount` (number, required) - 一次性佣金金额上限(分) +- `enable_one_time_commission` (boolean, optional) - 是否启用一次性佣金 +- `one_time_commission_threshold` (number, optional) - 一次性佣金触发阈值(分) +- `one_time_commission_trigger` (string, optional) - 一次性佣金触发类型 +- `enable_force_recharge` (boolean, optional) - 是否启用强制充值 +- `force_recharge_amount` (number, optional) - 强制充值金额(分) +- `force_recharge_trigger_type` (number, optional) - 强充触发类型 + +**更新请求字段** (`UpdateShopSeriesAllocationRequest`): +- 所有字段均为可选,允许部分更新 +- `enable_one_time_commission` (boolean, optional) +- `one_time_commission_amount` (number, optional) +- `one_time_commission_threshold` (number, optional) +- `one_time_commission_trigger` (string, optional) +- `enable_force_recharge` (boolean, optional) +- `force_recharge_amount` (number, optional) +- `force_recharge_trigger_type` (number, optional) +- `status` (number, optional) + +#### Scenario: 创建系列分配并启用一次性佣金 + +- **GIVEN** 平台管理员登录系统 +- **AND** 系统中存在ID为1的套餐系列和ID为100的店铺 +- **WHEN** 创建系列分配时提供以下配置: + - `series_id`: 1 + - `shop_id`: 100 + - `one_time_commission_amount`: 5000 + - `enable_one_time_commission`: true + - `one_time_commission_threshold`: 10000 + - `one_time_commission_trigger`: "first_recharge" +- **THEN** 系统SHALL创建系列分配记录 +- **AND** 响应数据中 `allocator_shop_id` 为 0(平台分配) +- **AND** 响应数据中包含所有一次性佣金配置字段 + +#### Scenario: 创建系列分配并启用强制充值 + +- **GIVEN** 代理商A登录系统,shop_id为50 +- **AND** 系统中存在ID为2的套餐系列和ID为101的下级代理店铺 +- **WHEN** 创建系列分配时提供以下配置: + - `series_id`: 2 + - `shop_id`: 101 + - `one_time_commission_amount`: 3000 + - `enable_force_recharge`: true + - `force_recharge_amount`: 15000 + - `force_recharge_trigger_type`: 1 (单次充值) +- **THEN** 系统SHALL创建系列分配记录 +- **AND** 响应数据中 `allocator_shop_id` 为 50(代理商A) +- **AND** 响应数据中包含所有强制充值配置字段 + +#### Scenario: 更新系列分配的一次性佣金配置 + +- **GIVEN** 系统中存在ID为10的系列分配 +- **WHEN** 更新该分配时提供以下字段: + - `one_time_commission_amount`: 8000 (从5000更新到8000) + - `one_time_commission_threshold`: 20000 +- **THEN** 系统SHALL更新指定字段 +- **AND** 其他字段保持不变 +- **AND** 响应数据中 `updated_at` 字段更新为当前时间 + +#### Scenario: 更新系列分配的强制充值配置 + +- **GIVEN** 系统中存在ID为10的系列分配 +- **WHEN** 更新该分配时提供以下字段: + - `enable_force_recharge`: true + - `force_recharge_amount`: 25000 + - `force_recharge_trigger_type`: 2 (累计充值) +- **THEN** 系统SHALL更新强制充值配置 +- **AND** 响应数据中包含更新后的强制充值字段 + +#### Scenario: 查询系列分配列表并显示分配者信息 + +- **GIVEN** 系统中存在多个系列分配,部分由平台创建,部分由代理商创建 +- **WHEN** 查询系列分配列表 (GET /api/admin/shop-series-allocations) +- **THEN** 系统SHALL返回所有分配记录 +- **AND** 每条记录包含 `allocator_shop_id` 和 `allocator_shop_name` +- **AND** 平台创建的分配记录中 `allocator_shop_id` 为 0 + +#### Scenario: 按分配者店铺ID筛选系列分配 + +- **GIVEN** 系统中存在多个系列分配 +- **WHEN** 使用查询参数 `allocator_shop_id=50` +- **THEN** 系统SHALL返回所有由店铺ID为50的代理商创建的分配记录 + +## REMOVED Requirements + +### Requirement: Base Commission Configuration + +旧版的 `base_commission` 配置模式已被移除,不再使用 `BaseCommissionConfig` 类型。 + +**Reason**: 新的佣金配置模型更加灵活和细粒度,将一次性佣金配置分散到独立字段中,便于查询和管理。 + +**Migration**: 现有使用 `base_commission` 字段的代码需要迁移到新的字段结构。如果有历史数据,需要后端提供数据迁移方案。 + +### Requirement: Tiered Commission Configuration + +旧版的 `TierCommissionConfig` (周期性梯度返佣) 配置已被移除。 + +**Reason**: 系列分配层面不再支持复杂的梯度返佣配置,改为在系列级别统一配置一次性佣金梯度。 + +**Migration**: 梯度佣金配置现在在套餐系列级别 (`PackageSeries.one_time_commission_config.tiers`) 进行管理。 + +## ADDED Requirements + +### Requirement: Allocator Information Tracking + +系统SHALL记录系列分配的创建者(分配者)信息,包括分配者店铺ID和名称。 + +#### Scenario: 平台创建的分配标记为平台分配 + +- **GIVEN** 平台管理员登录系统 +- **WHEN** 创建系列分配 +- **THEN** 系统SHALL自动设置 `allocator_shop_id` 为 0 +- **AND** `allocator_shop_name` 为 "平台" 或相应的平台标识 + +#### Scenario: 代理商创建的分配记录代理商信息 + +- **GIVEN** 代理商A登录系统,shop_id为50,shop_name为"代理商A" +- **WHEN** 创建系列分配给下级代理 +- **THEN** 系统SHALL自动设置 `allocator_shop_id` 为 50 +- **AND** `allocator_shop_name` 为 "代理商A" + +### Requirement: Series Code in Allocation Response + +系统SHALL在系列分配响应数据中包含套餐系列编码 (`series_code`)。 + +#### Scenario: 获取系列分配详情时包含系列编码 + +- **GIVEN** 系统中存在ID为10的系列分配,关联系列编码为"SERIES001" +- **WHEN** 请求获取该分配的详情 (GET /api/admin/shop-series-allocations/10) +- **THEN** 响应数据中SHALL包含 `series_code` 字段 +- **AND** 其值为 "SERIES001" diff --git a/openspec/changes/update-package-management-api/tasks.md b/openspec/changes/update-package-management-api/tasks.md new file mode 100644 index 0000000..3a7eb0a --- /dev/null +++ b/openspec/changes/update-package-management-api/tasks.md @@ -0,0 +1,62 @@ +# Implementation Tasks + +## 1. 类型定义更新 + +- [ ] 1.1 更新 `PackageSeriesResponse` - 新增一次性佣金字段 +- [ ] 1.2 新增 `SeriesOneTimeCommissionConfig` 类型定义 +- [ ] 1.3 新增 `OneTimeCommissionTier` 类型定义 +- [ ] 1.4 更新 `CreatePackageSeriesRequest` - 新增一次性佣金配置参数 +- [ ] 1.5 更新 `UpdatePackageSeriesRequest` - 新增一次性佣金配置参数 +- [ ] 1.6 更新 `PackageSeriesQueryParams` - 新增 `enable_one_time_commission` 筛选参数 +- [ ] 1.7 重构 `ShopSeriesAllocationResponse` - 替换为新的字段结构 +- [ ] 1.8 重构 `CreateShopSeriesAllocationRequest` - 更新为新的请求结构 +- [ ] 1.9 重构 `UpdateShopSeriesAllocationRequest` - 更新为新的请求结构 +- [ ] 1.10 更新 `ShopPackageAllocationResponse` - 新增系列关联和分配者字段 +- [ ] 1.11 更新 `ShopPackageAllocationQueryParams` - 新增筛选参数 +- [ ] 1.12 移除废弃的类型定义 (`BaseCommissionConfig`, 旧版 `OneTimeCommissionConfig` 等) + +## 2. 套餐系列管理页面适配 + +- [ ] 2.1 更新套餐系列列表 - 显示一次性佣金启用状态 +- [ ] 2.2 更新套餐系列表单 - 新增一次性佣金配置区块 +- [ ] 2.3 实现一次性佣金配置表单组件 (固定模式) +- [ ] 2.4 实现一次性佣金配置表单组件 (梯度模式) +- [ ] 2.5 实现强制充值配置表单组件 +- [ ] 2.6 实现时效配置表单组件 (永久/固定日期/相对时长) +- [ ] 2.7 添加一次性佣金配置的表单验证逻辑 +- [ ] 2.8 更新套餐系列详情展示 - 显示完整的一次性佣金配置 + +## 3. 系列分配管理页面适配 + +- [ ] 3.1 更新系列分配列表 - 适配新的响应字段结构 +- [ ] 3.2 移除基础返佣配置表单(旧版) +- [ ] 3.3 实现新的系列分配表单 - 强制充值配置 +- [ ] 3.4 实现新的系列分配表单 - 一次性佣金配置 +- [ ] 3.5 更新系列分配详情展示 - 显示所有新字段 +- [ ] 3.6 更新系列分配编辑表单 - 支持修改所有可选字段 +- [ ] 3.7 添加表单验证逻辑 - 确保必填字段和逻辑一致性 + +## 4. 单套餐分配页面适配 + +- [ ] 4.1 更新单套餐分配列表 - 显示系列信息和分配者信息 +- [ ] 4.2 更新列表筛选条件 - 新增系列分配ID和分配者店铺ID筛选 +- [ ] 4.3 更新单套餐分配详情展示 - 显示所有新字段 +- [ ] 4.4 移除列表中的"计算成本价"字段显示(已废弃) + +## 5. 测试与验证 + +- [ ] 5.1 测试套餐系列的创建 - 包含一次性佣金配置(固定模式) +- [ ] 5.2 测试套餐系列的创建 - 包含一次性佣金配置(梯度模式) +- [ ] 5.3 测试套餐系列的编辑 - 修改一次性佣金配置 +- [ ] 5.4 测试系列分配的创建 - 使用新的字段结构 +- [ ] 5.5 测试系列分配的编辑 - 修改强制充值和一次性佣金参数 +- [ ] 5.6 测试单套餐分配 - 验证系列关联字段正确显示 +- [ ] 5.7 测试单套餐分配筛选 - 验证新增筛选参数工作正常 +- [ ] 5.8 回归测试 - 确保所有现有功能仍然正常工作 + +## 6. 文档与清理 + +- [ ] 6.1 更新类型定义文件的注释 - 标注新增/修改的字段 +- [ ] 6.2 删除废弃的类型定义和相关代码 +- [ ] 6.3 更新相关组件的注释文档 +- [ ] 6.4 记录API变更影响和迁移说明(如需要) diff --git a/src/router/routes/asyncRoutes.ts b/src/router/routes/asyncRoutes.ts index 17252e7..383a89f 100644 --- a/src/router/routes/asyncRoutes.ts +++ b/src/router/routes/asyncRoutes.ts @@ -611,89 +611,98 @@ export const asyncRoutes: AppRouteRecord[] = [ // } // }, // 物联网卡管理系统模块 - // { - // path: '/card-management', - // name: 'CardManagement', - // component: RoutesAlias.Home, - // meta: { - // title: 'menus.cardManagement.title', - // icon: '' - // }, - // children: [ - // // { - // // path: 'card-detail', - // // name: 'CardDetail', - // // component: RoutesAlias.CardDetail, - // // meta: { - // // title: 'menus.cardManagement.cardDetail', - // // keepAlive: true - // // } - // // }, - // { - // path: 'card-assign', - // name: 'CardAssign', - // component: RoutesAlias.CardAssign, - // meta: { - // title: 'menus.cardManagement.cardAssign', - // keepAlive: true - // } - // }, - // { - // path: 'card-shutdown', - // name: 'CardShutdown', - // component: RoutesAlias.CardShutdown, - // meta: { - // title: 'menus.cardManagement.cardShutdown', - // keepAlive: true - // } - // }, - // { - // path: 'my-cards', - // name: 'MyCards', - // component: RoutesAlias.MyCards, - // meta: { - // title: 'menus.cardManagement.myCards', - // keepAlive: true - // } - // }, - // { - // path: 'card-transfer', - // name: 'CardTransfer', - // component: RoutesAlias.CardTransfer, - // meta: { - // title: 'menus.cardManagement.cardTransfer', - // keepAlive: true - // } - // }, - // { - // path: 'card-replacement', - // name: 'CardReplacement', - // component: RoutesAlias.CardReplacement, - // meta: { - // title: 'menus.cardManagement.cardReplacement', - // keepAlive: true - // } - // }, - // { - // path: 'package-gift', - // name: 'PackageGift', - // component: RoutesAlias.PackageGift, - // meta: { - // title: 'menus.cardManagement.packageGift', - // keepAlive: true - // } - // }, - // { - // path: 'card-change-card', - // name: 'CardChangeCard', - // component: RoutesAlias.CardChangeCard, - // meta: { - // title: 'menus.cardManagement.cardChange', - // keepAlive: true - // } - // } - // ] - // }, + { + path: '/card-management', + name: 'CardManagement', + component: RoutesAlias.Home, + meta: { + title: 'menus.cardManagement.title', + icon: '' + }, + children: [ + // { + // path: 'card-detail', + // name: 'CardDetail', + // component: RoutesAlias.CardDetail, + // meta: { + // title: 'menus.cardManagement.cardDetail', + // keepAlive: true + // } + // }, + { + path: 'single-card', + name: 'SingleCard', + component: RoutesAlias.SingleCard, + meta: { + title: 'menus.cardManagement.singleCard', + keepAlive: true + } + }, + // { + // path: 'card-assign', + // name: 'CardAssign', + // component: RoutesAlias.CardAssign, + // meta: { + // title: 'menus.cardManagement.cardAssign', + // keepAlive: true + // } + // }, + // { + // path: 'card-shutdown', + // name: 'CardShutdown', + // component: RoutesAlias.CardShutdown, + // meta: { + // title: 'menus.cardManagement.cardShutdown', + // keepAlive: true + // } + // }, + // { + // path: 'my-cards', + // name: 'MyCards', + // component: RoutesAlias.MyCards, + // meta: { + // title: 'menus.cardManagement.myCards', + // keepAlive: true + // } + // }, + // { + // path: 'card-transfer', + // name: 'CardTransfer', + // component: RoutesAlias.CardTransfer, + // meta: { + // title: 'menus.cardManagement.cardTransfer', + // keepAlive: true + // } + // }, + // { + // path: 'card-replacement', + // name: 'CardReplacement', + // component: RoutesAlias.CardReplacement, + // meta: { + // title: 'menus.cardManagement.cardReplacement', + // keepAlive: true + // } + // }, + // { + // path: 'package-gift', + // name: 'PackageGift', + // component: RoutesAlias.PackageGift, + // meta: { + // title: 'menus.cardManagement.packageGift', + // keepAlive: true + // } + // }, + // { + // path: 'card-change-card', + // name: 'CardChangeCard', + // component: RoutesAlias.CardChangeCard, + // meta: { + // title: 'menus.cardManagement.cardChange', + // keepAlive: true + // } + // } + ] + }, { path: '/package-management', name: 'PackageManagement', diff --git a/src/types/api/card.ts b/src/types/api/card.ts index c5a4790..8cb2094 100644 --- a/src/types/api/card.ts +++ b/src/types/api/card.ts @@ -349,6 +349,7 @@ export interface StandaloneIotCard { created_at: string // 创建时间 updated_at: string // 更新时间 series_id?: number | null // 套餐系列ID + series_name?: string // 套餐系列名称 } // ========== 单卡批量分配和回收相关 ========== diff --git a/src/types/api/packageManagement.ts b/src/types/api/packageManagement.ts index 87deeaf..4b7d8cb 100644 --- a/src/types/api/packageManagement.ts +++ b/src/types/api/packageManagement.ts @@ -7,6 +7,33 @@ import { PaginationParams } from './common' // ==================== 套餐系列管理 ==================== +/** + * 一次性佣金梯度档位配置(套餐系列级别) + */ +export interface OneTimeCommissionTier { + threshold: number // 达标阈值 + dimension: 'sales_count' | 'sales_amount' // 统计维度:销量或销售额 + amount: number // 佣金金额(分) + stat_scope?: 'self' | 'self_and_sub' // 统计范围:仅自己或自己+下级 +} + +/** + * 套餐系列一次性佣金配置 + */ +export interface SeriesOneTimeCommissionConfig { + enable: boolean // 是否启用一次性佣金 + commission_type: 'fixed' | 'tiered' // 佣金类型:固定或梯度 + commission_amount?: number // 固定佣金金额(分),commission_type=fixed时使用 + threshold: number // 触发阈值(分) + trigger_type: 'first_recharge' | 'accumulated_recharge' // 触发类型:首次充值或累计充值 + tiers?: OneTimeCommissionTier[] | null // 梯度配置列表,commission_type=tiered时使用 + enable_force_recharge: boolean // 是否启用强充 + force_amount?: number // 强充金额(分) + force_calc_type?: 'fixed' | 'dynamic' // 强充计算类型:固定或动态 + validity_type: 'permanent' | 'fixed_date' | 'relative' // 时效类型:永久、固定日期或相对时长 + validity_value?: string // 时效值(日期字符串或月数) +} + /** * 套餐系列响应 */ @@ -15,6 +42,8 @@ export interface PackageSeriesResponse { series_code: string series_name: string description?: string + enable_one_time_commission: boolean // 是否启用一次性佣金 + one_time_commission_config?: SeriesOneTimeCommissionConfig // 一次性佣金配置 status: number // 1:启用, 2:禁用 created_at: string updated_at: string @@ -26,6 +55,7 @@ export interface PackageSeriesResponse { export interface PackageSeriesQueryParams extends PaginationParams { series_name?: string // 系列名称(模糊搜索) status?: number // 状态筛选 + enable_one_time_commission?: boolean // 是否启用一次性佣金筛选 } /** @@ -35,15 +65,16 @@ export interface CreatePackageSeriesRequest { series_code: string // 系列编码,必填 series_name: string // 系列名称,必填 description?: string // 描述,可选 + one_time_commission_config?: SeriesOneTimeCommissionConfig // 一次性佣金配置,可选 } /** * 更新套餐系列请求 */ export interface UpdatePackageSeriesRequest { - series_code?: string series_name?: string description?: string + one_time_commission_config?: SeriesOneTimeCommissionConfig // 一次性佣金配置,可选 } /** @@ -70,6 +101,9 @@ export interface PackageResponse { virtual_data_mb: number // 虚流量额度(MB) duration_months: number // 有效期(月) price: number // 价格(分) + cost_price: number // 成本价(分) + suggested_retail_price: number // 建议零售价(分) + enable_virtual_data: boolean // 是否启用虚流量 shelf_status: number // 上架状态 (1:上架, 2:下架) status: number // 状态 (1:启用, 2:禁用) description?: string @@ -154,11 +188,14 @@ export interface ShopPackageAllocationResponse { package_id: number package_code: string package_name: string + series_id: number // 套餐系列ID + series_name: string // 套餐系列名称 shop_id: number shop_name: string - cost_price: number // 覆盖的成本价(分) - calculated_cost_price: number // 原计算成本价(分,供参考) - allocation_id?: number // 关联的系列分配ID + allocator_shop_id: number // 分配者店铺ID,0表示平台分配 + allocator_shop_name: string // 分配者店铺名称 + series_allocation_id?: number | null // 关联的系列分配ID(可空) + cost_price: number // 该代理的成本价(分) status: number // 1:启用, 2:禁用 created_at: string updated_at: string @@ -170,6 +207,8 @@ export interface ShopPackageAllocationResponse { export interface ShopPackageAllocationQueryParams extends PaginationParams { shop_id?: number // 店铺ID筛选 package_id?: number // 套餐ID筛选 + series_allocation_id?: number // 系列分配ID筛选 + allocator_shop_id?: number // 分配者店铺ID筛选 status?: number // 状态筛选 } @@ -198,54 +237,6 @@ export interface UpdateShopPackageAllocationStatusRequest { // ==================== 套餐系列分配 ==================== -/** - * 基础返佣配置 - */ -export interface BaseCommissionConfig { - mode: 'fixed' | 'percent' // 返佣模式:'fixed':固定金额, 'percent':百分比 - value: number // 返佣值:固定金额(分)或百分比的千分比(如200表示20%) -} - -/** - * 梯度档位配置 - */ -export interface TierEntry { - threshold: number // 阈值(销量或销售额) - mode: 'fixed' | 'percent' // 返佣模式 - value: number // 返佣值 -} - -/** - * 梯度返佣配置 - */ -export interface TierCommissionConfig { - period_type: 'monthly' | 'quarterly' | 'yearly' // 周期类型 - tier_type: 'sales_count' | 'sales_amount' // 梯度类型:销量或销售额 - tiers: TierEntry[] // 梯度档位数组 -} - -/** - * 一次性佣金梯度档位配置 - */ -export interface OneTimeCommissionTierEntry { - tier_type: 'sales_count' | 'sales_amount' // 梯度类型:销量或销售额 - threshold: number // 阈值 - mode: 'fixed' | 'percent' // 返佣模式 - value: number // 返佣值 -} - -/** - * 一次性佣金配置 - */ -export interface OneTimeCommissionConfig { - type: 'fixed' | 'tiered' // 佣金类型:'fixed':固定佣金, 'tiered':梯度佣金 - trigger: 'single_recharge' | 'accumulated_recharge' // 触发方式:'single_recharge':单笔充值, 'accumulated_recharge':累计充值 - threshold: number // 最低阈值(分) - mode?: 'fixed' | 'percent' // 返佣模式(固定佣金时必填) - value?: number // 返佣值(固定佣金时必填) - tiers?: OneTimeCommissionTierEntry[] | null // 梯度档位数组(梯度佣金时必填) -} - /** * 套餐系列分配响应 */ @@ -253,13 +244,18 @@ export interface ShopSeriesAllocationResponse { id: number series_id: number series_name: string + series_code: string // 套餐系列编码 shop_id: number shop_name: string - allocator_shop_id: number // 分配者店铺ID + allocator_shop_id: number // 分配者店铺ID,0表示平台分配 allocator_shop_name: string // 分配者店铺名称 - base_commission: BaseCommissionConfig // 基础返佣配置 enable_one_time_commission: boolean // 是否启用一次性佣金 - one_time_commission_config?: OneTimeCommissionConfig // 一次性佣金配置(可选) + one_time_commission_amount: number // 该代理能拿的一次性佣金金额上限(分) + one_time_commission_threshold?: number // 一次性佣金触发阈值(分) + one_time_commission_trigger?: 'first_recharge' | 'accumulated_recharge' // 一次性佣金触发类型 + enable_force_recharge: boolean // 是否启用强制充值 + force_recharge_amount?: number // 强制充值金额(分) + force_recharge_trigger_type?: 1 | 2 // 强充触发类型:1(单次充值)、2(累计充值) status: number // 1:启用, 2:禁用 created_at: string updated_at: string @@ -271,6 +267,7 @@ export interface ShopSeriesAllocationResponse { export interface ShopSeriesAllocationQueryParams extends PaginationParams { shop_id?: number // 店铺ID筛选 series_id?: number // 系列ID筛选 + allocator_shop_id?: number // 分配者店铺ID筛选 status?: number // 状态筛选 } @@ -280,18 +277,27 @@ export interface ShopSeriesAllocationQueryParams extends PaginationParams { export interface CreateShopSeriesAllocationRequest { series_id: number // 套餐系列ID,必填 shop_id: number // 店铺ID,必填 - base_commission: BaseCommissionConfig // 基础返佣配置,必填 - enable_one_time_commission?: boolean // 是否启用一次性佣金,可选(默认false) - one_time_commission_config?: OneTimeCommissionConfig // 一次性佣金配置,当enable_one_time_commission为true时必填 + one_time_commission_amount: number // 一次性佣金金额上限(分),必填 + enable_one_time_commission?: boolean // 是否启用一次性佣金,可选 + one_time_commission_threshold?: number // 一次性佣金触发阈值(分),可选 + one_time_commission_trigger?: 'first_recharge' | 'accumulated_recharge' // 一次性佣金触发类型,可选 + enable_force_recharge?: boolean // 是否启用强制充值,可选 + force_recharge_amount?: number // 强制充值金额(分),可选 + force_recharge_trigger_type?: 1 | 2 // 强充触发类型,可选 } /** * 更新套餐系列分配请求 */ export interface UpdateShopSeriesAllocationRequest { - base_commission?: BaseCommissionConfig // 基础返佣配置 enable_one_time_commission?: boolean // 是否启用一次性佣金 - one_time_commission_config?: OneTimeCommissionConfig // 一次性佣金配置 + one_time_commission_amount?: number // 一次性佣金金额上限(分) + one_time_commission_threshold?: number // 一次性佣金触发阈值(分) + one_time_commission_trigger?: 'first_recharge' | 'accumulated_recharge' // 一次性佣金触发类型 + enable_force_recharge?: boolean // 是否启用强制充值 + force_recharge_amount?: number // 强制充值金额(分) + force_recharge_trigger_type?: 1 | 2 // 强充触发类型 + status?: number // 状态 } /** diff --git a/src/views/asset-management/iot-card-management/index.vue b/src/views/asset-management/iot-card-management/index.vue index 252b545..b96b9c9 100644 --- a/src/views/asset-management/iot-card-management/index.vue +++ b/src/views/asset-management/iot-card-management/index.vue @@ -874,6 +874,7 @@ { label: '卡业务类型', prop: 'card_category' }, { label: '运营商', prop: 'carrier_name' }, { label: '店铺名称', prop: 'shop_name' }, + { label: '套餐系列', prop: 'series_name' }, { label: '成本价', prop: 'cost_price' }, { label: '分销价', prop: 'distribute_price' }, { label: '状态', prop: 'status' }, @@ -1025,6 +1026,12 @@ minWidth: 150, formatter: (row: StandaloneIotCard) => row.shop_name || '-' }, + { + prop: 'series_name', + label: '套餐系列', + width: 150, + formatter: (row: StandaloneIotCard) => row.series_name || '-' + }, { prop: 'cost_price', label: '成本价', diff --git a/src/views/order-management/order-list/index.vue b/src/views/order-management/order-list/index.vue index 43bc3a1..65195bf 100644 --- a/src/views/order-management/order-list/index.vue +++ b/src/views/order-management/order-list/index.vue @@ -58,34 +58,6 @@ - - - -
- {{ pkg.package_name }} - - ¥{{ (pkg.price / 100).toFixed(2) }} - -
-
-
-
+ + + +
+ {{ pkg.package_name }} + + ¥{{ (pkg.cost_price / 100).toFixed(2) }} + +
+
+
+