micro-bundle/internal/dao/taskDao_test.go
cjy 1572be00a2 调整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 谓词与外层完全对齐,保持"每用户取最新单"语义
2026-06-11 13:05:04 +08:00

177 lines
6.6 KiB
Go

package dao
import (
"errors"
"regexp"
"strings"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"micro-bundle/internal/model"
"micro-bundle/internal/testutil"
)
// ---- GetPendingTaskLayout ----
// sqlmock + gorm 配合:QueryMatcherEqual 要求 SQL 字面量精确匹配;但
// gorm 生成的字段顺序与时间字段处理可能因版本略有差异,这里用 Regexp 匹配
// 关键子串,避免对生成 SQL 的细节过度耦合。
//
// DAO 测试不用 t.Parallel():SetupTaskBenchDB 会改写全局 app.ModuleClients,
// 并行执行会导致 sqlmock 期望链互相污染。
var reSelectLayout = regexp.MustCompile(`SELECT \* FROM .task_pending_layout. WHERE id = \?`)
var reInsertLayout = regexp.MustCompile(`INSERT INTO .task_pending_layout. .*\)\s*VALUES \(.*\)\s*ON DUPLICATE KEY UPDATE .data.`)
var reInsertHidden = regexp.MustCompile(`INSERT INTO .task_assignee_hidden.`)
func TestGetPendingTaskLayout_NotFound(t *testing.T) {
mock := testutil.SetupTaskBenchDB(t)
mock.MatchExpectationsInOrder(false)
mock.ExpectQuery(reSelectLayout.String()).
WithArgs(1).
WillReturnError(gormNotFound())
got, err := GetPendingTaskLayout()
require.Error(t, err)
assert.Empty(t, got)
}
func TestGetPendingTaskLayout_Success(t *testing.T) {
mock := testutil.SetupTaskBenchDB(t)
mock.MatchExpectationsInOrder(false)
rows := sqlmock.NewRows([]string{"id", "data"}).AddRow(1, `[{"fieldKey":"subNum"}]`)
// gorm 的 First() 内部会带一个 LIMIT 参数,这里用 .* 兼容。
mock.ExpectQuery(reSelectLayout.String()).
WillReturnRows(rows)
got, err := GetPendingTaskLayout()
require.NoError(t, err)
assert.JSONEq(t, `[{"fieldKey":"subNum"}]`, got)
}
// ---- SetPendingTaskLayout ----
func TestSetPendingTaskLayout_Success(t *testing.T) {
mock := testutil.SetupTaskBenchDB(t)
mock.MatchExpectationsInOrder(false)
// gorm 会在 Create + OnConflict 时开事务,显式声明 Begin/Commit。
mock.ExpectBegin()
mock.ExpectExec(reInsertLayout.String()).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := SetPendingTaskLayout(`[{"fieldKey":"subNum"}]`)
require.NoError(t, err)
}
// ---- AddHiddenTaskAssignee ----
func TestAddHiddenTaskAssignee_Success(t *testing.T) {
mock := testutil.SetupTaskBenchDB(t)
mock.MatchExpectationsInOrder(false)
mock.ExpectBegin()
mock.ExpectExec(reInsertHidden.String()).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := AddHiddenTaskAssignee("员工A", "E001")
require.NoError(t, err)
}
// ---- helpers ----
// gormNotFound 返回 gorm.ErrRecordNotFound。
// gorm 内部 First()/Find() 命中零行时返回该错误。
func gormNotFound() error {
return gorm.ErrRecordNotFound
}
// 保留 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")
}
}