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
+108 -1
View File
@@ -239,9 +239,41 @@ type TaskView struct {
StatusText string
IsWarn bool // 失败 / 需人工,标黄提醒
ClientText string
CreatorText string
UpdatedAt string
}
const TaskCreatorHistoryValue = "__history__"
// TaskCreatorOption 是管理员创建人筛选的一项。
type TaskCreatorOption struct {
Value string
Text string
}
// TaskCreatorOptions 返回全部账号(含已禁用)以及存量历史任务选项。
func TaskCreatorOptions(db *sql.DB, actor *model.User) ([]TaskCreatorOption, error) {
if actor == nil {
return nil, ErrUnauthenticated
}
if !actor.IsAdmin() {
return nil, nil
}
users, err := repository.ListTaskCreatorUsers(db)
if err != nil {
return nil, err
}
options := []TaskCreatorOption{{Value: "", Text: "全部创建人"}, {Value: TaskCreatorHistoryValue, Text: "历史任务"}}
for _, user := range users {
text := user.Username
if user.Status == model.UserDisabled {
text += "(已禁用)"
}
options = append(options, TaskCreatorOption{Value: user.UserID, Text: text})
}
return options, nil
}
// TaskListResult 是列表页要的全部数据。
type TaskListResult struct {
Rows []TaskView
@@ -281,7 +313,7 @@ func ListTasksView(db *sql.DB, filter repository.TaskFilter, requestedPage int)
Counts: counts,
Total: total, Page: page, TotalPages: totalPages,
IsFiltered: filter.Type != "" || filter.Status != "" ||
strings.TrimSpace(filter.Keyword) != "",
strings.TrimSpace(filter.Keyword) != "" || filter.CreatorUserID != "" || filter.CreatorHistory,
}
for _, r := range rows {
result.Rows = append(result.Rows, TaskView{
@@ -292,12 +324,40 @@ func ListTasksView(db *sql.DB, filter repository.TaskFilter, requestedPage int)
StatusText: taskStatusText(r.Status),
IsWarn: isTaskWarn(r.Status),
ClientText: clientText(r.AssignedClient),
CreatorText: taskCreatorText(r.CreatedByUsername),
UpdatedAt: formatLocalTime(r.UpdatedAt),
})
}
return result, nil
}
func taskCreatorText(username string) string {
if strings.TrimSpace(username) == "" {
return "历史任务"
}
return username
}
// ListTasksViewForUser 在服务端固定当前账号范围,忽略采购员伪造的创建人筛选。
func ListTasksViewForUser(db *sql.DB, actor *model.User, filter repository.TaskFilter, creatorValue string, requestedPage int) (*TaskListResult, error) {
visibleUserID, err := visibleClientUserID(actor)
if err != nil {
return nil, err
}
filter.VisibleUserID = visibleUserID
filter.CreatorUserID = ""
filter.CreatorHistory = false
if actor.IsAdmin() {
creatorValue = strings.TrimSpace(creatorValue)
if creatorValue == TaskCreatorHistoryValue {
filter.CreatorHistory = true
} else if creatorValue != "" {
filter.CreatorUserID = creatorValue
}
}
return ListTasksView(db, filter, requestedPage)
}
// StatusLine 拼底部状态条,形如:
//
// 共 42 条 · 待分配 3 · 待领取 5 · 已领取 2 · 成功 30 · 需人工 1 · 失败 1 · 已取消 0
@@ -317,6 +377,34 @@ func DeleteTasks(db *sql.DB, taskIDs []string) (int64, error) {
return repository.DeleteTasks(db, dedupe(taskIDs))
}
// DeleteTasksForUser 保证整批删除要么全部在当前范围内成功,要么全部回滚。
func DeleteTasksForUser(db *sql.DB, actor *model.User, taskIDs []string) (int64, error) {
visibleUserID, err := visibleClientUserID(actor)
if err != nil {
return 0, err
}
ids := dedupe(taskIDs)
if len(ids) == 0 {
return 0, nil
}
tx, err := db.Begin()
if err != nil {
return 0, fmt.Errorf("开始删除任务事务失败: %w", err)
}
defer tx.Rollback()
n, err := repository.DeleteTasksInScope(tx, ids, visibleUserID)
if err != nil {
return 0, err
}
if n != int64(len(ids)) {
return 0, ErrTaskNotVisible
}
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("提交删除任务事务失败: %w", err)
}
return n, nil
}
// ---------- 详情弹窗 ----------
// resultDataLimit 是弹窗里展开显示 result_data 的最大字符数。
@@ -333,6 +421,7 @@ type TaskDetailView struct {
ExecutionModeText string
StatusText string
ClientText string
CreatorText string
ClaimedAt string
FinishedAt string
@@ -369,6 +458,10 @@ func GetTaskDetail(db *sql.DB, taskID string) (*TaskDetailView, error) {
if err != nil || t == nil {
return nil, err
}
creatorUsername, err := repository.GetTaskCreatorUsername(db, taskID)
if err != nil {
return nil, err
}
v := &TaskDetailView{
TaskID: t.TaskID,
@@ -376,6 +469,7 @@ func GetTaskDetail(db *sql.DB, taskID string) (*TaskDetailView, error) {
ExecutionModeText: taskExecutionModeText(t.TaskType, t.ExecutionMode),
StatusText: taskStatusText(t.Status),
ClientText: clientText(t.AssignedClient),
CreatorText: taskCreatorText(creatorUsername),
ClaimedAt: formatLocalTime(t.ClaimedAt),
FinishedAt: formatLocalTime(t.FinishedAt),
PddGoodsURL: t.PddGoodsURL,
@@ -401,6 +495,19 @@ func GetTaskDetail(db *sql.DB, taskID string) (*TaskDetailView, error) {
return v, nil
}
// GetTaskDetailForUser 对采购员隐藏其他人和历史任务,统一表现为不存在。
func GetTaskDetailForUser(db *sql.DB, actor *model.User, taskID string) (*TaskDetailView, error) {
visibleUserID, err := visibleClientUserID(actor)
if err != nil {
return nil, err
}
visible, err := repository.TaskVisibleToUser(db, taskID, visibleUserID)
if err != nil || !visible {
return nil, err
}
return GetTaskDetail(db, taskID)
}
// purchaseResult 只声明详情页需要展示的字段,避免把 Client 的完整结果结构
// 复制到 Admin。其余原始字段仍保存在 result_data 并可在详情底部展开查看。
type purchaseResult struct {