refactor: 一次性佣金配置从套餐级别提升到系列级别
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m29s
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:
322
tests/acceptance/README.md
Normal file
322
tests/acceptance/README.md
Normal 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)
|
||||
})
|
||||
```
|
||||
444
tests/acceptance/commission_calculation_acceptance_test.go
Normal file
444
tests/acceptance/commission_calculation_acceptance_test.go
Normal 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 = 10000(100元)
|
||||
// - 一级给二级:one_time_commission_amount = 8000(80元)
|
||||
// - 二级给三级:one_time_commission_amount = 5000(50元)
|
||||
// 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)
|
||||
}
|
||||
847
tests/acceptance/shop_series_allocation_acceptance_test.go
Normal file
847
tests/acceptance/shop_series_allocation_acceptance_test.go
Normal 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
541
tests/flows/README.md
Normal 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. 考虑将部分验证移到验收测试
|
||||
496
tests/flows/one_time_commission_chain_flow_test.go
Normal file
496
tests/flows/one_time_commission_chain_flow_test.go
Normal 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
|
||||
// 预期: 返回系列 ID,enable_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
|
||||
// 预期: 返回套餐 ID,series_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
|
||||
}
|
||||
@@ -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},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{},
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user