refactor: 一次性佣金配置从套餐级别提升到系列级别
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m29s

主要变更:
- 新增 tb_shop_series_allocation 表,存储系列级别的一次性佣金配置
- ShopPackageAllocation 移除 one_time_commission_amount 字段
- PackageSeries 新增 enable_one_time_commission 字段控制是否启用一次性佣金
- 新增 /api/admin/shop-series-allocations CRUD 接口
- 佣金计算逻辑改为从 ShopSeriesAllocation 获取一次性佣金金额
- 删除废弃的 ShopSeriesOneTimeCommissionTier 模型
- OpenAPI Tag '系列分配' 和 '单套餐分配' 合并为 '套餐分配'

迁移脚本:
- 000042: 重构佣金套餐模型
- 000043: 简化佣金分配
- 000044: 一次性佣金分配重构
- 000045: PackageSeries 添加 enable_one_time_commission 字段

测试:
- 新增验收测试 (shop_series_allocation, commission_calculation)
- 新增流程测试 (one_time_commission_chain)
- 删除过时的单元测试(已被验收测试覆盖)
This commit is contained in:
2026-02-04 14:28:44 +08:00
parent fba8e9e76b
commit b18ecfeb55
106 changed files with 9899 additions and 6608 deletions

322
tests/acceptance/README.md Normal file
View File

@@ -0,0 +1,322 @@
# 验收测试 (Acceptance Tests)
验收测试验证单个 API 的契约:给定输入,期望输出。
## 核心原则
1. **来源于 Spec**:每个测试用例对应 Spec 中的一个 Scenario
2. **测试先于实现**:在功能实现前生成,预期全部 FAIL
3. **契约验证**:验证 API 的输入输出契约,不测试内部实现
4. **必须有破坏点**:每个测试必须注释说明什么代码变更会导致失败
## 目录结构
```
tests/acceptance/
├── README.md # 本文件
├── account_acceptance_test.go # 账号管理验收测试
├── package_acceptance_test.go # 套餐管理验收测试
├── shop_package_acceptance_test.go # 店铺套餐分配验收测试
└── ...
```
## 测试模板
```go
package acceptance
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"junhong_cmp_fiber/tests/testutils"
)
// ============================================================
// 验收测试:{功能名称}
// 来源openspec/changes/{change-name}/specs/{capability}/spec.md
// ============================================================
func Test{Capability}_Acceptance(t *testing.T) {
env := testutils.NewIntegrationTestEnv(t)
// ------------------------------------------------------------
// Scenario: {场景名称}
// GIVEN: {前置条件}
// WHEN: {触发动作}
// THEN: {预期结果}
// AND: {额外验证}
//
// 破坏点:{描述什么代码变更会导致此测试失败}
// ------------------------------------------------------------
t.Run("Scenario_{场景名称}", func(t *testing.T) {
// GIVEN: 设置前置条件
client := env.AsSuperAdmin()
// WHEN: 执行操作
body := map[string]interface{}{
// 请求体
}
resp, err := client.Request("POST", "/api/admin/xxx", body)
require.NoError(t, err)
// THEN: 验证结果
assert.Equal(t, 200, resp.StatusCode)
var result map[string]interface{}
err = resp.JSON(&result)
require.NoError(t, err)
assert.Equal(t, 0, int(result["code"].(float64)))
// AND: 额外验证(如数据库状态)
// ...
})
}
```
## 测试分类
### 正常场景 (Happy Path)
```go
t.Run("Scenario_成功创建资源", func(t *testing.T) {
// 测试正常流程
})
```
### 参数校验
```go
t.Run("Scenario_参数缺失返回400", func(t *testing.T) {
// 测试缺少必填参数
})
t.Run("Scenario_参数格式错误返回400", func(t *testing.T) {
// 测试参数格式不符合要求
})
```
### 权限校验
```go
t.Run("Scenario_无权限返回403", func(t *testing.T) {
// 测试权限不足的情况
})
t.Run("Scenario_跨店铺访问返回403", func(t *testing.T) {
// 测试越权访问
})
```
### 业务规则
```go
t.Run("Scenario_重复创建返回409", func(t *testing.T) {
// 测试业务规则冲突
})
t.Run("Scenario_删除已使用资源返回400", func(t *testing.T) {
// 测试业务规则限制
})
```
## 破坏点注释规范
每个测试必须包含"破坏点"注释,说明什么代码变更会导致测试失败:
```go
// 破坏点:如果删除 handler.Create 中的 store.Create 调用,此测试将失败
// 破坏点:如果移除参数校验中的 name 必填检查,此测试将失败
// 破坏点:如果响应不包含创建的资源 ID此测试将失败
// 破坏点:如果删除权限检查中间件,此测试将失败
```
**为什么需要破坏点**
1. 证明测试真正验证了功能
2. 帮助理解测试意图
3. 重构时快速定位影响
## Table-Driven 模式
对于同一 API 的多个场景,使用 table-driven 模式:
```go
func TestPackage_Create_Validation(t *testing.T) {
env := testutils.NewIntegrationTestEnv(t)
tests := []struct {
name string
body map[string]interface{}
expectedStatus int
expectedCode int
breakpoint string
}{
{
name: "名称为空",
body: map[string]interface{}{
"name": "",
"price": 9900,
},
expectedStatus: 400,
expectedCode: 4000, // CodeInvalidParam
breakpoint: "移除 name 必填校验",
},
{
name: "价格为负",
body: map[string]interface{}{
"name": "测试套餐",
"price": -100,
},
expectedStatus: 400,
expectedCode: 4000,
breakpoint: "移除 price >= 0 校验",
},
{
name: "时长为0",
body: map[string]interface{}{
"name": "测试套餐",
"price": 9900,
"duration": 0,
},
expectedStatus: 400,
expectedCode: 4000,
breakpoint: "移除 duration > 0 校验",
},
}
for _, tt := range tests {
t.Run("Scenario_"+tt.name, func(t *testing.T) {
// 破坏点: {tt.breakpoint}
client := env.AsSuperAdmin()
resp, err := client.Request("POST", "/api/admin/packages", tt.body)
require.NoError(t, err)
assert.Equal(t, tt.expectedStatus, resp.StatusCode)
var result map[string]interface{}
err = resp.JSON(&result)
require.NoError(t, err)
assert.Equal(t, tt.expectedCode, int(result["code"].(float64)))
})
}
}
```
## 运行测试
```bash
# 运行所有验收测试
source .env.local && go test -v ./tests/acceptance/...
# 运行特定功能的验收测试
source .env.local && go test -v ./tests/acceptance/... -run TestPackage
# 运行特定场景
source .env.local && go test -v ./tests/acceptance/... -run "Scenario_成功创建"
```
## 测试环境
验收测试使用 `IntegrationTestEnv`,提供:
- **事务隔离**:每个测试在独立事务中运行,自动回滚
- **Redis 清理**:测试前自动清理相关 Redis 键
- **身份切换**:支持不同角色的请求
```go
env := testutils.NewIntegrationTestEnv(t)
// 以超级管理员身份请求
env.AsSuperAdmin().Request("GET", "/api/admin/xxx", nil)
// 以平台用户身份请求
env.AsPlatformUser(accountID).Request("GET", "/api/admin/xxx", nil)
// 以代理商身份请求
env.AsShopAgent(shopID).Request("GET", "/api/admin/xxx", nil)
// 以企业用户身份请求
env.AsEnterprise(enterpriseID).Request("GET", "/api/admin/xxx", nil)
```
## 与 Spec 的对应关系
```markdown
# Spec 中的 Scenario
#### Scenario: 成功创建套餐
- **GIVEN** 用户已登录且有创建权限
- **WHEN** POST /api/admin/packages with valid data
- **THEN** 返回 201 和套餐详情
- **AND** 数据库中存在该套餐记录
```
对应测试:
```go
// 直接从 Spec Scenario 转换
t.Run("Scenario_成功创建套餐", func(t *testing.T) {
// GIVEN: 用户已登录且有创建权限
client := env.AsSuperAdmin()
// WHEN: POST /api/admin/packages with valid data
resp, err := client.Request("POST", "/api/admin/packages", validBody)
// THEN: 返回 201 和套餐详情
assert.Equal(t, 201, resp.StatusCode)
// AND: 数据库中存在该套餐记录
// 验证数据库状态
})
```
## 常见问题
### Q: 验收测试和集成测试的区别?
| 方面 | 验收测试 | 集成测试 |
|------|---------|---------|
| 来源 | Spec Scenario | 开发者编写 |
| 目的 | 验证 API 契约 | 验证系统集成 |
| 粒度 | 单 API | 可能涉及多 API |
| 时机 | 实现前生成 | 实现后编写 |
### Q: 测试 PASS 了但功能还没实现?
说明测试写得太弱。检查:
1. 是否验证了响应状态码
2. 是否验证了响应体结构
3. 是否验证了数据库状态变化
4. 破坏点是否准确
### Q: 如何处理需要前置数据的测试?
在 GIVEN 阶段创建必要的前置数据:
```go
t.Run("Scenario_删除已分配的套餐失败", func(t *testing.T) {
// GIVEN: 存在一个已分配给店铺的套餐
client := env.AsSuperAdmin()
// 创建套餐
createResp, _ := client.Request("POST", "/api/admin/packages", packageBody)
var createResult map[string]interface{}
createResp.JSON(&createResult)
packageID := uint(createResult["data"].(map[string]interface{})["id"].(float64))
// 分配给店铺
client.Request("POST", "/api/admin/shop-packages", map[string]interface{}{
"package_id": packageID,
"shop_id": 1,
})
// WHEN: 尝试删除套餐
resp, _ := client.Request("DELETE", fmt.Sprintf("/api/admin/packages/%d", packageID), nil)
// THEN: 返回 400
assert.Equal(t, 400, resp.StatusCode)
})
```

View File

