package handler import ( "net/http" "strconv" "time" "github.com/gin-gonic/gin" "wx_service/internal/middleware" "wx_service/internal/model" smokeservice "wx_service/internal/smoke/service" ) // QuitPlanHandler 戒烟计划 Handler type QuitPlanHandler struct { smokeQuitPlanService *smokeservice.SmokeQuitPlanService } // NewQuitPlanHandler 创建戒烟计划 Handler func NewQuitPlanHandler(smokeQuitPlanService *smokeservice.SmokeQuitPlanService) *QuitPlanHandler { return &QuitPlanHandler{ smokeQuitPlanService: smokeQuitPlanService, } } // generateQuitPlanRequest 生成戒烟计划请求 type generateQuitPlanRequest struct { StartDate string `json:"start_date"` } // GenerateQuitPlan POST /api/smoke/quit-plan/generate - 生成戒烟计划 func (h *QuitPlanHandler) GenerateQuitPlan(c *gin.Context) { user := middleware.MustCurrentUser(c) var req generateQuitPlanRequest if err := c.ShouldBindJSON(&req); err != nil { // 允许空请求,使用默认开始日期 req = generateQuitPlanRequest{} } var startDate *time.Time if req.StartDate != "" { parsed, err := time.ParseInLocation(dateLayout, req.StartDate, time.Local) if err != nil { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "start_date 格式错误,应为 YYYY-MM-DD")) return } startDate = &parsed } plan, err := h.smokeQuitPlanService.GenerateQuitPlan(c.Request.Context(), user, smokeservice.GenerateQuitPlanRequest{ StartDate: startDate, }) if err != nil { switch { case err == smokeservice.ErrNoUserProfile: c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请先完成个人资料填写")) return case err == smokeservice.ErrPlanAlreadyActive: c.JSON(http.StatusConflict, model.Error(http.StatusConflict, "已有进行中的戒烟计划,请先重置")) return case err == smokeservice.ErrAIServiceDisabled: c.JSON(http.StatusServiceUnavailable, model.Error(http.StatusServiceUnavailable, "AI 服务暂不可用,请联系管理员")) return default: c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "生成计划失败,请稍后重试")) return } } c.JSON(http.StatusOK, model.Success(gin.H{ "id": plan.ID, "status": plan.Status, "start_date": plan.StartDate.Format(dateLayout), "end_date": plan.EndDate.Format(dateLayout), "current_stage": plan.CurrentStage, "current_day": plan.CurrentDay, "baseline_cigs": plan.BaselineCigsPerDay, "summary": plan.Summary, })) } // GetQuitPlan GET /api/smoke/quit-plan - 查询当前戒烟计划 func (h *QuitPlanHandler) GetQuitPlan(c *gin.Context) { user := middleware.MustCurrentUser(c) plan, err := h.smokeQuitPlanService.GetActivePlan(c.Request.Context(), int(user.ID)) if err != nil { if err == smokeservice.ErrQuitPlanNotFound { c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "暂无进行中的戒烟计划")) return } c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "查询计划失败,请稍后重试")) return } // 计算当前进度 today := time.Now() daysSinceStart := int(today.Sub(plan.StartDate).Hours() / 24) if daysSinceStart < 0 { daysSinceStart = 0 } currentDay := daysSinceStart + 1 if currentDay > 30 { currentDay = 30 } // 更新阶段 currentStage := plan.CurrentStage if currentDay > 21 { currentStage = "consolidating" } else if currentDay > 7 { currentStage = "reducing" } else { currentStage = "recording" } // 获取今日目标 var todayTarget *int var todayAdvice *string todayDate := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, time.Local) dayPlan, err := h.smokeQuitPlanService.GetPlanDayByDate(c.Request.Context(), int(user.ID), todayDate) if err == nil { todayTarget = &dayPlan.TargetCigs todayAdvice = &dayPlan.Advice } c.JSON(http.StatusOK, model.Success(gin.H{ "id": plan.ID, "status": plan.Status, "start_date": plan.StartDate.Format(dateLayout), "end_date": plan.EndDate.Format(dateLayout), "current_stage": currentStage, "current_day": currentDay, "completed_days": plan.CompletedDays, "baseline_cigs": plan.BaselineCigsPerDay, "summary": plan.Summary, "today_target": todayTarget, "today_advice": todayAdvice, })) } // GetQuitPlanDays GET /api/smoke/quit-plan/days - 查询每日明细 func (h *QuitPlanHandler) GetQuitPlanDays(c *gin.Context) { user := middleware.MustCurrentUser(c) // 获取计划 ID planIDStr := c.Query("plan_id") var planID int if planIDStr != "" { var err error planID, err = strconv.Atoi(planIDStr) if err != nil || planID <= 0 { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "plan_id 参数错误")) return } } else { // 默认获取活跃计划 plan, err := h.smokeQuitPlanService.GetActivePlan(c.Request.Context(), int(user.ID)) if err != nil { if err == smokeservice.ErrQuitPlanNotFound { c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "暂无进行中的戒烟计划")) return } c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "查询计划失败")) return } planID = int(plan.ID) } // 查询每日明细 days, err := h.smokeQuitPlanService.GetPlanDays(c.Request.Context(), planID) if err != nil { c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "查询明细失败")) return } // 转换为响应格式 result := make([]gin.H, len(days)) for i, d := range days { result[i] = gin.H{ "day": d.Day, "plan_date": d.PlanDate.Format(dateLayout), "stage": d.Stage, "target_cigs": d.TargetCigs, "target_reduced": d.TargetReduced, "advice": d.Advice, } // 如果有实际数据,也返回 if d.ActualCigs != nil { result[i]["actual_cigs"] = *d.ActualCigs } if d.ResistedCnt != nil { result[i]["resisted_cnt"] = *d.ResistedCnt } if d.Achieved != nil { result[i]["achieved"] = *d.Achieved } } c.JSON(http.StatusOK, model.Success(gin.H{ "plan_id": planID, "days": result, })) } // resetQuitPlanRequest 重置戒烟计划请求 type resetQuitPlanRequest struct { StartDate string `json:"start_date"` } // ResetQuitPlan POST /api/smoke/quit-plan/reset - 重置戒烟计划 func (h *QuitPlanHandler) ResetQuitPlan(c *gin.Context) { user := middleware.MustCurrentUser(c) var req resetQuitPlanRequest if err := c.ShouldBindJSON(&req); err != nil { req = resetQuitPlanRequest{} } var startDate *time.Time if req.StartDate != "" { parsed, err := time.ParseInLocation(dateLayout, req.StartDate, time.Local) if err != nil { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "start_date 格式错误,应为 YYYY-MM-DD")) return } startDate = &parsed } plan, err := h.smokeQuitPlanService.ResetPlan(c.Request.Context(), user, smokeservice.GenerateQuitPlanRequest{ StartDate: startDate, }) if err != nil { switch { case err == smokeservice.ErrNoUserProfile: c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请先完成个人资料填写")) return case err == smokeservice.ErrAIServiceDisabled: c.JSON(http.StatusServiceUnavailable, model.Error(http.StatusServiceUnavailable, "AI 服务暂不可用,请联系管理员")) return default: c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "重置计划失败,请稍后重试")) return } } c.JSON(http.StatusOK, model.Success(gin.H{ "id": plan.ID, "status": plan.Status, "start_date": plan.StartDate.Format(dateLayout), "end_date": plan.EndDate.Format(dateLayout), "current_stage": plan.CurrentStage, "current_day": plan.CurrentDay, "baseline_cigs": plan.BaselineCigsPerDay, "summary": plan.Summary, })) }