Files
cmautobuy/admin/service/task_test.go
T

482 lines
17 KiB
Go
Raw Normal View History

package service
import (
"database/sql"
2026-08-09 21:51:20 +08:00
"fmt"
"testing"
"cmautobuy/admin/model"
"cmautobuy/admin/repository"
)
2026-08-09 21:51:20 +08:00
func TestListTasksView_统一每页20条并收敛越界页(t *testing.T) {
db := newTestDB(t)
for i := 0; i < 25; i++ {
insertTestTask(t, db, testTaskParams{
taskID: fmt.Sprintf("PAGE-%02d", i), taskType: model.TaskCollect,
status: model.TaskPending, pddGoodsID: fmt.Sprintf("%d", i+1),
})
}
second, err := ListTasksView(db, repository.TaskFilter{}, 2)
if err != nil {
t.Fatal(err)
}
if len(second.Rows) != 5 || second.Total != 25 || second.Page != 2 || second.TotalPages != 2 {
t.Fatalf("第二页结果不对: rows=%d total=%d page=%d/%d",
len(second.Rows), second.Total, second.Page, second.TotalPages)
}
overflow, err := ListTasksView(db, repository.TaskFilter{}, 999)
if err != nil {
t.Fatal(err)
}
if overflow.Page != 2 || len(overflow.Rows) != 5 {
t.Fatalf("越界页应收敛到末页: page=%d rows=%d", overflow.Page, len(overflow.Rows))
}
}
// insertTestTask 插一条任务,字段按需覆盖默认值,供本文件的用例使用。
// 跟 client_test.go 里的 insertTask 不一样:那个专为"领取"场景写死了固定值,
// 这里需要覆盖类型、状态、options 等更多维度。
type testTaskParams struct {
taskID string
taskType model.TaskType
status model.TaskStatus
assignedClient string
orderNo string
pddGoodsID string
pddOptions string
quantity int
maxPriceCent int64
errorCode string
errorMessage string
resultData string
claimedAt string
finishedAt string
}
func insertTestTask(t *testing.T, db *sql.DB, p testTaskParams) {
t.Helper()
now := model.NowISO()
var assigned, orderNo, pddGoodsID, pddOptions, errCode, errMsg, resultData, claimedAt, finishedAt any
if p.assignedClient != "" {
assigned = p.assignedClient
}
if p.orderNo != "" {
orderNo = p.orderNo
}
if p.pddGoodsID != "" {
pddGoodsID = p.pddGoodsID
}
if p.pddOptions != "" {
pddOptions = p.pddOptions
}
if p.errorCode != "" {
errCode = p.errorCode
}
if p.errorMessage != "" {
errMsg = p.errorMessage
}
if p.resultData != "" {
resultData = p.resultData
}
if p.claimedAt != "" {
claimedAt = p.claimedAt
}
if p.finishedAt != "" {
finishedAt = p.finishedAt
}
var quantity, maxPrice any
if p.quantity > 0 {
quantity = p.quantity
}
if p.maxPriceCent > 0 {
maxPrice = p.maxPriceCent
}
_, err := db.Exec(`
INSERT INTO tasks (task_id, task_type, status, assigned_client, claimed_at,
order_no, pdd_goods_url, pdd_goods_id, pdd_options,
quantity, max_price_cent,
error_code, error_message, result_data, finished_at,
created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, 'https://mobile.yangkeduo.com/goods.html?goods_id=1', ?, ?,
?, ?, ?, ?, ?, ?, ?, ?)`,
p.taskID, string(p.taskType), string(p.status), assigned, claimedAt,
orderNo, pddGoodsID, pddOptions,
quantity, maxPrice,
errCode, errMsg, resultData, finishedAt,
now, now)
if err != nil {
t.Fatalf("插入测试任务 %s 失败: %v", p.taskID, err)
}
}
// ── 列表:同时显示采集和采购 ──────────────────────────
func TestListTasksView_同时显示采集和采购(t *testing.T) {
db := newTestDB(t)
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
insertTestTask(t, db, testTaskParams{taskID: "PUR-1", taskType: model.TaskPurchase, status: model.TaskAssigned, orderNo: "SO-1"})
2026-08-09 21:51:20 +08:00
result, err := ListTasksView(db, repository.TaskFilter{}, 1)
if err != nil {
t.Fatalf("查询失败: %v", err)
}
if len(result.Rows) != 2 {
t.Fatalf("应该看到 2 条任务,实际 %d 条", len(result.Rows))
}
}
// ── 类型 / 状态 / 关键词筛选 ──────────────────────────
func TestListTasksView_类型筛选生效(t *testing.T) {
db := newTestDB(t)
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
insertTestTask(t, db, testTaskParams{taskID: "PUR-1", taskType: model.TaskPurchase, status: model.TaskAssigned, orderNo: "SO-1"})
2026-08-09 21:51:20 +08:00
result, err := ListTasksView(db, repository.TaskFilter{Type: model.TaskCollect}, 1)
if err != nil {
t.Fatalf("查询失败: %v", err)
}
if len(result.Rows) != 1 || result.Rows[0].TaskID != "COL-1" {
t.Fatalf("筛选采集应该只剩 COL-1,实际 %+v", result.Rows)
}
if !result.IsFiltered {
t.Error("有类型筛选时 IsFiltered 应该为 true")
}
}
func TestListTasksView_状态筛选生效(t *testing.T) {
db := newTestDB(t)
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
insertTestTask(t, db, testTaskParams{taskID: "COL-2", taskType: model.TaskCollect, status: model.TaskSucceeded, pddGoodsID: "222"})
2026-08-09 21:51:20 +08:00
result, err := ListTasksView(db, repository.TaskFilter{Status: model.TaskSucceeded}, 1)
if err != nil {
t.Fatalf("查询失败: %v", err)
}
if len(result.Rows) != 1 || result.Rows[0].TaskID != "COL-2" {
t.Fatalf("筛选'成功'应该只剩 COL-2,实际 %+v", result.Rows)
}
}
func TestListTasksView_关键词同时匹配任务编号订单号PDD商品ID(t *testing.T) {
db := newTestDB(t)
insertTestTask(t, db, testTaskParams{taskID: "COL-ABC", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "737116531267"})
insertTestTask(t, db, testTaskParams{taskID: "PUR-1", taskType: model.TaskPurchase, status: model.TaskAssigned, orderNo: "SO-XYZ"})
insertTestTask(t, db, testTaskParams{taskID: "PUR-2", taskType: model.TaskPurchase, status: model.TaskAssigned, orderNo: "SO-OTHER"})
// 按任务编号命中
2026-08-09 21:51:20 +08:00
r, err := ListTasksView(db, repository.TaskFilter{Keyword: "ABC"}, 1)
if err != nil || len(r.Rows) != 1 || r.Rows[0].TaskID != "COL-ABC" {
t.Fatalf("按任务编号搜索失败: err=%v rows=%+v", err, r)
}
// 按订单号命中
2026-08-09 21:51:20 +08:00
r, err = ListTasksView(db, repository.TaskFilter{Keyword: "XYZ"}, 1)
if err != nil || len(r.Rows) != 1 || r.Rows[0].TaskID != "PUR-1" {
t.Fatalf("按订单号搜索失败: err=%v rows=%+v", err, r)
}
// 按 PDD 商品 ID 命中
2026-08-09 21:51:20 +08:00
r, err = ListTasksView(db, repository.TaskFilter{Keyword: "737116531267"}, 1)
if err != nil || len(r.Rows) != 1 || r.Rows[0].TaskID != "COL-ABC" {
t.Fatalf("按 PDD 商品 ID 搜索失败: err=%v rows=%+v", err, r)
}
}
// ── 目标列 ────────────────────────────────────────────
func TestListTasksView_目标列_采集任务显示PDD商品ID(t *testing.T) {
db := newTestDB(t)
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "737116531267"})
2026-08-09 21:51:20 +08:00
r, err := ListTasksView(db, repository.TaskFilter{}, 1)
if err != nil {
t.Fatalf("查询失败: %v", err)
}
want := "PDD 737116531267"
if r.Rows[0].Target != want {
t.Errorf("目标列 = %q,want %q", r.Rows[0].Target, want)
}
}
func TestListTasksView_目标列_采集任务能join到标题时追加显示(t *testing.T) {
db := newTestDB(t)
if _, err := db.Exec(`
INSERT INTO pdd_products (goods_id, url, title, collect_status, created_at, updated_at)
VALUES ('737116531267', 'https://mobile.yangkeduo.com/goods.html?goods_id=737116531267',
'秋冬保暖加绒卫衣', 'collected', ?, ?)`, model.NowISO(), model.NowISO()); err != nil {
t.Fatalf("插入 PDD 商品失败: %v", err)
}
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskSucceeded, pddGoodsID: "737116531267"})
2026-08-09 21:51:20 +08:00
r, err := ListTasksView(db, repository.TaskFilter{}, 1)
if err != nil {
t.Fatalf("查询失败: %v", err)
}
want := "PDD 737116531267 · 秋冬保暖加绒卫衣"
if r.Rows[0].Target != want {
t.Errorf("目标列 = %q,want %q", r.Rows[0].Target, want)
}
}
func TestListTasksView_目标列_商品已软删除时退回显示商品ID(t *testing.T) {
db := newTestDB(t)
now := model.NowISO()
if _, err := db.Exec(`
INSERT INTO pdd_products (goods_id, url, title, collect_status, deleted_at, created_at, updated_at)
VALUES ('737116531267', 'https://mobile.yangkeduo.com/goods.html?goods_id=737116531267',
'秋冬保暖加绒卫衣', 'collected', ?, ?, ?)`, now, now, now); err != nil {
t.Fatalf("插入 PDD 商品失败: %v", err)
}
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "737116531267"})
2026-08-09 21:51:20 +08:00
r, err := ListTasksView(db, repository.TaskFilter{}, 1)
if err != nil {
t.Fatalf("查询失败: %v", err)
}
want := "PDD 737116531267"
if r.Rows[0].Target != want {
t.Errorf("商品已软删除时应退回只显示商品 ID,实际 %q", r.Rows[0].Target)
}
}
func TestListTasksView_目标列_采购任务显示订单号规格数量价格上限(t *testing.T) {
db := newTestDB(t)
insertTestTask(t, db, testTaskParams{
taskID: "PUR-1", taskType: model.TaskPurchase, status: model.TaskAssigned,
orderNo: "SO-001", pddOptions: `{"color":"黑色","size":"M"}`,
quantity: 2, maxPriceCent: 4200,
})
2026-08-09 21:51:20 +08:00
r, err := ListTasksView(db, repository.TaskFilter{}, 1)
if err != nil {
t.Fatalf("查询失败: %v", err)
}
want := "SO-001 · 黑色/M · 2件 · ≤¥42.00"
if r.Rows[0].Target != want {
t.Errorf("目标列 = %q,want %q", r.Rows[0].Target, want)
}
}
// ── 客户端列 ──────────────────────────────────────────
func TestListTasksView_无主任务客户端列显示占位符(t *testing.T) {
db := newTestDB(t)
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
2026-08-09 21:51:20 +08:00
r, err := ListTasksView(db, repository.TaskFilter{}, 1)
if err != nil {
t.Fatalf("查询失败: %v", err)
}
if r.Rows[0].ClientText != "—" {
t.Errorf("无主任务客户端列应该显示占位符,实际 %q", r.Rows[0].ClientText)
}
}
// ── 统计跟随筛选 ──────────────────────────────────────
func TestListTasksView_统计跟随当前筛选(t *testing.T) {
db := newTestDB(t)
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
insertTestTask(t, db, testTaskParams{taskID: "COL-2", taskType: model.TaskCollect, status: model.TaskSucceeded, pddGoodsID: "222"})
insertTestTask(t, db, testTaskParams{taskID: "PUR-1", taskType: model.TaskPurchase, status: model.TaskAssigned, orderNo: "SO-1"})
insertTestTask(t, db, testTaskParams{taskID: "PUR-2", taskType: model.TaskPurchase, status: model.TaskAssigned, orderNo: "SO-2"})
2026-08-09 21:51:20 +08:00
all, err := ListTasksView(db, repository.TaskFilter{}, 1)
if err != nil {
t.Fatalf("查询失败: %v", err)
}
if all.Total != 4 {
t.Fatalf("不筛选时总数应该是 4,实际 %d", all.Total)
}
2026-08-09 21:51:20 +08:00
collectOnly, err := ListTasksView(db, repository.TaskFilter{Type: model.TaskCollect}, 1)
if err != nil {
t.Fatalf("查询失败: %v", err)
}
if collectOnly.Total != 2 {
t.Fatalf("筛选采集后总数应该是 2,实际 %d", collectOnly.Total)
}
if collectOnly.Counts[model.TaskAssigned] != 0 {
t.Errorf("筛选采集后不该统计到采购任务的'待领取',实际 %d", collectOnly.Counts[model.TaskAssigned])
}
if collectOnly.Counts[model.TaskPending] != 1 || collectOnly.Counts[model.TaskSucceeded] != 1 {
t.Errorf("筛选采集后的状态分布不对: %+v", collectOnly.Counts)
}
}
// ── 空状态 ────────────────────────────────────────────
func TestListTasksView_从未建过任务时不算筛选无结果(t *testing.T) {
db := newTestDB(t)
2026-08-09 21:51:20 +08:00
r, err := ListTasksView(db, repository.TaskFilter{}, 1)
if err != nil {
t.Fatalf("查询失败: %v", err)
}
if len(r.Rows) != 0 {
t.Fatalf("空库应该查出 0 条")
}
if r.IsFiltered {
t.Error("没有任何筛选条件时 IsFiltered 应该为 false,模板据此显示'还没有任务'而不是'筛选无结果'")
}
}
func TestListTasksView_筛选无结果时IsFiltered为true(t *testing.T) {
db := newTestDB(t)
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
2026-08-09 21:51:20 +08:00
r, err := ListTasksView(db, repository.TaskFilter{Type: model.TaskPurchase}, 1)
if err != nil {
t.Fatalf("查询失败: %v", err)
}
if len(r.Rows) != 0 {
t.Fatalf("筛选采购应该查出 0 条")
}
if !r.IsFiltered {
t.Error("筛选出 0 条时 IsFiltered 应该为 true,模板据此显示'筛选无结果'而不是'还没有任务'")
}
}
// ── 详情弹窗 ──────────────────────────────────────────
func TestGetTaskDetail_采购任务显示专有字段(t *testing.T) {
db := newTestDB(t)
insertTestTask(t, db, testTaskParams{
taskID: "PUR-1", taskType: model.TaskPurchase, status: model.TaskClaimed,
orderNo: "SO-001", pddOptions: `{"color":"黑色","size":"M"}`,
quantity: 2, maxPriceCent: 4200, assignedClient: "client-001",
claimedAt: model.NowISO(),
})
d, err := GetTaskDetail(db, "PUR-1")
if err != nil {
t.Fatalf("查询失败: %v", err)
}
if d == nil {
t.Fatal("任务应该存在")
}
if !d.IsPurchase {
t.Error("采购任务 IsPurchase 应该为 true")
}
if d.SpecText != "黑色/M" || d.QuantityText != "2件" || d.PriceLimitText != "≤¥42.00" {
t.Errorf("采购专有字段不对: spec=%q qty=%q price=%q", d.SpecText, d.QuantityText, d.PriceLimitText)
}
if d.ClientText != "client-001" {
t.Errorf("分配客户端 = %q, want client-001", d.ClientText)
}
}
func TestGetTaskDetail_采集任务不带采购专有字段(t *testing.T) {
db := newTestDB(t)
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
d, err := GetTaskDetail(db, "COL-1")
if err != nil {
t.Fatalf("查询失败: %v", err)
}
if d.IsPurchase {
t.Error("采集任务 IsPurchase 应该为 false,模板据此隐藏采购专有字段")
}
if d.SpecText != "" || d.QuantityText != "" || d.PriceLimitText != "" {
t.Errorf("采集任务不该填充采购专有字段: spec=%q qty=%q price=%q", d.SpecText, d.QuantityText, d.PriceLimitText)
}
}
func TestGetTaskDetail_有错误信息时显示(t *testing.T) {
db := newTestDB(t)
insertTestTask(t, db, testTaskParams{
taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskFailed,
pddGoodsID: "111", errorCode: "PDD_PAGE_TIMEOUT", errorMessage: "商品页加载超时",
})
d, err := GetTaskDetail(db, "COL-1")
if err != nil {
t.Fatalf("查询失败: %v", err)
}
if !d.HasError {
t.Fatal("应该检测到错误信息")
}
if d.ErrorCode != "PDD_PAGE_TIMEOUT" || d.ErrorMessage != "商品页加载超时" {
t.Errorf("错误信息不对: code=%q msg=%q", d.ErrorCode, d.ErrorMessage)
}
}
func TestGetTaskDetail_没有错误信息时不显示错误段(t *testing.T) {
db := newTestDB(t)
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
d, err := GetTaskDetail(db, "COL-1")
if err != nil {
t.Fatalf("查询失败: %v", err)
}
if d.HasError {
t.Error("没有错误码和错误说明时 HasError 应该为 false")
}
}
func TestGetTaskDetail_任务不存在返回nil(t *testing.T) {
db := newTestDB(t)
d, err := GetTaskDetail(db, "NOT-EXIST")
if err != nil {
t.Fatalf("不该报错: %v", err)
}
if d != nil {
t.Error("不存在的任务应该返回 nil")
}
}
// ── 删除 ──────────────────────────────────────────────
func TestDeleteTasks_批量删除(t *testing.T) {
db := newTestDB(t)
insertTestTask(t, db, testTaskParams{taskID: "COL-1", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "111"})
insertTestTask(t, db, testTaskParams{taskID: "COL-2", taskType: model.TaskCollect, status: model.TaskPending, pddGoodsID: "222"})
n, err := DeleteTasks(db, []string{"COL-1"})
if err != nil {
t.Fatalf("删除失败: %v", err)
}
if n != 1 {
t.Fatalf("应该删除 1 条,实际 %d", n)
}
2026-08-09 21:51:20 +08:00
r, err := ListTasksView(db, repository.TaskFilter{}, 1)
if err != nil {
t.Fatalf("查询失败: %v", err)
}
if len(r.Rows) != 1 || r.Rows[0].TaskID != "COL-2" {
t.Fatalf("剩余任务应该只有 COL-2,实际 %+v", r.Rows)
}
}
// ── 目标列拼接的边界情况 ──────────────────────────────
func TestSpecText_空或无法解析时返回占位符(t *testing.T) {
if got := specText(""); got != placeholder {
t.Errorf("空 options: got %q", got)
}
if got := specText("not json"); got != placeholder {
t.Errorf("无法解析: got %q", got)
}
}
func TestQuantityText_零或负数返回占位符(t *testing.T) {
if got := quantityText(0); got != placeholder+"件" {
t.Errorf("数量为 0: got %q", got)
}
if got := quantityText(3); got != "3件" {
t.Errorf("数量为 3: got %q", got)
}
}
func TestPriceLimitText_零或负数返回占位符(t *testing.T) {
if got := priceLimitText(0); got != "≤"+placeholder {
t.Errorf("价格为 0: got %q", got)
}
if got := priceLimitText(4200); got != "≤¥42.00" {
t.Errorf("价格为 4200 分: got %q", got)
}
}