Files
cmautobuy/admin/service/task_test.go
T

482 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"database/sql"
"fmt"
"testing"
"cmautobuy/admin/model"
"cmautobuy/admin/repository"
)
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"})
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"})
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"})
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"})
// 按任务编号命中
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)
}
// 按订单号命中
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 命中
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"})
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"})
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"})
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,
})
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"})
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"})
all, err := ListTasksView(db, repository.TaskFilter{}, 1)
if err != nil {
t.Fatalf("查询失败: %v", err)
}
if all.Total != 4 {
t.Fatalf("不筛选时总数应该是 4,实际 %d", all.Total)
}
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)
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"})
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)
}
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)
}
}