feat(admin): add fa_smoke CRUD APIs with Chinese comments (#45)

This commit is contained in:
root
2026-03-10 17:29:36 +08:00
parent 2ddb2403e4
commit a4b466744c
4 changed files with 2073 additions and 0 deletions
+962
View File
@@ -0,0 +1,962 @@
package handler
import (
"errors"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
adminservice "wx_service/internal/admin/service"
"wx_service/internal/model"
smokemodel "wx_service/internal/smoke/model"
)
// ===== 戒烟记录(fa_smoke_log =====
type smokeLogListQuery struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
UID int `form:"uid"`
DateFrom string `form:"date_from"`
DateTo string `form:"date_to"`
}
type smokeLogUpsertRequest struct {
UID *int `json:"uid"`
SmokeTime *string `json:"smoke_time"`
SmokeAt *string `json:"smoke_at"`
Remark *string `json:"remark"`
Level *int64 `json:"level"`
Num *int `json:"num"`
}
func (h *Handler) ListSmokeLogs(c *gin.Context) {
var query smokeLogListQuery
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.ListSmokeLogs(c.Request.Context(), adminservice.ListSmokeLogsQuery{
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 smoke logs failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) GetSmokeLog(c *gin.Context) {
id, err := strconv.Atoi(strings.TrimSpace(c.Param("id")))
if err != nil || id <= 0 {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid id"))
return
}
data, err := h.svc.GetSmokeLog(c.Request.Context(), id)
if err != nil {
if errors.Is(err, adminservice.ErrSmokeLogNotFound) {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke log not found"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load smoke log failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) CreateSmokeLog(c *gin.Context) {
var req smokeLogUpsertRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid request payload"))
return
}
input, err := buildSmokeLogInput(req)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error()))
return
}
data, err := h.svc.CreateSmokeLog(c.Request.Context(), input)
if err != nil {
if errors.Is(err, adminservice.ErrInvalidInput) {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "uid is required"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "create smoke log failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) UpdateSmokeLog(c *gin.Context) {
id, err := strconv.Atoi(strings.TrimSpace(c.Param("id")))
if err != nil || id <= 0 {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid id"))
return
}
var req smokeLogUpsertRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid request payload"))
return
}
input, err := buildSmokeLogInput(req)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error()))
return
}
data, err := h.svc.UpdateSmokeLog(c.Request.Context(), id, input)
if err != nil {
switch {
case errors.Is(err, adminservice.ErrSmokeLogNotFound):
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke log not found"))
case errors.Is(err, adminservice.ErrInvalidInput):
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid input"))
default:
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "update smoke log failed"))
}
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) DeleteSmokeLog(c *gin.Context) {
id, err := strconv.Atoi(strings.TrimSpace(c.Param("id")))
if err != nil || id <= 0 {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid id"))
return
}
if err := h.svc.DeleteSmokeLog(c.Request.Context(), id); err != nil {
if errors.Is(err, adminservice.ErrSmokeLogNotFound) {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke log not found"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "delete smoke log failed"))
return
}
c.JSON(http.StatusOK, model.Success(gin.H{"message": "删除成功"}))
}
func buildSmokeLogInput(req smokeLogUpsertRequest) (adminservice.SmokeLogUpsertInput, error) {
input := adminservice.SmokeLogUpsertInput{
UID: req.UID,
Remark: req.Remark,
Level: req.Level,
Num: req.Num,
}
if req.SmokeTime != nil {
parsed, err := parseDateOnlyRequired(*req.SmokeTime)
if err != nil {
return input, errors.New("invalid smoke_time, expected YYYY-MM-DD")
}
value := &parsed
input.SmokeTime = &value
}
if req.SmokeAt != nil {
parsed, err := parseDatetimeRequired(*req.SmokeAt)
if err != nil {
return input, errors.New("invalid smoke_at, expected YYYY-MM-DD HH:mm:ss")
}
value := &parsed
input.SmokeAt = &value
}
return input, nil
}
// ===== 用户画像(fa_smoke_user_profile =====
type smokeProfileListQuery struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
UID int `form:"uid"`
}
type smokeProfileUpsertRequest struct {
UID *int `json:"uid"`
BaselineCigsPerDay *int `json:"baseline_cigs_per_day"`
SmokingYears *float64 `json:"smoking_years"`
PackPriceCent *int `json:"pack_price_cent"`
SmokeMotivations []string `json:"smoke_motivations"`
QuitMotivations []string `json:"quit_motivations"`
WakeUpTime *string `json:"wake_up_time"`
SleepTime *string `json:"sleep_time"`
QuitDate *string `json:"quit_date"`
OnboardingCompletedAt *string `json:"onboarding_completed_at"`
}
func (h *Handler) ListSmokeProfiles(c *gin.Context) {
var query smokeProfileListQuery
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.ListSmokeProfiles(c.Request.Context(), adminservice.ListSmokeProfilesQuery{Page: query.Page, PageSize: query.PageSize, UID: query.UID})
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load smoke profiles failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) GetSmokeProfile(c *gin.Context) {
id, err := parseUintID(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid id"))
return
}
data, err := h.svc.GetSmokeProfile(c.Request.Context(), id)
if err != nil {
if errors.Is(err, adminservice.ErrSmokeProfileNotFound) {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke profile not found"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load smoke profile failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) CreateSmokeProfile(c *gin.Context) {
var req smokeProfileUpsertRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid request payload"))
return
}
input, err := buildSmokeProfileInput(req)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error()))
return
}
data, err := h.svc.CreateSmokeProfile(c.Request.Context(), input)
if err != nil {
if errors.Is(err, adminservice.ErrInvalidInput) {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "uid is required or duplicated"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "create smoke profile failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) UpdateSmokeProfile(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 smokeProfileUpsertRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid request payload"))
return
}
input, err := buildSmokeProfileInput(req)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error()))
return
}
data, err := h.svc.UpdateSmokeProfile(c.Request.Context(), id, input)
if err != nil {
if errors.Is(err, adminservice.ErrSmokeProfileNotFound) {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke profile not found"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "update smoke profile failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) DeleteSmokeProfile(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.DeleteSmokeProfile(c.Request.Context(), id); err != nil {
if errors.Is(err, adminservice.ErrSmokeProfileNotFound) {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke profile not found"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "delete smoke profile failed"))
return
}
c.JSON(http.StatusOK, model.Success(gin.H{"message": "删除成功"}))
}
func buildSmokeProfileInput(req smokeProfileUpsertRequest) (adminservice.SmokeProfileUpsertInput, error) {
input := adminservice.SmokeProfileUpsertInput{
UID: req.UID,
BaselineCigsPerDay: req.BaselineCigsPerDay,
SmokingYears: req.SmokingYears,
PackPriceCent: req.PackPriceCent,
WakeUpTime: req.WakeUpTime,
SleepTime: req.SleepTime,
}
if req.SmokeMotivations != nil {
values := smokemodel.StringSlice(req.SmokeMotivations)
input.SmokeMotivations = &values
}
if req.QuitMotivations != nil {
values := smokemodel.StringSlice(req.QuitMotivations)
input.QuitMotivations = &values
}
if req.QuitDate != nil {
parsed, err := parseDateOnlyRequired(*req.QuitDate)
if err != nil {
return input, errors.New("invalid quit_date, expected YYYY-MM-DD")
}
value := &parsed
input.QuitDate = &value
}
if req.OnboardingCompletedAt != nil {
parsed, err := parseDatetimeRequired(*req.OnboardingCompletedAt)
if err != nil {
return input, errors.New("invalid onboarding_completed_at, expected YYYY-MM-DD HH:mm:ss")
}
value := &parsed
input.OnboardingCompletedAt = &value
}
return input, nil
}
// ===== AI 建议(fa_smoke_ai_advice =====
type smokeAIAdviceListQuery struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
UID int `form:"uid"`
Type string `form:"type"`
AdviceDate string `form:"advice_date"`
}
type smokeAIAdviceUpsertRequest struct {
UID *int `json:"uid"`
Type *string `json:"type"`
AdviceDate *string `json:"advice_date"`
PromptVersion *string `json:"prompt_version"`
Provider *string `json:"provider"`
Model *string `json:"model"`
InputSnapshot *string `json:"input_snapshot"`
Advice *string `json:"advice"`
TokensIn *int `json:"tokens_in"`
TokensOut *int `json:"tokens_out"`
CostCent *int `json:"cost_cent"`
}
func (h *Handler) ListSmokeAIAdvices(c *gin.Context) {
var query smokeAIAdviceListQuery
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
}
adviceDate, err := parseDateOnly(query.AdviceDate)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid advice_date, expected YYYY-MM-DD"))
return
}
data, err := h.svc.ListSmokeAIAdvices(c.Request.Context(), adminservice.ListSmokeAIAdvicesQuery{Page: query.Page, PageSize: query.PageSize, UID: query.UID, Type: query.Type, AdviceDate: adviceDate})
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load smoke ai advices failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) GetSmokeAIAdvice(c *gin.Context) {
id, err := parseUintID(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid id"))
return
}
data, err := h.svc.GetSmokeAIAdvice(c.Request.Context(), id)
if err != nil {
if errors.Is(err, adminservice.ErrSmokeAIAdviceNotFound) {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke ai advice not found"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load smoke ai advice failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) CreateSmokeAIAdvice(c *gin.Context) {
var req smokeAIAdviceUpsertRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid request payload"))
return
}
input, err := buildSmokeAIAdviceInput(req)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error()))
return
}
data, err := h.svc.CreateSmokeAIAdvice(c.Request.Context(), input)
if err != nil {
if errors.Is(err, adminservice.ErrInvalidInput) {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "uid/advice/advice_date are required"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "create smoke ai advice failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) UpdateSmokeAIAdvice(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 smokeAIAdviceUpsertRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid request payload"))
return
}
input, err := buildSmokeAIAdviceInput(req)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error()))
return
}
data, err := h.svc.UpdateSmokeAIAdvice(c.Request.Context(), id, input)
if err != nil {
if errors.Is(err, adminservice.ErrSmokeAIAdviceNotFound) {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke ai advice not found"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "update smoke ai advice failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) DeleteSmokeAIAdvice(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.DeleteSmokeAIAdvice(c.Request.Context(), id); err != nil {
if errors.Is(err, adminservice.ErrSmokeAIAdviceNotFound) {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke ai advice not found"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "delete smoke ai advice failed"))
return
}
c.JSON(http.StatusOK, model.Success(gin.H{"message": "删除成功"}))
}
func buildSmokeAIAdviceInput(req smokeAIAdviceUpsertRequest) (adminservice.SmokeAIAdviceUpsertInput, error) {
input := adminservice.SmokeAIAdviceUpsertInput{
UID: req.UID,
Type: req.Type,
PromptVersion: req.PromptVersion,
Provider: req.Provider,
Model: req.Model,
InputSnapshot: req.InputSnapshot,
Advice: req.Advice,
TokensIn: req.TokensIn,
TokensOut: req.TokensOut,
CostCent: req.CostCent,
}
if req.AdviceDate != nil {
parsed, err := parseDateOnlyRequired(*req.AdviceDate)
if err != nil {
return input, errors.New("invalid advice_date, expected YYYY-MM-DD")
}
input.AdviceDate = &parsed
}
return input, nil
}
// ===== AI 解锁(fa_smoke_ai_advice_unlocks =====
type smokeAIUnlockListQuery struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
UID int `form:"uid"`
UnlockDate string `form:"unlock_date"`
}
type smokeAIUnlockUpsertRequest struct {
UID *int `json:"uid"`
UnlockDate *string `json:"unlock_date"`
AdWatchedAt *string `json:"ad_watched_at"`
}
func (h *Handler) ListSmokeAIUnlocks(c *gin.Context) {
var query smokeAIUnlockListQuery
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
}
unlockDate, err := parseDateOnly(query.UnlockDate)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid unlock_date, expected YYYY-MM-DD"))
return
}
data, err := h.svc.ListSmokeAIUnlocks(c.Request.Context(), adminservice.ListSmokeAIUnlocksQuery{Page: query.Page, PageSize: query.PageSize, UID: query.UID, UnlockDate: unlockDate})
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load smoke ai unlocks failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) GetSmokeAIUnlock(c *gin.Context) {
id, err := parseUintID(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid id"))
return
}
data, err := h.svc.GetSmokeAIUnlock(c.Request.Context(), id)
if err != nil {
if errors.Is(err, adminservice.ErrSmokeAIUnlockNotFound) {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke ai unlock not found"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load smoke ai unlock failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) CreateSmokeAIUnlock(c *gin.Context) {
var req smokeAIUnlockUpsertRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid request payload"))
return
}
input, err := buildSmokeAIUnlockInput(req)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error()))
return
}
data, err := h.svc.CreateSmokeAIUnlock(c.Request.Context(), input)
if err != nil {
if errors.Is(err, adminservice.ErrInvalidInput) {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "uid/unlock_date/ad_watched_at are required"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "create smoke ai unlock failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) UpdateSmokeAIUnlock(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 smokeAIUnlockUpsertRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid request payload"))
return
}
input, err := buildSmokeAIUnlockInput(req)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error()))
return
}
data, err := h.svc.UpdateSmokeAIUnlock(c.Request.Context(), id, input)
if err != nil {
if errors.Is(err, adminservice.ErrSmokeAIUnlockNotFound) {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke ai unlock not found"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "update smoke ai unlock failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) DeleteSmokeAIUnlock(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.DeleteSmokeAIUnlock(c.Request.Context(), id); err != nil {
if errors.Is(err, adminservice.ErrSmokeAIUnlockNotFound) {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke ai unlock not found"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "delete smoke ai unlock failed"))
return
}
c.JSON(http.StatusOK, model.Success(gin.H{"message": "删除成功"}))
}
func buildSmokeAIUnlockInput(req smokeAIUnlockUpsertRequest) (adminservice.SmokeAIUnlockUpsertInput, error) {
input := adminservice.SmokeAIUnlockUpsertInput{UID: req.UID}
if req.UnlockDate != nil {
parsed, err := parseDateOnlyRequired(*req.UnlockDate)
if err != nil {
return input, errors.New("invalid unlock_date, expected YYYY-MM-DD")
}
input.UnlockDate = &parsed
}
if req.AdWatchedAt != nil {
parsed, err := parseDatetimeRequired(*req.AdWatchedAt)
if err != nil {
return input, errors.New("invalid ad_watched_at, expected YYYY-MM-DD HH:mm:ss")
}
input.AdWatchedAt = &parsed
}
return input, nil
}
// ===== AI 下次抽烟节点(fa_smoke_ai_next_smoke =====
type smokeAINextListQuery struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
UID int `form:"uid"`
PlanDate string `form:"plan_date"`
}
type smokeAINextUpsertRequest struct {
UID *int `json:"uid"`
PlanDate *string `json:"plan_date"`
AIAdviceID *uint `json:"ai_advice_id"`
NodeType *string `json:"node_type"`
NodeAt *string `json:"node_at"`
}
func (h *Handler) ListSmokeAINexts(c *gin.Context) {
var query smokeAINextListQuery
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
}
planDate, err := parseDateOnly(query.PlanDate)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid plan_date, expected YYYY-MM-DD"))
return
}
data, err := h.svc.ListSmokeAINexts(c.Request.Context(), adminservice.ListSmokeAINextsQuery{Page: query.Page, PageSize: query.PageSize, UID: query.UID, PlanDate: planDate})
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load smoke ai next nodes failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) GetSmokeAINext(c *gin.Context) {
id, err := parseUintID(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid id"))
return
}
data, err := h.svc.GetSmokeAINext(c.Request.Context(), id)
if err != nil {
if errors.Is(err, adminservice.ErrSmokeAINextNotFound) {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke ai next node not found"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load smoke ai next node failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) CreateSmokeAINext(c *gin.Context) {
var req smokeAINextUpsertRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid request payload"))
return
}
input, err := buildSmokeAINextInput(req)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error()))
return
}
data, err := h.svc.CreateSmokeAINext(c.Request.Context(), input)
if err != nil {
if errors.Is(err, adminservice.ErrInvalidInput) {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "uid/plan_date/ai_advice_id/node_type/node_at are required"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "create smoke ai next node failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) UpdateSmokeAINext(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 smokeAINextUpsertRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid request payload"))
return
}
input, err := buildSmokeAINextInput(req)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, err.Error()))
return
}
data, err := h.svc.UpdateSmokeAINext(c.Request.Context(), id, input)
if err != nil {
switch {
case errors.Is(err, adminservice.ErrSmokeAINextNotFound):
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke ai next node not found"))
case errors.Is(err, adminservice.ErrInvalidInput):
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid input"))
default:
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "update smoke ai next node failed"))
}
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) DeleteSmokeAINext(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.DeleteSmokeAINext(c.Request.Context(), id); err != nil {
if errors.Is(err, adminservice.ErrSmokeAINextNotFound) {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke ai next node not found"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "delete smoke ai next node failed"))
return
}
c.JSON(http.StatusOK, model.Success(gin.H{"message": "删除成功"}))
}
func buildSmokeAINextInput(req smokeAINextUpsertRequest) (adminservice.SmokeAINextUpsertInput, error) {
input := adminservice.SmokeAINextUpsertInput{UID: req.UID, AIAdviceID: req.AIAdviceID, NodeType: req.NodeType}
if req.PlanDate != nil {
parsed, err := parseDateOnlyRequired(*req.PlanDate)
if err != nil {
return input, errors.New("invalid plan_date, expected YYYY-MM-DD")
}
input.PlanDate = &parsed
}
if req.NodeAt != nil {
parsed, err := parseDatetimeRequired(*req.NodeAt)
if err != nil {
return input, errors.New("invalid node_at, expected YYYY-MM-DD HH:mm:ss")
}
input.NodeAt = &parsed
}
return input, nil
}
// ===== 激励语模板(fa_smoke_motivation_quote =====
type smokeMotivationListQuery struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
Scene string `form:"scene"`
Type string `form:"type"`
Enabled string `form:"enabled"`
}
type smokeMotivationUpsertRequest struct {
Scene *string `json:"scene"`
Type *string `json:"type"`
Message *string `json:"message"`
AIPrompt *string `json:"ai_prompt"`
Enabled *bool `json:"enabled"`
Weight *int `json:"weight"`
}
func (h *Handler) ListSmokeMotivations(c *gin.Context) {
var query smokeMotivationListQuery
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
}
var enabled *bool
if strings.TrimSpace(query.Enabled) != "" {
parsed, err := parseOptionalBool(query.Enabled)
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid enabled, expected true/false/1/0"))
return
}
enabled = parsed
}
data, err := h.svc.ListSmokeMotivations(c.Request.Context(), adminservice.ListSmokeMotivationsQuery{
Page: query.Page, PageSize: query.PageSize, Scene: query.Scene, Type: query.Type, Enabled: enabled,
})
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load smoke motivations failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) GetSmokeMotivation(c *gin.Context) {
id, err := parseUintID(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid id"))
return
}
data, err := h.svc.GetSmokeMotivation(c.Request.Context(), id)
if err != nil {
if errors.Is(err, adminservice.ErrSmokeMotivationNotFound) {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke motivation not found"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "load smoke motivation failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) CreateSmokeMotivation(c *gin.Context) {
var req smokeMotivationUpsertRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid request payload"))
return
}
input := adminservice.SmokeMotivationUpsertInput{Scene: req.Scene, Type: req.Type, Message: req.Message, AIPrompt: req.AIPrompt, Enabled: req.Enabled, Weight: req.Weight}
data, err := h.svc.CreateSmokeMotivation(c.Request.Context(), input)
if err != nil {
if errors.Is(err, adminservice.ErrInvalidInput) {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "scene/type/message are required"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "create smoke motivation failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) UpdateSmokeMotivation(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 smokeMotivationUpsertRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "invalid request payload"))
return
}
input := adminservice.SmokeMotivationUpsertInput{Scene: req.Scene, Type: req.Type, Message: req.Message, AIPrompt: req.AIPrompt, Enabled: req.Enabled, Weight: req.Weight}
data, err := h.svc.UpdateSmokeMotivation(c.Request.Context(), id, input)
if err != nil {
if errors.Is(err, adminservice.ErrSmokeMotivationNotFound) {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke motivation not found"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "update smoke motivation failed"))
return
}
c.JSON(http.StatusOK, model.Success(data))
}
func (h *Handler) DeleteSmokeMotivation(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.DeleteSmokeMotivation(c.Request.Context(), id); err != nil {
if errors.Is(err, adminservice.ErrSmokeMotivationNotFound) {
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "smoke motivation not found"))
return
}
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "delete smoke motivation failed"))
return
}
c.JSON(http.StatusOK, model.Success(gin.H{"message": "删除成功"}))
}
// ===== 时间解析辅助函数 =====
func parseDateOnly(raw string) (*time.Time, error) {
text := strings.TrimSpace(raw)
if text == "" {
return nil, nil
}
parsed, err := parseDateOnlyRequired(text)
if err != nil {
return nil, err
}
return &parsed, nil
}
func parseDateOnlyRequired(raw string) (time.Time, error) {
return time.ParseInLocation("2006-01-02", strings.TrimSpace(raw), time.Local)
}
func parseDatetimeRequired(raw string) (time.Time, error) {
return time.ParseInLocation("2006-01-02 15:04:05", strings.TrimSpace(raw), time.Local)
}
File diff suppressed because it is too large Load Diff
+6
View File
@@ -18,6 +18,12 @@ var (
ErrMiniProgramAppIDUsed = errors.New("mini program app_id already exists") ErrMiniProgramAppIDUsed = errors.New("mini program app_id already exists")
ErrAdminUserNotFound = errors.New("admin user not found") ErrAdminUserNotFound = errors.New("admin user not found")
ErrMembershipRedeemCodeNotFound = errors.New("membership redeem code not found") ErrMembershipRedeemCodeNotFound = errors.New("membership redeem code not found")
ErrSmokeLogNotFound = errors.New("smoke log not found")
ErrSmokeProfileNotFound = errors.New("smoke profile not found")
ErrSmokeAIAdviceNotFound = errors.New("smoke ai advice not found")
ErrSmokeAIUnlockNotFound = errors.New("smoke ai unlock not found")
ErrSmokeAINextNotFound = errors.New("smoke ai next smoke not found")
ErrSmokeMotivationNotFound = errors.New("smoke motivation quote not found")
ErrInvalidInput = errors.New("invalid input") ErrInvalidInput = errors.New("invalid input")
ErrPasswordPolicyViolation = errors.New("password policy violation") ErrPasswordPolicyViolation = errors.New("password policy violation")
) )
+37
View File
@@ -52,6 +52,43 @@ func registerAdminRoutes(
protected.GET("/watermark/video-parse-unlocks", handler.ListVideoParseUnlocks) protected.GET("/watermark/video-parse-unlocks", handler.ListVideoParseUnlocks)
protected.GET("/watermark/video-download-failures", handler.ListVideoDownloadFailures) protected.GET("/watermark/video-download-failures", handler.ListVideoDownloadFailures)
// 戒烟小程序(fa_smoke)后台管理接口。
protected.GET("/smoke/logs", handler.ListSmokeLogs)
protected.GET("/smoke/logs/:id", handler.GetSmokeLog)
protected.POST("/smoke/logs", handler.CreateSmokeLog)
protected.PUT("/smoke/logs/:id", handler.UpdateSmokeLog)
protected.DELETE("/smoke/logs/:id", handler.DeleteSmokeLog)
protected.GET("/smoke/profiles", handler.ListSmokeProfiles)
protected.GET("/smoke/profiles/:id", handler.GetSmokeProfile)
protected.POST("/smoke/profiles", handler.CreateSmokeProfile)
protected.PUT("/smoke/profiles/:id", handler.UpdateSmokeProfile)
protected.DELETE("/smoke/profiles/:id", handler.DeleteSmokeProfile)
protected.GET("/smoke/ai-advices", handler.ListSmokeAIAdvices)
protected.GET("/smoke/ai-advices/:id", handler.GetSmokeAIAdvice)
protected.POST("/smoke/ai-advices", handler.CreateSmokeAIAdvice)
protected.PUT("/smoke/ai-advices/:id", handler.UpdateSmokeAIAdvice)
protected.DELETE("/smoke/ai-advices/:id", handler.DeleteSmokeAIAdvice)
protected.GET("/smoke/ai-unlocks", handler.ListSmokeAIUnlocks)
protected.GET("/smoke/ai-unlocks/:id", handler.GetSmokeAIUnlock)
protected.POST("/smoke/ai-unlocks", handler.CreateSmokeAIUnlock)
protected.PUT("/smoke/ai-unlocks/:id", handler.UpdateSmokeAIUnlock)
protected.DELETE("/smoke/ai-unlocks/:id", handler.DeleteSmokeAIUnlock)
protected.GET("/smoke/ai-next-smokes", handler.ListSmokeAINexts)
protected.GET("/smoke/ai-next-smokes/:id", handler.GetSmokeAINext)
protected.POST("/smoke/ai-next-smokes", handler.CreateSmokeAINext)
protected.PUT("/smoke/ai-next-smokes/:id", handler.UpdateSmokeAINext)
protected.DELETE("/smoke/ai-next-smokes/:id", handler.DeleteSmokeAINext)
protected.GET("/smoke/motivation-quotes", handler.ListSmokeMotivations)
protected.GET("/smoke/motivation-quotes/:id", handler.GetSmokeMotivation)
protected.POST("/smoke/motivation-quotes", handler.CreateSmokeMotivation)
protected.PUT("/smoke/motivation-quotes/:id", handler.UpdateSmokeMotivation)
protected.DELETE("/smoke/motivation-quotes/:id", handler.DeleteSmokeMotivation)
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)