@@ -0,0 +1,444 @@
package acceptance
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/response"
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ============================================================
// 验收测试:佣金计算重构
// 来源openspec/changes/refactor-one-time-commission-allocation/specs/commission-calculation/spec.md
// 来源openspec/changes/refactor-one-time-commission-allocation/specs/commission-trigger/spec.md
// ============================================================
func TestCommissionCalculation_SeriesAllocationQuery_Acceptance(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
parentShop := env.CreateTestShop("一级代理", 1, nil)
childShop := env.CreateTestShop("二级代理", 2, &parentShop.ID)
series := createCommissionTestSeries(t, env, "佣金测试系列")
createPlatformSeriesAllocationForCommission(t, env, parentShop.ID, series.ID, 10000)
createSeriesAllocationForCommission(t, env, parentShop.ID, childShop.ID, series.ID, 5000)
// ------------------------------------------------------------
// Scenario: 直接查询系列分配
// GIVEN: 存在 shop_id + series_id 的系列分配记录
// WHEN: 通过 shop_id 和 series_id 查询
// THEN: 返回唯一匹配的记录,包含 one_time_commission_amount
//
// 破坏点:如果查询 API 不支持 series_id 筛选,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_直接查询系列分配", func(t *testing.T) {
path := fmt.Sprintf("/api/admin/shop-series-allocations?shop_id=%d&series_id=%d",
childShop.ID, series.ID)
resp, err := env.AsSuperAdmin().Request("GET", path, nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
data := result.Data.(map[string]interface{})
items := data["items"].([]interface{})
require.Len(t, items, 1, "应返回唯一匹配记录")
allocation := items[0].(map[string]interface{})
assert.Equal(t, float64(5000), allocation["one_time_commission_amount"],
"佣金金额应为 5000 分")
})
// ------------------------------------------------------------
// Scenario: 系列分配不存在
// GIVEN: shop_id + series_id 组合不存在分配记录
// WHEN: 查询该组合
// THEN: 返回空列表
//
// 破坏点:如果查询不正确处理空结果,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_系列分配不存在", func(t *testing.T) {
path := fmt.Sprintf("/api/admin/shop-series-allocations?shop_id=%d&series_id=99999",
childShop.ID)
resp, err := env.AsSuperAdmin().Request("GET", path, nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
data := result.Data.(map[string]interface{})
list := data["items"].([]interface{})
assert.Empty(t, list, "不存在的组合应返回空列表")
})
}
func TestCommissionCalculation_EnableOneTimeCommission_Acceptance(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
series := createCommissionTestSeriesWithConfig(t, env, "启用佣金系列", true)
// ------------------------------------------------------------
// Scenario: 检查系列是否启用一次性佣金
// GIVEN: 系列配置 enable_one_time_commission = true
// WHEN: 查询系列详情
// THEN: 响应包含 enable_one_time_commission = true
//
// 破坏点:如果系列 API 不返回 enable_one_time_commission 字段,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_检查系列是否启用一次性佣金", func(t *testing.T) {
path := fmt.Sprintf("/api/admin/package-series/%d", series.ID)
resp, err := env.AsSuperAdmin().Request("GET", path, nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
data := result.Data.(map[string]interface{})
enableOneTime, ok := data["enable_one_time_commission"]
assert.True(t, ok, "响应应包含 enable_one_time_commission 字段")
assert.Equal(t, true, enableOneTime, "应为 true")
})
// ------------------------------------------------------------
// Scenario: 批量查询启用一次性佣金的系列
// GIVEN: 存在多个系列,部分启用一次性佣金
// WHEN: 查询系列列表并按 enable_one_time_commission 筛选
// THEN: 返回符合条件的系列
//
// 破坏点:如果不支持 enable_one_time_commission 筛选,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_批量查询启用一次性佣金的系列", func(t *testing.T) {
createCommissionTestSeriesWithConfig(t, env, "禁用佣金系列", false)
resp, err := env.AsSuperAdmin().
Request("GET", "/api/admin/package-series?enable_one_time_commission=true", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
data := result.Data.(map[string]interface{})
list := data["items"].([]interface{})
for _, item := range list {
seriesItem := item.(map[string]interface{})
enableVal, hasField := seriesItem["enable_one_time_commission"]
if hasField {
assert.Equal(t, true, enableVal, "筛选结果应全部为启用状态")
}
}
})
}
func TestCommissionCalculation_ChainAllocation_Acceptance(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
level1Shop := env.CreateTestShop("一级代理", 1, nil)
level2Shop := env.CreateTestShop("二级代理", 2, &level1Shop.ID)
level3Shop := env.CreateTestShop("三级代理", 3, &level2Shop.ID)
series := createCommissionTestSeries(t, env, "链式分配系列")
// ------------------------------------------------------------
// Scenario: 链式分配金额计算
// GIVEN:
// - 平台给一级one_time_commission_amount = 10000100元
// - 一级给二级one_time_commission_amount = 800080元
// - 二级给三级one_time_commission_amount = 500050元
// WHEN: 查询各级的系列分配
// THEN: 各级金额正确
//
// 破坏点:如果分配金额不正确保存,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_链式分配金额计算", func(t *testing.T) {
createPlatformSeriesAllocationForCommission(t, env, level1Shop.ID, series.ID, 10000)
createSeriesAllocationForCommission(t, env, level1Shop.ID, level2Shop.ID, series.ID, 8000)
createSeriesAllocationForCommission(t, env, level2Shop.ID, level3Shop.ID, series.ID, 5000)
verifyAllocationAmount(t, env, level1Shop.ID, series.ID, 10000)
verifyAllocationAmount(t, env, level2Shop.ID, series.ID, 8000)
verifyAllocationAmount(t, env, level3Shop.ID, series.ID, 5000)
})
// ------------------------------------------------------------
// Scenario: 单级代理
// GIVEN: 一级代理直接销售(无下级)
// WHEN: 查询一级的系列分配
// THEN: 一级获得完整的 one_time_commission_amount
//
// 破坏点:如果单级分配不生效,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_单级代理", func(t *testing.T) {
singleShop := env.CreateTestShop("单级代理", 1, nil)
singleSeries := createCommissionTestSeries(t, env, "单级系列")
createPlatformSeriesAllocationForCommission(t, env, singleShop.ID, singleSeries.ID, 10000)
verifyAllocationAmount(t, env, singleShop.ID, singleSeries.ID, 10000)
})
}
func TestCommissionCalculation_TriggerConfig_Acceptance(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
parentShop := env.CreateTestShop("一级代理", 1, nil)
childShop := env.CreateTestShop("二级代理", 2, &parentShop.ID)
series := createCommissionTestSeries(t, env, "触发配置系列")
createPlatformSeriesAllocationForCommission(t, env, parentShop.ID, series.ID, 10000)
// ------------------------------------------------------------
// Scenario: 累计达到阈值触发佣金配置
// GIVEN: 系列分配设置为累计充值触发,阈值 1000 元
// WHEN: 创建系列分配
// THEN: 配置正确保存
//
// 破坏点:如果触发配置不保存,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_累计达到阈值触发佣金配置", func(t *testing.T) {
body := map[string]interface{}{
"shop_id": childShop.ID,
"series_id": series.ID,
"one_time_commission_amount": 5000,
"enable_one_time_commission": true,
"one_time_commission_trigger": "accumulated_recharge",
"one_time_commission_threshold": 100000,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
data := result.Data.(map[string]interface{})
assert.Equal(t, true, data["enable_one_time_commission"])
assert.Equal(t, "accumulated_recharge", data["one_time_commission_trigger"])
assert.Equal(t, float64(100000), data["one_time_commission_threshold"])
})
// ------------------------------------------------------------
// Scenario: 首次充值触发配置
// GIVEN: 系列分配设置为首次充值触发,阈值 100 元
// WHEN: 创建系列分配
// THEN: 配置正确保存
//
// 破坏点:如果 first_recharge 触发类型不支持,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_首次充值触发配置", func(t *testing.T) {
newChildShop := env.CreateTestShop("首充测试店铺", 2, &parentShop.ID)
body := map[string]interface{}{
"shop_id": newChildShop.ID,
"series_id": series.ID,
"one_time_commission_amount": 5000,
"enable_one_time_commission": true,
"one_time_commission_trigger": "first_recharge",
"one_time_commission_threshold": 10000,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
data := result.Data.(map[string]interface{})
assert.Equal(t, "first_recharge", data["one_time_commission_trigger"])
})
}
func TestCommissionStats_Allocation_Acceptance(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
parentShop := env.CreateTestShop("一级代理", 1, nil)
series := createCommissionTestSeries(t, env, "统计测试系列")
// ------------------------------------------------------------
// Scenario: 创建佣金统计记录关联系列分配
// GIVEN: 存在系列分配记录
// WHEN: 查询佣金统计
// THEN: 统计记录的 allocation_id 指向 ShopSeriesAllocation.id
//
// 破坏点:如果统计不关联系列分配,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_佣金统计关联系列分配", func(t *testing.T) {
allocation := createPlatformSeriesAllocationForCommission(t, env, parentShop.ID, series.ID, 10000)
path := fmt.Sprintf("/api/admin/shop-series-commission-stats?shop_id=%d&series_id=%d",
parentShop.ID, series.ID)
resp, err := env.AsSuperAdmin().Request("GET", path, nil)
require.NoError(t, err)
defer resp.Body.Close()
if resp.StatusCode == 200 {
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
if result.Code == 0 && result.Data != nil {
data := result.Data.(map[string]interface{})
if list, ok := data["items"].([]interface{}); ok && len(list) > 0 {
stats := list[0].(map[string]interface{})
if allocationID, exists := stats["allocation_id"]; exists {
assert.Equal(t, float64(allocation.ID), allocationID,
"统计应关联到系列分配 ID")
}
}
}
}
})
}
// ============================================================
// 辅助函数
// ============================================================
func createCommissionTestSeries(t *testing.T, env *integ.IntegrationTestEnv, name string) *model.PackageSeries {
t.Helper()
timestamp := time.Now().UnixNano()
series := &model.PackageSeries{
SeriesCode: fmt.Sprintf("COMM_SERIES_%d", timestamp),
SeriesName: name,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(series).Error
require.NoError(t, err, "创建测试系列失败")
return series
}
func createCommissionTestSeriesWithConfig(t *testing.T, env *integ.IntegrationTestEnv, name string, enableOneTime bool) *model.PackageSeries {
t.Helper()
timestamp := time.Now().UnixNano()
series := &model.PackageSeries{
SeriesCode: fmt.Sprintf("COMM_SERIES_%d", timestamp),
SeriesName: name,
Status: constants.StatusEnabled,
EnableOneTimeCommission: enableOneTime,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(series).Error
require.NoError(t, err, "创建测试系列失败")
return series
}
func createPlatformSeriesAllocationForCommission(t *testing.T, env *integ.IntegrationTestEnv, shopID, seriesID uint, amount int64) *model.ShopSeriesAllocation {
t.Helper()
allocation := &model.ShopSeriesAllocation{
ShopID: shopID,
SeriesID: seriesID,
AllocatorShopID: 0,
OneTimeCommissionAmount: amount,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(allocation).Error
require.NoError(t, err, "创建平台系列分配失败")
return allocation
}
func createSeriesAllocationForCommission(t *testing.T, env *integ.IntegrationTestEnv, allocatorShopID, shopID, seriesID uint, amount int64) *model.ShopSeriesAllocation {
t.Helper()
allocation := &model.ShopSeriesAllocation{
ShopID: shopID,
SeriesID: seriesID,
AllocatorShopID: allocatorShopID,
OneTimeCommissionAmount: amount,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(allocation).Error
require.NoError(t, err, "创建系列分配失败")
return allocation
}
func verifyAllocationAmount(t *testing.T, env *integ.IntegrationTestEnv, shopID, seriesID uint, expectedAmount int64) {
t.Helper()
path := fmt.Sprintf("/api/admin/shop-series-allocations?shop_id=%d&series_id=%d", shopID, seriesID)
resp, err := env.AsSuperAdmin().Request("GET", path, nil)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
require.Equal(t, 0, result.Code)
data := result.Data.(map[string]interface{})
list := data["items"].([]interface{})
require.NotEmpty(t, list, "应存在分配记录")
allocation := list[0].(map[string]interface{})
assert.Equal(t, float64(expectedAmount), allocation["one_time_commission_amount"],
"店铺 %d 系列 %d 的佣金金额应为 %d", shopID, seriesID, expectedAmount)
}

View File

@@ -0,0 +1,847 @@
package acceptance
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/response"
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ============================================================
// 验收测试:套餐系列分配 (ShopSeriesAllocation)
// 来源openspec/changes/refactor-one-time-commission-allocation/specs/shop-series-allocation/spec.md
// ============================================================
func TestShopSeriesAllocation_Create_Acceptance(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
// 准备测试数据:创建店铺层级和套餐系列
parentShop := env.CreateTestShop("一级代理", 1, nil)
childShop := env.CreateTestShop("二级代理", 2, &parentShop.ID)
series := createTestPackageSeries(t, env, "测试系列")
// 先为一级代理创建系列分配(平台分配)
platformAllocation := createPlatformSeriesAllocation(t, env, parentShop.ID, series.ID, 10000)
// ------------------------------------------------------------
// Scenario: 成功分配套餐系列
// GIVEN: 代理已有该系列的分配权限
// WHEN: POST /api/admin/shop-series-allocations 设置 one_time_commission_amount = 5000
// THEN: 返回 200 和分配记录详情
// AND: 下级代理的一次性佣金上限为 50 元
//
// 破坏点:如果 Handler 不调用 Service.Create此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_成功分配套餐系列", func(t *testing.T) {
body := map[string]interface{}{
"shop_id": childShop.ID,
"series_id": series.ID,
"one_time_commission_amount": 5000, // 50 元
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code, "应返回成功: %s", result.Message)
// 验证响应包含 one_time_commission_amount
data, ok := result.Data.(map[string]interface{})
require.True(t, ok, "响应 data 应为对象")
assert.Equal(t, float64(5000), data["one_time_commission_amount"], "一次性佣金金额应为 5000 分")
})
// ------------------------------------------------------------
// Scenario: 下级金额不能超过上级
// GIVEN: 上级分配金额为 10000 分100 元)
// WHEN: 尝试为下级分配 15000 分150 元)
// THEN: 返回 400 错误 "一次性佣金金额不能超过您的分配上限"
//
// 破坏点:如果移除金额上限校验,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_下级金额不能超过上级", func(t *testing.T) {
newChildShop := env.CreateTestShop("新下级店铺", 2, &parentShop.ID)
body := map[string]interface{}{
"shop_id": newChildShop.ID,
"series_id": series.ID,
"one_time_commission_amount": 15000, // 超过上级的 10000
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 400, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code, "应返回错误")
assert.Contains(t, result.Message, "超过", "错误消息应包含'超过'")
})
// ------------------------------------------------------------
// Scenario: 分配时启用一次性佣金和强充
// GIVEN: 代理有分配权限
// WHEN: POST 创建分配,启用一次性佣金(累计充值触发,阈值 1000 元启用强充100 元)
// THEN: 系统保存完整配置
//
// 破坏点:如果不保存 enable_one_time_commission 等字段,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_分配时启用一次性佣金和强充", func(t *testing.T) {
newChildShop := env.CreateTestShop("新下级店铺2", 2, &parentShop.ID)
body := map[string]interface{}{
"shop_id": newChildShop.ID,
"series_id": series.ID,
"one_time_commission_amount": 5000,
"enable_one_time_commission": true,
"one_time_commission_trigger": "accumulated_recharge",
"one_time_commission_threshold": 100000, // 1000 元
"enable_force_recharge": true,
"force_recharge_amount": 10000, // 100 元
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
data := result.Data.(map[string]interface{})
assert.Equal(t, true, data["enable_one_time_commission"])
assert.Equal(t, "accumulated_recharge", data["one_time_commission_trigger"])
assert.Equal(t, float64(100000), data["one_time_commission_threshold"])
assert.Equal(t, true, data["enable_force_recharge"])
assert.Equal(t, float64(10000), data["force_recharge_amount"])
})
// ------------------------------------------------------------
// Scenario: 尝试分配未拥有的系列
// GIVEN: 代理没有某系列的分配权限
// WHEN: 尝试为下级分配该系列
// THEN: 返回 403/400 "您没有该套餐系列的分配权限"
//
// 破坏点:如果不检查代理是否拥有系列权限,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_尝试分配未拥有的系列", func(t *testing.T) {
unownedSeries := createTestPackageSeries(t, env, "未分配系列")
body := map[string]interface{}{
"shop_id": childShop.ID,
"series_id": unownedSeries.ID,
"one_time_commission_amount": 5000,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
// 应返回 400 或 403
assert.True(t, resp.StatusCode == 400 || resp.StatusCode == 403,
"应返回 400 或 403实际: %d", resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code)
})
// ------------------------------------------------------------
// Scenario: 尝试分配给非直属下级
// GIVEN: 店铺 A 是一级,店铺 B 是二级A 的下级),店铺 C 是三级B 的下级)
// WHEN: 店铺 A 尝试直接分配给店铺 C
// THEN: 返回 403 "只能为直属下级分配套餐"
//
// 破坏点:如果不检查是否为直属下级,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_尝试分配给非直属下级", func(t *testing.T) {
grandChildShop := env.CreateTestShop("三级代理", 3, &childShop.ID)
body := map[string]interface{}{
"shop_id": grandChildShop.ID, // 非直属下级
"series_id": series.ID,
"one_time_commission_amount": 5000,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.True(t, resp.StatusCode == 400 || resp.StatusCode == 403)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code)
})
// ------------------------------------------------------------
// Scenario: 重复分配同一系列
// GIVEN: 已为下级店铺分配了某系列
// WHEN: 再次尝试分配同一系列
// THEN: 返回 409 "该店铺已分配此套餐系列"
//
// 破坏点:如果不检查唯一索引,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_重复分配同一系列", func(t *testing.T) {
newChildShop := env.CreateTestShop("重复测试店铺", 2, &parentShop.ID)
// 第一次分配
body := map[string]interface{}{
"shop_id": newChildShop.ID,
"series_id": series.ID,
"one_time_commission_amount": 5000,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
// 第二次分配(应失败)
resp2, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp2.Body.Close()
assert.True(t, resp2.StatusCode == 400 || resp2.StatusCode == 409,
"重复分配应返回 400 或 409实际: %d", resp2.StatusCode)
})
_ = platformAllocation // 使用变量避免编译警告
}
func TestShopSeriesAllocation_List_Acceptance(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
parentShop := env.CreateTestShop("一级代理", 1, nil)
childShop := env.CreateTestShop("二级代理", 2, &parentShop.ID)
series := createTestPackageSeries(t, env, "列表测试系列")
// 创建分配记录
createPlatformSeriesAllocation(t, env, parentShop.ID, series.ID, 10000)
createSeriesAllocationDirectly(t, env, parentShop.ID, childShop.ID, series.ID, 5000)
// ------------------------------------------------------------
// Scenario: 查询所有分配
// GIVEN: 存在多条分配记录
// WHEN: GET /api/admin/shop-series-allocations 不带筛选条件
// THEN: 返回该代理创建的所有分配记录
// AND: 每条记录包含 one_time_commission_amount 字段
//
// 破坏点:如果 List API 不返回数据,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_查询所有分配", func(t *testing.T) {
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
Request("GET", "/api/admin/shop-series-allocations", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
// 验证返回列表格式
data := result.Data.(map[string]interface{})
items, ok := data["items"].([]interface{})
require.True(t, ok, "响应应包含 items 字段")
require.NotEmpty(t, items, "列表不应为空")
// 验证第一条记录包含 one_time_commission_amount
firstItem := items[0].(map[string]interface{})
_, hasAmount := firstItem["one_time_commission_amount"]
assert.True(t, hasAmount, "记录应包含 one_time_commission_amount 字段")
})
// ------------------------------------------------------------
// Scenario: 按店铺筛选
// GIVEN: 存在多个店铺的分配记录
// WHEN: GET /api/admin/shop-series-allocations?shop_id=xxx
// THEN: 只返回该店铺的分配记录
//
// 破坏点:如果不支持 shop_id 筛选,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_按店铺筛选", func(t *testing.T) {
path := fmt.Sprintf("/api/admin/shop-series-allocations?shop_id=%d", childShop.ID)
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
Request("GET", path, nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
// 验证筛选结果
data := result.Data.(map[string]interface{})
items := data["items"].([]interface{})
for _, item := range items {
record := item.(map[string]interface{})
assert.Equal(t, float64(childShop.ID), record["shop_id"],
"筛选结果应只包含指定店铺")
}
})
}
func TestShopSeriesAllocation_Update_Acceptance(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
parentShop := env.CreateTestShop("一级代理", 1, nil)
childShop := env.CreateTestShop("二级代理", 2, &parentShop.ID)
series := createTestPackageSeries(t, env, "更新测试系列")
createPlatformSeriesAllocation(t, env, parentShop.ID, series.ID, 10000)
allocation := createSeriesAllocationDirectly(t, env, parentShop.ID, childShop.ID, series.ID, 5000)
// ------------------------------------------------------------
// Scenario: 更新一次性佣金金额
// GIVEN: 存在一条分配记录,金额为 5000
// WHEN: PUT /api/admin/shop-series-allocations/:id 将金额改为 6000
// THEN: 更新成功,返回更新后的记录
//
// 破坏点:如果 Update API 不保存金额变更,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_更新一次性佣金金额", func(t *testing.T) {
body := map[string]interface{}{
"one_time_commission_amount": 6000,
}
jsonBody, _ := json.Marshal(body)
path := fmt.Sprintf("/api/admin/shop-series-allocations/%d", allocation.ID)
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
Request("PUT", path, jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
data := result.Data.(map[string]interface{})
assert.Equal(t, float64(6000), data["one_time_commission_amount"])
})
// ------------------------------------------------------------
// Scenario: 更新金额不能超过上级上限
// GIVEN: 上级分配金额上限为 10000
// WHEN: 尝试将金额更新为 15000
// THEN: 返回 400 错误
//
// 破坏点:如果更新时不检查金额上限,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_更新金额不能超过上级上限", func(t *testing.T) {
body := map[string]interface{}{
"one_time_commission_amount": 15000, // 超过上级的 10000
}
jsonBody, _ := json.Marshal(body)
path := fmt.Sprintf("/api/admin/shop-series-allocations/%d", allocation.ID)
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
Request("PUT", path, jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 400, resp.StatusCode)
})
// ------------------------------------------------------------
// Scenario: 更新强充配置
// GIVEN: 分配记录存在
// WHEN: PUT 启用强充,设置金额 100 元
// THEN: 配置更新成功
//
// 破坏点:如果不保存强充配置,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_更新强充配置", func(t *testing.T) {
body := map[string]interface{}{
"enable_force_recharge": true,
"force_recharge_amount": 10000,
}
jsonBody, _ := json.Marshal(body)
path := fmt.Sprintf("/api/admin/shop-series-allocations/%d", allocation.ID)
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
Request("PUT", path, jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
data := result.Data.(map[string]interface{})
assert.Equal(t, true, data["enable_force_recharge"])
assert.Equal(t, float64(10000), data["force_recharge_amount"])
})
// ------------------------------------------------------------
// Scenario: 更新不存在的分配
// GIVEN: 分配 ID 不存在
// WHEN: PUT /api/admin/shop-series-allocations/99999
// THEN: 返回 404 "分配记录不存在"
//
// 破坏点:如果不检查记录是否存在,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_更新不存在的分配", func(t *testing.T) {
body := map[string]interface{}{
"one_time_commission_amount": 5000,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
Request("PUT", "/api/admin/shop-series-allocations/99999", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 404, resp.StatusCode)
})
}
func TestShopSeriesAllocation_Delete_Acceptance(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
parentShop := env.CreateTestShop("一级代理", 1, nil)
childShop := env.CreateTestShop("二级代理", 2, &parentShop.ID)
series := createTestPackageSeries(t, env, "删除测试系列")
createPlatformSeriesAllocation(t, env, parentShop.ID, series.ID, 10000)
// ------------------------------------------------------------
// Scenario: 删除系列分配时检查套餐分配
// GIVEN: 系列分配存在,且有依赖的套餐分配
// WHEN: DELETE /api/admin/shop-series-allocations/:id
// THEN: 返回 400 "存在关联的套餐分配,无法删除"
//
// 破坏点:如果不检查套餐分配依赖,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_删除系列分配时检查套餐分配", func(t *testing.T) {
// 创建系列分配
allocation := createSeriesAllocationDirectly(t, env, parentShop.ID, childShop.ID, series.ID, 5000)
// 创建依赖的套餐分配
pkg := createTestPackage(t, env, series.ID, "测试套餐")
createPackageAllocationWithSeriesAllocation(t, env, childShop.ID, pkg.ID, allocation.ID)
// 尝试删除系列分配
path := fmt.Sprintf("/api/admin/shop-series-allocations/%d", allocation.ID)
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
Request("DELETE", path, nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 400, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Contains(t, result.Message, "关联", "错误消息应提及关联")
})
// ------------------------------------------------------------
// Scenario: 成功删除无依赖的系列分配
// GIVEN: 系列分配存在,无套餐分配依赖
// WHEN: DELETE /api/admin/shop-series-allocations/:id
// THEN: 删除成功
//
// 破坏点:如果 Delete API 不工作,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_成功删除无依赖的系列分配", func(t *testing.T) {
newChildShop := env.CreateTestShop("新下级", 2, &parentShop.ID)
allocation := createSeriesAllocationDirectly(t, env, parentShop.ID, newChildShop.ID, series.ID, 5000)
path := fmt.Sprintf("/api/admin/shop-series-allocations/%d", allocation.ID)
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
Request("DELETE", path, nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
})
}
func TestShopSeriesAllocation_Platform_Acceptance(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
parentShop := env.CreateTestShop("一级代理", 1, nil)
series := createTestPackageSeries(t, env, "平台分配测试系列")
// 设置系列的一次性佣金上限(假设固定 150 元)
setSeriesOneTimeCommissionLimit(t, env, series.ID, 15000)
// ------------------------------------------------------------
// Scenario: 平台为一级代理分配
// GIVEN: 平台管理员
// WHEN: POST 为一级代理分配套餐系列,设置 one_time_commission_amount = 10000
// THEN: 分配成功
//
// 破坏点:如果平台无法创建分配,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_平台为一级代理分配", func(t *testing.T) {
body := map[string]interface{}{
"shop_id": parentShop.ID,
"series_id": series.ID,
"one_time_commission_amount": 10000,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
})
// ------------------------------------------------------------
// Scenario: 平台可自由设定金额
// GIVEN: 平台管理员
// WHEN: 平台为一级代理分配任意金额(如 20000
// THEN: 分配成功(平台无上限限制)
//
// 破坏点:如果平台分配被上限限制,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_平台可自由设定金额", func(t *testing.T) {
newShop := env.CreateTestShop("新一级代理", 1, nil)
body := map[string]interface{}{
"shop_id": newShop.ID,
"series_id": series.ID,
"one_time_commission_amount": 20000,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
})
// ------------------------------------------------------------
// Scenario: 平台配置强充要求
// GIVEN: 平台管理员
// WHEN: POST 为一级代理分配系列启用强充force_recharge_amount = 10000
// THEN: 配置保存成功
//
// 破坏点:如果不保存强充配置,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_平台配置强充要求", func(t *testing.T) {
newShop := env.CreateTestShop("强充测试店铺", 1, nil)
body := map[string]interface{}{
"shop_id": newShop.ID,
"series_id": series.ID,
"one_time_commission_amount": 10000,
"enable_force_recharge": true,
"force_recharge_amount": 10000,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
data := result.Data.(map[string]interface{})
assert.Equal(t, true, data["enable_force_recharge"])
assert.Equal(t, float64(10000), data["force_recharge_amount"])
})
}
func TestShopPackageAllocation_SeriesDependency_Acceptance(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
parentShop := env.CreateTestShop("一级代理", 1, nil)
childShop := env.CreateTestShop("二级代理", 2, &parentShop.ID)
series := createTestPackageSeries(t, env, "依赖测试系列")
pkg := createTestPackage(t, env, series.ID, "依赖测试套餐")
// ------------------------------------------------------------
// Scenario: 未分配系列时分配套餐失败
// GIVEN: 下级店铺未被分配系列 X
// WHEN: 代理尝试为下级分配套餐 A属于系列 X
// THEN: 返回 400 "请先分配该套餐所属的系列"
//
// 破坏点:如果不检查系列分配依赖,此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_未分配系列时分配套餐失败", func(t *testing.T) {
body := map[string]interface{}{
"shop_id": childShop.ID,
"package_id": pkg.ID,
"cost_price": 5000,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().
Request("POST", "/api/admin/shop-package-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 400, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Contains(t, result.Message, "系列", "错误消息应提及系列")
})
// ------------------------------------------------------------
// Scenario: 先分配系列再分配套餐
// GIVEN: 下级店铺已被分配系列 X
// WHEN: 代理为下级分配套餐 A属于系列 X
// THEN: 分配成功,套餐分配关联到系列分配记录
//
// 破坏点:如果不关联 series_allocation_id此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_先分配系列再分配套餐", func(t *testing.T) {
// 先分配系列
createPlatformSeriesAllocation(t, env, parentShop.ID, series.ID, 10000)
seriesAllocation := createSeriesAllocationDirectly(t, env, parentShop.ID, childShop.ID, series.ID, 5000)
// 再分配套餐
body := map[string]interface{}{
"shop_id": childShop.ID,
"package_id": pkg.ID,
"cost_price": 5000,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().
Request("POST", "/api/admin/shop-package-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
// 验证关联
data := result.Data.(map[string]interface{})
assert.Equal(t, float64(seriesAllocation.ID), data["series_allocation_id"],
"套餐分配应关联到系列分配")
})
// ------------------------------------------------------------
// Scenario: 套餐分配只包含成本价
// GIVEN: 套餐分配 API
// WHEN: 创建或查询套餐分配
// THEN: 请求/响应只包含 cost_price不包含 one_time_commission_amount
//
// 破坏点:如果响应包含 one_time_commission_amount此测试将失败
// ------------------------------------------------------------
t.Run("Scenario_套餐分配只包含成本价", func(t *testing.T) {
// 查询已创建的套餐分配
resp, err := env.AsSuperAdmin().
Request("GET", fmt.Sprintf("/api/admin/shop-package-allocations?shop_id=%d", childShop.ID), nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
data := result.Data.(map[string]interface{})
items := data["items"].([]interface{})
if len(items) > 0 {
firstItem := items[0].(map[string]interface{})
_, hasCostPrice := firstItem["cost_price"]
_, hasOneTimeCommission := firstItem["one_time_commission_amount"]
assert.True(t, hasCostPrice, "应包含 cost_price")
assert.False(t, hasOneTimeCommission, "不应包含 one_time_commission_amount")
}
})
}
// ============================================================
// 辅助函数
// ============================================================
func createTestPackageSeries(t *testing.T, env *integ.IntegrationTestEnv, name string) *model.PackageSeries {
t.Helper()
timestamp := time.Now().UnixNano()
series := &model.PackageSeries{
SeriesCode: fmt.Sprintf("SERIES_%d", timestamp),
SeriesName: name,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(series).Error
require.NoError(t, err, "创建测试套餐系列失败")
return series
}
func createTestPackage(t *testing.T, env *integ.IntegrationTestEnv, seriesID uint, name string) *model.Package {
t.Helper()
timestamp := time.Now().UnixNano()
pkg := &model.Package{
PackageCode: fmt.Sprintf("PKG_%d", timestamp),
PackageName: name,
SeriesID: seriesID,
PackageType: "formal",
DurationMonths: 1,
CostPrice: 5000,
SuggestedRetailPrice: 9900,
Status: constants.StatusEnabled,
ShelfStatus: 1,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(pkg).Error
require.NoError(t, err, "创建测试套餐失败")
return pkg
}
func createTestAgentAccount(t *testing.T, env *integ.IntegrationTestEnv, shopID uint) *model.Account {
t.Helper()
return env.CreateTestAccount("agent", "password123", constants.UserTypeAgent, &shopID, nil)
}
// createPlatformSeriesAllocation 模拟平台为一级代理创建的系列分配
// 注意:由于 ShopSeriesAllocation 模型可能尚未创建,这里直接通过数据库操作模拟
func createPlatformSeriesAllocation(t *testing.T, env *integ.IntegrationTestEnv, shopID, seriesID uint, amount int64) *model.ShopSeriesAllocation {
t.Helper()
allocation := &model.ShopSeriesAllocation{
ShopID: shopID,
SeriesID: seriesID,
AllocatorShopID: 0, // 平台分配
OneTimeCommissionAmount: amount,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(allocation).Error
require.NoError(t, err, "创建平台系列分配失败")
return allocation
}
// createSeriesAllocationDirectly 直接在数据库创建系列分配记录
func createSeriesAllocationDirectly(t *testing.T, env *integ.IntegrationTestEnv, allocatorShopID, shopID, seriesID uint, amount int64) *model.ShopSeriesAllocation {
t.Helper()
allocation := &model.ShopSeriesAllocation{
ShopID: shopID,
SeriesID: seriesID,
AllocatorShopID: allocatorShopID,
OneTimeCommissionAmount: amount,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(allocation).Error
require.NoError(t, err, "创建系列分配失败")
return allocation
}
// createPackageAllocationWithSeriesAllocation 创建关联系列分配的套餐分配
func createPackageAllocationWithSeriesAllocation(t *testing.T, env *integ.IntegrationTestEnv, shopID, packageID, seriesAllocationID uint) *model.ShopPackageAllocation {
t.Helper()
allocation := &model.ShopPackageAllocation{
ShopID: shopID,
PackageID: packageID,
SeriesAllocationID: &seriesAllocationID,
CostPrice: 5000,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(allocation).Error
require.NoError(t, err, "创建套餐分配失败")
return allocation
}
// setSeriesOneTimeCommissionLimit 设置系列的一次性佣金上限(假设在 PackageSeries 或配置中)
func setSeriesOneTimeCommissionLimit(t *testing.T, env *integ.IntegrationTestEnv, seriesID uint, limit int64) {
t.Helper()
// 更新系列配置
err := env.TX.Model(&model.PackageSeries{}).Where("id = ?", seriesID).Updates(map[string]interface{}{
"enable_one_time_commission": true,
// 假设有 one_time_commission_config 字段存储配置
}).Error
require.NoError(t, err, "设置系列佣金上限失败")
}

541
tests/flows/README.md Normal file
View File

@@ -0,0 +1,541 @@
# 业务流程测试 (Flow Tests)
流程测试验证多个 API 组合的完整业务场景,确保端到端流程正确。
## 核心原则
1. **来源于 Spec Business Flow**:每个测试对应 Spec 中的一个 Business Flow
2. **跨 API 验证**:验证多个 API 调用的组合行为
3. **状态共享**:流程中的数据(如 ID在 steps 之间传递
4. **角色切换**:不同 step 可能由不同角色执行
5. **必须有破坏点和依赖声明**
## 目录结构
```
tests/flows/
├── README.md # 本文件
├── package_lifecycle_flow_test.go # 套餐完整生命周期
├── order_purchase_flow_test.go # 订单购买流程
├── commission_settlement_flow_test.go # 佣金结算流程
├── iot_card_import_activate_flow_test.go # IoT 卡导入激活流程
└── ...
```
## 测试模板
```go
package flows
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"junhong_cmp_fiber/tests/testutils"
)
// ============================================================
// 流程测试:{流程名称}
// 来源openspec/changes/{change-name}/specs/{capability}/spec.md
// 参与者:{角色1}, {角色2}, ...
// ============================================================
func TestFlow_{FlowName}(t *testing.T) {
env := testutils.NewIntegrationTestEnv(t)
// ========================================================
// 流程级共享状态
// 在 steps 之间传递的数据
// ========================================================
var (
resourceID uint
orderID uint
// 其他需要共享的状态...
)
// ------------------------------------------------------------
// Step 1: {步骤名称}
// 角色: {执行角色}
// 调用: {HTTP Method} {Path}
// 预期: {预期结果}
//
// 依赖: 无(首个步骤)
// 破坏点:{描述什么代码变更会导致此测试失败}
// ------------------------------------------------------------
t.Run("Step1_{步骤名称}", func(t *testing.T) {
client := env.AsSuperAdmin() // 或其他角色
body := map[string]interface{}{
// 请求体
}
resp, err := client.Request("POST", "/api/admin/xxx", body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
var result map[string]interface{}
err = resp.JSON(&result)
require.NoError(t, err)
// 提取共享状态
data := result["data"].(map[string]interface{})
resourceID = uint(data["id"].(float64))
require.NotZero(t, resourceID, "资源 ID 不能为空")
})
// ------------------------------------------------------------
// Step 2: {步骤名称}
// 角色: {执行角色}
// 调用: {HTTP Method} {Path}
// 预期: {预期结果}
//
// 依赖: Step 1 的 resourceID
// 破坏点:{描述什么代码变更会导致此测试失败}
// ------------------------------------------------------------
t.Run("Step2_{步骤名称}", func(t *testing.T) {
if resourceID == 0 {
t.Skip("依赖 Step 1 创建的 resourceID")
}
client := env.AsShopAgent(1) // 切换到代理商角色
resp, err := client.Request("GET", fmt.Sprintf("/api/admin/xxx/%d", resourceID), nil)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
// 验证和提取数据...
})
// 更多 steps...
}
```
## 流程测试 vs 验收测试
| 方面 | 验收测试 | 流程测试 |
|------|---------|---------|
| 来源 | Spec Scenario | Spec Business Flow |
| 粒度 | 单 API | 多 API 组合 |
| 状态 | 独立 | steps 之间共享 |
| 角色 | 通常单一 | 可能多角色 |
| 目的 | 验证 API 契约 | 验证业务场景 |
## 状态共享模式
### 使用包级变量
```go
func TestFlow_PackageLifecycle(t *testing.T) {
env := testutils.NewIntegrationTestEnv(t)
// 流程级共享状态
var (
packageID uint
allocationID uint
orderID uint
)
t.Run("Step1_创建套餐", func(t *testing.T) {
// ... 创建套餐
packageID = extractedID
})
t.Run("Step2_分配套餐", func(t *testing.T) {
if packageID == 0 {
t.Skip("依赖 Step 1")
}
// 使用 packageID
allocationID = extractedID
})
t.Run("Step3_创建订单", func(t *testing.T) {
if allocationID == 0 {
t.Skip("依赖 Step 2")
}
// 使用 allocationID
orderID = extractedID
})
}
```
### 依赖声明规范
每个 step 必须声明依赖:
```go
// ------------------------------------------------------------
// Step 3: 代理商查看可售套餐
// 角色: 代理商
// 调用: GET /api/admin/shop-packages
// 预期: 列表包含刚分配的套餐
//
// 依赖: Step 1 的 packageID, Step 2 的分配操作
// 破坏点:如果查询不按 shop_id 过滤,代理商会看到其他店铺的套餐
// ------------------------------------------------------------
t.Run("Step3_代理商查看可售套餐", func(t *testing.T) {
if packageID == 0 {
t.Skip("依赖 Step 1 创建的 packageID")
}
// ...
})
```
## 多角色流程
```go
func TestFlow_CrossRoleWorkflow(t *testing.T) {
env := testutils.NewIntegrationTestEnv(t)
var (
resourceID uint
shopID uint = 1
)
// Step 1: 平台管理员创建资源
t.Run("Step1_平台创建资源", func(t *testing.T) {
client := env.AsSuperAdmin()
// ...
resourceID = extractedID
})
// Step 2: 平台管理员分配给代理商
t.Run("Step2_分配给代理商", func(t *testing.T) {
client := env.AsSuperAdmin()
// ...
})
// Step 3: 代理商查看资源(角色切换!)
t.Run("Step3_代理商查看", func(t *testing.T) {
client := env.AsShopAgent(shopID) // 切换到代理商
// ...
})
// Step 4: 代理商创建订单
t.Run("Step4_代理商创建订单", func(t *testing.T) {
client := env.AsShopAgent(shopID)
// ...
})
// Step 5: 平台管理员查看统计(再次切换)
t.Run("Step5_平台查看统计", func(t *testing.T) {
client := env.AsSuperAdmin()
// ...
})
}
```
## 破坏点注释规范
流程测试的破坏点更侧重于跨 API 的影响:
```go
// 破坏点:如果套餐创建 API 不返回 ID后续步骤无法执行
// 破坏点:如果分配 API 不检查套餐是否存在,可能分配无效套餐
// 破坏点:如果代理商查询不过滤 shop_id会看到其他店铺的数据
// 破坏点:如果订单创建不验证套餐有效期,可能购买过期套餐
// 破坏点:如果佣金计算不在事务中,可能导致数据不一致
```
## 异常流程测试
流程测试也应覆盖异常场景:
```go
func TestFlow_PackageLifecycle_Exceptions(t *testing.T) {
env := testutils.NewIntegrationTestEnv(t)
// ------------------------------------------------------------
// 异常流程:尝试删除已分配的套餐
// 预期:删除失败,返回业务错误
// ------------------------------------------------------------
t.Run("Exception_删除已分配套餐", func(t *testing.T) {
// Step 1: 创建套餐
// Step 2: 分配给店铺
// Step 3: 尝试删除(预期失败)
})
// ------------------------------------------------------------
// 异常流程:代理商访问其他店铺的套餐
// 预期:访问被拒绝
// ------------------------------------------------------------
t.Run("Exception_跨店铺访问", func(t *testing.T) {
// Step 1: 平台创建并分配给店铺 A
// Step 2: 店铺 B 的代理商尝试访问(预期 403
})
}
```
## 运行测试
```bash
# 运行所有流程测试
source .env.local && go test -v ./tests/flows/...
# 运行特定流程
source .env.local && go test -v ./tests/flows/... -run TestFlow_PackageLifecycle
# 运行特定步骤
source .env.local && go test -v ./tests/flows/... -run "Step3"
```
## 与 Spec Business Flow 的对应关系
```markdown
# Spec 中的 Business Flow
### Flow: 套餐完整生命周期
**参与者**: 平台管理员, 代理商
**流程步骤**:
1. **创建套餐**
- 角色: 平台管理员
- 调用: POST /api/admin/packages
- 预期: 返回套餐 ID
2. **分配给代理商**
- 角色: 平台管理员
- 调用: POST /api/admin/shop-packages
- 输入: 套餐 ID + 店铺 ID
- 预期: 分配成功
3. **代理商查看可售套餐**
- 角色: 代理商
- 调用: GET /api/admin/shop-packages
- 预期: 列表包含刚分配的套餐
```
直接转换为测试代码:
```go
func TestFlow_PackageLifecycle(t *testing.T) {
env := testutils.NewIntegrationTestEnv(t)
var packageID uint
t.Run("Step1_平台管理员创建套餐", func(t *testing.T) {
// POST /api/admin/packages
})
t.Run("Step2_分配给代理商", func(t *testing.T) {
// POST /api/admin/shop-packages
})
t.Run("Step3_代理商查看可售套餐", func(t *testing.T) {
// GET /api/admin/shop-packages
})
}
```
## 完整示例
```go
package flows
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"junhong_cmp_fiber/tests/testutils"
)
// ============================================================
// 流程测试IoT 卡导入到激活完整流程
// 来源openspec/changes/iot-card-management/specs/iot-card/spec.md
// 参与者:平台管理员, 系统
// ============================================================
func TestFlow_IotCardImportActivate(t *testing.T) {
env := testutils.NewIntegrationTestEnv(t)
// 流程级共享状态
var (
taskID string
cardICCIDs []string
)
// ------------------------------------------------------------
// Step 1: 上传 CSV 文件
// 角色: 平台管理员
// 调用: POST /api/admin/iot-cards/import
// 预期: 返回导入任务 ID
//
// 依赖: 无
// 破坏点:如果文件上传不创建异步任务,后续无法追踪进度
// ------------------------------------------------------------
t.Run("Step1_上传CSV文件", func(t *testing.T) {
client := env.AsSuperAdmin()
// 创建测试 CSV 内容
csvContent := "iccid,msisdn,operator\n" +
"89860000000000000001,13800000001,中国移动\n" +
"89860000000000000002,13800000002,中国移动\n"
resp, err := client.UploadFile("POST", "/api/admin/iot-cards/import",
"file", "cards.csv", []byte(csvContent))
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
var result map[string]interface{}
err = resp.JSON(&result)
require.NoError(t, err)
data := result["data"].(map[string]interface{})
taskID = data["task_id"].(string)
require.NotEmpty(t, taskID, "任务 ID 不能为空")
})
// ------------------------------------------------------------
// Step 2: 查询导入任务状态
// 角色: 平台管理员
// 调用: GET /api/admin/iot-cards/import/{taskID}
// 预期: 任务状态为 completed导入成功数量 = 2
//
// 依赖: Step 1 的 taskID
// 破坏点:如果异步任务不更新状态,查询会一直返回 pending
// ------------------------------------------------------------
t.Run("Step2_查询导入状态", func(t *testing.T) {
if taskID == "" {
t.Skip("依赖 Step 1 创建的 taskID")
}
client := env.AsSuperAdmin()
// 轮询等待任务完成(最多等待 30 秒)
var status string
for i := 0; i < 30; i++ {
resp, err := client.Request("GET",
fmt.Sprintf("/api/admin/iot-cards/import/%s", taskID), nil)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
var result map[string]interface{}
resp.JSON(&result)
data := result["data"].(map[string]interface{})
status = data["status"].(string)
if status == "completed" || status == "failed" {
break
}
time.Sleep(time.Second)
}
require.Equal(t, "completed", status, "导入任务应该成功完成")
})
// ------------------------------------------------------------
// Step 3: 验证卡片已入库
// 角色: 平台管理员
// 调用: GET /api/admin/iot-cards
// 预期: 能查询到导入的卡片
//
// 依赖: Step 2 确认任务完成
// 破坏点:如果导入任务不写入数据库,查询不到卡片
// ------------------------------------------------------------
t.Run("Step3_验证卡片入库", func(t *testing.T) {
if taskID == "" {
t.Skip("依赖前置步骤")
}
client := env.AsSuperAdmin()
resp, err := client.Request("GET", "/api/admin/iot-cards", map[string]interface{}{
"iccid": "89860000000000000001",
})
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
var result map[string]interface{}
resp.JSON(&result)
data := result["data"].(map[string]interface{})
list := data["list"].([]interface{})
require.Len(t, list, 1, "应该能查询到导入的卡片")
card := list[0].(map[string]interface{})
cardICCIDs = append(cardICCIDs, card["iccid"].(string))
})
// ------------------------------------------------------------
// Step 4: 激活卡片
// 角色: 平台管理员
// 调用: POST /api/admin/iot-cards/{iccid}/activate
// 预期: 卡片状态变为 active
//
// 依赖: Step 3 获取的 cardICCIDs
// 破坏点:如果激活 API 不调用运营商接口,状态不会真正变化
// ------------------------------------------------------------
t.Run("Step4_激活卡片", func(t *testing.T) {
if len(cardICCIDs) == 0 {
t.Skip("依赖 Step 3 获取的卡片 ICCID")
}
client := env.AsSuperAdmin()
resp, err := client.Request("POST",
fmt.Sprintf("/api/admin/iot-cards/%s/activate", cardICCIDs[0]), nil)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
})
// ------------------------------------------------------------
// Step 5: 验证卡片状态
// 角色: 平台管理员
// 调用: GET /api/admin/iot-cards/{iccid}
// 预期: 卡片状态为 active
//
// 依赖: Step 4 激活操作
// 破坏点:如果激活后不更新数据库状态,查询还是旧状态
// ------------------------------------------------------------
t.Run("Step5_验证激活状态", func(t *testing.T) {
if len(cardICCIDs) == 0 {
t.Skip("依赖前置步骤")
}
client := env.AsSuperAdmin()
resp, err := client.Request("GET",
fmt.Sprintf("/api/admin/iot-cards/%s", cardICCIDs[0]), nil)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
var result map[string]interface{}
resp.JSON(&result)
data := result["data"].(map[string]interface{})
assert.Equal(t, "active", data["status"], "卡片状态应该是 active")
})
}
```
## 常见问题
### Q: Step 之间必须按顺序执行吗?
是的。Go 的 t.Run 保证同一个父测试内的子测试按顺序执行。如果前置 step 失败,后续 step 会因为依赖检查而 skip。
### Q: 如何处理异步操作?
使用轮询等待:
```go
// 等待异步任务完成
for i := 0; i < maxRetries; i++ {
status := checkStatus()
if status == "completed" {
break
}
time.Sleep(interval)
}
```
### Q: 流程测试太慢怎么办?
1. 使用 `t.Parallel()` 让不同流程并行(注意数据隔离)
2. 减少 sleep 时间,增加轮询频率
3. 考虑将部分验证移到验收测试

View File

@@ -0,0 +1,496 @@
package flows
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/response"
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ============================================================
// 流程测试:一次性佣金链式分配
// 来源openspec/changes/refactor-one-time-commission-allocation/specs/shop-series-allocation/spec.md
// 参与者:平台管理员, 一级代理, 二级代理, 三级代理
// ============================================================
func TestFlow_OneTimeCommissionChainAllocation(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
// ========================================================
// 流程级共享状态
// ========================================================
var (
seriesID uint
level1ShopID uint
level2ShopID uint
level3ShopID uint
level1AllocationID uint
level2AllocationID uint
level3AllocationID uint
packageID uint
level3PackageAllocID uint
)
// ------------------------------------------------------------
// Step 1: 平台创建套餐系列并启用一次性佣金
// 角色: 平台管理员
// 调用: POST /api/admin/package-series
// 预期: 返回系列 IDenable_one_time_commission = true
//
// 依赖: 无
// 破坏点:如果系列创建不支持 enable_one_time_commission后续分配无法启用
// ------------------------------------------------------------
t.Run("Step1_平台创建套餐系列", func(t *testing.T) {
body := map[string]interface{}{
"series_code": fmt.Sprintf("CHAIN_SERIES_%d", time.Now().UnixNano()),
"series_name": "链式分配测试系列",
"description": "测试一次性佣金链式分配",
"status": 1,
"enable_one_time_commission": true,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/package-series", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
require.Equal(t, 0, result.Code, "创建系列失败: %s", result.Message)
data := result.Data.(map[string]interface{})
seriesID = uint(data["id"].(float64))
require.NotZero(t, seriesID, "系列 ID 不能为空")
})
// ------------------------------------------------------------
// Step 2: 创建三级店铺层级
// 角色: 平台管理员
// 调用: POST /api/admin/shops (3次)
// 预期: 创建一级、二级、三级店铺
//
// 依赖: 无
// 破坏点:如果店铺层级关系不正确,后续分配权限检查会失败
// ------------------------------------------------------------
t.Run("Step2_创建三级店铺层级", func(t *testing.T) {
level1Shop := env.CreateTestShop("一级代理_链式", 1, nil)
level1ShopID = level1Shop.ID
require.NotZero(t, level1ShopID)
level2Shop := env.CreateTestShop("二级代理_链式", 2, &level1ShopID)
level2ShopID = level2Shop.ID
require.NotZero(t, level2ShopID)
level3Shop := env.CreateTestShop("三级代理_链式", 3, &level2ShopID)
level3ShopID = level3Shop.ID
require.NotZero(t, level3ShopID)
})
// ------------------------------------------------------------
// Step 3: 平台为一级代理分配系列(金额上限 100 元)
// 角色: 平台管理员
// 调用: POST /api/admin/shop-series-allocations
// 预期: 分配成功one_time_commission_amount = 10000
//
// 依赖: Step 1 的 seriesID, Step 2 的 level1ShopID
// 破坏点:如果平台无法分配系列,链式分配无法开始
// ------------------------------------------------------------
t.Run("Step3_平台为一级代理分配系列", func(t *testing.T) {
if seriesID == 0 || level1ShopID == 0 {
t.Skip("依赖前置步骤")
}
body := map[string]interface{}{
"shop_id": level1ShopID,
"series_id": seriesID,
"one_time_commission_amount": 10000,
"enable_one_time_commission": true,
"one_time_commission_trigger": "accumulated_recharge",
"one_time_commission_threshold": 100000,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
require.Equal(t, 0, result.Code, "平台分配失败: %s", result.Message)
data := result.Data.(map[string]interface{})
level1AllocationID = uint(data["id"].(float64))
assert.Equal(t, float64(10000), data["one_time_commission_amount"])
})
// ------------------------------------------------------------
// Step 4: 一级代理为二级代理分配系列(金额上限 80 元)
// 角色: 一级代理
// 调用: POST /api/admin/shop-series-allocations
// 预期: 分配成功one_time_commission_amount = 8000
//
// 依赖: Step 3 的 level1AllocationID
// 破坏点:如果一级无法为下级分配,链式传递中断
// ------------------------------------------------------------
t.Run("Step4_一级为二级分配系列", func(t *testing.T) {
if level1AllocationID == 0 {
t.Skip("依赖 Step 3")
}
level1Account := env.CreateTestAccount("level1_agent", "password123", constants.UserTypeAgent, &level1ShopID, nil)
body := map[string]interface{}{
"shop_id": level2ShopID,
"series_id": seriesID,
"one_time_commission_amount": 8000,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(level1Account).Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
require.Equal(t, 0, result.Code, "一级分配给二级失败: %s", result.Message)
data := result.Data.(map[string]interface{})
level2AllocationID = uint(data["id"].(float64))
assert.Equal(t, float64(8000), data["one_time_commission_amount"])
})
// ------------------------------------------------------------
// Step 5: 二级代理为三级代理分配系列(金额上限 50 元)
// 角色: 二级代理
// 调用: POST /api/admin/shop-series-allocations
// 预期: 分配成功one_time_commission_amount = 5000
//
// 依赖: Step 4 的 level2AllocationID
// 破坏点:如果二级无法为下级分配,链式传递中断
// ------------------------------------------------------------
t.Run("Step5_二级为三级分配系列", func(t *testing.T) {
if level2AllocationID == 0 {
t.Skip("依赖 Step 4")
}
level2Account := env.CreateTestAccount("level2_agent", "password123", constants.UserTypeAgent, &level2ShopID, nil)
body := map[string]interface{}{
"shop_id": level3ShopID,
"series_id": seriesID,
"one_time_commission_amount": 5000,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(level2Account).Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
require.Equal(t, 0, result.Code, "二级分配给三级失败: %s", result.Message)
data := result.Data.(map[string]interface{})
level3AllocationID = uint(data["id"].(float64))
assert.Equal(t, float64(5000), data["one_time_commission_amount"])
})
// ------------------------------------------------------------
// Step 6: 验证链式分配金额正确
// 角色: 平台管理员
// 调用: GET /api/admin/shop-series-allocations?shop_id=xxx&series_id=xxx (3次)
// 预期: 一级 10000二级 8000三级 5000
//
// 依赖: Step 3-5 的分配记录
// 破坏点:如果金额查询不正确,佣金计算会出错
// ------------------------------------------------------------
t.Run("Step6_验证链式分配金额", func(t *testing.T) {
if level3AllocationID == 0 {
t.Skip("依赖前置步骤")
}
verifyChainAllocationAmount(t, env, level1ShopID, seriesID, 10000)
verifyChainAllocationAmount(t, env, level2ShopID, seriesID, 8000)
verifyChainAllocationAmount(t, env, level3ShopID, seriesID, 5000)
})
// ------------------------------------------------------------
// Step 7: 平台创建套餐并关联系列
// 角色: 平台管理员
// 调用: POST /api/admin/packages
// 预期: 返回套餐 IDseries_id 正确关联
//
// 依赖: Step 1 的 seriesID
// 破坏点:如果套餐不关联系列,佣金计算无法找到配置
// ------------------------------------------------------------
t.Run("Step7_创建套餐", func(t *testing.T) {
if seriesID == 0 {
t.Skip("依赖 Step 1")
}
body := map[string]interface{}{
"package_code": fmt.Sprintf("CHAIN_PKG_%d", time.Now().UnixNano()),
"package_name": "链式分配测试套餐",
"series_id": seriesID,
"package_type": "formal",
"duration_months": 1,
"cost_price": 5000,
"suggested_retail_price": 9900,
"status": 1,
"shelf_status": 1,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/packages", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
require.Equal(t, 0, result.Code, "创建套餐失败: %s", result.Message)
data := result.Data.(map[string]interface{})
packageID = uint(data["id"].(float64))
require.NotZero(t, packageID)
})
// ------------------------------------------------------------
// Step 8: 为三级代理分配套餐(需先有系列分配)
// 角色: 平台管理员
// 调用: POST /api/admin/shop-package-allocations
// 预期: 分配成功series_allocation_id 关联到系列分配
//
// 依赖: Step 5 的 level3AllocationID, Step 7 的 packageID
// 破坏点:如果套餐分配不检查系列依赖,此测试将失败
// ------------------------------------------------------------
t.Run("Step8_为三级代理分配套餐", func(t *testing.T) {
if level3AllocationID == 0 || packageID == 0 {
t.Skip("依赖前置步骤")
}
body := map[string]interface{}{
"shop_id": level3ShopID,
"package_id": packageID,
"cost_price": 6000,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-package-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
require.Equal(t, 0, result.Code, "套餐分配失败: %s", result.Message)
data := result.Data.(map[string]interface{})
level3PackageAllocID = uint(data["id"].(float64))
if allocID, ok := data["series_allocation_id"]; ok && allocID != nil {
assert.Equal(t, float64(level3AllocationID), allocID,
"套餐分配应关联到系列分配")
}
})
// ------------------------------------------------------------
// Step 9: 验证完整分配链路
// 角色: 平台管理员
// 调用: GET APIs
// 预期: 所有分配记录正确关联
//
// 依赖: 所有前置步骤
// 破坏点:如果任何环节数据不一致,此验证将失败
// ------------------------------------------------------------
t.Run("Step9_验证完整分配链路", func(t *testing.T) {
if level3PackageAllocID == 0 {
t.Skip("依赖前置步骤")
}
assert.NotZero(t, seriesID, "系列已创建")
assert.NotZero(t, level1AllocationID, "一级系列分配已创建")
assert.NotZero(t, level2AllocationID, "二级系列分配已创建")
assert.NotZero(t, level3AllocationID, "三级系列分配已创建")
assert.NotZero(t, packageID, "套餐已创建")
assert.NotZero(t, level3PackageAllocID, "三级套餐分配已创建")
})
_ = level1AllocationID
_ = level3PackageAllocID
}
func TestFlow_OneTimeCommissionChainAllocation_Exceptions(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
// ------------------------------------------------------------
// 异常流程:下级金额超过上级上限
// 预期:分配失败,返回错误
// ------------------------------------------------------------
t.Run("Exception_下级金额超过上级", func(t *testing.T) {
parentShop := env.CreateTestShop("超限测试_父级", 1, nil)
childShop := env.CreateTestShop("超限测试_子级", 2, &parentShop.ID)
series := createFlowTestSeries(t, env, "超限测试系列")
createFlowPlatformAllocation(t, env, parentShop.ID, series.ID, 10000)
body := map[string]interface{}{
"shop_id": childShop.ID,
"series_id": series.ID,
"one_time_commission_amount": 15000,
}
jsonBody, _ := json.Marshal(body)
parentAccount := env.CreateTestAccount("parent_agent", "password123", constants.UserTypeAgent, &parentShop.ID, nil)
resp, err := env.AsUser(parentAccount).Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.True(t, resp.StatusCode == 400 || resp.StatusCode == 403,
"超限分配应返回 400 或 403实际: %d", resp.StatusCode)
})
// ------------------------------------------------------------
// 异常流程:未分配系列就分配套餐
// 预期:套餐分配失败
// ------------------------------------------------------------
t.Run("Exception_未分配系列就分配套餐", func(t *testing.T) {
shop := env.CreateTestShop("无系列分配店铺", 1, nil)
series := createFlowTestSeries(t, env, "未分配系列")
pkg := createFlowTestPackage(t, env, series.ID, "未分配测试套餐")
body := map[string]interface{}{
"shop_id": shop.ID,
"package_id": pkg.ID,
"cost_price": 5000,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-package-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 400, resp.StatusCode, "未分配系列时分配套餐应失败")
})
}
// ============================================================
// 辅助函数
// ============================================================
func verifyChainAllocationAmount(t *testing.T, env *integ.IntegrationTestEnv, shopID, seriesID uint, expectedAmount int64) {
t.Helper()
path := fmt.Sprintf("/api/admin/shop-series-allocations?shop_id=%d&series_id=%d", shopID, seriesID)
resp, err := env.AsSuperAdmin().Request("GET", path, nil)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
require.Equal(t, 0, result.Code)
data := result.Data.(map[string]interface{})
items := data["items"].([]interface{})
require.NotEmpty(t, items, "店铺 %d 应存在系列 %d 的分配记录", shopID, seriesID)
allocation := items[0].(map[string]interface{})
assert.Equal(t, float64(expectedAmount), allocation["one_time_commission_amount"],
"店铺 %d 的佣金金额应为 %d", shopID, expectedAmount)
}
func createFlowTestSeries(t *testing.T, env *integ.IntegrationTestEnv, name string) *model.PackageSeries {
t.Helper()
timestamp := time.Now().UnixNano()
series := &model.PackageSeries{
SeriesCode: fmt.Sprintf("FLOW_SERIES_%d", timestamp),
SeriesName: name,
Status: constants.StatusEnabled,
EnableOneTimeCommission: true,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(series).Error
require.NoError(t, err)
return series
}
func createFlowTestPackage(t *testing.T, env *integ.IntegrationTestEnv, seriesID uint, name string) *model.Package {
t.Helper()
timestamp := time.Now().UnixNano()
pkg := &model.Package{
PackageCode: fmt.Sprintf("FLOW_PKG_%d", timestamp),
PackageName: name,
SeriesID: seriesID,
PackageType: "formal",
DurationMonths: 1,
CostPrice: 5000,
SuggestedRetailPrice: 9900,
Status: constants.StatusEnabled,
ShelfStatus: 1,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(pkg).Error
require.NoError(t, err)
return pkg
}
func createFlowPlatformAllocation(t *testing.T, env *integ.IntegrationTestEnv, shopID, seriesID uint, amount int64) *model.ShopSeriesAllocation {
t.Helper()
allocation := &model.ShopSeriesAllocation{
ShopID: shopID,
SeriesID: seriesID,
AllocatorShopID: 0,
OneTimeCommissionAmount: amount,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(allocation).Error
require.NoError(t, err)
return allocation
}

View File

@@ -377,7 +377,6 @@ func TestDevice_BatchSetSeriesBinding(t *testing.T) {
t.Run("设置禁用的系列-应失败", func(t *testing.T) {
disabledSeries := createTestPackageSeries(t, env, "禁用系列")
env.TX.Model(&model.PackageSeries{}).Where("id = ?", disabledSeries.ID).Update("status", constants.StatusDisabled)
env.TX.Model(&model.ShopSeriesAllocation{}).Where("id = ?", disabledSeries.ID).Update("status", constants.StatusDisabled)
body := map[string]interface{}{
"device_ids": []uint{devices[2].ID},

View File

@@ -672,7 +672,6 @@ func TestIotCard_BatchSetSeriesBinding(t *testing.T) {
// 创建一个禁用的分配
disabledSeries := createTestPackageSeries(t, env, "禁用系列")
env.TX.Model(&model.PackageSeries{}).Where("id = ?", disabledSeries.ID).Update("status", constants.StatusDisabled)
env.TX.Model(&model.ShopSeriesAllocation{}).Where("id = ?", disabledSeries.ID).Update("status", constants.StatusDisabled)
body := map[string]interface{}{
"iccids": []string{cards[2].ICCID},
@@ -799,3 +798,64 @@ func TestIotCard_BatchSetSeriesBinding(t *testing.T) {
}
})
}
func createTestPackageSeries(t *testing.T, env *integ.IntegrationTestEnv, name string) *model.PackageSeries {
t.Helper()
timestamp := time.Now().UnixNano()
series := &model.PackageSeries{
SeriesCode: fmt.Sprintf("SERIES_%d", timestamp),
SeriesName: name,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(series).Error
require.NoError(t, err, "创建测试套餐系列失败")
return series
}
func createTestAllocation(t *testing.T, env *integ.IntegrationTestEnv, shopID, seriesID, allocatorShopID uint) *model.ShopPackageAllocation {
t.Helper()
timestamp := time.Now().UnixNano()
pkg := &model.Package{
PackageCode: fmt.Sprintf("PKG_%d", timestamp),
PackageName: "测试套餐",
SeriesID: seriesID,
PackageType: "formal",
DurationMonths: 1,
RealDataMB: 1024,
CostPrice: 5000,
SuggestedRetailPrice: 12800,
Status: constants.StatusEnabled,
ShelfStatus: 1,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(pkg).Error
require.NoError(t, err, "创建测试套餐失败")
allocation := &model.ShopPackageAllocation{
ShopID: shopID,
PackageID: pkg.ID,
AllocatorShopID: allocatorShopID,
CostPrice: 5000,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err = env.TX.Create(allocation).Error
require.NoError(t, err, "创建测试分配失败")
return allocation
}

View File

@@ -1,253 +0,0 @@
package integration
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/response"
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMyPackageAPI_ListMyPackages(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
parentShop := env.CreateTestShop("一级店铺", 1, nil)
childShop := env.CreateTestShop("二级店铺", 2, &parentShop.ID)
agentAccount := env.CreateTestAccount("agent_my_pkg", "password123", constants.UserTypeAgent, &childShop.ID, nil)
series := createTestPackageSeriesForMyPkg(t, env, "测试系列")
pkg := createTestPackageForMyPkg(t, env, series.ID, "测试套餐")
createTestAllocationForMyPkg(t, env, parentShop.ID, series.ID, 0)
createTestAllocationForMyPkg(t, env, childShop.ID, series.ID, parentShop.ID)
t.Run("代理查看可售套餐列表", func(t *testing.T) {
resp, err := env.AsUser(agentAccount).Request("GET", "/api/admin/my-packages?page=1&page_size=20", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code, "应返回成功: %s", result.Message)
t.Logf("ListMyPackages response: %+v", result.Data)
})
t.Run("按系列ID筛选", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/my-packages?series_id=%d", series.ID)
resp, err := env.AsUser(agentAccount).Request("GET", url, nil)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
})
t.Run("按套餐类型筛选", func(t *testing.T) {
resp, err := env.AsUser(agentAccount).Request("GET", "/api/admin/my-packages?package_type=formal", nil)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
})
_ = pkg
}
func TestMyPackageAPI_GetMyPackage(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
parentShop := env.CreateTestShop("一级店铺", 1, nil)
childShop := env.CreateTestShop("二级店铺", 2, &parentShop.ID)
agentAccount := env.CreateTestAccount("agent_get_pkg", "password123", constants.UserTypeAgent, &childShop.ID, nil)
series := createTestPackageSeriesForMyPkg(t, env, "测试系列")
pkg := createTestPackageForMyPkg(t, env, series.ID, "测试套餐")
createTestAllocationForMyPkg(t, env, parentShop.ID, series.ID, 0)
createTestAllocationForMyPkg(t, env, childShop.ID, series.ID, parentShop.ID)
t.Run("获取可售套餐详情", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/my-packages/%d", pkg.ID)
resp, err := env.AsUser(agentAccount).Request("GET", url, nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code, "应返回成功: %s", result.Message)
if result.Data != nil {
dataMap := result.Data.(map[string]interface{})
assert.Equal(t, float64(pkg.ID), dataMap["id"])
t.Logf("套餐详情: %+v", dataMap)
}
})
t.Run("获取不存在的套餐", func(t *testing.T) {
resp, err := env.AsUser(agentAccount).Request("GET", "/api/admin/my-packages/999999", nil)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code, "应返回错误码")
})
}
func TestMyPackageAPI_ListMySeriesAllocations(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
parentShop := env.CreateTestShop("一级店铺", 1, nil)
childShop := env.CreateTestShop("二级店铺", 2, &parentShop.ID)
agentAccount := env.CreateTestAccount("agent_series_alloc", "password123", constants.UserTypeAgent, &childShop.ID, nil)
series1 := createTestPackageSeriesForMyPkg(t, env, "系列1")
series2 := createTestPackageSeriesForMyPkg(t, env, "系列2")
createTestAllocationForMyPkg(t, env, parentShop.ID, series1.ID, 0)
createTestAllocationForMyPkg(t, env, childShop.ID, series1.ID, parentShop.ID)
createTestAllocationForMyPkg(t, env, parentShop.ID, series2.ID, 0)
createTestAllocationForMyPkg(t, env, childShop.ID, series2.ID, parentShop.ID)
t.Run("获取被分配的系列列表", func(t *testing.T) {
resp, err := env.AsUser(agentAccount).Request("GET", "/api/admin/my-series-allocations?page=1&page_size=20", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code, "应返回成功: %s", result.Message)
t.Logf("ListMySeriesAllocations response: %+v", result.Data)
})
t.Run("分页参数生效", func(t *testing.T) {
resp, err := env.AsUser(agentAccount).Request("GET", "/api/admin/my-series-allocations?page=1&page_size=1", nil)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
})
}
func TestMyPackageAPI_Auth(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
t.Run("未认证请求应返回错误", func(t *testing.T) {
resp, err := env.ClearAuth().Request("GET", "/api/admin/my-packages", nil)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code, "未认证请求应返回错误码")
})
t.Run("未认证访问系列分配列表", func(t *testing.T) {
resp, err := env.ClearAuth().Request("GET", "/api/admin/my-series-allocations", nil)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code, "未认证请求应返回错误码")
})
}
func createTestPackageSeriesForMyPkg(t *testing.T, env *integ.IntegrationTestEnv, name string) *model.PackageSeries {
t.Helper()
timestamp := time.Now().UnixNano()
series := &model.PackageSeries{
SeriesCode: fmt.Sprintf("SERIES_MY_%d", timestamp),
SeriesName: name,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(series).Error
require.NoError(t, err, "创建测试套餐系列失败")
return series
}
func createTestPackageForMyPkg(t *testing.T, env *integ.IntegrationTestEnv, seriesID uint, name string) *model.Package {
t.Helper()
timestamp := time.Now().UnixNano()
pkg := &model.Package{
PackageCode: fmt.Sprintf("PKG_%d", timestamp),
PackageName: name,
SeriesID: seriesID,
PackageType: "formal",
DurationMonths: 1,
DataType: "real",
RealDataMB: 1024,
DataAmountMB: 1024,
Price: 9900,
SuggestedRetailPrice: 12800,
Status: constants.StatusEnabled,
ShelfStatus: 1,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(pkg).Error
require.NoError(t, err, "创建测试套餐失败")
return pkg
}
func createTestAllocationForMyPkg(t *testing.T, env *integ.IntegrationTestEnv, shopID, seriesID, allocatorShopID uint) *model.ShopSeriesAllocation {
t.Helper()
allocation := &model.ShopSeriesAllocation{
ShopID: shopID,
SeriesID: seriesID,
AllocatorShopID: allocatorShopID,
BaseCommissionMode: "fixed",
BaseCommissionValue: 500,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(allocation).Error
require.NoError(t, err, "创建测试分配失败")
return allocation
}

View File

@@ -416,7 +416,6 @@ func TestPackageAPI_Get(t *testing.T) {
PackageName: "测试套餐",
PackageType: "formal",
DurationMonths: 12,
Price: 99900,
Status: constants.StatusEnabled,
ShelfStatus: 2,
BaseModel: model.BaseModel{
@@ -452,7 +451,6 @@ func TestPackageAPI_List(t *testing.T) {
PackageName: "列表测试套餐1",
PackageType: "formal",
DurationMonths: 12,
Price: 99900,
Status: constants.StatusEnabled,
ShelfStatus: 1,
BaseModel: model.BaseModel{Creator: 1},
@@ -462,7 +460,6 @@ func TestPackageAPI_List(t *testing.T) {
PackageName: "列表测试套餐2",
PackageType: "addon",
DurationMonths: 1,
Price: 9990,
Status: constants.StatusEnabled,
ShelfStatus: 2,
BaseModel: model.BaseModel{Creator: 1},
@@ -495,7 +492,6 @@ func TestPackageAPI_Update(t *testing.T) {
PackageName: "原始套餐名称",
PackageType: "formal",
DurationMonths: 12,
Price: 99900,
Status: constants.StatusEnabled,
ShelfStatus: 2,
BaseModel: model.BaseModel{
@@ -538,7 +534,6 @@ func TestPackageAPI_Delete(t *testing.T) {
PackageName: "测试套餐",
PackageType: "formal",
DurationMonths: 12,
Price: 99900,
Status: constants.StatusEnabled,
ShelfStatus: 2,
BaseModel: model.BaseModel{

View File

@@ -31,7 +31,6 @@ func TestBatchAllocationAPI_Create(t *testing.T) {
"mode": "fixed",
"value": 1000,
},
"enable_tier_commission": false,
}
jsonBody, _ := json.Marshal(body)
@@ -59,7 +58,6 @@ func TestBatchAllocationAPI_Create(t *testing.T) {
"mode": "percent",
"value": 200,
},
"enable_tier_commission": false,
}
jsonBody, _ := json.Marshal(body)
@@ -89,7 +87,6 @@ func TestBatchAllocationAPI_Create(t *testing.T) {
"mode": "fixed",
"value": 800,
},
"enable_tier_commission": false,
}
jsonBody, _ := json.Marshal(body)
@@ -115,7 +112,7 @@ func TestBatchAllocationAPI_Create(t *testing.T) {
"mode": "percent",
"value": 150,
},
"enable_tier_commission": true,
"tier_config": map[string]interface{}{
"period_type": "monthly",
"tier_type": "sales_count",
@@ -149,7 +146,6 @@ func TestBatchAllocationAPI_Create(t *testing.T) {
"mode": "fixed",
"value": 1000,
},
"enable_tier_commission": false,
}
jsonBody, _ := json.Marshal(body)
@@ -192,15 +188,15 @@ func createBatchTestPackages(t *testing.T, env *integ.IntegrationTestEnv, series
for i := 0; i < count; i++ {
pkg := &model.Package{
PackageCode: fmt.Sprintf("BATCH_PKG_%d_%d", timestamp, i),
PackageName: fmt.Sprintf("批量测试套餐%d", i+1),
SeriesID: seriesID,
PackageType: "formal",
DurationMonths: 1,
Price: 9900 + int64(i*1000),
SuggestedCostPrice: 5000 + int64(i*500),
Status: constants.StatusEnabled,
ShelfStatus: 1,
PackageCode: fmt.Sprintf("BATCH_PKG_%d_%d", timestamp, i),
PackageName: fmt.Sprintf("批量测试套餐%d", i+1),
SeriesID: seriesID,
PackageType: "formal",
DurationMonths: 1,
CostPrice: 5000 + int64(i*500),
SuggestedRetailPrice: 9900 + int64(i*1000),
Status: constants.StatusEnabled,
ShelfStatus: 1,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,

View File

@@ -179,15 +179,14 @@ func createPricingTestPackages(t *testing.T, env *integ.IntegrationTestEnv, seri
for i := 0; i < count; i++ {
pkg := &model.Package{
PackageCode: fmt.Sprintf("PRICING_PKG_%d_%d", timestamp, i),
PackageName: fmt.Sprintf("调价测试套餐%d", i+1),
SeriesID: seriesID,
PackageType: "formal",
DurationMonths: 1,
Price: 9900,
SuggestedCostPrice: 5000,
Status: constants.StatusEnabled,
ShelfStatus: 1,
PackageCode: fmt.Sprintf("PRICING_PKG_%d_%d", timestamp, i),
PackageName: fmt.Sprintf("调价测试套餐%d", i+1),
SeriesID: seriesID,
PackageType: "formal",
DurationMonths: 1,
CostPrice: 5000,
Status: constants.StatusEnabled,
ShelfStatus: 1,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
@@ -207,11 +206,11 @@ func createPricingTestAllocation(t *testing.T, env *integ.IntegrationTestEnv, sh
t.Helper()
allocation := &model.ShopPackageAllocation{
ShopID: shopID,
PackageID: packageID,
AllocationID: 0,
CostPrice: costPrice,
Status: constants.StatusEnabled,
ShopID: shopID,
PackageID: packageID,
AllocatorShopID: 0,
CostPrice: costPrice,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,

View File

@@ -1,579 +0,0 @@
package integration
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/response"
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ==================== 套餐系列分配 API 测试 ====================
func TestShopSeriesAllocationAPI_Create(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
shop := env.CreateTestShop("一级店铺", 1, nil)
series := createTestPackageSeries(t, env, "测试系列")
t.Run("平台为一级店铺分配套餐系列", func(t *testing.T) {
body := map[string]interface{}{
"shop_id": shop.ID,
"series_id": series.ID,
"base_commission": map[string]interface{}{
"mode": "fixed",
"value": 1000,
},
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code, "应返回成功: %s", result.Message)
if result.Data != nil {
dataMap := result.Data.(map[string]interface{})
assert.Equal(t, float64(shop.ID), dataMap["shop_id"])
assert.Equal(t, float64(series.ID), dataMap["series_id"])
if baseComm, ok := dataMap["base_commission"].(map[string]interface{}); ok {
assert.Equal(t, "fixed", baseComm["mode"])
assert.Equal(t, float64(1000), baseComm["value"])
}
t.Logf("创建的分配 ID: %v", dataMap["id"])
}
})
t.Run("一级店铺为二级店铺分配套餐系列", func(t *testing.T) {
parentShop := env.CreateTestShop("另一个一级店铺", 1, nil)
childShop := env.CreateTestShop("二级店铺", 2, &parentShop.ID)
agentAccount := env.CreateTestAccount("agent_create", "password123", constants.UserTypeAgent, &parentShop.ID, nil)
series2 := createTestPackageSeries(t, env, "系列2")
createTestAllocation(t, env, parentShop.ID, series2.ID, 0)
body := map[string]interface{}{
"shop_id": childShop.ID,
"series_id": series2.ID,
"base_commission": map[string]interface{}{
"mode": "percent",
"value": 100,
},
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(agentAccount).Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code, "应返回成功: %s", result.Message)
})
t.Run("平台不能为二级店铺分配", func(t *testing.T) {
parent := env.CreateTestShop("父店铺", 1, nil)
child := env.CreateTestShop("子店铺", 2, &parent.ID)
series3 := createTestPackageSeries(t, env, "系列3")
body := map[string]interface{}{
"shop_id": child.ID,
"series_id": series3.ID,
"base_commission": map[string]interface{}{
"mode": "fixed",
"value": 500,
},
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code, "平台不能为二级店铺分配")
})
t.Run("重复分配应失败", func(t *testing.T) {
newShop := env.CreateTestShop("新店铺", 1, nil)
series4 := createTestPackageSeries(t, env, "系列4")
createTestAllocation(t, env, newShop.ID, series4.ID, 0)
body := map[string]interface{}{
"shop_id": newShop.ID,
"series_id": series4.ID,
"base_commission": map[string]interface{}{
"mode": "fixed",
"value": 1000,
},
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code, "重复分配应返回错误")
})
}
func TestShopSeriesAllocationAPI_Get(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
shop := env.CreateTestShop("一级店铺", 1, nil)
series := createTestPackageSeries(t, env, "测试系列")
allocation := createTestAllocation(t, env, shop.ID, series.ID, 0)
t.Run("获取分配详情", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/shop-series-allocations/%d", allocation.ID)
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
dataMap := result.Data.(map[string]interface{})
assert.Equal(t, float64(allocation.ID), dataMap["id"])
assert.Equal(t, float64(shop.ID), dataMap["shop_id"])
})
t.Run("获取不存在的分配", func(t *testing.T) {
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/shop-series-allocations/999999", nil)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code, "应返回错误码")
})
}
func TestShopSeriesAllocationAPI_Update(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
shop := env.CreateTestShop("一级店铺", 1, nil)
series := createTestPackageSeries(t, env, "测试系列")
allocation := createTestAllocation(t, env, shop.ID, series.ID, 0)
t.Run("更新基础佣金", func(t *testing.T) {
body := map[string]interface{}{
"base_commission": map[string]interface{}{
"mode": "percent",
"value": 150,
},
}
jsonBody, _ := json.Marshal(body)
url := fmt.Sprintf("/api/admin/shop-series-allocations/%d", allocation.ID)
resp, err := env.AsSuperAdmin().Request("PUT", url, jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
dataMap := result.Data.(map[string]interface{})
if baseComm, ok := dataMap["base_commission"].(map[string]interface{}); ok {
assert.Equal(t, "percent", baseComm["mode"])
assert.Equal(t, float64(150), baseComm["value"])
}
})
t.Run("启用梯度佣金", func(t *testing.T) {
enableTier := true
body := map[string]interface{}{
"enable_tier_commission": &enableTier,
}
jsonBody, _ := json.Marshal(body)
url := fmt.Sprintf("/api/admin/shop-series-allocations/%d", allocation.ID)
resp, err := env.AsSuperAdmin().Request("PUT", url, jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
})
}
func TestShopSeriesAllocationAPI_Delete(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
shop := env.CreateTestShop("一级店铺", 1, nil)
series := createTestPackageSeries(t, env, "测试系列")
allocation := createTestAllocation(t, env, shop.ID, series.ID, 0)
t.Run("删除分配", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/shop-series-allocations/%d", allocation.ID)
resp, err := env.AsSuperAdmin().Request("DELETE", url, nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
getResp, err := env.AsSuperAdmin().Request("GET", url, nil)
require.NoError(t, err)
defer getResp.Body.Close()
var getResult response.Response
json.NewDecoder(getResp.Body).Decode(&getResult)
assert.NotEqual(t, 0, getResult.Code, "删除后应无法获取")
})
}
func TestShopSeriesAllocationAPI_List(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
shop1 := env.CreateTestShop("店铺1", 1, nil)
shop2 := env.CreateTestShop("店铺2", 1, nil)
series := createTestPackageSeries(t, env, "测试系列")
createTestAllocation(t, env, shop1.ID, series.ID, 0)
createTestAllocation(t, env, shop2.ID, series.ID, 0)
t.Run("获取分配列表", func(t *testing.T) {
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/shop-series-allocations?page=1&page_size=20", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
})
t.Run("按店铺ID筛选", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/shop-series-allocations?shop_id=%d", shop1.ID)
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
})
t.Run("按系列ID筛选", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/shop-series-allocations?series_id=%d", series.ID)
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
})
}
func TestShopSeriesAllocationAPI_UpdateStatus(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
shop := env.CreateTestShop("一级店铺", 1, nil)
series := createTestPackageSeries(t, env, "测试系列")
allocation := createTestAllocation(t, env, shop.ID, series.ID, 0)
t.Run("禁用分配", func(t *testing.T) {
body := map[string]interface{}{
"status": constants.StatusDisabled,
}
jsonBody, _ := json.Marshal(body)
url := fmt.Sprintf("/api/admin/shop-series-allocations/%d/status", allocation.ID)
resp, err := env.AsSuperAdmin().Request("PUT", url, jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
getURL := fmt.Sprintf("/api/admin/shop-series-allocations/%d", allocation.ID)
getResp, _ := env.AsSuperAdmin().Request("GET", getURL, nil)
defer getResp.Body.Close()
var getResult response.Response
json.NewDecoder(getResp.Body).Decode(&getResult)
dataMap := getResult.Data.(map[string]interface{})
assert.Equal(t, float64(constants.StatusDisabled), dataMap["status"])
})
t.Run("启用分配", func(t *testing.T) {
body := map[string]interface{}{
"status": constants.StatusEnabled,
}
jsonBody, _ := json.Marshal(body)
url := fmt.Sprintf("/api/admin/shop-series-allocations/%d/status", allocation.ID)
resp, err := env.AsSuperAdmin().Request("PUT", url, jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
})
}
// ==================== 一次性佣金配置测试 ====================
func TestShopSeriesAllocationAPI_OneTimeCommission(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
t.Run("创建分配-固定类型一次性佣金配置落库", func(t *testing.T) {
shop := env.CreateTestShop("一次性佣金测试店铺1", 1, nil)
series := createTestPackageSeries(t, env, "一次性佣金测试系列1")
body := map[string]interface{}{
"shop_id": shop.ID,
"series_id": series.ID,
"base_commission": map[string]interface{}{
"mode": "fixed",
"value": 1000,
},
"enable_one_time_commission": true,
"one_time_commission_config": map[string]interface{}{
"type": "fixed",
"trigger": "accumulated_recharge",
"threshold": 10000,
"mode": "fixed",
"value": 500,
},
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code, "应返回成功: %s", result.Message)
dataMap := result.Data.(map[string]interface{})
assert.Equal(t, true, dataMap["enable_one_time_commission"])
if cfg, ok := dataMap["one_time_commission_config"].(map[string]interface{}); ok {
assert.Equal(t, "fixed", cfg["type"])
assert.Equal(t, "accumulated_recharge", cfg["trigger"])
assert.Equal(t, float64(10000), cfg["threshold"])
assert.Equal(t, "fixed", cfg["mode"])
assert.Equal(t, float64(500), cfg["value"])
} else {
t.Error("一次性佣金配置应返回")
}
})
t.Run("创建分配-梯度类型一次性佣金配置落库", func(t *testing.T) {
shop := env.CreateTestShop("一次性佣金测试店铺2", 1, nil)
series := createTestPackageSeries(t, env, "一次性佣金测试系列2")
body := map[string]interface{}{
"shop_id": shop.ID,
"series_id": series.ID,
"base_commission": map[string]interface{}{
"mode": "fixed",
"value": 1000,
},
"enable_one_time_commission": true,
"one_time_commission_config": map[string]interface{}{
"type": "tiered",
"trigger": "single_recharge",
"threshold": 5000,
"tiers": []map[string]interface{}{
{"tier_type": "sales_count", "threshold": 10, "mode": "fixed", "value": 100},
{"tier_type": "sales_count", "threshold": 50, "mode": "fixed", "value": 500},
{"tier_type": "sales_amount", "threshold": 100000, "mode": "percent", "value": 50},
},
},
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code, "应返回成功: %s", result.Message)
dataMap := result.Data.(map[string]interface{})
assert.Equal(t, true, dataMap["enable_one_time_commission"])
if cfg, ok := dataMap["one_time_commission_config"].(map[string]interface{}); ok {
assert.Equal(t, "tiered", cfg["type"])
assert.Equal(t, "single_recharge", cfg["trigger"])
if tiers, ok := cfg["tiers"].([]interface{}); ok {
assert.Equal(t, 3, len(tiers), "应有3个梯度档位")
} else {
t.Error("梯度档位应返回")
}
}
})
t.Run("创建分配-启用一次性佣金但未提供配置应失败", func(t *testing.T) {
shop := env.CreateTestShop("一次性佣金测试店铺3", 1, nil)
series := createTestPackageSeries(t, env, "一次性佣金测试系列3")
body := map[string]interface{}{
"shop_id": shop.ID,
"series_id": series.ID,
"base_commission": map[string]interface{}{
"mode": "fixed",
"value": 1000,
},
"enable_one_time_commission": true,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-series-allocations", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code, "启用一次性佣金但未提供配置应失败")
})
t.Run("更新分配-更新一次性佣金配置", func(t *testing.T) {
shop := env.CreateTestShop("一次性佣金测试店铺4", 1, nil)
series := createTestPackageSeries(t, env, "一次性佣金测试系列4")
allocation := createTestAllocation(t, env, shop.ID, series.ID, 0)
enableOneTime := true
body := map[string]interface{}{
"enable_one_time_commission": &enableOneTime,
"one_time_commission_config": map[string]interface{}{
"type": "fixed",
"trigger": "accumulated_recharge",
"threshold": 20000,
"mode": "percent",
"value": 100,
},
}
jsonBody, _ := json.Marshal(body)
url := fmt.Sprintf("/api/admin/shop-series-allocations/%d", allocation.ID)
resp, err := env.AsSuperAdmin().Request("PUT", url, jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code, "更新应成功: %s", result.Message)
dataMap := result.Data.(map[string]interface{})
assert.Equal(t, true, dataMap["enable_one_time_commission"])
if cfg, ok := dataMap["one_time_commission_config"].(map[string]interface{}); ok {
assert.Equal(t, float64(20000), cfg["threshold"])
assert.Equal(t, "percent", cfg["mode"])
assert.Equal(t, float64(100), cfg["value"])
}
})
}
// ==================== 梯度佣金 API 测试 ====================
// ==================== 权限测试 ====================
func TestShopSeriesAllocationAPI_Auth(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
t.Run("未认证请求应返回错误", func(t *testing.T) {
resp, err := env.ClearAuth().Request("GET", "/api/admin/shop-series-allocations", nil)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code, "未认证请求应返回错误码")
})
}
// ==================== 辅助函数 ====================
// createTestPackageSeries 创建测试套餐系列
func createTestPackageSeries(t *testing.T, env *integ.IntegrationTestEnv, name string) *model.PackageSeries {
t.Helper()
timestamp := time.Now().UnixNano()
series := &model.PackageSeries{
SeriesCode: fmt.Sprintf("SERIES_%d", timestamp),
SeriesName: name,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(series).Error
require.NoError(t, err, "创建测试套餐系列失败")
return series
}
// createTestAllocation 创建测试分配
func createTestAllocation(t *testing.T, env *integ.IntegrationTestEnv, shopID, seriesID, allocatorShopID uint) *model.ShopSeriesAllocation {
t.Helper()
allocation := &model.ShopSeriesAllocation{
ShopID: shopID,
SeriesID: seriesID,
AllocatorShopID: allocatorShopID,
BaseCommissionMode: "fixed",
BaseCommissionValue: 1000,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := env.TX.Create(allocation).Error
require.NoError(t, err, "创建测试分配失败")
return allocation
}

View File

@@ -85,7 +85,6 @@ func GetTestDB(t *testing.T) *gorm.DB {
&model.PackageSeries{},
&model.Package{},
&model.ShopPackageAllocation{},
&model.ShopSeriesAllocation{},
&model.EnterpriseCardAuthorization{},
&model.EnterpriseDeviceAuthorization{},
&model.AssetAllocationRecord{},

View File

@@ -1,410 +0,0 @@
package unit
import (
"context"
"fmt"
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/service/commission_calculation"
"github.com/break/junhong_cmp_fiber/internal/service/commission_stats"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/tests/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
func TestCommissionCalculation_AccumulatedRecharge(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
ctx := context.Background()
t.Run("累计充值触发-每次支付都写回累计金额", func(t *testing.T) {
shop := &model.Shop{
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
ShopName: "测试店铺",
ShopCode: fmt.Sprintf("SHOP_%d", time.Now().UnixNano()),
Level: 1,
Status: constants.StatusEnabled,
}
require.NoError(t, tx.Create(shop).Error)
series := &model.PackageSeries{
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
SeriesCode: fmt.Sprintf("SERIES_%d", time.Now().UnixNano()),
SeriesName: "测试系列",
Status: constants.StatusEnabled,
}
require.NoError(t, tx.Create(series).Error)
allocation := &model.ShopSeriesAllocation{
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
ShopID: shop.ID,
SeriesID: series.ID,
AllocatorShopID: 0,
BaseCommissionMode: model.CommissionModeFixed,
BaseCommissionValue: 1000,
EnableOneTimeCommission: true,
OneTimeCommissionType: model.OneTimeCommissionTypeFixed,
OneTimeCommissionTrigger: model.OneTimeCommissionTriggerAccumulatedRecharge,
OneTimeCommissionThreshold: 10000,
OneTimeCommissionMode: model.CommissionModeFixed,
OneTimeCommissionValue: 500,
Status: constants.StatusEnabled,
}
require.NoError(t, tx.Create(allocation).Error)
card := &model.IotCard{
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
ICCID: fmt.Sprintf("898600%013d", time.Now().Unix()%10000000000000),
CardCategory: "normal",
CarrierID: 1,
CarrierType: "CMCC",
CarrierName: "中国移动",
Status: 3,
ShopID: &shop.ID,
SeriesID: &allocation.ID,
FirstCommissionPaid: false,
AccumulatedRecharge: 0,
}
require.NoError(t, tx.Create(card).Error)
wallet := &model.Wallet{
ResourceType: "shop",
ResourceID: shop.ID,
WalletType: "commission",
Balance: 0,
Version: 0,
Status: 1,
}
require.NoError(t, tx.Create(wallet).Error)
iotCardStore := postgres.NewIotCardStore(tx, rdb)
shopSeriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
t.Run("第一次支付-累计金额更新为3000", func(t *testing.T) {
order1 := &model.Order{
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
OrderNo: fmt.Sprintf("ORDER_%d_1", time.Now().UnixNano()),
OrderType: model.OrderTypeSingleCard,
BuyerType: model.BuyerTypeAgent,
BuyerID: shop.ID,
IotCardID: &card.ID,
SellerShopID: &shop.ID,
SeriesID: &series.ID,
TotalAmount: 3000,
SellerCostPrice: 2000,
PaymentStatus: model.PaymentStatusPaid,
CommissionStatus: model.CommissionStatusPending,
}
require.NoError(t, tx.Create(order1).Error)
cardBefore, err := iotCardStore.GetByID(ctx, card.ID)
require.NoError(t, err)
assert.Equal(t, int64(0), cardBefore.AccumulatedRecharge)
alloc, err := shopSeriesAllocationStore.GetByID(ctx, *card.SeriesID)
require.NoError(t, err)
if alloc.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
newAccumulated := cardBefore.AccumulatedRecharge + order1.TotalAmount
err := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).
Update("accumulated_recharge", newAccumulated).Error
require.NoError(t, err)
}
cardAfter, err := iotCardStore.GetByID(ctx, card.ID)
require.NoError(t, err)
assert.Equal(t, int64(3000), cardAfter.AccumulatedRecharge, "第一次支付后累计金额应为3000")
assert.False(t, cardAfter.FirstCommissionPaid, "未达阈值不应标记为已发放")
})
t.Run("第二次支付-累计金额更新为7000", func(t *testing.T) {
order2 := &model.Order{
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
OrderNo: fmt.Sprintf("ORDER_%d_2", time.Now().UnixNano()),
OrderType: model.OrderTypeSingleCard,
BuyerType: model.BuyerTypeAgent,
BuyerID: shop.ID,
IotCardID: &card.ID,
SellerShopID: &shop.ID,
SeriesID: &series.ID,
TotalAmount: 4000,
SellerCostPrice: 3000,
PaymentStatus: model.PaymentStatusPaid,
CommissionStatus: model.CommissionStatusPending,
}
require.NoError(t, tx.Create(order2).Error)
cardBefore, err := iotCardStore.GetByID(ctx, card.ID)
require.NoError(t, err)
assert.Equal(t, int64(3000), cardBefore.AccumulatedRecharge)
alloc, err := shopSeriesAllocationStore.GetByID(ctx, *card.SeriesID)
require.NoError(t, err)
if alloc.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
newAccumulated := cardBefore.AccumulatedRecharge + order2.TotalAmount
err := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).
Update("accumulated_recharge", newAccumulated).Error
require.NoError(t, err)
}
cardAfter, err := iotCardStore.GetByID(ctx, card.ID)
require.NoError(t, err)
assert.Equal(t, int64(7000), cardAfter.AccumulatedRecharge, "第二次支付后累计金额应为7000")
assert.False(t, cardAfter.FirstCommissionPaid, "仍未达阈值不应标记为已发放")
})
t.Run("第三次支付-累计金额更新为11000且达到阈值", func(t *testing.T) {
order3 := &model.Order{
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
OrderNo: fmt.Sprintf("ORDER_%d_3", time.Now().UnixNano()),
OrderType: model.OrderTypeSingleCard,
BuyerType: model.BuyerTypeAgent,
BuyerID: shop.ID,
IotCardID: &card.ID,
SellerShopID: &shop.ID,
SeriesID: &series.ID,
TotalAmount: 4000,
SellerCostPrice: 3000,
PaymentStatus: model.PaymentStatusPaid,
CommissionStatus: model.CommissionStatusPending,
}
require.NoError(t, tx.Create(order3).Error)
cardBefore, err := iotCardStore.GetByID(ctx, card.ID)
require.NoError(t, err)
assert.Equal(t, int64(7000), cardBefore.AccumulatedRecharge)
alloc, err := shopSeriesAllocationStore.GetByID(ctx, *card.SeriesID)
require.NoError(t, err)
if alloc.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
newAccumulated := cardBefore.AccumulatedRecharge + order3.TotalAmount
err := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).
Update("accumulated_recharge", newAccumulated).Error
require.NoError(t, err)
}
cardAfter, err := iotCardStore.GetByID(ctx, card.ID)
require.NoError(t, err)
assert.Equal(t, int64(11000), cardAfter.AccumulatedRecharge, "第三次支付后累计金额应为11000")
if cardAfter.AccumulatedRecharge >= alloc.OneTimeCommissionThreshold && !cardAfter.FirstCommissionPaid {
assert.GreaterOrEqual(t, cardAfter.AccumulatedRecharge, alloc.OneTimeCommissionThreshold, "累计金额已达阈值")
err := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).
Update("first_commission_paid", true).Error
require.NoError(t, err)
cardFinal, err := iotCardStore.GetByID(ctx, card.ID)
require.NoError(t, err)
assert.True(t, cardFinal.FirstCommissionPaid, "达到阈值应标记为已发放")
}
})
t.Run("第四次支付-累计金额继续更新但不重复发放", func(t *testing.T) {
order4 := &model.Order{
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
OrderNo: fmt.Sprintf("ORDER_%d_4", time.Now().UnixNano()),
OrderType: model.OrderTypeSingleCard,
BuyerType: model.BuyerTypeAgent,
BuyerID: shop.ID,
IotCardID: &card.ID,
SellerShopID: &shop.ID,
SeriesID: &series.ID,
TotalAmount: 3000,
SellerCostPrice: 2000,
PaymentStatus: model.PaymentStatusPaid,
CommissionStatus: model.CommissionStatusPending,
}
require.NoError(t, tx.Create(order4).Error)
cardBefore, err := iotCardStore.GetByID(ctx, card.ID)
require.NoError(t, err)
assert.Equal(t, int64(11000), cardBefore.AccumulatedRecharge)
assert.True(t, cardBefore.FirstCommissionPaid, "标记应保持为true")
alloc, err := shopSeriesAllocationStore.GetByID(ctx, *card.SeriesID)
require.NoError(t, err)
if alloc.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
newAccumulated := cardBefore.AccumulatedRecharge + order4.TotalAmount
err := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).
Update("accumulated_recharge", newAccumulated).Error
require.NoError(t, err)
}
cardAfter, err := iotCardStore.GetByID(ctx, card.ID)
require.NoError(t, err)
assert.Equal(t, int64(14000), cardAfter.AccumulatedRecharge, "第四次支付后累计金额应为14000")
assert.True(t, cardAfter.FirstCommissionPaid, "已发放标记不应改变")
})
})
}
func TestCommissionCalculation_OneTimeCommissionLogic(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
ctx := context.Background()
commissionRecordStore := postgres.NewCommissionRecordStore(tx, rdb)
shopStore := postgres.NewShopStore(tx, rdb)
shopSeriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
shopSeriesOneTimeCommissionTierStore := postgres.NewShopSeriesOneTimeCommissionTierStore(tx)
iotCardStore := postgres.NewIotCardStore(tx, rdb)
deviceStore := postgres.NewDeviceStore(tx, rdb)
walletStore := postgres.NewWalletStore(tx, rdb)
walletTransactionStore := postgres.NewWalletTransactionStore(tx, rdb)
orderStore := postgres.NewOrderStore(tx, rdb)
orderItemStore := postgres.NewOrderItemStore(tx, rdb)
packageStore := postgres.NewPackageStore(tx)
shopSeriesCommissionStatsStore := postgres.NewShopSeriesCommissionStatsStore(tx)
commissionStatsService := commission_stats.New(shopSeriesCommissionStatsStore)
logger, _ := zap.NewDevelopment()
commCalcService := commission_calculation.New(
tx,
commissionRecordStore,
shopStore,
shopSeriesAllocationStore,
shopSeriesOneTimeCommissionTierStore,
iotCardStore,
deviceStore,
walletStore,
walletTransactionStore,
orderStore,
orderItemStore,
packageStore,
commissionStatsService,
logger,
)
t.Run("单次充值触发-达到阈值时发放佣金", func(t *testing.T) {
shop := &model.Shop{
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
ShopName: "单次触发店铺",
ShopCode: fmt.Sprintf("SHOP_%d", time.Now().UnixNano()),
Level: 1,
Status: constants.StatusEnabled,
}
require.NoError(t, tx.Create(shop).Error)
series := &model.PackageSeries{
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
SeriesCode: fmt.Sprintf("SERIES_%d", time.Now().UnixNano()),
SeriesName: "单次触发系列",
Status: constants.StatusEnabled,
}
require.NoError(t, tx.Create(series).Error)
allocation := &model.ShopSeriesAllocation{
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
ShopID: shop.ID,
SeriesID: series.ID,
AllocatorShopID: 0,
BaseCommissionMode: model.CommissionModeFixed,
BaseCommissionValue: 500,
EnableOneTimeCommission: true,
OneTimeCommissionType: model.OneTimeCommissionTypeFixed,
OneTimeCommissionTrigger: model.OneTimeCommissionTriggerSingleRecharge,
OneTimeCommissionThreshold: 5000,
OneTimeCommissionMode: model.CommissionModeFixed,
OneTimeCommissionValue: 300,
Status: constants.StatusEnabled,
}
require.NoError(t, tx.Create(allocation).Error)
card := &model.IotCard{
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
ICCID: fmt.Sprintf("898600%013d", time.Now().Unix()%10000000000000+1),
CardCategory: "normal",
CarrierID: 1,
CarrierType: "CMCC",
CarrierName: "中国移动",
Status: 3,
ShopID: &shop.ID,
SeriesID: &allocation.ID,
FirstCommissionPaid: false,
AccumulatedRecharge: 0,
}
require.NoError(t, tx.Create(card).Error)
wallet := &model.Wallet{
ResourceType: "shop",
ResourceID: shop.ID,
WalletType: "commission",
Balance: 0,
Version: 0,
Status: 1,
}
require.NoError(t, tx.Create(wallet).Error)
t.Run("单次充值未达阈值-不触发", func(t *testing.T) {
order := &model.Order{
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
OrderNo: fmt.Sprintf("ORDER_%d", time.Now().UnixNano()),
OrderType: model.OrderTypeSingleCard,
BuyerType: model.BuyerTypeAgent,
BuyerID: shop.ID,
IotCardID: &card.ID,
SellerShopID: &shop.ID,
SeriesID: &series.ID,
TotalAmount: 3000,
SellerCostPrice: 2500,
PaymentStatus: model.PaymentStatusPaid,
CommissionStatus: model.CommissionStatusPending,
}
require.NoError(t, tx.Create(order).Error)
err := commCalcService.CalculateCommission(ctx, order.ID)
require.NoError(t, err)
cardAfter, err := iotCardStore.GetByID(ctx, card.ID)
require.NoError(t, err)
assert.False(t, cardAfter.FirstCommissionPaid, "单次充值未达阈值不应发放")
})
t.Run("单次充值达到阈值-触发", func(t *testing.T) {
order := &model.Order{
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
OrderNo: fmt.Sprintf("ORDER_%d", time.Now().UnixNano()+1),
OrderType: model.OrderTypeSingleCard,
BuyerType: model.BuyerTypeAgent,
BuyerID: shop.ID,
IotCardID: &card.ID,
SellerShopID: &shop.ID,
SeriesID: &series.ID,
TotalAmount: 6000,
SellerCostPrice: 5500,
PaymentStatus: model.PaymentStatusPaid,
CommissionStatus: model.CommissionStatusPending,
}
require.NoError(t, tx.Create(order).Error)
err := commCalcService.CalculateCommission(ctx, order.ID)
require.NoError(t, err)
cardAfter, err := iotCardStore.GetByID(ctx, card.ID)
require.NoError(t, err)
assert.True(t, cardAfter.FirstCommissionPaid, "单次充值达到阈值应发放")
var commRecords []model.CommissionRecord
err = tx.Where("order_id = ? AND commission_source = ?", order.ID, model.CommissionSourceOneTime).
Find(&commRecords).Error
require.NoError(t, err)
assert.Len(t, commRecords, 1, "应有一条一次性佣金记录")
assert.Equal(t, int64(300), commRecords[0].Amount, "佣金金额应为300")
})
})
}