feat: 戒烟成就、梦想图标预设、打卡统计与依赖注入调整

- 成就系统、连续打卡天数计算、管理后台成就 CRUD
- 梦想目标图标预设 DreamPreset 与用户端 dream-presets 接口
- 管理后台梦想图标 CRUD;戒烟打卡 summary 修正
- 忽略根目录编译产物 /api

Made-with: Cursor
This commit is contained in:
nepiedg
2026-04-04 14:55:50 +08:00
parent 1c0aeb152a
commit fd097729d7
20 changed files with 849 additions and 12 deletions
@@ -0,0 +1,69 @@
package handler
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"wx_service/internal/achievement"
"wx_service/internal/middleware"
"wx_service/internal/model"
)
func (h *SmokeHandler) ListAchievementThemes(c *gin.Context) {
themes, err := h.achievementService.ListActiveThemes(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取成就主题失败"))
return
}
c.JSON(http.StatusOK, model.Success(gin.H{"themes": themes}))
}
func (h *SmokeHandler) GetAchievement(c *gin.Context) {
user := middleware.MustCurrentUser(c)
ctx := c.Request.Context()
uid := int(user.ID)
now := time.Now()
profile, err := h.smokeProfileService.Get(ctx, uid)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取用户信息失败"))
return
}
if profile == nil || profile.AchievementThemeID == nil {
c.JSON(http.StatusOK, model.Success(gin.H{"achievement": nil}))
return
}
var days int
if profile.Mode == "quit" {
homeData, err := h.quitCheckinService.Home(ctx, uid, now)
if err != nil {
log.Printf("achievement: quitcheckin home err uid=%d: %v", uid, err)
days = 0
} else {
days = homeData.Summary.CurrentStreakDays
}
} else {
streakDays, err := h.smokeLogService.GetStreakDays(ctx, uid, now)
if err != nil {
log.Printf("achievement: smoke streak err uid=%d: %v", uid, err)
days = 0
}
days = streakDays
}
ach, err := h.achievementService.GetUserAchievement(ctx, *profile.AchievementThemeID, days)
if err != nil {
if err == achievement.ErrThemeNotFound {
c.JSON(http.StatusOK, model.Success(gin.H{"achievement": nil}))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取成就信息失败"))
return
}
c.JSON(http.StatusOK, model.Success(gin.H{"achievement": ach}))
}
+8
View File
@@ -10,8 +10,10 @@ import (
"github.com/gin-gonic/gin"
"wx_service/internal/achievement"
"wx_service/internal/middleware"
"wx_service/internal/model"
quitcheckinservice "wx_service/internal/quitcheckin/service"
smokeservice "wx_service/internal/smoke/service"
)
@@ -22,6 +24,8 @@ type SmokeHandler struct {
smokeNextService *smokeservice.SmokeNextService
smokeAINextService *smokeservice.SmokeAINextSmokeService
smokeShareService *smokeservice.SmokeShareService
achievementService *achievement.Service
quitCheckinService *quitcheckinservice.Service
}
func NewSmokeHandler(
@@ -31,6 +35,8 @@ func NewSmokeHandler(
smokeNextService *smokeservice.SmokeNextService,
smokeAINextService *smokeservice.SmokeAINextSmokeService,
smokeShareService *smokeservice.SmokeShareService,
achievementService *achievement.Service,
quitCheckinService *quitcheckinservice.Service,
) *SmokeHandler {
return &SmokeHandler{
smokeLogService: smokeLogService,
@@ -39,6 +45,8 @@ func NewSmokeHandler(
smokeNextService: smokeNextService,
smokeAINextService: smokeAINextService,
smokeShareService: smokeShareService,
achievementService: achievementService,
quitCheckinService: quitCheckinService,
}
}
+16 -10
View File
@@ -27,6 +27,8 @@ type upsertSmokeProfileRequest struct {
SleepTime *string `json:"sleep_time"`
QuitDate *string `json:"quit_date"`
AchievementThemeID *uint `json:"achievement_theme_id"`
}
func (h *SmokeHandler) GetProfile(c *gin.Context) {
@@ -95,17 +97,21 @@ func (h *SmokeHandler) UpsertProfile(c *gin.Context) {
}
}
achievementThemeIDProvided := req.AchievementThemeID != nil
view, err := h.smokeProfileService.Upsert(c.Request.Context(), int(user.ID), smokeservice.UpsertSmokeProfileRequest{
BaselineCigsPerDay: req.BaselineCigsPerDay,
SmokingYears: req.SmokingYears,
PackPriceCent: req.PackPriceCent,
Mode: req.Mode,
SmokeMotivations: req.SmokeMotivations,
QuitMotivations: req.QuitMotivations,
WakeUpTime: req.WakeUpTime,
SleepTime: req.SleepTime,
QuitDateProvided: quitDateProvided,
QuitDate: quitDate,
BaselineCigsPerDay: req.BaselineCigsPerDay,
SmokingYears: req.SmokingYears,
PackPriceCent: req.PackPriceCent,
Mode: req.Mode,
SmokeMotivations: req.SmokeMotivations,
QuitMotivations: req.QuitMotivations,
WakeUpTime: req.WakeUpTime,
SleepTime: req.SleepTime,
QuitDateProvided: quitDateProvided,
QuitDate: quitDate,
AchievementThemeIDProvided: achievementThemeIDProvided,
AchievementThemeID: req.AchievementThemeID,
})
if err != nil {
if errors.Is(err, smokeservice.ErrSmokeProfileInvalidTime) {