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 {
|
type SmokeProfileItem struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
UID int `json:"uid"`
|
UID int `json:"uid"`
|
||||||
|
Mode string `json:"mode,omitempty"`
|
||||||
BaselineCigsPerDay int `json:"baseline_cigs_per_day"`
|
BaselineCigsPerDay int `json:"baseline_cigs_per_day"`
|
||||||
SmokingYears float64 `json:"smoking_years"`
|
SmokingYears float64 `json:"smoking_years"`
|
||||||
PackPriceCent int `json:"pack_price_cent"`
|
PackPriceCent int `json:"pack_price_cent"`
|
||||||
@@ -255,6 +256,7 @@ type SmokeProfileItem struct {
|
|||||||
WakeUpTime string `json:"wake_up_time"`
|
WakeUpTime string `json:"wake_up_time"`
|
||||||
SleepTime string `json:"sleep_time"`
|
SleepTime string `json:"sleep_time"`
|
||||||
QuitDate *time.Time `json:"quit_date,omitempty"`
|
QuitDate *time.Time `json:"quit_date,omitempty"`
|
||||||
|
AchievementThemeID *uint `json:"achievement_theme_id,omitempty"`
|
||||||
OnboardingCompletedAt *time.Time `json:"onboarding_completed_at,omitempty"`
|
OnboardingCompletedAt *time.Time `json:"onboarding_completed_at,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -368,6 +370,7 @@ func convertSmokeProfile(row smokemodel.SmokeUserProfile) SmokeProfileItem {
|
|||||||
return SmokeProfileItem{
|
return SmokeProfileItem{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
UID: row.UID,
|
UID: row.UID,
|
||||||
|
Mode: row.Mode,
|
||||||
BaselineCigsPerDay: row.BaselineCigsPerDay,
|
BaselineCigsPerDay: row.BaselineCigsPerDay,
|
||||||
SmokingYears: row.SmokingYears,
|
SmokingYears: row.SmokingYears,
|
||||||
PackPriceCent: row.PackPriceCent,
|
PackPriceCent: row.PackPriceCent,
|
||||||
@@ -376,6 +379,7 @@ func convertSmokeProfile(row smokemodel.SmokeUserProfile) SmokeProfileItem {
|
|||||||
WakeUpTime: row.WakeUpTime,
|
WakeUpTime: row.WakeUpTime,
|
||||||
SleepTime: row.SleepTime,
|
SleepTime: row.SleepTime,
|
||||||
QuitDate: row.QuitDate,
|
QuitDate: row.QuitDate,
|
||||||
|
AchievementThemeID: row.AchievementThemeID,
|
||||||
OnboardingCompletedAt: row.OnboardingCompletedAt,
|
OnboardingCompletedAt: row.OnboardingCompletedAt,
|
||||||
CreatedAt: row.CreatedAt,
|
CreatedAt: row.CreatedAt,
|
||||||
UpdatedAt: row.UpdatedAt,
|
UpdatedAt: row.UpdatedAt,
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ func registerAdminRoutes(
|
|||||||
protected.PUT("/dream-presets/:id", handler.UpdateDreamPreset)
|
protected.PUT("/dream-presets/:id", handler.UpdateDreamPreset)
|
||||||
protected.DELETE("/dream-presets/:id", handler.DeleteDreamPreset)
|
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/overview", handler.MembershipOverview)
|
||||||
protected.GET("/memberships/redeem-codes", handler.ListMembershipRedeemCodes)
|
protected.GET("/memberships/redeem-codes", handler.ListMembershipRedeemCodes)
|
||||||
protected.POST("/memberships/redeem-codes", handler.CreateMembershipRedeemCodes)
|
protected.POST("/memberships/redeem-codes", handler.CreateMembershipRedeemCodes)
|
||||||
|
|||||||
Reference in New Issue
Block a user