package service import ( "database/sql" "errors" "fmt" "strings" "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 createdByUserID 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_by_user_id, 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, nullableString(p.createdByUserID), now, now) if err != nil { t.Fatalf("插入测试任务 %s 失败: %v", p.taskID, err) } } func nullableString(value string) any { if value == "" { return nil } return value } // ── 列表:同时显示采集和采购 ────────────────────────── 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_显示PDD店铺和客户端名称并保留ID提示(t *testing.T) { db := newTestDB(t) now := model.NowISO() if _, err := db.Exec(` INSERT INTO pdd_products (goods_id, url, title, shop_name, collect_status, created_at, updated_at) VALUES ('737116531267', 'https://mobile.yangkeduo.com/goods.html?goods_id=737116531267', '测试商品', '测试旗舰店', 'collected', ?, ?)`, now, now); err != nil { t.Fatal(err) } if _, err := db.Exec(` INSERT INTO clients (client_id, name, last_seen_at, created_at, updated_at) VALUES ('client-001', '办公室手机', ?, ?, ?)`, now, now, now); err != nil { t.Fatal(err) } insertTestTask(t, db, testTaskParams{ taskID: "COL-SHOP", taskType: model.TaskCollect, status: model.TaskAssigned, pddGoodsID: "737116531267", assignedClient: "client-001", }) r, err := ListTasksView(db, repository.TaskFilter{}, 1) if err != nil { t.Fatal(err) } if len(r.Rows) != 1 || r.Rows[0].PddShopText != "测试旗舰店" { t.Fatalf("列表应显示 PDD 店铺,实际 %+v", r.Rows) } if r.Rows[0].ClientText != "办公室手机" || r.Rows[0].ClientTitle != "办公室手机(client-001)" { t.Errorf("客户端应显示名称并在提示保留 ID,实际 text=%q title=%q", r.Rows[0].ClientText, r.Rows[0].ClientTitle) } if _, err := db.Exec(`UPDATE clients SET name='仓库手机' WHERE client_id='client-001'`); err != nil { t.Fatal(err) } renamed, err := ListTasksView(db, repository.TaskFilter{}, 1) if err != nil { t.Fatal(err) } if renamed.Rows[0].ClientText != "仓库手机" { t.Errorf("客户端重命名后列表应显示当前名称,实际 %q", renamed.Rows[0].ClientText) } detail, err := GetTaskDetail(db, "COL-SHOP") if err != nil { t.Fatal(err) } if detail.PddShopText != "测试旗舰店" || detail.ClientText != "仓库手机" || detail.ClientIDText != "client-001" { t.Errorf("详情展示信息不完整:%+v", detail) } } func TestListTasksView_关联缺失时店铺占位且客户端回退ID(t *testing.T) { db := newTestDB(t) insertTestTask(t, db, testTaskParams{ taskID: "COL-FALLBACK", taskType: model.TaskCollect, status: model.TaskAssigned, pddGoodsID: "missing-product", assignedClient: "deleted-client", }) r, err := ListTasksView(db, repository.TaskFilter{}, 1) if err != nil { t.Fatal(err) } if r.Rows[0].PddShopText != "—" || r.Rows[0].ClientText != "deleted-client" || r.Rows[0].ClientTitle != "deleted-client" { t.Errorf("关联缺失回退错误:%+v", r.Rows[0]) } } // ── 统计跟随筛选 ────────────────────────────────────── 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_采购成功独立显示PDD核单结果(t *testing.T) { db := newTestDB(t) insertTestTask(t, db, testTaskParams{ taskID: "PUR-ORDER", taskType: model.TaskPurchase, status: model.TaskSucceeded, resultData: `{"purchase":{"order_no":"PDD-20260810-001","ordered_at":"2026-08-10T11:12:13Z","payment_status":"unpaid","match_status":"matched","order_submitted":true}}`, }) d, err := GetTaskDetail(db, "PUR-ORDER") if err != nil { t.Fatal(err) } if !d.HasPddOrder || d.PddOrderNo != "PDD-20260810-001" { t.Fatalf("PDD 订单编号没有结构化显示: %+v", d) } if d.PddOrderedAt != formatLocalTime("2026-08-10T11:12:13Z") { t.Errorf("UTC 下单时间应转为本地时间,实际 %q", d.PddOrderedAt) } if d.PddPaymentStatusText != "未付款(Client 上报时)" || d.PddOrderResultText != "Admin 不会查询 PDD 实时付款状态。" { t.Errorf("付款状态说明不明确: status=%q help=%q", d.PddPaymentStatusText, d.PddOrderResultText) } if !d.HasResult || !strings.Contains(d.ResultData, `"order_no":"PDD-20260810-001"`) { t.Error("结构化展示后仍必须保留原始结果") } } func TestGetTaskDetail_核单结果状态分别显示(t *testing.T) { db := newTestDB(t) tests := []struct { name string status model.TaskStatus resultData string want string }{ {"等待上报", model.TaskClaimed, "", "等待 Client 核单上报。"}, {"人工处理", model.TaskManualReview, "", "订单结果不确定,请结合错误信息人工核对。"}, {"非法JSON", model.TaskSucceeded, `{`, "采购结果格式异常,请展开完整结果进行诊断。"}, {"缺少purchase", model.TaskSucceeded, `{}`, "采购结果格式异常,请展开完整结果进行诊断。"}, {"尚未匹配", model.TaskSucceeded, `{"purchase":{"match_status":"not_found"}}`, "本次结果没有已匹配的 PDD 订单。"}, {"时间非法", model.TaskSucceeded, `{"purchase":{"order_no":"PDD-1","ordered_at":"2026-08-10 11:12","payment_status":"unpaid","match_status":"matched","order_submitted":true}}`, "采购结果中的下单时间格式异常,请展开完整结果进行诊断。"}, } for i, tc := range tests { t.Run(tc.name, func(t *testing.T) { taskID := fmt.Sprintf("PUR-STATE-%d", i) insertTestTask(t, db, testTaskParams{ taskID: taskID, taskType: model.TaskPurchase, status: tc.status, resultData: tc.resultData, }) d, err := GetTaskDetail(db, taskID) if err != nil { t.Fatal(err) } if d.HasPddOrder || d.PddOrderResultText != tc.want { t.Errorf("状态说明不对: HasPddOrder=%t text=%q", d.HasPddOrder, d.PddOrderResultText) } }) } } 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) } if d.HasPddOrder || d.PddOrderResultText != "" { t.Error("采集任务不应生成 PDD 核单结果展示") } } 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 TestTaskScope_采购员只能查看自己的任务且不能伪造筛选(t *testing.T) { db := newTestDB(t) admin, buyerA, buyerB := insertTaskUsers(t, db) insertTestTask(t, db, testTaskParams{taskID: "OWN-A", taskType: model.TaskCollect, status: model.TaskPending, createdByUserID: buyerA.UserID}) insertTestTask(t, db, testTaskParams{taskID: "OWN-B", taskType: model.TaskCollect, status: model.TaskSucceeded, createdByUserID: buyerB.UserID}) insertTestTask(t, db, testTaskParams{taskID: "HISTORY", taskType: model.TaskCollect, status: model.TaskFailed}) buyerResult, err := ListTasksViewForUser(db, buyerA, repository.TaskFilter{}, TaskCreatorHistoryValue, 1) if err != nil { t.Fatal(err) } if buyerResult.Total != 1 || len(buyerResult.Rows) != 1 || buyerResult.Rows[0].TaskID != "OWN-A" { t.Fatalf("采购员范围不正确: %+v", buyerResult.Rows) } if buyerResult.Counts[model.TaskSucceeded] != 0 || buyerResult.Counts[model.TaskFailed] != 0 { t.Fatalf("采购员统计泄露了其他任务: %+v", buyerResult.Counts) } history, err := ListTasksViewForUser(db, admin, repository.TaskFilter{}, TaskCreatorHistoryValue, 1) if err != nil || history.Total != 1 || history.Rows[0].TaskID != "HISTORY" { t.Fatalf("管理员历史任务筛选不正确: err=%v rows=%+v", err, history.Rows) } byBuyer, err := ListTasksViewForUser(db, admin, repository.TaskFilter{}, buyerB.UserID, 1) if err != nil || byBuyer.Total != 1 || byBuyer.Rows[0].TaskID != "OWN-B" { t.Fatalf("管理员创建人筛选不正确: err=%v rows=%+v", err, byBuyer.Rows) } } func TestTaskScope_详情越权返回不存在且批量删除完整回滚(t *testing.T) { db := newTestDB(t) admin, buyerA, buyerB := insertTaskUsers(t, db) insertTestTask(t, db, testTaskParams{taskID: "OWN-A", taskType: model.TaskCollect, status: model.TaskPending, createdByUserID: buyerA.UserID}) insertTestTask(t, db, testTaskParams{taskID: "OWN-B", taskType: model.TaskCollect, status: model.TaskPending, createdByUserID: buyerB.UserID}) detail, err := GetTaskDetailForUser(db, buyerA, "OWN-B") if err != nil || detail != nil { t.Fatalf("越权详情应表现为不存在: detail=%+v err=%v", detail, err) } if _, err := DeleteTasksForUser(db, buyerA, []string{"OWN-A", "OWN-B"}); !errors.Is(err, ErrTaskNotVisible) { t.Fatalf("混合越权删除应整批拒绝: %v", err) } for _, id := range []string{"OWN-A", "OWN-B"} { visible, err := repository.TaskVisibleToUser(db, id, "") if err != nil || !visible { t.Fatalf("回滚后任务 %s 应仍存在: visible=%t err=%v", id, visible, err) } } if n, err := DeleteTasksForUser(db, buyerA, []string{"OWN-A"}); err != nil || n != 1 { t.Fatalf("采购员删除本人任务失败: n=%d err=%v", n, err) } if n, err := DeleteTasksForUser(db, admin, []string{"OWN-B"}); err != nil || n != 1 { t.Fatalf("管理员删除任意任务失败: n=%d err=%v", n, err) } } func TestTaskCreatorOptions_包含禁用账号和历史任务(t *testing.T) { db := newTestDB(t) admin, _, buyerB := insertTaskUsers(t, db) if _, err := db.Exec(`UPDATE users SET status='disabled' WHERE user_id=?`, buyerB.UserID); err != nil { t.Fatal(err) } options, err := TaskCreatorOptions(db, admin) if err != nil { t.Fatal(err) } texts := map[string]string{} for _, option := range options { texts[option.Value] = option.Text } if texts[TaskCreatorHistoryValue] != "历史任务" || texts[buyerB.UserID] != buyerB.Username+"(已禁用)" { t.Fatalf("创建人选项不完整: %+v", texts) } } func insertTaskUsers(t *testing.T, db *sql.DB) (*model.User, *model.User, *model.User) { t.Helper() now := model.NowISO() users := []*model.User{ {UserID: "task-admin", Username: "task-admin", PasswordHash: "hash", Role: model.RoleAdmin, Status: model.UserActive}, {UserID: "task-buyer-a", Username: "task-buyer-a", PasswordHash: "hash", Role: model.RolePurchaser, Status: model.UserActive}, {UserID: "task-buyer-b", Username: "task-buyer-b", PasswordHash: "hash", Role: model.RolePurchaser, Status: model.UserActive}, } for _, user := range users { user.PasswordChangedAt, user.CreatedAt, user.UpdatedAt = now, now, now if err := repository.CreateUser(db, *user); err != nil { t.Fatalf("插入任务测试用户失败: %v", err) } } return users[0], users[1], users[2] } // ── 目标列拼接的边界情况 ────────────────────────────── 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) } }