feat(admin): add quit-checkin admin endpoints and smoke profile fields
Made-with: Cursor
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user