From a6f0bfd4e87dc860b73f15c1eb2c115ed23babb8 Mon Sep 17 00:00:00 2001 From: nepiedg Date: Tue, 7 Apr 2026 22:10:31 +0800 Subject: [PATCH] feat(admin): add quit-checkin admin endpoints and smoke profile fields Made-with: Cursor --- .../handler/quit_checkin_admin_handler.go | 92 +++++++++ .../service/quit_checkin_admin_service.go | 179 ++++++++++++++++++ internal/admin/service/smoke_service.go | 4 + internal/routes/admin_routes.go | 3 + 4 files changed, 278 insertions(+) create mode 100644 internal/admin/handler/quit_checkin_admin_handler.go create mode 100644 internal/admin/service/quit_checkin_admin_service.go diff --git a/internal/admin/handler/quit_checkin_admin_handler.go b/internal/admin/handler/quit_checkin_admin_handler.go new file mode 100644 index 0000000..de34197 --- /dev/null +++ b/internal/admin/handler/quit_checkin_admin_handler.go @@ -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)) +} diff --git a/internal/admin/service/quit_checkin_admin_service.go b/internal/admin/service/quit_checkin_admin_service.go new file mode 100644 index 0000000..bf6c3fe --- /dev/null +++ b/internal/admin/service/quit_checkin_admin_service.go @@ -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 +} diff --git a/internal/admin/service/smoke_service.go b/internal/admin/service/smoke_service.go index b0d87ba..5c4f5ad 100644 --- a/internal/admin/service/smoke_service.go +++ b/internal/admin/service/smoke_service.go @@ -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, diff --git a/internal/routes/admin_routes.go b/internal/routes/admin_routes.go index c9c49f7..25dfdd9 100644 --- a/internal/routes/admin_routes.go +++ b/internal/routes/admin_routes.go @@ -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)