Files
wx_service/internal/admin/handler/smoke_handler.go
T

963 lines
32 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}