feat: 按创建人隔离采集采购任务 (#127)

This commit is contained in:
chengma
2026-08-10 23:10:28 +08:00
parent 74f99f37d0
commit a9f22b9b0e
24 changed files with 655 additions and 74 deletions
+117 -17
View File
@@ -2,6 +2,7 @@ package service
import (
"database/sql"
"errors"
"fmt"
"strings"
"testing"
@@ -39,20 +40,21 @@ func TestListTasksView_统一每页20条并收敛越界页(t *testing.T) {
// 跟 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
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) {
@@ -100,20 +102,27 @@ func insertTestTask(t *testing.T, db *sql.DB, p testTaskParams) {
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,
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,
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) {
@@ -515,6 +524,97 @@ func TestDeleteTasks_批量删除(t *testing.T) {
}
}
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) {