Version: 0.9.38.dev.260423
后端: 1. 四象限任务新增修改与删除接口——部分更新语义 + 硬删除 + 幂等信息码 - 新增 PUT/task/update:指针字段部分更新(title / priority_group / deadline_at / urgency_threshold_at),优先级 1~4 校验,空更新检测 - 新增 DELETE /task/delete:硬删除,重复删除返回 10003 幂等信息码 - 新增错误码 TaskUpdateNoFields (40063) 与 TaskAlreadyDeleted (10003) 前端: 1. 四象限卡片对接修改与删除 - 任务项重构为三区布局:勾选、内容点击编辑、悬浮删除按钮 - 创建弹窗复用为编辑模式,新增 urgency_threshold_at 字段 - 删除走二次确认弹窗,空状态增加 SVG 插画 2. 今日时间轴微调——色调简化为取模轮换,午休/晚餐改称午间/晚休
This commit is contained in:
@@ -85,7 +85,7 @@ func (th *TaskHandler) CompleteTask(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 4. 调用 Service 执行“标记完成”逻辑。
|
||||
// 4. 调用 Service 执行"标记完成"逻辑。
|
||||
resp, err := th.svc.CompleteTask(ctx, &req, userID)
|
||||
if err != nil {
|
||||
respond.DealWithError(c, err)
|
||||
@@ -96,12 +96,12 @@ func (th *TaskHandler) CompleteTask(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, resp))
|
||||
}
|
||||
|
||||
// UndoCompleteTask 取消任务“已完成”勾选。
|
||||
// UndoCompleteTask 取消任务"已完成"勾选。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责解析请求与读取 user_id;
|
||||
// 2. 负责调用 Service 执行业务恢复;
|
||||
// 3. 不负责“任务是否已完成”的业务判断(由 Service/DAO 负责)。
|
||||
// 3. 不负责"任务是否已完成"的业务判断(由 Service/DAO 负责)。
|
||||
func (th *TaskHandler) UndoCompleteTask(c *gin.Context) {
|
||||
// 1. 绑定请求参数。
|
||||
var req model.UserUndoCompleteTaskRequest
|
||||
@@ -118,7 +118,7 @@ func (th *TaskHandler) UndoCompleteTask(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 4. 调用 Service 执行“取消已完成勾选”逻辑。
|
||||
// 4. 调用 Service 执行"取消已完成勾选"逻辑。
|
||||
resp, err := th.svc.UndoCompleteTask(ctx, &req, userID)
|
||||
if err != nil {
|
||||
respond.DealWithError(c, err)
|
||||
@@ -128,3 +128,69 @@ func (th *TaskHandler) UndoCompleteTask(c *gin.Context) {
|
||||
// 5. 返回统一响应结构。
|
||||
c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, resp))
|
||||
}
|
||||
|
||||
// UpdateTask 更新任务属性(部分更新)。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责解析请求与读取 user_id;
|
||||
// 2. 负责调用 Service 执行业务;
|
||||
// 3. 不负责幂等校验(幂等由路由中间件处理)。
|
||||
func (th *TaskHandler) UpdateTask(c *gin.Context) {
|
||||
// 1. 绑定请求参数。
|
||||
var req model.UserUpdateTaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, respond.WrongParamType)
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 从鉴权上下文读取 user_id,保证只操作当前用户任务。
|
||||
userID := c.GetInt("user_id")
|
||||
|
||||
// 3. 设置短超时,避免该写接口占用连接过久。
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 4. 调用 Service 执行更新逻辑。
|
||||
resp, err := th.svc.UpdateTask(ctx, &req, userID)
|
||||
if err != nil {
|
||||
respond.DealWithError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 5. 返回统一响应结构。
|
||||
c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, resp))
|
||||
}
|
||||
|
||||
// DeleteTask 永久删除指定任务。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责解析请求与读取 user_id;
|
||||
// 2. 负责调用 Service 执行删除;
|
||||
// 3. 不负责幂等校验(幂等由路由中间件处理)。
|
||||
func (th *TaskHandler) DeleteTask(c *gin.Context) {
|
||||
// 1. 绑定请求参数。
|
||||
var req model.UserCompleteTaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, respond.WrongParamType)
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 从鉴权上下文读取 user_id,保证只操作当前用户任务。
|
||||
userID := c.GetInt("user_id")
|
||||
|
||||
// 3. 设置短超时,避免该写接口占用连接过久。
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 4. 调用 Service 执行删除逻辑。
|
||||
taskID, err := th.svc.DeleteTask(ctx, &req, userID)
|
||||
if err != nil {
|
||||
respond.DealWithError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 5. 返回统一响应结构。
|
||||
c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, gin.H{"task_id": taskID}))
|
||||
}
|
||||
|
||||
@@ -55,3 +55,24 @@ func ModelToGetUserTasksResp(tasks []model.Task) []model.GetUserTaskResp {
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// ModelToGetUserTaskResp 将单个 Task 模型转换为 GetUserTaskResp。
|
||||
func ModelToGetUserTaskResp(task *model.Task) model.GetUserTaskResp {
|
||||
status := "incomplete"
|
||||
if task.IsCompleted {
|
||||
status = "completed"
|
||||
}
|
||||
deadline := ""
|
||||
if task.DeadlineAt != nil {
|
||||
deadline = task.DeadlineAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
return model.GetUserTaskResp{
|
||||
ID: task.ID,
|
||||
UserID: task.UserID,
|
||||
Title: task.Title,
|
||||
PriorityGroup: task.Priority,
|
||||
Status: status,
|
||||
Deadline: deadline,
|
||||
IsCompleted: task.IsCompleted,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +46,10 @@ func (dao *TaskDAO) GetTasksByUserID(userID int) ([]model.Task, error) {
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// CompleteTaskByID 将指定任务标记为“已完成”。
|
||||
// CompleteTaskByID 将指定任务标记为"已完成"。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只负责“当前用户 + 指定 task_id”的完成状态更新;
|
||||
// 1. 只负责"当前用户 + 指定 task_id"的完成状态更新;
|
||||
// 2. 不负责幂等中间件(由路由层统一挂载);
|
||||
// 3. 不负责业务层响应包装(由 Service 层处理)。
|
||||
//
|
||||
@@ -62,12 +62,12 @@ func (dao *TaskDAO) GetTasksByUserID(userID int) ([]model.Task, error) {
|
||||
// 3.1 gorm.ErrRecordNotFound:任务不存在或不属于当前用户;
|
||||
// 3.2 其他 error:数据库异常。
|
||||
func (dao *TaskDAO) CompleteTaskByID(ctx context.Context, userID int, taskID int) (*model.Task, bool, error) {
|
||||
// 1. 基础兜底:非法参数直接返回“记录不存在”语义,避免下游误写。
|
||||
// 1. 基础兜底:非法参数直接返回"记录不存在"语义,避免下游误写。
|
||||
if userID <= 0 || taskID <= 0 {
|
||||
return nil, false, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
// 2. 先查询目标任务,明确区分“已完成”与“不存在”。
|
||||
// 2. 先查询目标任务,明确区分"已完成"与"不存在"。
|
||||
var target model.Task
|
||||
findErr := dao.db.WithContext(ctx).
|
||||
Where("id = ? AND user_id = ?", taskID, userID).
|
||||
@@ -116,7 +116,7 @@ func (dao *TaskDAO) CompleteTaskByID(ctx context.Context, userID int, taskID int
|
||||
return &target, false, nil
|
||||
}
|
||||
|
||||
// UndoCompleteTaskByID 将指定任务从“已完成”恢复为“未完成”。
|
||||
// UndoCompleteTaskByID 将指定任务从"已完成"恢复为"未完成"。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只负责当前用户(user_id)下指定 task_id 的状态恢复;
|
||||
@@ -127,15 +127,15 @@ func (dao *TaskDAO) CompleteTaskByID(ctx context.Context, userID int, taskID int
|
||||
// 1. *model.Task:恢复后的任务快照;
|
||||
// 2. error:
|
||||
// 2.1 gorm.ErrRecordNotFound:任务不存在或不属于当前用户;
|
||||
// 2.2 respond.TaskNotCompleted:任务当前不是“已完成”状态,不能执行取消勾选;
|
||||
// 2.2 respond.TaskNotCompleted:任务当前不是"已完成"状态,不能执行取消勾选;
|
||||
// 2.3 其他 error:数据库异常。
|
||||
func (dao *TaskDAO) UndoCompleteTaskByID(ctx context.Context, userID int, taskID int) (*model.Task, error) {
|
||||
// 1. 参数兜底:非法 user/task 参数统一按“记录不存在”处理,避免误写。
|
||||
// 1. 参数兜底:非法 user/task 参数统一按"记录不存在"处理,避免误写。
|
||||
if userID <= 0 || taskID <= 0 {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
// 2. 先读取目标任务,明确区分“不存在”和“状态不允许恢复”。
|
||||
// 2. 先读取目标任务,明确区分"不存在"和"状态不允许恢复"。
|
||||
var target model.Task
|
||||
findErr := dao.db.WithContext(ctx).
|
||||
Where("id = ? AND user_id = ?", taskID, userID).
|
||||
@@ -145,7 +145,7 @@ func (dao *TaskDAO) UndoCompleteTaskByID(ctx context.Context, userID int, taskID
|
||||
}
|
||||
|
||||
// 3. 严格业务约束:若任务当前未完成,直接返回业务错误。
|
||||
// 3.1 这是本接口和“标记完成”接口的关键差异:这里不做幂等成功。
|
||||
// 3.1 这是本接口和"标记完成"接口的关键差异:这里不做幂等成功。
|
||||
if !target.IsCompleted {
|
||||
return nil, respond.TaskNotCompleted
|
||||
}
|
||||
@@ -164,7 +164,7 @@ func (dao *TaskDAO) UndoCompleteTaskByID(ctx context.Context, userID int, taskID
|
||||
|
||||
// 5. 并发兜底:
|
||||
// 5.1 若 RowsAffected=0,说明可能被并发请求先一步恢复;
|
||||
// 5.2 重新读取当前状态,若已是未完成则按业务规则返回“任务未完成”错误。
|
||||
// 5.2 重新读取当前状态,若已是未完成则按业务规则返回"任务未完成"错误。
|
||||
if updateResult.RowsAffected == 0 {
|
||||
var check model.Task
|
||||
checkErr := dao.db.WithContext(ctx).
|
||||
@@ -184,10 +184,10 @@ func (dao *TaskDAO) UndoCompleteTaskByID(ctx context.Context, userID int, taskID
|
||||
return &target, nil
|
||||
}
|
||||
|
||||
// PromoteTaskUrgencyByIDs 批量执行“任务紧急性平移”。
|
||||
// PromoteTaskUrgencyByIDs 批量执行"任务紧急性平移"。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只负责把满足条件的任务从“不紧急象限”平移到“紧急象限”:
|
||||
// 1. 只负责把满足条件的任务从"不紧急象限"平移到"紧急象限":
|
||||
// 1.1 priority=2 -> 1(重要不紧急 -> 重要且紧急);
|
||||
// 1.2 priority=4 -> 3(不简单不重要 -> 简单不重要);
|
||||
// 2. 只更新本次指定 user_id + task_ids 范围内的数据;
|
||||
@@ -209,7 +209,7 @@ func (dao *TaskDAO) PromoteTaskUrgencyByIDs(ctx context.Context, userID int, tas
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// 3. 条件更新:只更新“已到紧急分界线且仍处于非紧急象限”的任务。
|
||||
// 3. 条件更新:只更新"已到紧急分界线且仍处于非紧急象限"的任务。
|
||||
result := dao.db.WithContext(ctx).
|
||||
Model(&model.Task{UserID: userID}).
|
||||
Where("user_id = ?", userID).
|
||||
@@ -225,7 +225,101 @@ func (dao *TaskDAO) PromoteTaskUrgencyByIDs(ctx context.Context, userID int, tas
|
||||
return result.RowsAffected, nil
|
||||
}
|
||||
|
||||
// compactPositiveIntIDs 对 int 切片做“去重 + 过滤非正数”。
|
||||
// UpdateTaskByID 按 task_id + user_id 更新指定字段。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只负责按 updates map 执行 SET 子句更新;
|
||||
// 2. 不负责业务规则(如优先级范围校验),由 Service 层处理;
|
||||
// 3. 使用 Model(&model.Task{UserID: userID}) 让 cache_deleter 回调拿到 user_id。
|
||||
//
|
||||
// 返回语义:
|
||||
// 1. *model.Task:更新后的完整任务快照;
|
||||
// 2. error:
|
||||
// 2.1 gorm.ErrRecordNotFound:任务不存在或不属于当前用户;
|
||||
// 2.2 其他 error:数据库异常。
|
||||
func (dao *TaskDAO) UpdateTaskByID(ctx context.Context, userID int, taskID int, updates map[string]interface{}) (*model.Task, error) {
|
||||
// 1. 参数兜底:非法参数直接返回"记录不存在"语义。
|
||||
if userID <= 0 || taskID <= 0 {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
// 2. 先查询目标任务,确认存在且归属当前用户。
|
||||
var target model.Task
|
||||
findErr := dao.db.WithContext(ctx).
|
||||
Where("id = ? AND user_id = ?", taskID, userID).
|
||||
First(&target).Error
|
||||
if findErr != nil {
|
||||
return nil, findErr
|
||||
}
|
||||
|
||||
// 3. 执行部分字段更新。
|
||||
// 3.1 使用 Model(&model.Task{UserID: userID}) 触发 cache_deleter。
|
||||
// 3.2 限定 id + user_id 条件,避免误更新。
|
||||
updateResult := dao.db.WithContext(ctx).
|
||||
Model(&model.Task{UserID: userID}).
|
||||
Where("id = ? AND user_id = ?", taskID, userID).
|
||||
Updates(updates)
|
||||
if updateResult.Error != nil {
|
||||
return nil, updateResult.Error
|
||||
}
|
||||
|
||||
// 4. 更新后重新读取,保证返回完整且一致的快照。
|
||||
var updated model.Task
|
||||
if err := dao.db.WithContext(ctx).
|
||||
Where("id = ? AND user_id = ?", taskID, userID).
|
||||
First(&updated).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &updated, nil
|
||||
}
|
||||
|
||||
// DeleteTaskByID 永久删除指定任务(硬删除)。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只负责删除 user_id + task_id 对应的记录;
|
||||
// 2. 使用 Model(&model.Task{UserID: userID}) 触发 cache_deleter 删除用户任务缓存;
|
||||
// 3. 不负责级联清理日程(tasks 与 schedule_events 无直接外键关联)。
|
||||
//
|
||||
// 返回语义:
|
||||
// 1. *model.Task:被删除的任务快照(用于响应前端);
|
||||
// 2. error:
|
||||
// 2.1 gorm.ErrRecordNotFound:任务不存在或不属于当前用户;
|
||||
// 2.2 其他 error:数据库异常。
|
||||
func (dao *TaskDAO) DeleteTaskByID(ctx context.Context, userID int, taskID int) (*model.Task, error) {
|
||||
// 1. 参数兜底。
|
||||
if userID <= 0 || taskID <= 0 {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
// 2. 先查询目标任务,确认存在且归属当前用户,同时获取快照用于响应。
|
||||
var target model.Task
|
||||
findErr := dao.db.WithContext(ctx).
|
||||
Where("id = ? AND user_id = ?", taskID, userID).
|
||||
First(&target).Error
|
||||
if findErr != nil {
|
||||
return nil, findErr
|
||||
}
|
||||
|
||||
// 3. 执行硬删除。
|
||||
// 3.1 使用 Model(&model.Task{UserID: userID}) 触发 cache_deleter。
|
||||
deleteResult := dao.db.WithContext(ctx).
|
||||
Model(&model.Task{UserID: userID}).
|
||||
Where("id = ? AND user_id = ?", taskID, userID).
|
||||
Delete(&model.Task{})
|
||||
if deleteResult.Error != nil {
|
||||
return nil, deleteResult.Error
|
||||
}
|
||||
|
||||
// 4. 并发兜底:RowsAffected=0 说明被并发请求先一步删除。
|
||||
if deleteResult.RowsAffected == 0 {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return &target, nil
|
||||
}
|
||||
|
||||
// compactPositiveIntIDs 对 int 切片做"去重 + 过滤非正数"。
|
||||
//
|
||||
// 说明:
|
||||
// 1. 该函数是 DAO 内部参数清洗工具,不参与任何业务判定;
|
||||
|
||||
@@ -7,13 +7,13 @@ import "time"
|
||||
// 职责边界:
|
||||
// 1. 负责映射 tasks 表字段;
|
||||
// 2. 不负责接口入参校验和业务规则判断;
|
||||
// 3. 不负责“自动平移”执行(自动平移由 Service + Outbox 事件链路负责)。
|
||||
// 3. 不负责"自动平移"执行(自动平移由 Service + Outbox 事件链路负责)。
|
||||
type Task struct {
|
||||
// 1. 主键。
|
||||
ID int `gorm:"primaryKey;autoIncrement"`
|
||||
// 2. 归属用户 ID。
|
||||
// 2.1 单列索引用于常规按用户查任务;
|
||||
// 2.2 同时参与“懒触发平移”复合索引的最左前缀。
|
||||
// 2.2 同时参与"懒触发平移"复合索引的最左前缀。
|
||||
UserID int `gorm:"column:user_id;index;index:idx_user_done_threshold_priority,priority:1"`
|
||||
// 3. 任务标题。
|
||||
Title string `gorm:"type:varchar(255)"`
|
||||
@@ -23,7 +23,7 @@ type Task struct {
|
||||
// 4.3 3=简单不重要;
|
||||
// 4.4 4=不简单不重要。
|
||||
//
|
||||
// 说明:该字段参与“懒触发平移”复合索引。
|
||||
// 说明:该字段参与"懒触发平移"复合索引。
|
||||
Priority int `gorm:"not null;index:idx_user_done_threshold_priority,priority:4"`
|
||||
// 5. 完成状态。
|
||||
//
|
||||
@@ -34,10 +34,10 @@ type Task struct {
|
||||
// 7. 紧急分界时间(自动平移阈值)。
|
||||
//
|
||||
// 规则:
|
||||
// 7.1 到达该时间后,任务可从“不紧急象限”自动平移到“紧急象限”;
|
||||
// 7.1 到达该时间后,任务可从"不紧急象限"自动平移到"紧急象限";
|
||||
// 7.2 该值由上游(例如 LLM 规划)给出,不在模型层做推断;
|
||||
// 7.3 为空表示该任务不参与自动平移;
|
||||
// 7.4 该字段参与“懒触发平移”复合索引。
|
||||
// 7.4 该字段参与"懒触发平移"复合索引。
|
||||
UrgencyThresholdAt *time.Time `gorm:"column:urgency_threshold_at;index:idx_user_done_threshold_priority,priority:3"`
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ type UserAddTaskRequest struct {
|
||||
DeadlineAt *time.Time `json:"deadline_at"`
|
||||
}
|
||||
|
||||
// UserCompleteTaskRequest 是“标记任务完成”接口的请求体。
|
||||
// UserCompleteTaskRequest 是"标记任务完成"接口的请求体。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只承载目标任务 ID;
|
||||
@@ -65,7 +65,7 @@ type UserCompleteTaskRequest struct {
|
||||
TaskID int `json:"task_id"`
|
||||
}
|
||||
|
||||
// UserCompleteTaskResponse 是“标记任务完成”接口的响应体。
|
||||
// UserCompleteTaskResponse 是"标记任务完成"接口的响应体。
|
||||
//
|
||||
// 字段语义:
|
||||
// 1. TaskID:本次操作的目标任务;
|
||||
@@ -81,7 +81,7 @@ type UserCompleteTaskResponse struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// UserUndoCompleteTaskRequest 是“取消任务已完成勾选”接口请求体。
|
||||
// UserUndoCompleteTaskRequest 是"取消任务已完成勾选"接口请求体。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只承载目标 task_id;
|
||||
@@ -90,7 +90,7 @@ type UserUndoCompleteTaskRequest struct {
|
||||
TaskID int `json:"task_id"`
|
||||
}
|
||||
|
||||
// UserUndoCompleteTaskResponse 是“取消任务已完成勾选”接口响应体。
|
||||
// UserUndoCompleteTaskResponse 是"取消任务已完成勾选"接口响应体。
|
||||
//
|
||||
// 字段语义:
|
||||
// 1. TaskID:本次操作目标任务;
|
||||
@@ -112,10 +112,24 @@ type GetUserTaskResp struct {
|
||||
IsCompleted bool `json:"is_completed"`
|
||||
}
|
||||
|
||||
// TaskUrgencyPromoteRequestedPayload 是“任务紧急性平移请求”事件载荷。
|
||||
// UserUpdateTaskRequest 是"更新任务属性"接口的请求体。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只承载“哪个用户的哪些任务需要尝试平移”;
|
||||
// 1. 指针字段表示"部分更新"语义:nil 表示不修改,非 nil 表示更新为指定值;
|
||||
// 2. TaskID 为必填;
|
||||
// 3. 不承载 user_id(由鉴权中间件注入,防止越权)。
|
||||
type UserUpdateTaskRequest struct {
|
||||
TaskID int `json:"task_id"`
|
||||
Title *string `json:"title"`
|
||||
PriorityGroup *int `json:"priority_group"`
|
||||
DeadlineAt *time.Time `json:"deadline_at"`
|
||||
UrgencyThresholdAt *time.Time `json:"urgency_threshold_at"`
|
||||
}
|
||||
|
||||
// TaskUrgencyPromoteRequestedPayload 是"任务紧急性平移请求"事件载荷。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只承载"哪个用户的哪些任务需要尝试平移";
|
||||
// 2. 不包含 outbox/kafka 协议字段(这些由基础设施层统一封装);
|
||||
// 3. TriggeredAt 只用于追踪触发时间,最终是否更新仍以消费时数据库条件为准。
|
||||
type TaskUrgencyPromoteRequestedPayload struct {
|
||||
|
||||
@@ -37,7 +37,7 @@ func DealWithError(c *gin.Context, err error) { //处理错误,返回对应的
|
||||
return
|
||||
}
|
||||
var resp Response
|
||||
if errors.Is(err, UserTasksEmpty) || errors.Is(err, NoOngoingOrUpcomingSchedule) {
|
||||
if errors.Is(err, UserTasksEmpty) || errors.Is(err, NoOngoingOrUpcomingSchedule) || errors.Is(err, TaskAlreadyDeleted) {
|
||||
c.JSON(http.StatusOK, err)
|
||||
return
|
||||
}
|
||||
@@ -379,6 +379,16 @@ var ( //请求相关的响应
|
||||
Info: "duplicate task_item_id in request",
|
||||
}
|
||||
|
||||
TaskUpdateNoFields = Response{ //更新任务未指定任何字段
|
||||
Status: "40063",
|
||||
Info: "no fields to update",
|
||||
}
|
||||
|
||||
TaskAlreadyDeleted = Response{ //任务已删除或不存在(幂等信息码)
|
||||
Status: "10003",
|
||||
Info: "task already deleted or not found",
|
||||
}
|
||||
|
||||
RouteControlInternalError = Response{ //路由控制码内部错误
|
||||
Status: "50001",
|
||||
Info: "route control failed",
|
||||
|
||||
@@ -55,6 +55,8 @@ func RegisterRouters(handlers *api.ApiHandlers, cache *dao.CacheDAO, userRepo *d
|
||||
taskGroup.POST("/create", middleware.IdempotencyMiddleware(cache), handlers.TaskHandler.AddTask)
|
||||
taskGroup.PUT("/complete", middleware.IdempotencyMiddleware(cache), handlers.TaskHandler.CompleteTask)
|
||||
taskGroup.PUT("/undo-complete", middleware.IdempotencyMiddleware(cache), handlers.TaskHandler.UndoCompleteTask)
|
||||
taskGroup.PUT("/update", middleware.IdempotencyMiddleware(cache), handlers.TaskHandler.UpdateTask)
|
||||
taskGroup.DELETE("/delete", middleware.IdempotencyMiddleware(cache), handlers.TaskHandler.DeleteTask)
|
||||
taskGroup.GET("/get", handlers.TaskHandler.GetUserTasks)
|
||||
}
|
||||
courseGroup := apiGroup.Group("/course")
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// taskUrgencyPromoteDedupeTTL 是“同一任务平移请求”的去重锁有效期。
|
||||
// taskUrgencyPromoteDedupeTTL 是"同一任务平移请求"的去重锁有效期。
|
||||
//
|
||||
// 设计考虑:
|
||||
// 1. 太短会导致消费稍慢时被重复投递;
|
||||
@@ -55,7 +55,7 @@ func NewTaskService(taskDAO *dao.TaskDAO, cacheDAO *dao.CacheDAO, eventPublisher
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责参数转换、优先级合法性校验与写库;
|
||||
// 2. 不负责“紧急性自动平移”逻辑(该逻辑发生在任务读取时的懒触发链路)。
|
||||
// 2. 不负责"紧急性自动平移"逻辑(该逻辑发生在任务读取时的懒触发链路)。
|
||||
func (ts *TaskService) AddTask(ctx context.Context, req *model.UserAddTaskRequest, userID int) (*model.UserAddTaskResponse, error) {
|
||||
// 1. 把用户请求转换为内部模型,避免 API 层结构直接泄漏到 DAO。
|
||||
taskModel := conv.UserAddTaskRequestToModel(req, userID)
|
||||
@@ -73,7 +73,7 @@ func (ts *TaskService) AddTask(ctx context.Context, req *model.UserAddTaskReques
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// CompleteTask 将用户指定任务标记为“已完成”。
|
||||
// CompleteTask 将用户指定任务标记为"已完成"。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责入参校验与业务错误映射;
|
||||
@@ -86,7 +86,7 @@ func (ts *TaskService) CompleteTask(ctx context.Context, req *model.UserComplete
|
||||
return nil, respond.WrongTaskID
|
||||
}
|
||||
|
||||
// 2. 调用 DAO 执行“查询 + 必要时更新”。
|
||||
// 2. 调用 DAO 执行"查询 + 必要时更新"。
|
||||
updatedTask, alreadyCompleted, err := ts.dao.CompleteTaskByID(ctx, userID, req.TaskID)
|
||||
if err != nil {
|
||||
// 2.1 任务不存在或不属于当前用户时,统一映射为 WrongTaskID。
|
||||
@@ -113,7 +113,7 @@ func (ts *TaskService) CompleteTask(ctx context.Context, req *model.UserComplete
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// UndoCompleteTask 取消用户任务的“已完成勾选”。
|
||||
// UndoCompleteTask 取消用户任务的"已完成勾选"。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责入参校验与业务错误映射;
|
||||
@@ -126,7 +126,7 @@ func (ts *TaskService) UndoCompleteTask(ctx context.Context, req *model.UserUndo
|
||||
return nil, respond.WrongTaskID
|
||||
}
|
||||
|
||||
// 2. 调用 DAO 执行“恢复未完成”逻辑。
|
||||
// 2. 调用 DAO 执行"恢复未完成"逻辑。
|
||||
updatedTask, err := ts.dao.UndoCompleteTaskByID(ctx, userID, req.TaskID)
|
||||
if err != nil {
|
||||
// 2.1 任务不存在或不属于当前用户,统一映射为 WrongTaskID。
|
||||
@@ -154,12 +154,12 @@ func (ts *TaskService) UndoCompleteTask(ctx context.Context, req *model.UserUndo
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetUserTasks 获取用户任务列表(含“读时紧急性派生”与“异步平移触发”)。
|
||||
// GetUserTasks 获取用户任务列表(含"读时紧急性派生"与"异步平移触发")。
|
||||
//
|
||||
// 核心流程(步骤化):
|
||||
// 1. 先读缓存,未命中再回源 DB,并把“原始模型”回填缓存;
|
||||
// 2. 在内存里做“读时派生”:仅用于本次返回给前端,不直接改库;
|
||||
// 3. 收集“已到紧急分界线且仍处于非紧急象限”的任务 ID;
|
||||
// 1. 先读缓存,未命中再回源 DB,并把"原始模型"回填缓存;
|
||||
// 2. 在内存里做"读时派生":仅用于本次返回给前端,不直接改库;
|
||||
// 3. 收集"已到紧急分界线且仍处于非紧急象限"的任务 ID;
|
||||
// 4. 通过 Redis SETNX 去重后,发布 outbox 事件异步落库;
|
||||
// 5. 无论发布成功与否,都优先返回本次派生结果,保证用户读体验。
|
||||
//
|
||||
@@ -189,7 +189,7 @@ func (ts *TaskService) GetTasksWithUrgencyPromotion(ctx context.Context, userID
|
||||
return derivedTasks, nil
|
||||
}
|
||||
|
||||
// getRawUserTasks 读取“原始任务模型”。
|
||||
// getRawUserTasks 读取"原始任务模型"。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责缓存命中/回源 DB/回填缓存;
|
||||
@@ -220,16 +220,16 @@ func (ts *TaskService) getRawUserTasks(ctx context.Context, userID int) ([]model
|
||||
return dbTasks, nil
|
||||
}
|
||||
|
||||
// deriveTaskUrgencyForRead 对任务做“读时紧急性派生”,并收集需要异步落库的任务 ID。
|
||||
// deriveTaskUrgencyForRead 对任务做"读时紧急性派生",并收集需要异步落库的任务 ID。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只在内存里改本次返回值,不写 DB;
|
||||
// 2. 只做“到线且未完成任务”的优先级映射;
|
||||
// 2. 只做"到线且未完成任务"的优先级映射;
|
||||
// 3. 不处理去重锁和事件发布。
|
||||
//
|
||||
// 返回语义:
|
||||
// 1. 第一个返回值:可直接用于响应前端的派生任务切片;
|
||||
// 2. 第二个返回值:需要发“异步平移事件”的任务 ID 列表(可能为空)。
|
||||
// 2. 第二个返回值:需要发"异步平移事件"的任务 ID 列表(可能为空)。
|
||||
func deriveTaskUrgencyForRead(tasks []model.Task, now time.Time) ([]model.Task, []int) {
|
||||
// 1. 拷贝切片,避免修改调用方持有的原始数据。
|
||||
derived := make([]model.Task, len(tasks))
|
||||
@@ -237,7 +237,7 @@ func deriveTaskUrgencyForRead(tasks []model.Task, now time.Time) ([]model.Task,
|
||||
|
||||
pendingPromoteTaskIDs := make([]int, 0, len(derived))
|
||||
|
||||
// 2. 逐条判断是否满足“自动平移”条件。
|
||||
// 2. 逐条判断是否满足"自动平移"条件。
|
||||
for idx := range derived {
|
||||
current := &derived[idx]
|
||||
|
||||
@@ -254,7 +254,7 @@ func deriveTaskUrgencyForRead(tasks []model.Task, now time.Time) ([]model.Task,
|
||||
continue
|
||||
}
|
||||
|
||||
// 2.4 到线后,仅把“不紧急象限”平移到对应“紧急象限”。
|
||||
// 2.4 到线后,仅把"不紧急象限"平移到对应"紧急象限"。
|
||||
// 2.4.1 重要不紧急(2) -> 重要且紧急(1)
|
||||
// 2.4.2 不简单不重要(4) -> 简单不重要(3)
|
||||
switch current.Priority {
|
||||
@@ -271,12 +271,12 @@ func deriveTaskUrgencyForRead(tasks []model.Task, now time.Time) ([]model.Task,
|
||||
return derived, pendingPromoteTaskIDs
|
||||
}
|
||||
|
||||
// tryEnqueueTaskUrgencyPromote 尝试发布“任务紧急性平移请求”事件。
|
||||
// tryEnqueueTaskUrgencyPromote 尝试发布"任务紧急性平移请求"事件。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责 Redis 去重锁 + outbox 发布;
|
||||
// 2. 不负责真正落库(由消费者负责);
|
||||
// 3. 发布失败时要释放本次抢到的去重锁,避免任务被长时间“误判已投递”。
|
||||
// 3. 发布失败时要释放本次抢到的去重锁,避免任务被长时间"误判已投递"。
|
||||
func (ts *TaskService) tryEnqueueTaskUrgencyPromote(ctx context.Context, userID int, taskIDs []int) {
|
||||
// 1. 基础兜底:无发布器或无候选任务时直接返回。
|
||||
if ts.eventPublisher == nil || userID <= 0 || len(taskIDs) == 0 {
|
||||
@@ -312,7 +312,7 @@ func (ts *TaskService) tryEnqueueTaskUrgencyPromote(ctx context.Context, userID
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 发布 outbox 事件:这里只保证“成功入 outbox 或返回错误”,不等待消费者执行完成。
|
||||
// 4. 发布 outbox 事件:这里只保证"成功入 outbox 或返回错误",不等待消费者执行完成。
|
||||
publishErr := eventsvc.PublishTaskUrgencyPromoteRequested(ctx, ts.eventPublisher, model.TaskUrgencyPromoteRequestedPayload{
|
||||
UserID: userID,
|
||||
TaskIDs: lockedTaskIDs,
|
||||
@@ -331,7 +331,7 @@ func (ts *TaskService) tryEnqueueTaskUrgencyPromote(ctx context.Context, userID
|
||||
// releaseTaskPromoteLocks 释放任务平移去重锁。
|
||||
//
|
||||
// 说明:
|
||||
// 1. 仅用于“发布失败回滚”场景;
|
||||
// 1. 仅用于"发布失败回滚"场景;
|
||||
// 2. 使用 Background 避免请求上下文已取消时导致锁释放失败。
|
||||
func (ts *TaskService) releaseTaskPromoteLocks(lockKeys []string) {
|
||||
if len(lockKeys) == 0 {
|
||||
@@ -347,7 +347,7 @@ func (ts *TaskService) releaseTaskPromoteLocks(lockKeys []string) {
|
||||
}
|
||||
}
|
||||
|
||||
// compactPositiveUniqueTaskIDs 对任务 ID 做“过滤非正数 + 去重”。
|
||||
// compactPositiveUniqueTaskIDs 对任务 ID 做"过滤非正数 + 去重"。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只做参数清洗;
|
||||
@@ -367,3 +367,80 @@ func compactPositiveUniqueTaskIDs(taskIDs []int) []int {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// UpdateTask 更新用户指定任务的属性(部分更新)。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责参数校验:task_id 合法性、priority_group 范围;
|
||||
// 2. 负责将请求 DTO 转换为 DAO 层的 updates map;
|
||||
// 3. 空请求体(无字段需要更新)返回明确业务错误;
|
||||
// 4. 不负责缓存删除(由 GORM cache_deleter 回调自动处理)。
|
||||
func (ts *TaskService) UpdateTask(ctx context.Context, req *model.UserUpdateTaskRequest, userID int) (model.GetUserTaskResp, error) {
|
||||
// 1. 参数兜底。
|
||||
if req == nil || userID <= 0 || req.TaskID <= 0 {
|
||||
return model.GetUserTaskResp{}, respond.WrongTaskID
|
||||
}
|
||||
|
||||
// 2. 构造 updates map:只有非 nil 的字段才写入。
|
||||
updates := make(map[string]interface{})
|
||||
if req.Title != nil {
|
||||
updates["title"] = *req.Title
|
||||
}
|
||||
if req.PriorityGroup != nil {
|
||||
// 2.1 优先级范围校验:当前任务体系只允许 1~4。
|
||||
if *req.PriorityGroup < 1 || *req.PriorityGroup > 4 {
|
||||
return model.GetUserTaskResp{}, respond.InvalidPriority
|
||||
}
|
||||
// 2.2 JSON 字段名是 priority_group,数据库列名是 priority。
|
||||
updates["priority"] = *req.PriorityGroup
|
||||
}
|
||||
if req.DeadlineAt != nil {
|
||||
updates["deadline_at"] = *req.DeadlineAt
|
||||
}
|
||||
if req.UrgencyThresholdAt != nil {
|
||||
updates["urgency_threshold_at"] = *req.UrgencyThresholdAt
|
||||
}
|
||||
|
||||
// 3. 空更新检测:至少需要一个可更新字段。
|
||||
if len(updates) == 0 {
|
||||
return model.GetUserTaskResp{}, respond.TaskUpdateNoFields
|
||||
}
|
||||
|
||||
// 4. 调用 DAO 执行更新。
|
||||
updatedTask, err := ts.dao.UpdateTaskByID(ctx, userID, req.TaskID, updates)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return model.GetUserTaskResp{}, respond.WrongTaskID
|
||||
}
|
||||
return model.GetUserTaskResp{}, err
|
||||
}
|
||||
|
||||
// 5. 转换为响应 DTO。
|
||||
return conv.ModelToGetUserTaskResp(updatedTask), nil
|
||||
}
|
||||
|
||||
// DeleteTask 永久删除用户指定任务。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责入参校验与业务错误映射;
|
||||
// 2. 负责调用 DAO 执行硬删除;
|
||||
// 3. 任务不存在时返回幂等信息码(TaskAlreadyDeleted);
|
||||
// 4. 不负责缓存删除(由 GORM cache_deleter 回调自动处理)。
|
||||
func (ts *TaskService) DeleteTask(ctx context.Context, req *model.UserCompleteTaskRequest, userID int) (int, error) {
|
||||
// 1. 参数兜底。
|
||||
if req == nil || userID <= 0 || req.TaskID <= 0 {
|
||||
return 0, respond.WrongTaskID
|
||||
}
|
||||
|
||||
// 2. 调用 DAO 执行删除。
|
||||
deletedTask, err := ts.dao.DeleteTaskByID(ctx, userID, req.TaskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// 2.1 任务不存在或不属于当前用户:按幂等语义返回信息码。
|
||||
return 0, respond.TaskAlreadyDeleted
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return deletedTask.ID, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user