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:
@@ -60,6 +60,7 @@ func main() {
|
||||
&smokemodel.SmokeAIAdvice{},
|
||||
&smokemodel.SmokeAIAdviceUnlock{},
|
||||
&smokemodel.SmokeAINextSmoke{},
|
||||
&smokemodel.SmokeMotivationQuote{},
|
||||
); err != nil {
|
||||
log.Fatalf("auto migrate failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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("e).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
|
||||
}
|
||||
Reference in New Issue
Block a user