963 lines
32 KiB
Go
963 lines
32 KiB
Go
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)
|
||
}
|