调整GetPendingAssignBySubNums与GetArtistUploadStatsList的BundleOrder过滤规则

- taskStatsQueryOptions 改写:
  * 普通任务分支改为 Case A OR Case B:
    - Case A: order_type=1 AND contract_tpl_type=1 AND status=2 (套餐-普通,仅已签已支付)
    - Case B: order_type=2 AND contract_tpl_type=2 AND status IN (1,2,4) AND pay_later_status IN (1,2) (套餐先用后付存量)
  * 先用后付任务分支保持不变: order_type=2 AND contract_tpl_type=3 AND pay_later_status IN (1,2) AND status IN (1,2,4)
  * 去掉对 order_mode / IFNULL 的依赖,改用 order_type + contract_tpl_type 区分
  * NOT EXISTS 内层 bor2 谓词与外层完全对齐,保持"每用户取最新单"语义
This commit is contained in:
cjy 2026-06-11 13:05:04 +08:00
parent 6d5b96a7d0
commit 1572be00a2
2 changed files with 105 additions and 9 deletions

View File

@ -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))
)`

View File

@ -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")
}
}