feat(admin): add quit-checkin admin endpoints and smoke profile fields

Made-with: Cursor
This commit is contained in:
nepiedg
2026-04-07 22:10:31 +08:00
parent fd097729d7
commit a6f0bfd4e8
4 changed files with 278 additions and 0 deletions
@@ -0,0 +1,92 @@
package handler
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
adminservice "wx_service/internal/admin/service"
"wx_service/internal/model"
)
type quitDailyListQuery struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
UID int `form:"uid"`
DateFrom string `form:"date_from"`
DateTo string `form:"date_to"`
}
// ListQuitDailyStatuses GET /api/admin/quit-checkin/daily-statuses
func (h *Handler) ListQuitDailyStatuses(c *gin.Context) {
var query quitDailyListQuery
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid query"))
return
}
if query.Page == 0 {
query.Page = 1
}
if query.PageSize == 0 {
query.PageSize = 20
}
dateFrom, err := parseDateOnly(query.DateFrom)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid date_from, expected YYYY-MM-DD"))
return
}
dateTo, err := parseDateOnly(query.DateTo)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid date_to, expected YYYY-MM-DD"))
return
}
data, err := h.svc.ListQuitDailyStatuses(c.Request.Context(), adminservice.ListQuitDailyStatusesQuery{
Page: query.Page,
PageSize: query.PageSize,
UID: query.UID,
DateFrom: dateFrom,
DateTo: dateTo,
})
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load quit daily statuses failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
type quitRewardGoalsListQuery struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
UID int `form:"uid"`
Status string `form:"status"`
}
// ListQuitRewardGoals GET /api/admin/quit-checkin/reward-goals
func (h *Handler) ListQuitRewardGoals(c *gin.Context) {
var query quitRewardGoalsListQuery
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid query"))
return
}
if query.Page == 0 {
query.Page = 1
}
if query.PageSize == 0 {
query.PageSize = 20
}
data, err := h.svc.ListQuitRewardGoals(c.Request.Context(), adminservice.ListQuitRewardGoalsQuery{
Page: query.Page,
PageSize: query.PageSize,
UID: query.UID,
Status: strings.TrimSpace(query.Status),
})
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load reward goals failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
@@ -0,0 +1,179 @@
package service
import (
"context"
"strings"
"time"
quitmodel "wx_service/internal/quitcheckin/model"
)
// ListQuitDailyStatusesQuery 戒烟打卡每日状态列表查询。
type ListQuitDailyStatusesQuery struct {
Page int
PageSize int
UID int
DateFrom *time.Time
DateTo *time.Time
}
// QuitDailyStatusItem 管理端展示用(含 uid)。
type QuitDailyStatusItem struct {
ID int `json:"id"`
UID int `json:"uid"`
Date string `json:"date"`
Status string `json:"status"`
CheckInAt *time.Time `json:"check_in_at,omitempty"`
RelapsedAt *time.Time `json:"relapsed_at,omitempty"`
RelapseNum int `json:"relapse_num"`
Reason string `json:"reason,omitempty"`
Note string `json:"note,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ListQuitDailyStatusesResult 分页结果。
type ListQuitDailyStatusesResult struct {
List []QuitDailyStatusItem `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// ListQuitDailyStatuses 分页查询 fa_quit_checkin_daily_status。
func (s *Service) ListQuitDailyStatuses(ctx context.Context, q ListQuitDailyStatusesQuery) (*ListQuitDailyStatusesResult, error) {
q.Page, q.PageSize = normalizePage(q.Page, q.PageSize)
dbQuery := s.db.WithContext(ctx).Model(&quitmodel.DailyStatus{})
if q.UID > 0 {
dbQuery = dbQuery.Where("uid = ?", q.UID)
}
if q.DateFrom != nil {
dbQuery = dbQuery.Where("date >= ?", q.DateFrom.Format("2006-01-02"))
}
if q.DateTo != nil {
dbQuery = dbQuery.Where("date <= ?", q.DateTo.Format("2006-01-02"))
}
var total int64
if err := dbQuery.Count(&total).Error; err != nil {
return nil, err
}
var rows []quitmodel.DailyStatus
if total > 0 {
if err := dbQuery.Order("date DESC, id DESC").
Limit(q.PageSize).
Offset((q.Page - 1) * q.PageSize).
Find(&rows).Error; err != nil {
return nil, err
}
}
list := make([]QuitDailyStatusItem, 0, len(rows))
for _, r := range rows {
dateStr := ""
if !r.Date.IsZero() {
dateStr = r.Date.Format("2006-01-02")
}
list = append(list, QuitDailyStatusItem{
ID: int(r.ID),
UID: r.UID,
Date: dateStr,
Status: r.Status,
CheckInAt: r.CheckInAt,
RelapsedAt: r.RelapsedAt,
RelapseNum: r.RelapseNum,
Reason: r.Reason,
Note: r.Note,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
})
}
return &ListQuitDailyStatusesResult{
List: list,
Total: total,
Page: q.Page,
PageSize: q.PageSize,
}, nil
}
// ListQuitRewardGoalsQuery 用户梦想目标列表查询。
type ListQuitRewardGoalsQuery struct {
Page int
PageSize int
UID int
Status string
}
// QuitRewardGoalItem 管理端展示用。
type QuitRewardGoalItem struct {
ID int `json:"id"`
UID int `json:"uid"`
Title string `json:"title"`
TargetAmountCent int `json:"target_amount_cent"`
CoverImage string `json:"cover_image,omitempty"`
Status string `json:"status"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ListQuitRewardGoalsResult 分页结果。
type ListQuitRewardGoalsResult struct {
List []QuitRewardGoalItem `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// ListQuitRewardGoals 分页查询 fa_quit_checkin_reward_goal。
func (s *Service) ListQuitRewardGoals(ctx context.Context, q ListQuitRewardGoalsQuery) (*ListQuitRewardGoalsResult, error) {
q.Page, q.PageSize = normalizePage(q.Page, q.PageSize)
dbQuery := s.db.WithContext(ctx).Model(&quitmodel.RewardGoal{})
if q.UID > 0 {
dbQuery = dbQuery.Where("uid = ?", q.UID)
}
if st := strings.TrimSpace(q.Status); st != "" && st != "all" {
dbQuery = dbQuery.Where("status = ?", st)
}
var total int64
if err := dbQuery.Count(&total).Error; err != nil {
return nil, err
}
var rows []quitmodel.RewardGoal
if total > 0 {
if err := dbQuery.Order("id DESC").
Limit(q.PageSize).
Offset((q.Page - 1) * q.PageSize).
Find(&rows).Error; err != nil {
return nil, err
}
}
list := make([]QuitRewardGoalItem, 0, len(rows))
for _, r := range rows {
list = append(list, QuitRewardGoalItem{
ID: int(r.ID),
UID: r.UID,
Title: r.Title,
TargetAmountCent: r.TargetAmountCent,
CoverImage: r.CoverImage,
Status: r.Status,
CompletedAt: r.CompletedAt,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
})
}
return &ListQuitRewardGoalsResult{
List: list,
Total: total,
Page: q.Page,
PageSize: q.PageSize,
}, nil
}
+4
View File
@@ -247,6 +247,7 @@ type ListSmokeProfilesQuery struct {
type SmokeProfileItem struct {
ID uint `json:"id"`
UID int `json:"uid"`
Mode string `json:"mode,omitempty"`
BaselineCigsPerDay int `json:"baseline_cigs_per_day"`
SmokingYears float64 `json:"smoking_years"`
PackPriceCent int `json:"pack_price_cent"`
@@ -255,6 +256,7 @@ type SmokeProfileItem struct {
WakeUpTime string `json:"wake_up_time"`
SleepTime string `json:"sleep_time"`
QuitDate *time.Time `json:"quit_date,omitempty"`
AchievementThemeID *uint `json:"achievement_theme_id,omitempty"`
OnboardingCompletedAt *time.Time `json:"onboarding_completed_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -368,6 +370,7 @@ func convertSmokeProfile(row smokemodel.SmokeUserProfile) SmokeProfileItem {
return SmokeProfileItem{
ID: row.ID,
UID: row.UID,
Mode: row.Mode,
BaselineCigsPerDay: row.BaselineCigsPerDay,
SmokingYears: row.SmokingYears,
PackPriceCent: row.PackPriceCent,
@@ -376,6 +379,7 @@ func convertSmokeProfile(row smokemodel.SmokeUserProfile) SmokeProfileItem {
WakeUpTime: row.WakeUpTime,
SleepTime: row.SleepTime,
QuitDate: row.QuitDate,
AchievementThemeID: row.AchievementThemeID,
OnboardingCompletedAt: row.OnboardingCompletedAt,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
+3
View File
@@ -105,6 +105,9 @@ func registerAdminRoutes(
protected.PUT("/dream-presets/:id", handler.UpdateDreamPreset)
protected.DELETE("/dream-presets/:id", handler.DeleteDreamPreset)
protected.GET("/quit-checkin/daily-statuses", handler.ListQuitDailyStatuses)
protected.GET("/quit-checkin/reward-goals", handler.ListQuitRewardGoals)
protected.GET("/memberships/overview", handler.MembershipOverview)
protected.GET("/memberships/redeem-codes", handler.ListMembershipRedeemCodes)
protected.POST("/memberships/redeem-codes", handler.CreateMembershipRedeemCodes)