- 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 谓词与外层完全对齐,保持"每用户取最新单"语义
177 lines
6.6 KiB
Go
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")
|
|
}
|
|
}
|