Files
cmautobuy/admin/service/task_test.go
T

646 lines
25 KiB
Go
Raw Normal View History

package service
import (
"database/sql"
"errors"
2026-08-09 21:51:20 +08:00
"fmt"
"strings"
"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
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"})
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_采购成功独立显示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)
}
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 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)
}
}