Add motivation quote feature to smoking tracking API

- Introduced a new database model `SmokeMotivationQuote` for storing motivational quotes.
- Added a new API endpoint `GET /api/v1/smoke/motivation` to retrieve motivation quotes for users.
- Updated the main.go file to include the new model in the auto-migration process.
- Enhanced smoke_routes.go to register the new motivation route with the smoke handler.
This commit is contained in:
nepiedg
2026-01-25 09:53:18 +00:00
parent c9ebfd5873
commit 3154365ab2
5 changed files with 179 additions and 0 deletions
+1
View File
@@ -60,6 +60,7 @@ func main() {
&smokemodel.SmokeAIAdvice{},
&smokemodel.SmokeAIAdviceUnlock{},
&smokemodel.SmokeAINextSmoke{},
&smokemodel.SmokeMotivationQuote{},
); err != nil {
log.Fatalf("auto migrate failed: %v", err)
}
+1
View File
@@ -26,6 +26,7 @@ func registerSmokeRoutes(protected *gin.RouterGroup, smokeHandler *smokehandler.
smoke.GET("/logs/:id", smokeHandler.Get)
smoke.POST("/logs/:id", smokeHandler.Update)
smoke.DELETE("/logs/:id", smokeHandler.Delete)
smoke.GET("/motivation", smokeHandler.Motivation)
// AI 戒烟建议(会员优先;非会员需看广告解锁)
smoke.GET("/ai/advice", smokeHandler.GetAIAdvice)
@@ -0,0 +1,33 @@
package handler
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"wx_service/internal/middleware"
"wx_service/internal/model"
)
func (h *SmokeHandler) Motivation(c *gin.Context) {
user, ok := middleware.CurrentUser(c)
if !ok {
c.JSON(http.StatusUnauthorized, model.Error(http.StatusUnauthorized, "未登录或登录已过期"))
return
}
profile, err := h.smokeProfileService.Get(c.Request.Context(), int(user.ID))
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取基础信息失败,请稍后重试"))
return
}
result, err := h.smokeLogService.Motivation(c.Request.Context(), int(user.ID), time.Now().In(time.Local), profile)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "生成激励语失败,请稍后重试"))
return
}
c.JSON(http.StatusOK, model.Success(result))
}
+29
View File
@@ -0,0 +1,29 @@
package model
import (
"time"
"gorm.io/gorm"
)
// SmokeMotivationQuote 存储不同场景的激励语模板(可附带 AI 提示词)。
type SmokeMotivationQuote struct {
ID uint `gorm:"primaryKey;autoIncrement;comment:记录ID" json:"id"`
Scene string `gorm:"column:scene;size:40;index:idx_smoke_motivation_scene;comment:场景类型" json:"scene"`
Type string `gorm:"column:type;size:30;comment:文案类型" json:"type"`
Message string `gorm:"column:message;type:text;comment:展示文案" json:"message"`
AIPrompt string `gorm:"column:ai_prompt;type:text;comment:AI提示词" json:"ai_prompt,omitempty"`
Enabled bool `gorm:"column:enabled;default:true;comment:是否启用" json:"enabled"`
Weight int `gorm:"column:weight;default:1;comment:权重(越大越优先)" json:"weight"`
CreatedAt time.Time `gorm:"comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"-"`
}
func (SmokeMotivationQuote) TableName() string {
return "fa_smoke_motivation_quote"
}
func (SmokeMotivationQuote) TableComment() string {
return "戒烟-激励语模板"
}
@@ -0,0 +1,115 @@
package service
import (
"context"
"errors"
"time"
"gorm.io/gorm"
smokemodel "wx_service/internal/smoke/model"
)
type SmokeMotivation struct {
Message string `json:"message"`
Type string `json:"type"`
}
func (s *SmokeLogService) Motivation(ctx context.Context, uid int, asOf time.Time, profile *smokemodel.SmokeUserProfile) (SmokeMotivation, error) {
home, err := s.HomeSummary(ctx, uid, asOf)
if err != nil {
return SmokeMotivation{}, err
}
minutesSinceLast := -1
if home.LastSmokeAt != nil {
diff := int(asOf.Sub(*home.LastSmokeAt).Minutes())
if diff < 0 {
diff = 0
}
minutesSinceLast = diff
}
dailyTarget := 0
if profile != nil && profile.BaselineCigsPerDay > 0 {
dailyTarget = profile.BaselineCigsPerDay
}
quitMotivation := ""
if profile != nil && len(profile.QuitMotivations) > 0 {
quitMotivation = profile.QuitMotivations[0]
}
scene, fallback := buildMotivationScene(home, minutesSinceLast, dailyTarget, quitMotivation)
if quote, ok, err := s.loadMotivationQuote(ctx, scene); err != nil {
return fallback, err
} else if ok {
return SmokeMotivation{Message: quote.Message, Type: quote.Type}, nil
}
if scene != "default" {
if quote, ok, err := s.loadMotivationQuote(ctx, "default"); err != nil {
return fallback, err
} else if ok {
return SmokeMotivation{Message: quote.Message, Type: quote.Type}, nil
}
}
return fallback, nil
}
func buildMotivationScene(home SmokeHomeSummary, minutesSinceLast int, dailyTarget int, quitMotivation string) (string, SmokeMotivation) {
if home.ResistedCount > 0 && minutesSinceLast >= 0 && minutesSinceLast < 30 {
return "recent_resist", SmokeMotivation{
Message: "太棒了!你刚刚成功抵抗了一次烟瘾",
Type: "praise",
}
}
if dailyTarget > 0 && home.TodayCount < int(float64(dailyTarget)*0.5) {
return "below_half_target", SmokeMotivation{
Message: "今天的表现非常出色,继续保持!",
Type: "encourage",
}
}
if dailyTarget > 0 && home.TodayCount == dailyTarget-1 {
return "near_limit", SmokeMotivation{
Message: "还剩最后一支配额,考虑把它留到睡前?",
Type: "hint",
}
}
if dailyTarget > 0 && home.TodayCount > dailyTarget {
msg := "没关系,明天是新的一天。"
if quitMotivation != "" {
msg = msg + "记住你为什么要戒烟:" + quitMotivation
}
return "over_target", SmokeMotivation{
Message: msg,
Type: "comfort",
}
}
return "default", SmokeMotivation{
Message: "保持连胜纪录!",
Type: "encourage",
}
}
func (s *SmokeLogService) loadMotivationQuote(ctx context.Context, scene string) (smokemodel.SmokeMotivationQuote, bool, error) {
var quote smokemodel.SmokeMotivationQuote
if err := s.db.WithContext(ctx).
Where("scene = ? AND enabled = 1", scene).
Order("weight DESC, id ASC").
First(&quote).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return smokemodel.SmokeMotivationQuote{}, false, nil
}
return smokemodel.SmokeMotivationQuote{}, false, err
}
if quote.Message == "" || quote.Type == "" {
return smokemodel.SmokeMotivationQuote{}, false, nil
}
return quote, true, nil
}