93bcc6c787
- 新增 Model 层: SmokeQuitPlan, SmokeQuitPlanDay 结构体 - 新增 Service 层: GenerateQuitPlan, GetActivePlan, GetPlanDays, ResetPlan - 新增 Handler 层: POST /generate, GET /, GET /days, POST /reset - 集成 AI 生成 30 天个性化戒烟减量方案 - 支持重置计划功能
260 lines
7.8 KiB
Go
260 lines
7.8 KiB
Go
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,
|
|
}))
|
|
}
|