6cf7eb2294
- Added new API endpoint `GET /api/v1/smoke/next_smoke_time` to provide AI-generated suggestions for the next smoking time based on user data. - Introduced a new database table `fa_smoke_ai_next_smoke` to store structured AI time node suggestions. - Updated smoke handler and service to integrate the new AI next smoke time functionality. - Enhanced documentation to reflect the new API endpoint and its usage, including details on how to generate AI time nodes.
141 lines
4.5 KiB
Go
141 lines
4.5 KiB
Go
package handler
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"wx_service/internal/middleware"
|
|
"wx_service/internal/model"
|
|
smokeservice "wx_service/internal/smoke/service"
|
|
)
|
|
|
|
type nextSmokeTimeUnifiedResponse struct {
|
|
Source string `json:"source"`
|
|
NotBeforeAt string `json:"not_before_at"`
|
|
SuggestedAt string `json:"suggested_at"`
|
|
TimeNodes []string `json:"time_nodes,omitempty"`
|
|
Advice string `json:"advice,omitempty"`
|
|
Default smokeservice.NextSmokeSuggestion `json:"default"`
|
|
AI *smokeservice.AINextSmokeSuggestion `json:"ai,omitempty"`
|
|
}
|
|
|
|
func (h *SmokeHandler) GetNextSmokeTime(c *gin.Context) {
|
|
user, ok := middleware.CurrentUser(c)
|
|
if !ok {
|
|
c.JSON(http.StatusUnauthorized, model.Error(http.StatusUnauthorized, "未登录或登录已过期"))
|
|
return
|
|
}
|
|
|
|
asOf := time.Now().In(time.Local)
|
|
planDate := dateOnlyLocal(asOf)
|
|
if v := strings.TrimSpace(c.Query("date")); v != "" {
|
|
switch strings.ToLower(v) {
|
|
case "today":
|
|
planDate = dateOnlyLocal(asOf)
|
|
case "tomorrow":
|
|
planDate = dateOnlyLocal(asOf).AddDate(0, 0, 1)
|
|
default:
|
|
parsed, err := time.ParseInLocation(dateLayout, v, time.Local)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "date 格式错误,应为 YYYY-MM-DD 或 today/tomorrow"))
|
|
return
|
|
}
|
|
planDate = parsed
|
|
}
|
|
}
|
|
if planDate.Before(dateOnlyLocal(asOf)) {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "date 不能早于今天"))
|
|
return
|
|
}
|
|
|
|
view, err := h.smokeProfileService.GetView(c.Request.Context(), int(user.ID))
|
|
if err != nil {
|
|
if errors.Is(err, smokeservice.ErrSmokeProfileInvalidTime) {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "作息时间格式错误,应为 HH:MM"))
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取基础信息失败,请稍后重试"))
|
|
return
|
|
}
|
|
|
|
defaultSuggestion, err := h.smokeNextService.GetDefaultSuggestion(c.Request.Context(), int(user.ID), asOf, planDate, view)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "计算失败,请稍后重试"))
|
|
return
|
|
}
|
|
|
|
mode := strings.ToLower(strings.TrimSpace(c.DefaultQuery("mode", "auto")))
|
|
|
|
formatPtr := func(t *time.Time) string {
|
|
if t == nil {
|
|
return ""
|
|
}
|
|
return t.In(time.Local).Format(time.RFC3339)
|
|
}
|
|
|
|
resp := nextSmokeTimeUnifiedResponse{
|
|
Source: "default",
|
|
NotBeforeAt: formatPtr(defaultSuggestion.NextSmokeAt),
|
|
SuggestedAt: formatPtr(defaultSuggestion.NextSmokeAt),
|
|
Default: defaultSuggestion,
|
|
}
|
|
|
|
// mode=default: 永远返回默认建议
|
|
if mode == "default" {
|
|
c.JSON(http.StatusOK, model.Success(resp))
|
|
return
|
|
}
|
|
|
|
// mode=auto: 仅在“已存在 AI 时间节点”时使用 AI(不主动生成)
|
|
// mode=ai: 尝试生成/刷新 AI,再优先使用 AI;失败则回落到默认
|
|
var ai smokeservice.AINextSmokeSuggestion
|
|
var hasAI bool
|
|
|
|
if mode == "ai" {
|
|
v, err := h.smokeAINextService.GetOrGenerate(c.Request.Context(), user, asOf, planDate, "v1", defaultSuggestion)
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, smokeservice.ErrAINextLocked):
|
|
c.JSON(http.StatusForbidden, model.Error(http.StatusForbidden, "需要观看广告解锁后才可生成"))
|
|
return
|
|
case errors.Is(err, smokeservice.ErrAINextServiceDisabled):
|
|
c.JSON(http.StatusServiceUnavailable, model.Error(http.StatusServiceUnavailable, "AI 服务暂不可用,请联系管理员"))
|
|
return
|
|
default:
|
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "生成 AI 建议失败,请稍后重试"))
|
|
return
|
|
}
|
|
}
|
|
ai = v
|
|
hasAI = len(ai.TimeNodes) > 0
|
|
} else {
|
|
v, ok, err := h.smokeAINextService.GetCached(c.Request.Context(), user, planDate, "v1")
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取 AI 建议失败,请稍后重试"))
|
|
return
|
|
}
|
|
ai = v
|
|
hasAI = ok && len(ai.TimeNodes) > 0
|
|
}
|
|
|
|
if hasAI {
|
|
resp.Source = "ai"
|
|
resp.NotBeforeAt = ai.NotBeforeAt
|
|
resp.SuggestedAt = ai.SuggestedAt
|
|
resp.TimeNodes = ai.TimeNodes
|
|
resp.Advice = ai.Advice
|
|
resp.AI = &ai
|
|
}
|
|
|
|
c.JSON(http.StatusOK, model.Success(resp))
|
|
}
|
|
|
|
func dateOnlyLocal(t time.Time) time.Time {
|
|
local := t.In(time.Local)
|
|
return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, time.Local)
|
|
}
|