feat(smoke): 添加个性化戒烟计划生成功能 (Issue #46)
- 新增 Model 层: SmokeQuitPlan, SmokeQuitPlanDay 结构体 - 新增 Service 层: GenerateQuitPlan, GetActivePlan, GetPlanDays, ResetPlan - 新增 Handler 层: POST /generate, GET /, GET /days, POST /reset - 集成 AI 生成 30 天个性化戒烟减量方案 - 支持重置计划功能
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
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,
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user