diff --git a/internal/dao/taskDao.go b/internal/dao/taskDao.go index e595cbd..435e5ef 100644 --- a/internal/dao/taskDao.go +++ b/internal/dao/taskDao.go @@ -67,16 +67,29 @@ func normalizeBundleTaskType(bundleTaskType int) int { func taskStatsQueryOptions(bundleTaskType int) (int, int, string, string) { taskType := normalizeBundleTaskType(bundleTaskType) if taskType == model.BundleTaskTypePayLater { - // 先用后付任务分支:只取 contract_tpl_type=3 的增值先用后付订单(只有视频额度) - // 套餐先用后付主订单 (contract_tpl_type=2) 归到普通任务分支 - return taskType, model.BillingTypeBNPL, "bor.order_mode = 2 AND bor.contract_tpl_type = 3 AND bor.pay_later_status IN (1, 2)", "" + // 先用后付任务分支:仅纳入套餐订单 (order_type=2) 中 contract_tpl_type=3 的增值先用后付订单 + // 活跃判定:pay_later_status IN (1, 2) AND status IN (1, 2, 4) + // 不做"每用户最新"去重,沿用历史取所有命中订单 + return taskType, model.BillingTypeBNPL, ` + bor.order_type = 2 + AND bor.contract_tpl_type = 3 + AND bor.pay_later_status IN (1, 2) + AND bor.status IN (1, 2, 4) + `, "" } - // 普通任务分支:order_mode=1 的普通套餐 + contract_tpl_type=2 的套餐先用后付主订单 - // 套餐先用后付主订单用 BNPL 语义 (pay_later_status IN (1,2)) 判定活跃 + // 普通任务分支:两个互斥 case + // - Case A: 套餐-普通 (order_type=1, contract_tpl_type=1),仅已签已支付 (status=2) + // - Case B: 套餐先用后付的另一种存法 (order_type=2, contract_tpl_type=2), + // pay_later_status IN (1, 2) AND status IN (1, 2, 4) + // 每用户只保留 created_at 最新的一单 (created_at 相同时取 id 较大者) return taskType, model.BillingTypeNormal, ` - ((bor.status = 2 AND IFNULL(bor.order_mode, 1) = 1) - OR (bor.contract_tpl_type = 2 AND bor.pay_later_status IN (1, 2))) + ( + (bor.order_type = 1 AND bor.contract_tpl_type = 1 AND bor.status = 2) + OR + (bor.order_type = 2 AND bor.contract_tpl_type = 2 + AND bor.status IN (1, 2, 4) AND bor.pay_later_status IN (1, 2)) + ) `, ` AND NOT EXISTS ( SELECT 1 FROM bundle_order_records bor2 @@ -84,8 +97,10 @@ func taskStatsQueryOptions(bundleTaskType int) (int, int, string, string) { AND bor2.deleted_at IS NULL AND bor2.customer_id IS NOT NULL AND ( - (bor2.status = 2 AND IFNULL(bor2.order_mode, 1) = 1) - OR (bor2.contract_tpl_type = 2 AND bor2.pay_later_status IN (1, 2)) + (bor2.order_type = 1 AND bor2.contract_tpl_type = 1 AND bor2.status = 2) + OR + (bor2.order_type = 2 AND bor2.contract_tpl_type = 2 + AND bor2.status IN (1, 2, 4) AND bor2.pay_later_status IN (1, 2)) ) AND (bor2.created_at > bor.created_at OR (bor2.created_at = bor.created_at AND bor2.id > bor.id)) )` diff --git a/internal/dao/taskDao_test.go b/internal/dao/taskDao_test.go index f412fa7..1b6e39d 100644 --- a/internal/dao/taskDao_test.go +++ b/internal/dao/taskDao_test.go @@ -3,6 +3,7 @@ package dao import ( "errors" "regexp" + "strings" "testing" "github.com/DATA-DOG/go-sqlmock" @@ -10,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "gorm.io/gorm" + "micro-bundle/internal/model" "micro-bundle/internal/testutil" ) @@ -93,3 +95,82 @@ func gormNotFound() error { // 保留 errors 包,留给后续事务测试使用 dialError 等 var _ = errors.New + +// ---- taskStatsQueryOptions ---- + +// 工具函数:把字符串压成"压缩"形式,便于断言不依赖空白。 +// 例如把多行 raw-string 拼成一行,去掉首尾空白,折叠中间空白。 +func compactSQL(s string) string { + // 把任意空白序列替换成单个空格,再去掉首尾。 + re := regexp.MustCompile(`\s+`) + return strings.TrimSpace(re.ReplaceAllString(s, " ")) +} + +// TestTaskStatsQueryOptions_PayLater 验证先用后付分支的过滤条件。 +func TestTaskStatsQueryOptions_PayLater(t *testing.T) { + taskType, billingType, orderFilter, latestOrderFilter := taskStatsQueryOptions(model.BundleTaskTypePayLater) + + assert.Equal(t, model.BundleTaskTypePayLater, taskType) + assert.Equal(t, model.BillingTypeBNPL, billingType) + assert.Equal(t, "", latestOrderFilter, "pay-later 分支不应启用 NOT EXISTS 去重") + + compact := compactSQL(orderFilter) + // 必备条件 + // pay-later 分支对应增值先用后付,OrderType 必须是增值服务订单 (order_type=2) + assert.Contains(t, compact, "bor.order_type = 2") + assert.Contains(t, compact, "bor.contract_tpl_type = 3") + assert.Contains(t, compact, "bor.pay_later_status IN (1, 2)") + assert.Contains(t, compact, "bor.status IN (1, 2, 4)") + // 不应再依赖 order_mode + assert.NotContains(t, compact, "order_mode") +} + +// TestTaskStatsQueryOptions_Normal 验证普通任务分支的过滤条件。 +// 该分支由两个互斥 case 组成: +// - Case A: 套餐-普通 (order_type=1, contract_tpl_type=1, status=2) +// - Case B: 套餐先用后付的另一种存法 (order_type=2, contract_tpl_type=2, +// status IN (1,2,4) AND pay_later_status IN (1,2)) +func TestTaskStatsQueryOptions_Normal(t *testing.T) { + taskType, billingType, orderFilter, latestOrderFilter := taskStatsQueryOptions(model.BundleTaskTypeNormal) + + assert.Equal(t, model.BundleTaskTypeNormal, taskType) + assert.Equal(t, model.BillingTypeNormal, billingType) + + compact := compactSQL(orderFilter) + // Case A: 套餐-普通,仅已签已支付 + assert.Contains(t, compact, "bor.order_type = 1") + assert.Contains(t, compact, "bor.contract_tpl_type = 1") + assert.Contains(t, compact, "bor.status = 2") + // Case B: 套餐先用后付的另一种存法 + assert.Contains(t, compact, "bor.order_type = 2") + assert.Contains(t, compact, "bor.contract_tpl_type = 2") + assert.Contains(t, compact, "bor.status IN (1, 2, 4)") + assert.Contains(t, compact, "bor.pay_later_status IN (1, 2)") + // 不应再依赖 order_mode(回归保护) + assert.NotContains(t, compact, "order_mode") + + // NOT EXISTS 内层必须与外层对齐,使用 bor2 别名 + compactLatest := compactSQL(latestOrderFilter) + // Case A 在内层的镜像 + assert.Contains(t, compactLatest, "bor2.order_type = 1") + assert.Contains(t, compactLatest, "bor2.contract_tpl_type = 1") + assert.Contains(t, compactLatest, "bor2.status = 2") + // Case B 在内层的镜像 + assert.Contains(t, compactLatest, "bor2.order_type = 2") + assert.Contains(t, compactLatest, "bor2.contract_tpl_type = 2") + assert.Contains(t, compactLatest, "bor2.status IN (1, 2, 4)") + assert.Contains(t, compactLatest, "bor2.pay_later_status IN (1, 2)") + // 平局规则保持不变 + assert.Contains(t, compactLatest, "(bor2.created_at > bor.created_at OR (bor2.created_at = bor.created_at AND bor2.id > bor.id))") +} + +// TestTaskStatsQueryOptions_DefaultToNormal 非 pay-later 任意值都走普通任务分支。 +func TestTaskStatsQueryOptions_DefaultToNormal(t *testing.T) { + for _, v := range []int{0, 1, 99, -1} { + taskType, billingType, orderFilter, _ := taskStatsQueryOptions(v) + assert.Equal(t, model.BundleTaskTypeNormal, taskType, "input=%d 应走 normal 分支", v) + assert.Equal(t, model.BillingTypeNormal, billingType, "input=%d 计费类型应为 Normal", v) + assert.Contains(t, compactSQL(orderFilter), "bor.order_type = 1") + assert.NotContains(t, compactSQL(orderFilter), "order_mode") + } +}