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,162 @@
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"wx_service/internal/model"
)
type achievementThemeRequest struct {
Name string `json:"name"`
Key string `json:"key"`
Icon string `json:"icon"`
SortOrder int `json:"sort_order"`
IsActive *bool `json:"is_active"`
}
type achievementLevelRequest struct {
ThemeID uint `json:"theme_id"`
Name string `json:"name"`
Icon string `json:"icon"`
RequiredDays int `json:"required_days"`
SortOrder int `json:"sort_order"`
}
func (h *Handler) ListAchievementThemes(c *gin.Context) {
themes, err := h.svc.ListAchievementThemes(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 *Handler) GetAchievementTheme(c *gin.Context) {
id, err := parseUintID(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid id"))
return
}
theme, err := h.svc.GetAchievementTheme(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "主题不存在"))
return
}
c.JSON(http.StatusOK, model.Success(theme))
}
func (h *Handler) CreateAchievementTheme(c *gin.Context) {
var req achievementThemeRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误"))
return
}
if req.Name == "" || req.Key == "" {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "name 和 key 必填"))
return
}
theme, err := h.svc.CreateAchievementTheme(c.Request.Context(), req.Name, req.Key, req.Icon, req.SortOrder, req.IsActive)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "创建主题失败"))
return
}
c.JSON(http.StatusOK, model.Success(theme))
}
func (h *Handler) UpdateAchievementTheme(c *gin.Context) {
id, err := parseUintID(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid id"))
return
}
var req achievementThemeRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误"))
return
}
theme, err := h.svc.UpdateAchievementTheme(c.Request.Context(), id, req.Name, req.Key, req.Icon, req.SortOrder, req.IsActive)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "更新主题失败"))
return
}
c.JSON(http.StatusOK, model.Success(theme))
}
func (h *Handler) DeleteAchievementTheme(c *gin.Context) {
id, err := parseUintID(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid id"))
return
}
if err := h.svc.DeleteAchievementTheme(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "删除主题失败"))
return
}
c.JSON(http.StatusOK, model.Success(gin.H{"deleted": true}))
}
func (h *Handler) ListAchievementLevels(c *gin.Context) {
themeID, err := parseUintID(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid theme id"))
return
}
levels, err := h.svc.ListAchievementLevels(c.Request.Context(), themeID)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取等级列表失败"))
return
}
c.JSON(http.StatusOK, model.Success(gin.H{"levels": levels}))
}
func (h *Handler) CreateAchievementLevel(c *gin.Context) {
var req achievementLevelRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误"))
return
}
if req.Name == "" || req.ThemeID == 0 {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "name 和 theme_id 必填"))
return
}
level, err := h.svc.CreateAchievementLevel(c.Request.Context(), req.ThemeID, req.Name, req.Icon, req.RequiredDays, req.SortOrder)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "创建等级失败"))
return
}
c.JSON(http.StatusOK, model.Success(level))
}
func (h *Handler) UpdateAchievementLevel(c *gin.Context) {
id, err := parseUintID(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid id"))
return
}
var req achievementLevelRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误"))
return
}
level, err := h.svc.UpdateAchievementLevel(c.Request.Context(), id, req.Name, req.Icon, req.RequiredDays, req.SortOrder)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "更新等级失败"))
return
}
c.JSON(http.StatusOK, model.Success(level))
}
func (h *Handler) DeleteAchievementLevel(c *gin.Context) {
id, err := parseUintID(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid id"))
return
}
if err := h.svc.DeleteAchievementLevel(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "删除等级失败"))
return
}
c.JSON(http.StatusOK, model.Success(gin.H{"deleted": true}))
}
@@ -0,0 +1,83 @@
package handler
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"wx_service/internal/model"
)
type dreamPresetRequest struct {
Title string `json:"title"`
CoverImage string `json:"cover_image"`
SortOrder int `json:"sort_order"`
IsActive *bool `json:"is_active"`
}
func (h *Handler) ListDreamPresets(c *gin.Context) {
presets, err := h.svc.ListDreamPresets(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取预设目标失败"))
return
}
c.JSON(http.StatusOK, model.Success(gin.H{"items": presets}))
}
func (h *Handler) CreateDreamPreset(c *gin.Context) {
var req dreamPresetRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "参数错误"))
return
}
if strings.TrimSpace(req.CoverImage) == "" {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "图标不能为空"))
return
}
preset, err := h.svc.CreateDreamPreset(c.Request.Context(), req.Title, req.CoverImage, req.SortOrder, req.IsActive)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "创建失败"))
return
}
c.JSON(http.StatusOK, model.Success(preset))
}
type dreamPresetUpdateRequest struct {
Title *string `json:"title"`
CoverImage *string `json:"cover_image"`
SortOrder *int `json:"sort_order"`
IsActive *bool `json:"is_active"`
}
func (h *Handler) UpdateDreamPreset(c *gin.Context) {
id, err := parseUintID(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "id 参数错误"))
return
}
var req dreamPresetUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "参数错误"))
return
}
preset, err := h.svc.UpdateDreamPreset(c.Request.Context(), id, req.Title, req.CoverImage, req.SortOrder, req.IsActive)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "更新失败"))
return
}
c.JSON(http.StatusOK, model.Success(preset))
}
func (h *Handler) DeleteDreamPreset(c *gin.Context) {
id, err := parseUintID(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "id 参数错误"))
return
}
if err := h.svc.DeleteDreamPreset(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "删除失败"))
return
}
c.JSON(http.StatusOK, model.Success(gin.H{"deleted": true}))
}
@@ -0,0 +1,127 @@
package service
import (
"context"
"fmt"
"gorm.io/gorm"
"wx_service/internal/achievement"
)
func (s *Service) ListAchievementThemes(ctx context.Context) ([]achievement.Theme, error) {
var themes []achievement.Theme
err := s.db.WithContext(ctx).
Preload("Levels", func(db *gorm.DB) *gorm.DB {
return db.Order("required_days ASC, sort_order ASC")
}).
Order("sort_order ASC, id ASC").
Find(&themes).Error
if err != nil {
return nil, fmt.Errorf("list themes: %w", err)
}
return themes, nil
}
func (s *Service) GetAchievementTheme(ctx context.Context, id uint) (*achievement.Theme, error) {
var theme achievement.Theme
err := s.db.WithContext(ctx).
Preload("Levels").
First(&theme, id).Error
if err != nil {
return nil, err
}
return &theme, nil
}
func (s *Service) CreateAchievementTheme(ctx context.Context, name, key, icon string, sortOrder int, isActive *bool) (*achievement.Theme, error) {
active := true
if isActive != nil {
active = *isActive
}
theme := achievement.Theme{
Name: name,
Key: key,
Icon: icon,
SortOrder: sortOrder,
IsActive: active,
}
if err := s.db.WithContext(ctx).Create(&theme).Error; err != nil {
return nil, fmt.Errorf("create theme: %w", err)
}
return &theme, nil
}
func (s *Service) UpdateAchievementTheme(ctx context.Context, id uint, name, key, icon string, sortOrder int, isActive *bool) (*achievement.Theme, error) {
var theme achievement.Theme
if err := s.db.WithContext(ctx).First(&theme, id).Error; err != nil {
return nil, err
}
if name != "" {
theme.Name = name
}
if key != "" {
theme.Key = key
}
if icon != "" {
theme.Icon = icon
}
theme.SortOrder = sortOrder
if isActive != nil {
theme.IsActive = *isActive
}
if err := s.db.WithContext(ctx).Save(&theme).Error; err != nil {
return nil, err
}
return &theme, nil
}
func (s *Service) DeleteAchievementTheme(ctx context.Context, id uint) error {
return s.db.WithContext(ctx).Delete(&achievement.Theme{}, id).Error
}
func (s *Service) ListAchievementLevels(ctx context.Context, themeID uint) ([]achievement.Level, error) {
var levels []achievement.Level
err := s.db.WithContext(ctx).
Where("theme_id = ?", themeID).
Order("required_days ASC, sort_order ASC").
Find(&levels).Error
return levels, err
}
func (s *Service) CreateAchievementLevel(ctx context.Context, themeID uint, name, icon string, requiredDays, sortOrder int) (*achievement.Level, error) {
level := achievement.Level{
ThemeID: themeID,
Name: name,
Icon: icon,
RequiredDays: requiredDays,
SortOrder: sortOrder,
}
if err := s.db.WithContext(ctx).Create(&level).Error; err != nil {
return nil, err
}
return &level, nil
}
func (s *Service) UpdateAchievementLevel(ctx context.Context, id uint, name, icon string, requiredDays, sortOrder int) (*achievement.Level, error) {
var level achievement.Level
if err := s.db.WithContext(ctx).First(&level, id).Error; err != nil {
return nil, err
}
if name != "" {
level.Name = name
}
if icon != "" {
level.Icon = icon
}
level.RequiredDays = requiredDays
level.SortOrder = sortOrder
if err := s.db.WithContext(ctx).Save(&level).Error; err != nil {
return nil, err
}
return &level, nil
}
func (s *Service) DeleteAchievementLevel(ctx context.Context, id uint) error {
return s.db.WithContext(ctx).Delete(&achievement.Level{}, id).Error
}
@@ -0,0 +1,63 @@
package service
import (
"context"
"fmt"
quitmodel "wx_service/internal/quitcheckin/model"
)
func (s *Service) ListDreamPresets(ctx context.Context) ([]quitmodel.DreamPreset, error) {
var presets []quitmodel.DreamPreset
err := s.db.WithContext(ctx).
Order("sort_order ASC, id ASC").
Find(&presets).Error
if err != nil {
return nil, fmt.Errorf("list dream presets: %w", err)
}
return presets, nil
}
func (s *Service) CreateDreamPreset(ctx context.Context, title, coverImage string, sortOrder int, isActive *bool) (*quitmodel.DreamPreset, error) {
active := true
if isActive != nil {
active = *isActive
}
preset := quitmodel.DreamPreset{
Title: title,
CoverImage: coverImage,
SortOrder: sortOrder,
IsActive: active,
}
if err := s.db.WithContext(ctx).Create(&preset).Error; err != nil {
return nil, fmt.Errorf("create dream preset: %w", err)
}
return &preset, nil
}
func (s *Service) UpdateDreamPreset(ctx context.Context, id uint, title, coverImage *string, sortOrder *int, isActive *bool) (*quitmodel.DreamPreset, error) {
var preset quitmodel.DreamPreset
if err := s.db.WithContext(ctx).First(&preset, id).Error; err != nil {
return nil, err
}
if title != nil {
preset.Title = *title
}
if coverImage != nil {
preset.CoverImage = *coverImage
}
if sortOrder != nil {
preset.SortOrder = *sortOrder
}
if isActive != nil {
preset.IsActive = *isActive
}
if err := s.db.WithContext(ctx).Save(&preset).Error; err != nil {
return nil, err
}
return &preset, nil
}
func (s *Service) DeleteDreamPreset(ctx context.Context, id uint) error {
return s.db.WithContext(ctx).Delete(&quitmodel.DreamPreset{}, id).Error
}