server: 平台抽象层+bilibili、服务端加密凭据库、角色中间件;web: 精简页面/路由、macOS 客户端(Swift)与多份方案文档;移除误入库的编译产物

This commit is contained in:
Qiufeng
2026-08-21 10:53:32 +08:00
parent 0daa9782c9
commit 3585c39bab
103 changed files with 5842 additions and 3197 deletions
+104 -2
View File
@@ -2,6 +2,7 @@ package handlers
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
@@ -39,6 +40,54 @@ func marshalJSONArray[T any](arr []T) string {
return string(raw)
}
func (h *TaskHandler) validateTargets(wsid uint64, accountIDs, materialIDs []uint64) error {
var accounts []models.Account
if err := h.DB.Where("workspace_id = ? and id in ? and status = ?", wsid, accountIDs, "active").Find(&accounts).Error; err != nil {
return err
}
if len(accounts) != len(uniqueIDs(accountIDs)) {
return fmt.Errorf("账号不存在、不属于当前工作区或尚未完成登录")
}
var materials []models.Material
if err := h.DB.Where("workspace_id = ? and id in ? and status = ?", wsid, materialIDs, "ready").Find(&materials).Error; err != nil {
return err
}
if len(materials) != len(uniqueIDs(materialIDs)) {
return fmt.Errorf("素材不存在、未就绪或不属于当前工作区")
}
return nil
}
func uniqueIDs(values []uint64) []uint64 {
seen := make(map[uint64]struct{}, len(values))
result := make([]uint64, 0, len(values))
for _, value := range values {
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
return result
}
func recordTaskEvent(db *gorm.DB, row *models.Task, eventType, message string, data interface{}) {
raw := ""
if data != nil {
if encoded, err := json.Marshal(data); err == nil {
raw = string(encoded)
}
}
_ = db.Create(&models.TaskEvent{
WorkspaceID: row.WorkspaceID,
TaskID: row.ID,
Type: eventType,
Status: row.Status,
Message: message,
Data: raw,
}).Error
}
// List 任务列表(状态筛选 + 分页)
func (h *TaskHandler) List(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
@@ -46,7 +95,7 @@ func (h *TaskHandler) List(c *gin.Context) {
if page < 1 {
page = 1
}
if size < 1 || size > 100 {
if size < 1 || size > 300 {
size = 20
}
q := h.DB.Model(&models.Task{}).Where("workspace_id = ?", c.GetUint64("wsid"))
@@ -82,6 +131,10 @@ func (h *TaskHandler) Create(c *gin.Context) {
response.Fail(c, http.StatusBadRequest, 1001, "至少选择一个素材")
return
}
if err := h.validateTargets(c.GetUint64("wsid"), req.AccountIDs, req.MaterialIDs); err != nil {
response.Fail(c, http.StatusBadRequest, 1004, err.Error())
return
}
priority := req.Priority
if priority < 1 || priority > 10 {
priority = 5
@@ -100,16 +153,36 @@ func (h *TaskHandler) Create(c *gin.Context) {
}
if req.ScheduleAt != nil {
ts := time.UnixMilli(*req.ScheduleAt)
if !ts.After(time.Now()) {
response.Fail(c, http.StatusBadRequest, 1001, "定时发布时间必须晚于当前时间")
return
}
row.ScheduleAt = &ts
}
if err := h.DB.Create(&row).Error; err != nil {
response.Fail(c, http.StatusInternalServerError, 5000, "创建失败")
return
}
recordTaskEvent(h.DB, &row, "created", "任务已创建", nil)
response.Audit(c, "task.create", "task:"+itoa(row.ID), gin.H{"title": req.Title})
response.OK(c, row)
}
// Events 返回任务时间线,并强制按 workspace 隔离。
func (h *TaskHandler) Events(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.Fail(c, http.StatusBadRequest, 1001, "参数不合法")
return
}
var events []models.TaskEvent
if err = h.DB.Where("task_id = ? and workspace_id = ?", id, c.GetUint64("wsid")).Order("id asc").Find(&events).Error; err != nil {
response.Fail(c, http.StatusInternalServerError, 5000, "任务时间线加载失败")
return
}
response.OK(c, gin.H{"list": events})
}
// Get 任务详情
func (h *TaskHandler) Get(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
@@ -146,6 +219,14 @@ func (h *TaskHandler) Update(c *gin.Context) {
response.Fail(c, http.StatusConflict, 3005, "仅草稿或已驳回任务可编辑")
return
}
if len(req.AccountIDs) == 0 || len(req.MaterialIDs) == 0 {
response.Fail(c, http.StatusBadRequest, 1001, "至少选择一个账号和素材")
return
}
if err := h.validateTargets(c.GetUint64("wsid"), req.AccountIDs, req.MaterialIDs); err != nil {
response.Fail(c, http.StatusBadRequest, 1004, err.Error())
return
}
updates := map[string]interface{}{
"title": req.Title,
"content": req.Content,
@@ -155,7 +236,15 @@ func (h *TaskHandler) Update(c *gin.Context) {
}
if req.ScheduleAt != nil {
ts := time.UnixMilli(*req.ScheduleAt)
if !ts.After(time.Now()) {
response.Fail(c, http.StatusBadRequest, 1001, "定时发布时间必须晚于当前时间")
return
}
updates["schedule_at"] = &ts
} else {
// nil represents an explicit immediate schedule in the JSON contract.
// The update endpoint has no meaningful partial-update semantics.
updates["schedule_at"] = nil
}
if err = h.DB.Model(&row).Updates(updates).Error; err != nil {
response.Fail(c, http.StatusInternalServerError, 5000, "更新失败")
@@ -215,6 +304,7 @@ func (h *TaskHandler) apply(c *gin.Context, action task.Action, extra map[string
return nil
}
row.Status = next
recordTaskEvent(h.DB, &row, string(action), "状态变更为 "+next, extra)
return &row
}
@@ -230,9 +320,15 @@ func (h *TaskHandler) Submit(c *gin.Context) {
func (h *TaskHandler) Approve(c *gin.Context) {
if row := h.apply(c, task.ActionApprove, nil); row != nil {
_ = Notify(h.DB, c.GetUint64("wsid"), []uint64{row.CreatedBy}, "task", "任务已通过审核", row.Title)
if h.Dispatcher != nil && h.Dispatcher.Hub != nil {
h.Dispatcher.Hub.BroadcastWS(row.WorkspaceID, "notification.new", gin.H{"kind": "task", "title": "任务已通过审核", "content": row.Title})
}
response.Audit(c, "task.approve", "task:"+itoa(row.ID), nil)
if h.Dispatcher != nil {
_ = h.Dispatcher.TryDispatch(row.ID) // 在线设备立即下发
_ = h.Dispatcher.TryDispatch(row.ID)
// TryDispatch can synchronously advance a Web-only mock task. Return
// the persisted state instead of the stale queued copy from apply.
_ = h.DB.Where("id = ? and workspace_id = ?", row.ID, row.WorkspaceID).First(row).Error
}
response.OK(c, row)
}
@@ -246,6 +342,9 @@ func (h *TaskHandler) Reject(c *gin.Context) {
_ = c.ShouldBindJSON(&req)
if row := h.apply(c, task.ActionReject, map[string]interface{}{"review_note": req.Note, "reviewed_by": c.GetUint64("uid")}); row != nil {
_ = Notify(h.DB, c.GetUint64("wsid"), []uint64{row.CreatedBy}, "task", "任务被驳回", req.Note)
if h.Dispatcher != nil && h.Dispatcher.Hub != nil {
h.Dispatcher.Hub.BroadcastWS(row.WorkspaceID, "notification.new", gin.H{"kind": "task", "title": "任务被驳回", "content": req.Note})
}
response.Audit(c, "task.reject", "task:"+itoa(row.ID), gin.H{"note": req.Note})
response.OK(c, row)
}
@@ -273,6 +372,9 @@ func (h *TaskHandler) Retry(c *gin.Context) {
// Cancel 取消
func (h *TaskHandler) Cancel(c *gin.Context) {
if row := h.apply(c, task.ActionCancel, nil); row != nil {
if h.Dispatcher != nil {
h.Dispatcher.Cancel(row.ID)
}
response.Audit(c, "task.cancel", "task:"+itoa(row.ID), nil)
response.OK(c, row)
}