feat: 戒烟成就、梦想图标预设、打卡统计与依赖注入调整
- 成就系统、连续打卡天数计算、管理后台成就 CRUD - 梦想目标图标预设 DreamPreset 与用户端 dream-presets 接口 - 管理后台梦想图标 CRUD;戒烟打卡 summary 修正 - 忽略根目录编译产物 /api Made-with: Cursor
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user