fetch(modify):修改套餐接口
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m40s

This commit is contained in:
sexygoat
2026-02-04 18:14:52 +08:00
parent 20e8c13e61
commit d97dc5f007
12 changed files with 979 additions and 452 deletions

View File

@@ -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. 完成后删除旧的、不再使用的类型定义

View File

@@ -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` 字段必填,且为表示月数的数字字符串

View File

@@ -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) - 分配者店铺ID0表示平台分配
- `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为50shop_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返回所有关联该系列分配的单套餐分配记录

View File

@@ -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) - 分配者店铺ID0表示平台分配
- `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为50shop_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"

View File

@@ -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变更影响和迁移说明如需要

View File

@@ -611,89 +611,98 @@ export const asyncRoutes: AppRouteRecord[] = [
// } // }
// }, // },
// 物联网卡管理系统模块 // 物联网卡管理系统模块
// { {
// path: '/card-management', path: '/card-management',
// name: 'CardManagement', name: 'CardManagement',
// component: RoutesAlias.Home, component: RoutesAlias.Home,
// meta: { meta: {
// title: 'menus.cardManagement.title', title: 'menus.cardManagement.title',
// icon: '' icon: ''
// }, },
// children: [ children: [
// // { // {
// // path: 'card-detail', // path: 'card-detail',
// // name: 'CardDetail', // name: 'CardDetail',
// // component: RoutesAlias.CardDetail, // component: RoutesAlias.CardDetail,
// // meta: { // meta: {
// // title: 'menus.cardManagement.cardDetail', // title: 'menus.cardManagement.cardDetail',
// // keepAlive: true // keepAlive: true
// // } // }
// // }, // },
// { {
// path: 'card-assign', path: 'single-card',
// name: 'CardAssign', name: 'SingleCard',
// component: RoutesAlias.CardAssign, component: RoutesAlias.SingleCard,
// meta: { meta: {
// title: 'menus.cardManagement.cardAssign', title: 'menus.cardManagement.singleCard',
// keepAlive: true keepAlive: true
// } }
// }, },
// { // {
// path: 'card-shutdown', // path: 'card-assign',
// name: 'CardShutdown', // name: 'CardAssign',
// component: RoutesAlias.CardShutdown, // component: RoutesAlias.CardAssign,
// meta: { // meta: {
// title: 'menus.cardManagement.cardShutdown', // title: 'menus.cardManagement.cardAssign',
// keepAlive: true // keepAlive: true
// } // }
// }, // },
// { // {
// path: 'my-cards', // path: 'card-shutdown',
// name: 'MyCards', // name: 'CardShutdown',
// component: RoutesAlias.MyCards, // component: RoutesAlias.CardShutdown,
// meta: { // meta: {
// title: 'menus.cardManagement.myCards', // title: 'menus.cardManagement.cardShutdown',
// keepAlive: true // keepAlive: true
// } // }
// }, // },
// { // {
// path: 'card-transfer', // path: 'my-cards',
// name: 'CardTransfer', // name: 'MyCards',
// component: RoutesAlias.CardTransfer, // component: RoutesAlias.MyCards,
// meta: { // meta: {
// title: 'menus.cardManagement.cardTransfer', // title: 'menus.cardManagement.myCards',
// keepAlive: true // keepAlive: true
// } // }
// }, // },
// { // {
// path: 'card-replacement', // path: 'card-transfer',
// name: 'CardReplacement', // name: 'CardTransfer',
// component: RoutesAlias.CardReplacement, // component: RoutesAlias.CardTransfer,
// meta: { // meta: {
// title: 'menus.cardManagement.cardReplacement', // title: 'menus.cardManagement.cardTransfer',
// keepAlive: true // keepAlive: true
// } // }
// }, // },
// { // {
// path: 'package-gift', // path: 'card-replacement',
// name: 'PackageGift', // name: 'CardReplacement',
// component: RoutesAlias.PackageGift, // component: RoutesAlias.CardReplacement,
// meta: { // meta: {
// title: 'menus.cardManagement.packageGift', // title: 'menus.cardManagement.cardReplacement',
// keepAlive: true // keepAlive: true
// } // }
// }, // },
// { // {
// path: 'card-change-card', // path: 'package-gift',
// name: 'CardChangeCard', // name: 'PackageGift',
// component: RoutesAlias.CardChangeCard, // component: RoutesAlias.PackageGift,
// meta: { // meta: {
// title: 'menus.cardManagement.cardChange', // title: 'menus.cardManagement.packageGift',
// keepAlive: true // keepAlive: true
// } // }
// } // },
// ] // {
// }, // path: 'card-change-card',
// name: 'CardChangeCard',
// component: RoutesAlias.CardChangeCard,
// meta: {
// title: 'menus.cardManagement.cardChange',
// keepAlive: true
// }
// }
]
},
{ {
path: '/package-management', path: '/package-management',
name: 'PackageManagement', name: 'PackageManagement',

View File

@@ -349,6 +349,7 @@ export interface StandaloneIotCard {
created_at: string // 创建时间 created_at: string // 创建时间
updated_at: string // 更新时间 updated_at: string // 更新时间
series_id?: number | null // 套餐系列ID series_id?: number | null // 套餐系列ID
series_name?: string // 套餐系列名称
} }
// ========== 单卡批量分配和回收相关 ========== // ========== 单卡批量分配和回收相关 ==========

View File

@@ -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_code: string
series_name: string series_name: string
description?: string description?: string
enable_one_time_commission: boolean // 是否启用一次性佣金
one_time_commission_config?: SeriesOneTimeCommissionConfig // 一次性佣金配置
status: number // 1:启用, 2:禁用 status: number // 1:启用, 2:禁用
created_at: string created_at: string
updated_at: string updated_at: string
@@ -26,6 +55,7 @@ export interface PackageSeriesResponse {
export interface PackageSeriesQueryParams extends PaginationParams { export interface PackageSeriesQueryParams extends PaginationParams {
series_name?: string // 系列名称(模糊搜索) series_name?: string // 系列名称(模糊搜索)
status?: number // 状态筛选 status?: number // 状态筛选
enable_one_time_commission?: boolean // 是否启用一次性佣金筛选
} }
/** /**
@@ -35,15 +65,16 @@ export interface CreatePackageSeriesRequest {
series_code: string // 系列编码,必填 series_code: string // 系列编码,必填
series_name: string // 系列名称,必填 series_name: string // 系列名称,必填
description?: string // 描述,可选 description?: string // 描述,可选
one_time_commission_config?: SeriesOneTimeCommissionConfig // 一次性佣金配置,可选
} }
/** /**
* 更新套餐系列请求 * 更新套餐系列请求
*/ */
export interface UpdatePackageSeriesRequest { export interface UpdatePackageSeriesRequest {
series_code?: string
series_name?: string series_name?: string
description?: string description?: string
one_time_commission_config?: SeriesOneTimeCommissionConfig // 一次性佣金配置,可选
} }
/** /**
@@ -70,6 +101,9 @@ export interface PackageResponse {
virtual_data_mb: number // 虚流量额度MB virtual_data_mb: number // 虚流量额度MB
duration_months: number // 有效期(月) duration_months: number // 有效期(月)
price: number // 价格(分) price: number // 价格(分)
cost_price: number // 成本价(分)
suggested_retail_price: number // 建议零售价(分)
enable_virtual_data: boolean // 是否启用虚流量
shelf_status: number // 上架状态 (1:上架, 2:下架) shelf_status: number // 上架状态 (1:上架, 2:下架)
status: number // 状态 (1:启用, 2:禁用) status: number // 状态 (1:启用, 2:禁用)
description?: string description?: string
@@ -154,11 +188,14 @@ export interface ShopPackageAllocationResponse {
package_id: number package_id: number
package_code: string package_code: string
package_name: string package_name: string
series_id: number // 套餐系列ID
series_name: string // 套餐系列名称
shop_id: number shop_id: number
shop_name: string shop_name: string
cost_price: number // 覆盖的成本价(分) allocator_shop_id: number // 分配者店铺ID0表示平台分配
calculated_cost_price: number // 原计算成本价(分,供参考) allocator_shop_name: string // 分配者店铺名称
allocation_id?: number // 关联的系列分配ID series_allocation_id?: number | null // 关联的系列分配ID(可空)
cost_price: number // 该代理的成本价(分)
status: number // 1:启用, 2:禁用 status: number // 1:启用, 2:禁用
created_at: string created_at: string
updated_at: string updated_at: string
@@ -170,6 +207,8 @@ export interface ShopPackageAllocationResponse {
export interface ShopPackageAllocationQueryParams extends PaginationParams { export interface ShopPackageAllocationQueryParams extends PaginationParams {
shop_id?: number // 店铺ID筛选 shop_id?: number // 店铺ID筛选
package_id?: number // 套餐ID筛选 package_id?: number // 套餐ID筛选
series_allocation_id?: number // 系列分配ID筛选
allocator_shop_id?: number // 分配者店铺ID筛选
status?: number // 状态筛选 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 id: number
series_id: number series_id: number
series_name: string series_name: string
series_code: string // 套餐系列编码
shop_id: number shop_id: number
shop_name: string shop_name: string
allocator_shop_id: number // 分配者店铺ID allocator_shop_id: number // 分配者店铺ID0表示平台分配
allocator_shop_name: string // 分配者店铺名称 allocator_shop_name: string // 分配者店铺名称
base_commission: BaseCommissionConfig // 基础返佣配置
enable_one_time_commission: boolean // 是否启用一次性佣金 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:禁用 status: number // 1:启用, 2:禁用
created_at: string created_at: string
updated_at: string updated_at: string
@@ -271,6 +267,7 @@ export interface ShopSeriesAllocationResponse {
export interface ShopSeriesAllocationQueryParams extends PaginationParams { export interface ShopSeriesAllocationQueryParams extends PaginationParams {
shop_id?: number // 店铺ID筛选 shop_id?: number // 店铺ID筛选
series_id?: number // 系列ID筛选 series_id?: number // 系列ID筛选
allocator_shop_id?: number // 分配者店铺ID筛选
status?: number // 状态筛选 status?: number // 状态筛选
} }
@@ -280,18 +277,27 @@ export interface ShopSeriesAllocationQueryParams extends PaginationParams {
export interface CreateShopSeriesAllocationRequest { export interface CreateShopSeriesAllocationRequest {
series_id: number // 套餐系列ID必填 series_id: number // 套餐系列ID必填
shop_id: number // 店铺ID必填 shop_id: number // 店铺ID必填
base_commission: BaseCommissionConfig // 基础返佣配置,必填 one_time_commission_amount: number // 一次性佣金金额上限(分),必填
enable_one_time_commission?: boolean // 是否启用一次性佣金,可选默认false enable_one_time_commission?: boolean // 是否启用一次性佣金,可选
one_time_commission_config?: OneTimeCommissionConfig // 一次性佣金配置当enable_one_time_commission为true时必填 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 { export interface UpdateShopSeriesAllocationRequest {
base_commission?: BaseCommissionConfig // 基础返佣配置
enable_one_time_commission?: boolean // 是否启用一次性佣金 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 // 状态
} }
/** /**

View File

@@ -874,6 +874,7 @@
{ label: '卡业务类型', prop: 'card_category' }, { label: '卡业务类型', prop: 'card_category' },
{ label: '运营商', prop: 'carrier_name' }, { label: '运营商', prop: 'carrier_name' },
{ label: '店铺名称', prop: 'shop_name' }, { label: '店铺名称', prop: 'shop_name' },
{ label: '套餐系列', prop: 'series_name' },
{ label: '成本价', prop: 'cost_price' }, { label: '成本价', prop: 'cost_price' },
{ label: '分销价', prop: 'distribute_price' }, { label: '分销价', prop: 'distribute_price' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
@@ -1025,6 +1026,12 @@
minWidth: 150, minWidth: 150,
formatter: (row: StandaloneIotCard) => row.shop_name || '-' formatter: (row: StandaloneIotCard) => row.shop_name || '-'
}, },
{
prop: 'series_name',
label: '套餐系列',
width: 150,
formatter: (row: StandaloneIotCard) => row.series_name || '-'
},
{ {
prop: 'cost_price', prop: 'cost_price',
label: '成本价', label: '成本价',

View File

@@ -58,34 +58,6 @@
<ElOption :label="t('orderManagement.orderType.device')" value="device" /> <ElOption :label="t('orderManagement.orderType.device')" value="device" />
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem>
<ElFormItem :label="t('orderManagement.createForm.packageIds')" prop="package_ids">
<ElSelect
v-model="createForm.package_ids"
:placeholder="t('orderManagement.createForm.packageIdsPlaceholder')"
multiple
filterable
remote
reserve-keyword
:remote-method="searchPackages"
:loading="packageSearchLoading"
clearable
style="width: 100%"
>
<ElOption
v-for="pkg in packageOptions"
:key="pkg.id"
:label="`${pkg.package_name} (¥${(pkg.price / 100).toFixed(2)})`"
:value="pkg.id"
>
<div style="display: flex; justify-content: space-between">
<span>{{ pkg.package_name }}</span>
<span style="color: var(--el-text-color-secondary); font-size: 12px">
¥{{ (pkg.price / 100).toFixed(2) }}
</span>
</div>
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem <ElFormItem
v-if="createForm.order_type === 'single_card'" v-if="createForm.order_type === 'single_card'"
:label="t('orderManagement.createForm.iotCardId')" :label="t('orderManagement.createForm.iotCardId')"
@@ -148,6 +120,35 @@
</ElOption> </ElOption>
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem>
<ElFormItem :label="t('orderManagement.createForm.packageIds')" prop="package_ids">
<ElSelect
v-model="createForm.package_ids"
:placeholder="t('orderManagement.createForm.packageIdsPlaceholder')"
multiple
filterable
remote
reserve-keyword
:remote-method="handlePackageSearch"
:loading="packageSearchLoading"
:disabled="(!createForm.iot_card_id && !createForm.device_id) || (createForm.order_type === 'single_card' && !createForm.iot_card_id) || (createForm.order_type === 'device' && !createForm.device_id)"
clearable
style="width: 100%"
>
<ElOption
v-for="pkg in packageOptions"
:key="pkg.id"
:label="`${pkg.package_name} (¥${(pkg.cost_price / 100).toFixed(2)})`"
:value="pkg.id"
>
<div style="display: flex; justify-content: space-between">
<span>{{ pkg.package_name }}</span>
<span style="color: var(--el-text-color-secondary); font-size: 12px">
¥{{ (pkg.cost_price / 100).toFixed(2) }}
</span>
</div>
</ElOption>
</ElSelect>
</ElFormItem>
</ElForm> </ElForm>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
@@ -416,12 +417,13 @@
const deviceOptions = ref<Device[]>([]) const deviceOptions = ref<Device[]>([])
const deviceSearchLoading = ref(false) const deviceSearchLoading = ref(false)
// 搜索套餐(根据套餐名称) // 搜索套餐(根据套餐名称可选按series_id筛选
const searchPackages = async (query: string) => { const searchPackages = async (query: string, seriesId?: number) => {
packageSearchLoading.value = true packageSearchLoading.value = true
try { try {
const res = await PackageManageService.getPackages({ const res = await PackageManageService.getPackages({
package_name: query || undefined, package_name: query || undefined,
series_id: seriesId,
page: 1, page: 1,
page_size: 20, page_size: 20,
status: 1, // 只获取启用的套餐 status: 1, // 只获取启用的套餐
@@ -438,6 +440,25 @@
} }
} }
// 套餐远程搜索方法自动使用当前选中的IOT卡/设备的series_id
const handlePackageSearch = (query: string) => {
let seriesId: number | undefined
// 如果是单卡订单并且已选择IOT卡使用该卡的series_id筛选
if (createForm.order_type === 'single_card' && createForm.iot_card_id) {
const selectedCard = iotCardOptions.value.find((card) => card.id === createForm.iot_card_id)
if (selectedCard && selectedCard.series_id) {
seriesId = selectedCard.series_id
}
} else if (createForm.order_type === 'device' && createForm.device_id) {
// 如果是设备订单并且已选择设备使用设备的series_id筛选
const selectedDevice = deviceOptions.value.find((dev) => dev.id === createForm.device_id)
if (selectedDevice && selectedDevice.series_id) {
seriesId = selectedDevice.series_id
}
}
searchPackages(query, seriesId)
}
// 搜索IoT卡根据ICCID // 搜索IoT卡根据ICCID
const searchIotCards = async (query: string) => { const searchIotCards = async (query: string) => {
cardSearchLoading.value = true cardSearchLoading.value = true
@@ -623,6 +644,42 @@
} }
]) ])
// 当选择IOT卡时根据series_id筛选套餐
watch(
() => createForm.iot_card_id,
(newCardId) => {
if (newCardId && createForm.order_type === 'single_card') {
// 找到选中的IOT卡
const selectedCard = iotCardOptions.value.find((card) => card.id === newCardId)
if (selectedCard && selectedCard.series_id) {
// 根据series_id重新加载套餐列表
loadDefaultPackages(selectedCard.series_id)
} else {
// 如果没有series_id加载所有套餐
loadDefaultPackages()
}
}
}
)
// 当选择设备时根据series_id筛选套餐
watch(
() => createForm.device_id,
(newDeviceId) => {
if (newDeviceId && createForm.order_type === 'device') {
// 找到选中的设备
const selectedDevice = deviceOptions.value.find((dev) => dev.id === newDeviceId)
if (selectedDevice && selectedDevice.series_id) {
// 根据series_id重新加载套餐列表
loadDefaultPackages(selectedDevice.series_id)
} else {
// 如果没有series_id加载所有套餐
loadDefaultPackages()
}
}
}
)
onMounted(() => { onMounted(() => {
getTableData() getTableData()
}) })
@@ -642,7 +699,7 @@
} }
const res = await OrderService.getOrders(params) const res = await OrderService.getOrders(params)
if (res.code === 0) { if (res.code === 0) {
orderList.value = res.data.items || [] orderList.value = res.data.list || []
pagination.total = res.data.total || 0 pagination.total = res.data.total || 0
} }
} catch (error) { } catch (error) {
@@ -692,15 +749,16 @@
// 显示创建订单对话框 // 显示创建订单对话框
const showCreateDialog = async () => { const showCreateDialog = async () => {
createDialogVisible.value = true createDialogVisible.value = true
// 默认加载20条套餐、IoT卡设备数据 // 加载IoT卡和设备列表套餐列表在选择IoT卡/设备后才加载
await Promise.all([loadDefaultPackages(), loadDefaultIotCards(), loadDefaultDevices()]) await Promise.all([loadDefaultIotCards(), loadDefaultDevices()])
} }
// 加载默认套餐列表 // 加载默认套餐列表可选按series_id筛选
const loadDefaultPackages = async () => { const loadDefaultPackages = async (seriesId?: number) => {
packageSearchLoading.value = true packageSearchLoading.value = true
try { try {
const res = await PackageManageService.getPackages({ const res = await PackageManageService.getPackages({
series_id: seriesId,
page: 1, page: 1,
page_size: 20, page_size: 20,
status: 1, // 只获取启用的套餐 status: 1, // 只获取启用的套餐

View File

@@ -283,7 +283,6 @@
{ label: '套餐名称', prop: 'package_name' }, { label: '套餐名称', prop: 'package_name' },
{ label: '店铺名称', prop: 'shop_name' }, { label: '店铺名称', prop: 'shop_name' },
{ label: '成本价', prop: 'cost_price' }, { label: '成本价', prop: 'cost_price' },
{ label: '原计算成本价', prop: 'calculated_cost_price' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' }, { label: '创建时间', prop: 'created_at' },
{ label: '操作', prop: 'operation' } { label: '操作', prop: 'operation' }
@@ -372,13 +371,6 @@
) )
} }
}, },
{
prop: 'calculated_cost_price',
label: '原计算成本价',
width: 120,
formatter: (row: ShopPackageAllocationResponse) =>
`¥${(row.calculated_cost_price / 100).toFixed(2)}`
},
{ {
prop: 'status', prop: 'status',
label: '状态', label: '状态',

View File

@@ -103,141 +103,69 @@
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem>
<!-- 基础返佣配置 --> <!-- 一次性佣金配置 -->
<ElDivider content-position="left">基础返佣配置</ElDivider> <ElDivider content-position="left">一次性佣金配置</ElDivider>
<ElFormItem label="返佣模式" prop="base_commission.mode">
<ElRadioGroup v-model="form.base_commission.mode"> <ElFormItem label="佣金金额上限(分)" prop="one_time_commission_amount">
<ElRadio value="fixed">固定金额</ElRadio>
<ElRadio value="percent">百分比</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
:label="form.base_commission.mode === 'fixed' ? '返佣金额(分)' : '返佣百分比(千分比)'"
prop="base_commission.value"
>
<ElInputNumber <ElInputNumber
v-model="form.base_commission.value" v-model="form.one_time_commission_amount"
:min="0" :min="0"
:controls="false" :controls="false"
style="width: 100%" style="width: 100%"
:placeholder=" placeholder="请输入该代理能拿的一次性佣金金额上限(分)"
form.base_commission.mode === 'fixed'
? '请输入固定返佣金额(分)'
: '请输入返佣百分比的千分比(如200表示20%)'
"
/> />
<div class="form-tip"> <div class="form-tip">该代理在此系列分配下能获得的一次性佣金金额上限单位</div>
{{
form.base_commission.mode === 'fixed'
? '每笔交易返佣该固定金额(单位:分)'
: '返佣百分比的千分比如200表示20%,即每笔交易返佣 = 交易金额 × 20%'
}}
</div>
</ElFormItem> </ElFormItem>
<!-- 一次性佣金配置 -->
<ElDivider content-position="left">一次性佣金设置可选</ElDivider>
<ElFormItem label="启用一次性佣金"> <ElFormItem label="启用一次性佣金">
<ElSwitch v-model="form.enable_one_time_commission" /> <ElSwitch v-model="form.enable_one_time_commission" />
</ElFormItem> </ElFormItem>
<template v-if="form.enable_one_time_commission"> <template v-if="form.enable_one_time_commission">
<ElFormItem label="一次性佣金类型" prop="one_time_commission_config.type"> <ElFormItem label="触发阈值(分)" prop="one_time_commission_threshold">
<ElRadioGroup v-model="form.one_time_commission_config.type"> <ElInputNumber
<ElRadio value="fixed">固定</ElRadio> v-model="form.one_time_commission_threshold"
<ElRadio value="tiered">梯度</ElRadio> :min="0"
</ElRadioGroup> :controls="false"
style="width: 100%"
placeholder="请输入触发阈值(分)"
/>
<div class="form-tip">达到此充值金额后触发一次性佣金</div>
</ElFormItem> </ElFormItem>
<ElFormItem label="触发条件" prop="one_time_commission_config.trigger"> <ElFormItem label="触发类型" prop="one_time_commission_trigger">
<ElRadioGroup v-model="form.one_time_commission_config.trigger"> <ElRadioGroup v-model="form.one_time_commission_trigger">
<ElRadio value="single_recharge">次充值</ElRadio> <ElRadio value="first_recharge">次充值</ElRadio>
<ElRadio value="accumulated_recharge">累计充值</ElRadio> <ElRadio value="accumulated_recharge">累计充值</ElRadio>
</ElRadioGroup> </ElRadioGroup>
</ElFormItem> </ElFormItem>
</template>
<ElFormItem label="最低阈值(分)" prop="one_time_commission_config.threshold"> <!-- 强制充值配置 -->
<ElDivider content-position="left">强制充值配置可选</ElDivider>
<ElFormItem label="启用强制充值">
<ElSwitch v-model="form.enable_force_recharge" />
</ElFormItem>
<template v-if="form.enable_force_recharge">
<ElFormItem label="强充金额(分)" prop="force_recharge_amount">
<ElInputNumber <ElInputNumber
v-model="form.one_time_commission_config.threshold" v-model="form.force_recharge_amount"
:min="1" :min="0"
:controls="false" :controls="false"
style="width: 100%" style="width: 100%"
placeholder="请输入最低阈值(分)" placeholder="请输入强制充值金额(分)"
/> />
<div class="form-tip">用户需要达到的强制充值金额</div>
</ElFormItem> </ElFormItem>
<!-- 固定类型配置 --> <ElFormItem label="强充触发类型" prop="force_recharge_trigger_type">
<template v-if="form.one_time_commission_config.type === 'fixed'"> <ElRadioGroup v-model="form.force_recharge_trigger_type">
<ElFormItem label="返佣模式" prop="one_time_commission_config.mode"> <ElRadio :value="1">单次充值</ElRadio>
<ElRadioGroup v-model="form.one_time_commission_config.mode"> <ElRadio :value="2">累计充值</ElRadio>
<ElRadio value="fixed">固定金额</ElRadio> </ElRadioGroup>
<ElRadio value="percent">百分比</ElRadio> </ElFormItem>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
:label="
form.one_time_commission_config.mode === 'fixed'
? '佣金金额(分)'
: '佣金比例(千分比)'
"
prop="one_time_commission_config.value"
>
<ElInputNumber
v-model="form.one_time_commission_config.value"
:min="1"
:controls="false"
style="width: 100%"
:placeholder="
form.one_time_commission_config.mode === 'fixed'
? '请输入佣金金额(分)'
: '请输入佣金比例的千分比(如200表示20%)'
"
/>
</ElFormItem>
</template>
<!-- 梯度类型配置 -->
<template v-if="form.one_time_commission_config.type === 'tiered'">
<ElFormItem label="梯度档位">
<div class="tier-list">
<div
v-for="(tier, index) in form.one_time_commission_config.tiers"
:key="index"
class="tier-item"
>
<ElSelect
v-model="tier.tier_type"
placeholder="梯度类型"
style="width: 120px"
>
<ElOption label="销量" value="sales_count" />
<ElOption label="销售额" value="sales_amount" />
</ElSelect>
<ElInputNumber
v-model="tier.threshold"
:min="1"
:controls="false"
placeholder="阈值"
style="width: 120px"
/>
<ElSelect v-model="tier.mode" placeholder="返佣模式" style="width: 120px">
<ElOption label="固定金额" value="fixed" />
<ElOption label="百分比" value="percent" />
</ElSelect>
<ElInputNumber
v-model="tier.value"
:min="1"
:controls="false"
placeholder="返佣值"
style="width: 120px"
/>
<ElButton type="danger" @click="removeTier(index)">删除</ElButton>
</div>
<ElButton type="primary" @click="addTier">添加档位</ElButton>
</div>
</ElFormItem>
</template>
</template> </template>
</ElForm> </ElForm>
<template #footer> <template #footer>
@@ -368,8 +296,9 @@
{ label: '系列名称', prop: 'series_name' }, { label: '系列名称', prop: 'series_name' },
{ label: '店铺名称', prop: 'shop_name' }, { label: '店铺名称', prop: 'shop_name' },
{ label: '分配者店铺', prop: 'allocator_shop_name' }, { label: '分配者店铺', prop: 'allocator_shop_name' },
{ label: '基础返佣', prop: 'base_commission' }, { label: '一次性佣金金额', prop: 'one_time_commission_amount' },
{ label: '一次性佣金', prop: 'enable_one_time_commission' }, { label: '一次性佣金状态', prop: 'enable_one_time_commission' },
{ label: '强制充值', prop: 'enable_force_recharge' },
{ label: '状态', prop: 'status' }, { label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' }, { label: '创建时间', prop: 'created_at' },
{ label: '操作', prop: 'operation' } { label: '操作', prop: 'operation' }
@@ -383,19 +312,13 @@
series_name: '', series_name: '',
shop_name: '', shop_name: '',
allocator_shop_name: '', allocator_shop_name: '',
base_commission: {
mode: 'fixed',
value: 0
},
enable_one_time_commission: false, enable_one_time_commission: false,
one_time_commission_config: { one_time_commission_amount: 0,
type: 'fixed', one_time_commission_threshold: undefined,
trigger: 'single_recharge', one_time_commission_trigger: 'first_recharge' as 'first_recharge' | 'accumulated_recharge',
threshold: 0, enable_force_recharge: false,
mode: 'fixed', force_recharge_amount: undefined,
value: 0, force_recharge_trigger_type: undefined as 1 | 2 | undefined
tiers: []
}
}) })
// 动态验证规则 // 动态验证规则
@@ -403,15 +326,14 @@
const baseRules: FormRules = { const baseRules: FormRules = {
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }], series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
shop_id: [{ required: true, message: '请选择店铺', trigger: 'change' }], shop_id: [{ required: true, message: '请选择店铺', trigger: 'change' }],
'base_commission.mode': [{ required: true, message: '请选择返佣模式', trigger: 'change' }], one_time_commission_amount: [
'base_commission.value': [ { required: true, message: '请输入一次性佣金金额上限', trigger: 'blur' },
{ required: true, message: '请输入返佣值', trigger: 'blur' },
{ {
validator: (rule, value, callback) => { validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') { if (value === undefined || value === null || value === '') {
callback(new Error('请输入返佣值')) callback(new Error('请输入一次性佣金金额上限'))
} else if (value < 0) { } else if (value < 0) {
callback(new Error('返佣值不能小于0')) callback(new Error('一次性佣金金额不能小于0'))
} else { } else {
callback() callback()
} }
@@ -421,27 +343,24 @@
] ]
} }
// 如果启用了一次性佣金,添加验证规则 // 如果启用了一次性佣金,添加额外验证规则
if (form.enable_one_time_commission) { if (form.enable_one_time_commission) {
baseRules['one_time_commission_config.type'] = [ baseRules.one_time_commission_threshold = [
{ required: true, message: '请选择一次性佣金类型', trigger: 'change' } { required: true, message: '请输入触发阈值', trigger: 'blur' }
] ]
baseRules['one_time_commission_config.trigger'] = [ baseRules.one_time_commission_trigger = [
{ required: true, message: '请选择触发条件', trigger: 'change' } { required: true, message: '请选择触发类型', trigger: 'change' }
]
baseRules['one_time_commission_config.threshold'] = [
{ required: true, message: '请输入最低阈值', trigger: 'blur' }
] ]
}
// 固定类型验证 // 如果启用了强制充值,添加验证规则
if (form.one_time_commission_config.type === 'fixed') { if (form.enable_force_recharge) {
baseRules['one_time_commission_config.mode'] = [ baseRules.force_recharge_amount = [
{ required: true, message: '请选择返佣模式', trigger: 'change' } { required: true, message: '请输入强制充值金额', trigger: 'blur' }
] ]
baseRules['one_time_commission_config.value'] = [ baseRules.force_recharge_trigger_type = [
{ required: true, message: '请输入佣金值', trigger: 'blur' } { required: true, message: '请选择强充触发类型', trigger: 'change' }
] ]
}
} }
return baseRules return baseRules
@@ -476,23 +395,21 @@
} }
}, },
{ {
prop: 'base_commission', prop: 'one_time_commission_amount',
label: '基础返佣', label: '一次性佣金金额',
width: 150, width: 150,
formatter: (row: ShopSeriesAllocationResponse) => { formatter: (row: ShopSeriesAllocationResponse) => {
if (!row.base_commission) return '-' return h(
const { mode, value } = row.base_commission 'span',
if (mode === 'fixed') { { style: 'color: #f56c6c; font-weight: bold' },
return `固定 ¥${(value / 100).toFixed(2)}` `¥${(row.one_time_commission_amount / 100).toFixed(2)}`
} else { )
return `百分比 ${(value / 10).toFixed(1)}%`
}
} }
}, },
{ {
prop: 'enable_one_time_commission', prop: 'enable_one_time_commission',
label: '一次性佣金', label: '一次性佣金状态',
width: 120, width: 130,
formatter: (row: ShopSeriesAllocationResponse) => { formatter: (row: ShopSeriesAllocationResponse) => {
return h( return h(
ElTag, ElTag,
@@ -501,6 +418,18 @@
) )
} }
}, },
{
prop: 'enable_force_recharge',
label: '强制充值',
width: 100,
formatter: (row: ShopSeriesAllocationResponse) => {
return h(
ElTag,
{ type: row.enable_force_recharge ? 'warning' : 'info', size: 'small' },
() => (row.enable_force_recharge ? '已启用' : '未启用')
)
}
},
{ {
prop: 'status', prop: 'status',
label: '状态', label: '状态',
@@ -750,21 +679,6 @@
getTableData() getTableData()
} }
// 添加档位
const addTier = () => {
form.one_time_commission_config.tiers.push({
tier_type: 'sales_count',
threshold: 0,
mode: 'fixed',
value: 0
})
}
// 删除档位
const removeTier = (index: number) => {
form.one_time_commission_config.tiers.splice(index, 1)
}
// 显示新增/编辑对话框 // 显示新增/编辑对话框
const showDialog = (type: string, row?: ShopSeriesAllocationResponse) => { const showDialog = (type: string, row?: ShopSeriesAllocationResponse) => {
dialogVisible.value = true dialogVisible.value = true
@@ -777,30 +691,13 @@
form.series_name = row.series_name form.series_name = row.series_name
form.shop_name = row.shop_name form.shop_name = row.shop_name
form.allocator_shop_name = row.allocator_shop_name form.allocator_shop_name = row.allocator_shop_name
form.base_commission = {
mode: row.base_commission.mode,
value: row.base_commission.value
}
form.enable_one_time_commission = row.enable_one_time_commission form.enable_one_time_commission = row.enable_one_time_commission
if (row.enable_one_time_commission && row.one_time_commission_config) { form.one_time_commission_amount = row.one_time_commission_amount
form.one_time_commission_config = { form.one_time_commission_threshold = row.one_time_commission_threshold
type: row.one_time_commission_config.type, form.one_time_commission_trigger = row.one_time_commission_trigger || 'first_recharge'
trigger: row.one_time_commission_config.trigger, form.enable_force_recharge = row.enable_force_recharge
threshold: row.one_time_commission_config.threshold, form.force_recharge_amount = row.force_recharge_amount
mode: row.one_time_commission_config.mode || 'fixed', form.force_recharge_trigger_type = row.force_recharge_trigger_type
value: row.one_time_commission_config.value || 0,
tiers: row.one_time_commission_config.tiers?.map((t) => ({ ...t })) || []
}
} else {
form.one_time_commission_config = {
type: 'fixed',
trigger: 'single_recharge',
threshold: 0,
mode: 'fixed',
value: 0,
tiers: []
}
}
} else { } else {
form.id = 0 form.id = 0
form.series_id = undefined form.series_id = undefined
@@ -808,19 +705,13 @@
form.series_name = '' form.series_name = ''
form.shop_name = '' form.shop_name = ''
form.allocator_shop_name = '' form.allocator_shop_name = ''
form.base_commission = {
mode: 'fixed',
value: 0
}
form.enable_one_time_commission = false form.enable_one_time_commission = false
form.one_time_commission_config = { form.one_time_commission_amount = 0
type: 'fixed', form.one_time_commission_threshold = undefined
trigger: 'single_recharge', form.one_time_commission_trigger = 'first_recharge'
threshold: 0, form.enable_force_recharge = false
mode: 'fixed', form.force_recharge_amount = undefined
value: 0, form.force_recharge_trigger_type = undefined
tiers: []
}
} }
// 重置表单验证状态 // 重置表单验证状态
@@ -840,19 +731,13 @@
form.series_name = '' form.series_name = ''
form.shop_name = '' form.shop_name = ''
form.allocator_shop_name = '' form.allocator_shop_name = ''
form.base_commission = {
mode: 'fixed',
value: 0
}
form.enable_one_time_commission = false form.enable_one_time_commission = false
form.one_time_commission_config = { form.one_time_commission_amount = 0
type: 'fixed', form.one_time_commission_threshold = undefined
trigger: 'single_recharge', form.one_time_commission_trigger = 'first_recharge'
threshold: 0, form.enable_force_recharge = false
mode: 'fixed', form.force_recharge_amount = undefined
value: 0, form.force_recharge_trigger_type = undefined
tiers: []
}
} }
// 删除分配 // 删除分配
@@ -886,59 +771,24 @@
await formEl.validate(async (valid) => { await formEl.validate(async (valid) => {
if (valid) { if (valid) {
// 验证一次性佣金配置
if (form.enable_one_time_commission) {
if (form.one_time_commission_config.type === 'tiered') {
if (form.one_time_commission_config.tiers.length === 0) {
ElMessage.warning('启用梯度类型时至少需要添加一个档位')
return
}
// 验证档位阈值递增
const thresholds = form.one_time_commission_config.tiers.map((t: any) => t.threshold)
for (let i = 1; i < thresholds.length; i++) {
if (thresholds[i] <= thresholds[i - 1]) {
ElMessage.warning('档位阈值必须递增')
return
}
}
}
}
submitLoading.value = true submitLoading.value = true
try { try {
const data: any = { const data: any = {
base_commission: { one_time_commission_amount: form.one_time_commission_amount,
mode: form.base_commission.mode, enable_one_time_commission: form.enable_one_time_commission,
value: form.base_commission.value enable_force_recharge: form.enable_force_recharge
},
enable_one_time_commission: form.enable_one_time_commission
} }
// 如果启用了一次性佣金,加入配置 // 如果启用了一次性佣金,加入相关字段
if (form.enable_one_time_commission) { if (form.enable_one_time_commission) {
data.one_time_commission_config = { data.one_time_commission_threshold = form.one_time_commission_threshold
type: form.one_time_commission_config.type, data.one_time_commission_trigger = form.one_time_commission_trigger
trigger: form.one_time_commission_config.trigger, }
threshold: form.one_time_commission_config.threshold
}
// 固定类型配置 // 如果启用了强制充值,加入相关字段
if (form.one_time_commission_config.type === 'fixed') { if (form.enable_force_recharge) {
data.one_time_commission_config.mode = form.one_time_commission_config.mode data.force_recharge_amount = form.force_recharge_amount
data.one_time_commission_config.value = form.one_time_commission_config.value data.force_recharge_trigger_type = form.force_recharge_trigger_type
}
// 梯度类型配置
else if (form.one_time_commission_config.type === 'tiered') {
data.one_time_commission_config.tiers = form.one_time_commission_config.tiers.map(
(t: any) => ({
tier_type: t.tier_type,
threshold: t.threshold,
mode: t.mode,
value: t.value
})
)
}
} }
if (dialogType.value === 'add') { if (dialogType.value === 'add') {