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) - 删除过时的单元测试(已被验收测试覆盖)
445 lines
16 KiB
Go
445 lines
16 KiB
Go
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)
|
||
}
|