Files
junhong_cmp_fiber/tests/acceptance/commission_calculation_acceptance_test.go
huang b18ecfeb55
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m29s
refactor: 一次性佣金配置从套餐级别提升到系列级别
主要变更:
- 新增 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)
- 删除过时的单元测试(已被验收测试覆盖)
2026-02-04 14:28:44 +08:00

445 lines
16 KiB
Go
Raw Blame History

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