From cd95aeeaaaac9c0ddba924550732a3432c1513b6 Mon Sep 17 00:00:00 2001 From: Losita <2810873701@qq.com> Date: Tue, 17 Mar 2026 22:54:07 +0800 Subject: [PATCH] =?UTF-8?q?Version:=200.6.8.dev.260317=20-=20=F0=9F=A7=B9?= =?UTF-8?q?=20=E5=88=A0=E9=99=A4=20`docs/apifox`=20=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=EF=BC=8C=E6=8E=A5=E5=8F=A3=E5=A5=91=E7=BA=A6=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=B9=B6=E7=BB=B4=E6=8A=A4=E4=BA=8E=20Apifox?= =?UTF-8?q?=20=E4=BA=91=E7=AB=AF=20-=20=E2=9C=A8=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E2=80=9C=E5=8F=96=E6=B6=88=E4=BB=BB=E5=8A=A1=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E2=80=9D=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/task.go | 33 +++++++++ backend/dao/task.go | 68 +++++++++++++++++ backend/model/task.go | 21 ++++++ backend/respond/respond.go | 5 ++ backend/routers/routers.go | 1 + backend/service/task.go | 41 +++++++++++ docs/apifox/agent-chat.openapi.yaml | 110 ---------------------------- 7 files changed, 169 insertions(+), 110 deletions(-) delete mode 100644 docs/apifox/agent-chat.openapi.yaml diff --git a/backend/api/task.go b/backend/api/task.go index 91b805c..56762ab 100644 --- a/backend/api/task.go +++ b/backend/api/task.go @@ -95,3 +95,36 @@ func (th *TaskHandler) CompleteTask(c *gin.Context) { // 5. 返回统一响应结构。 c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, resp)) } + +// UndoCompleteTask 取消任务“已完成”勾选。 +// +// 职责边界: +// 1. 负责解析请求与读取 user_id; +// 2. 负责调用 Service 执行业务恢复; +// 3. 不负责“任务是否已完成”的业务判断(由 Service/DAO 负责)。 +func (th *TaskHandler) UndoCompleteTask(c *gin.Context) { + // 1. 绑定请求参数。 + var req model.UserUndoCompleteTaskRequest + 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.UndoCompleteTask(ctx, &req, userID) + if err != nil { + respond.DealWithError(c, err) + return + } + + // 5. 返回统一响应结构。 + c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, resp)) +} diff --git a/backend/dao/task.go b/backend/dao/task.go index 56fc6d2..b7d392c 100644 --- a/backend/dao/task.go +++ b/backend/dao/task.go @@ -116,6 +116,74 @@ func (dao *TaskDAO) CompleteTaskByID(ctx context.Context, userID int, taskID int return &target, false, nil } +// UndoCompleteTaskByID 将指定任务从“已完成”恢复为“未完成”。 +// +// 职责边界: +// 1. 只负责当前用户(user_id)下指定 task_id 的状态恢复; +// 2. 若任务本就未完成,按业务要求返回明确错误,不做幂等成功; +// 3. 不负责响应文案拼装(由 Service 层处理)。 +// +// 返回语义: +// 1. *model.Task:恢复后的任务快照; +// 2. error: +// 2.1 gorm.ErrRecordNotFound:任务不存在或不属于当前用户; +// 2.2 respond.TaskNotCompleted:任务当前不是“已完成”状态,不能执行取消勾选; +// 2.3 其他 error:数据库异常。 +func (dao *TaskDAO) UndoCompleteTaskByID(ctx context.Context, userID int, taskID int) (*model.Task, error) { + // 1. 参数兜底:非法 user/task 参数统一按“记录不存在”处理,避免误写。 + 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 这是本接口和“标记完成”接口的关键差异:这里不做幂等成功。 + if !target.IsCompleted { + return nil, respond.TaskNotCompleted + } + + // 4. 执行状态恢复(is_completed=true -> false)。 + // + // 4.1 使用 Model(&model.Task{UserID:userID}) 的目的是让 cache_deleter 拿到 user_id, + // 从而在回调中正确删除该用户任务缓存。 + updateResult := dao.db.WithContext(ctx). + Model(&model.Task{UserID: userID}). + Where("id = ? AND user_id = ?", taskID, userID). + Update("is_completed", false) + if updateResult.Error != nil { + return nil, updateResult.Error + } + + // 5. 并发兜底: + // 5.1 若 RowsAffected=0,说明可能被并发请求先一步恢复; + // 5.2 重新读取当前状态,若已是未完成则按业务规则返回“任务未完成”错误。 + if updateResult.RowsAffected == 0 { + var check model.Task + checkErr := dao.db.WithContext(ctx). + Where("id = ? AND user_id = ?", taskID, userID). + First(&check).Error + if checkErr != nil { + return nil, checkErr + } + if !check.IsCompleted { + return nil, respond.TaskNotCompleted + } + return nil, errors.New("取消任务完成状态失败") + } + + // 6. 回填恢复后状态并返回。 + target.IsCompleted = false + return &target, nil +} + // PromoteTaskUrgencyByIDs 批量执行“任务紧急性平移”。 // // 职责边界: diff --git a/backend/model/task.go b/backend/model/task.go index 7b60b25..99a0c7a 100644 --- a/backend/model/task.go +++ b/backend/model/task.go @@ -81,6 +81,27 @@ type UserCompleteTaskResponse struct { Status string `json:"status"` } +// UserUndoCompleteTaskRequest 是“取消任务已完成勾选”接口请求体。 +// +// 职责边界: +// 1. 只承载目标 task_id; +// 2. 不承载 user_id(user_id 始终由鉴权中间件注入,防止越权操作)。 +type UserUndoCompleteTaskRequest struct { + TaskID int `json:"task_id"` +} + +// UserUndoCompleteTaskResponse 是“取消任务已完成勾选”接口响应体。 +// +// 字段语义: +// 1. TaskID:本次操作目标任务; +// 2. IsCompleted:操作后完成状态(成功时恒为 false); +// 3. Status:给前端的简短状态文案。 +type UserUndoCompleteTaskResponse struct { + TaskID int `json:"task_id"` + IsCompleted bool `json:"is_completed"` + Status string `json:"status"` +} + type GetUserTaskResp struct { ID int `json:"id"` UserID int `json:"user_id"` diff --git a/backend/respond/respond.go b/backend/respond/respond.go index b793658..4bf1202 100644 --- a/backend/respond/respond.go +++ b/backend/respond/respond.go @@ -323,4 +323,9 @@ var ( //请求相关的响应 Status: "40051", Info: "token usage exceeds limit", } + + TaskNotCompleted = Response{ //任务未完成,无法取消勾选 + Status: "40052", + Info: "task is not completed", + } ) diff --git a/backend/routers/routers.go b/backend/routers/routers.go index 20ebfa3..131edbb 100644 --- a/backend/routers/routers.go +++ b/backend/routers/routers.go @@ -54,6 +54,7 @@ func RegisterRouters(handlers *api.ApiHandlers, cache *dao.CacheDAO, userRepo *d taskGroup.Use(middleware.JWTTokenAuth(cache), middleware.RateLimitMiddleware(limiter, 20, 1)) 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.GET("/get", handlers.TaskHandler.GetUserTasks) } courseGroup := apiGroup.Group("/course") diff --git a/backend/service/task.go b/backend/service/task.go index 5986e9b..af1d088 100644 --- a/backend/service/task.go +++ b/backend/service/task.go @@ -113,6 +113,47 @@ func (ts *TaskService) CompleteTask(ctx context.Context, req *model.UserComplete return resp, nil } +// UndoCompleteTask 取消用户任务的“已完成勾选”。 +// +// 职责边界: +// 1. 负责入参校验与业务错误映射; +// 2. 负责调用 DAO 执行状态恢复; +// 3. 不负责幂等缓存(本接口按需求要求:任务未完成时必须报错); +// 4. 不负责缓存删除细节(由 GORM cache_deleter 回调自动处理)。 +func (ts *TaskService) UndoCompleteTask(ctx context.Context, req *model.UserUndoCompleteTaskRequest, userID int) (*model.UserUndoCompleteTaskResponse, error) { + // 1. 参数兜底:请求体为空、非法 user 或非法 task_id 直接返回业务错误。 + if req == nil || userID <= 0 || req.TaskID <= 0 { + return nil, respond.WrongTaskID + } + + // 2. 调用 DAO 执行“恢复未完成”逻辑。 + updatedTask, err := ts.dao.UndoCompleteTaskByID(ctx, userID, req.TaskID) + if err != nil { + // 2.1 任务不存在或不属于当前用户,统一映射为 WrongTaskID。 + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, respond.WrongTaskID + } + // 2.2 任务本来就未完成:按需求返回明确业务错误。 + if errors.Is(err, respond.TaskNotCompleted) { + return nil, respond.TaskNotCompleted + } + // 2.3 其余数据库异常继续向上透传。 + return nil, err + } + if updatedTask == nil { + // 3. 极端防御:DAO 成功但返回 nil,视为内部异常。 + return nil, errors.New("undo complete task succeeded but task is nil") + } + + // 4. 组装响应:恢复成功后 is_completed 恒为 false。 + resp := &model.UserUndoCompleteTaskResponse{ + TaskID: updatedTask.ID, + IsCompleted: false, + Status: "uncompleted", + } + return resp, nil +} + // GetUserTasks 获取用户任务列表(含“读时紧急性派生”与“异步平移触发”)。 // // 核心流程(步骤化): diff --git a/docs/apifox/agent-chat.openapi.yaml b/docs/apifox/agent-chat.openapi.yaml deleted file mode 100644 index 77efea1..0000000 --- a/docs/apifox/agent-chat.openapi.yaml +++ /dev/null @@ -1,110 +0,0 @@ -openapi: 3.0.1 -info: - title: '' - version: 1.0.0 -paths: - /agent/chat: - post: - summary: AI Agent&聊天 - deprecated: false - description: >- - 本接口既支持带着消息新建对话,也支持通过旧对话继续聊天。 - 在 JSON 中传入 conversation_id,后端查库:存在则延续,不存在则创建新对话后再聊天。 - - 流式响应采用 OpenAI/DeepSeek 兼容格式: - - 思考流:choices[0].delta.reasoning_content - - 正文流:choices[0].delta.content - - 结束标记:data: [DONE] - tags: - - Agent模块 - parameters: - - name: Authorization - in: header - description: token - required: false - example: '' - schema: - type: string - - name: Content-Type - in: header - description: '' - required: false - example: - - application/json - schema: - type: array - items: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - conversation_id: - type: string - description: 可选。不传时后端自动生成,并通过 X-Conversation-ID 响应头返回。 - x-apifox-mock: '{{$string.uuid}}' - message: - type: string - description: 用户输入内容 - model: - type: string - description: 可选,worker 或 strategist(默认 worker) - thinking: - type: boolean - description: 是否开启深度思考 - required: - - message - - thinking - x-apifox-orders: - - conversation_id - - message - - model - - thinking - example: - conversation_id: 0b6eac35-ccaa-46d1-aa58-d33bc2cd48aa - message: 提醒我有空的时候给自己挑一件新衣服 - model: worker - thinking: true - responses: - '200': - description: '' - content: - text/event-stream: - schema: - type: string - description: >- - 每条 SSE 事件都是 `data: {JSON}`,最终以 `data: [DONE]` 结束。 - Apifox 可按 OpenAI 兼容格式自动合并,并区分 reasoning_content 与 content。 - example: |- - data: {"id":"chatcmpl-3f3ee5d6-8c4f-4b5b-a2a8-7f5b9bde8b9d","object":"chat.completion.chunk","created":1740637581,"model":"worker","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"先分析一下你的需求。"},"finish_reason":null}]} - - data: {"id":"chatcmpl-3f3ee5d6-8c4f-4b5b-a2a8-7f5b9bde8b9d","object":"chat.completion.chunk","created":1740637581,"model":"worker","choices":[{"index":0,"delta":{"reasoning_content":"你提到的是空闲时提醒。"},"finish_reason":null}]} - - data: {"id":"chatcmpl-3f3ee5d6-8c4f-4b5b-a2a8-7f5b9bde8b9d","object":"chat.completion.chunk","created":1740637581,"model":"worker","choices":[{"index":0,"delta":{"content":"可以,我会在你有空时提醒你。"},"finish_reason":null}]} - - data: {"id":"chatcmpl-3f3ee5d6-8c4f-4b5b-a2a8-7f5b9bde8b9d","object":"chat.completion.chunk","created":1740637581,"model":"worker","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} - - data: [DONE] - headers: - X-Conversation-ID: - example: 0b6eac35-ccaa-46d1-aa58-d33bc2cd48aa - required: false - description: 生效的会话 ID,用于后续续聊 - schema: - type: string - x-apifox-name: 成功 - x-apifox-ordering: 0 - security: [] - x-apifox-folder: Agent模块 - x-apifox-status: developing -components: - schemas: {} - responses: {} - securitySchemes: {} -servers: - - url: http://127.0.0.1:8080/api/v1 - description: 开发环境 -security: []