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.
462 lines
13 KiB
Go
462 lines
13 KiB
Go
package handler
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"wx_service/internal/middleware"
|
|
"wx_service/internal/model"
|
|
smokeservice "wx_service/internal/smoke/service"
|
|
)
|
|
|
|
type SmokeHandler struct {
|
|
smokeLogService *smokeservice.SmokeLogService
|
|
smokeAIAdviceService *smokeservice.SmokeAIAdviceService
|
|
smokeProfileService *smokeservice.SmokeProfileService
|
|
smokeNextService *smokeservice.SmokeNextService
|
|
smokeAINextService *smokeservice.SmokeAINextSmokeService
|
|
}
|
|
|
|
func NewSmokeHandler(
|
|
smokeLogService *smokeservice.SmokeLogService,
|
|
smokeAIAdviceService *smokeservice.SmokeAIAdviceService,
|
|
smokeProfileService *smokeservice.SmokeProfileService,
|
|
smokeNextService *smokeservice.SmokeNextService,
|
|
smokeAINextService *smokeservice.SmokeAINextSmokeService,
|
|
) *SmokeHandler {
|
|
return &SmokeHandler{
|
|
smokeLogService: smokeLogService,
|
|
smokeAIAdviceService: smokeAIAdviceService,
|
|
smokeProfileService: smokeProfileService,
|
|
smokeNextService: smokeNextService,
|
|
smokeAINextService: smokeAINextService,
|
|
}
|
|
}
|
|
|
|
// dateLayout 用于解析前端传入的日期字符串(例如:2025-12-31)
|
|
const dateLayout = "2006-01-02"
|
|
const dateTimeLayout = "2006-01-02 15:04:05"
|
|
|
|
type createSmokeLogRequest struct {
|
|
// 只记录“日期”即可;如果不传,后端会按当天处理
|
|
SmokeTime string `json:"smoke_time"`
|
|
// 真实抽烟时间(精确到时分秒,可补录)
|
|
SmokeAt string `json:"smoke_at"`
|
|
Remark string `json:"remark"`
|
|
Level *int64 `json:"level"`
|
|
Num *int `json:"num"`
|
|
}
|
|
|
|
func (h *SmokeHandler) Create(c *gin.Context) {
|
|
user, ok := middleware.CurrentUser(c)
|
|
if !ok {
|
|
c.JSON(http.StatusUnauthorized, model.Error(http.StatusUnauthorized, "未登录或登录已过期"))
|
|
return
|
|
}
|
|
|
|
var req createSmokeLogRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误"))
|
|
return
|
|
}
|
|
|
|
level := int64(1)
|
|
if req.Level != nil {
|
|
if *req.Level < 0 {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "level 不能为负数"))
|
|
return
|
|
}
|
|
level = *req.Level
|
|
}
|
|
num := 1
|
|
if req.Num != nil {
|
|
if *req.Num < 0 {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "num 不能为负数"))
|
|
return
|
|
}
|
|
num = *req.Num
|
|
}
|
|
|
|
if level < 0 || num < 0 {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "level/num 不能为负数"))
|
|
return
|
|
}
|
|
|
|
var smokeTime *time.Time
|
|
if req.SmokeTime != "" {
|
|
parsed, err := time.ParseInLocation(dateLayout, req.SmokeTime, time.Local)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "smoke_time 格式错误,应为 YYYY-MM-DD"))
|
|
return
|
|
}
|
|
smokeTime = &parsed
|
|
}
|
|
|
|
var smokeAt *time.Time
|
|
if req.SmokeAt != "" {
|
|
parsed, err := time.ParseInLocation(dateTimeLayout, req.SmokeAt, time.Local)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "smoke_at 格式错误,应为 YYYY-MM-DD HH:MM:SS"))
|
|
return
|
|
}
|
|
smokeAt = &parsed
|
|
}
|
|
|
|
record, err := h.smokeLogService.Create(c.Request.Context(), int(user.ID), smokeservice.CreateSmokeLogRequest{
|
|
SmokeTime: smokeTime,
|
|
SmokeAt: smokeAt,
|
|
Remark: req.Remark,
|
|
Level: level,
|
|
Num: num,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "创建记录失败,请稍后重试"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, model.Success(record))
|
|
}
|
|
|
|
type resistedSmokeLogRequest struct {
|
|
SmokeTime string `json:"smoke_time"`
|
|
SmokeAt string `json:"smoke_at"`
|
|
Remark string `json:"remark"`
|
|
}
|
|
|
|
// Resist 表示“想抽但忍住了”:在 fa_smoke_log 中写入 level=0,num=0。
|
|
func (h *SmokeHandler) Resist(c *gin.Context) {
|
|
user, ok := middleware.CurrentUser(c)
|
|
if !ok {
|
|
c.JSON(http.StatusUnauthorized, model.Error(http.StatusUnauthorized, "未登录或登录已过期"))
|
|
return
|
|
}
|
|
|
|
var req resistedSmokeLogRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误"))
|
|
return
|
|
}
|
|
|
|
var smokeTime *time.Time
|
|
if req.SmokeTime != "" {
|
|
parsed, err := time.ParseInLocation(dateLayout, req.SmokeTime, time.Local)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "smoke_time 格式错误,应为 YYYY-MM-DD"))
|
|
return
|
|
}
|
|
smokeTime = &parsed
|
|
}
|
|
|
|
var smokeAt *time.Time
|
|
if req.SmokeAt != "" {
|
|
parsed, err := time.ParseInLocation(dateTimeLayout, req.SmokeAt, time.Local)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "smoke_at 格式错误,应为 YYYY-MM-DD HH:MM:SS"))
|
|
return
|
|
}
|
|
smokeAt = &parsed
|
|
}
|
|
|
|
record, err := h.smokeLogService.Create(c.Request.Context(), int(user.ID), smokeservice.CreateSmokeLogRequest{
|
|
SmokeTime: smokeTime,
|
|
SmokeAt: smokeAt,
|
|
Remark: req.Remark,
|
|
Level: 0,
|
|
Num: 0,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "创建记录失败,请稍后重试"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, model.Success(record))
|
|
}
|
|
|
|
func (h *SmokeHandler) Get(c *gin.Context) {
|
|
user, ok := middleware.CurrentUser(c)
|
|
if !ok {
|
|
c.JSON(http.StatusUnauthorized, model.Error(http.StatusUnauthorized, "未登录或登录已过期"))
|
|
return
|
|
}
|
|
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil || id <= 0 {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "id 参数错误"))
|
|
return
|
|
}
|
|
|
|
record, err := h.smokeLogService.GetByID(c.Request.Context(), int(user.ID), id)
|
|
if err != nil {
|
|
if errors.Is(err, smokeservice.ErrSmokeLogNotFound) {
|
|
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "记录不存在"))
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "查询失败,请稍后重试"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, model.Success(record))
|
|
}
|
|
|
|
func (h *SmokeHandler) List(c *gin.Context) {
|
|
user, ok := middleware.CurrentUser(c)
|
|
if !ok {
|
|
c.JSON(http.StatusUnauthorized, model.Error(http.StatusUnauthorized, "未登录或登录已过期"))
|
|
return
|
|
}
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
|
|
|
var start *time.Time
|
|
if v := c.Query("start"); v != "" {
|
|
parsed, err := time.ParseInLocation(dateLayout, v, time.Local)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "start 格式错误,应为 YYYY-MM-DD"))
|
|
return
|
|
}
|
|
start = &parsed
|
|
}
|
|
var end *time.Time
|
|
if v := c.Query("end"); v != "" {
|
|
parsed, err := time.ParseInLocation(dateLayout, v, time.Local)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "end 格式错误,应为 YYYY-MM-DD"))
|
|
return
|
|
}
|
|
end = &parsed
|
|
}
|
|
|
|
result, err := h.smokeLogService.List(c.Request.Context(), int(user.ID), smokeservice.ListSmokeLogsRequest{
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
Start: start,
|
|
End: end,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "查询列表失败,请稍后重试"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, model.Success(gin.H{
|
|
"items": result.Items,
|
|
"total": result.Total,
|
|
"page": result.Page,
|
|
"page_size": result.PageSize,
|
|
}))
|
|
}
|
|
|
|
func (h *SmokeHandler) Dashboard(c *gin.Context) {
|
|
user, ok := middleware.CurrentUser(c)
|
|
if !ok {
|
|
c.JSON(http.StatusUnauthorized, model.Error(http.StatusUnauthorized, "未登录或登录已过期"))
|
|
return
|
|
}
|
|
|
|
now := time.Now()
|
|
defaultStart, defaultEnd := defaultDashboardRange(now)
|
|
|
|
startDate := defaultStart
|
|
startProvided := false
|
|
if v := c.Query("start"); v != "" {
|
|
parsed, err := time.ParseInLocation(dateLayout, v, time.Local)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "start 格式错误,应为 YYYY-MM-DD"))
|
|
return
|
|
}
|
|
startDate = parsed
|
|
startProvided = true
|
|
}
|
|
|
|
endDate := defaultEnd
|
|
if v := c.Query("end"); v != "" {
|
|
parsed, err := time.ParseInLocation(dateLayout, v, time.Local)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "end 格式错误,应为 YYYY-MM-DD"))
|
|
return
|
|
}
|
|
endDate = parsed
|
|
} else if startProvided {
|
|
endDate = startDate.AddDate(0, 0, 6)
|
|
}
|
|
|
|
if endDate.Before(startDate) {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "end 不能早于 start"))
|
|
return
|
|
}
|
|
|
|
result, err := h.smokeLogService.Dashboard(c.Request.Context(), int(user.ID), smokeservice.SmokeDashboardRequest{
|
|
Start: startDate,
|
|
End: endDate,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取看板概览失败,请稍后重试"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, model.Success(result))
|
|
}
|
|
|
|
func (h *SmokeHandler) LatestLogs(c *gin.Context) {
|
|
user, ok := middleware.CurrentUser(c)
|
|
if !ok {
|
|
c.JSON(http.StatusUnauthorized, model.Error(http.StatusUnauthorized, "未登录或登录已过期"))
|
|
return
|
|
}
|
|
|
|
limit, err := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "limit 应为数字"))
|
|
return
|
|
}
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
|
|
items, err := h.smokeLogService.ListLatest(c.Request.Context(), int(user.ID), limit)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取最近记录失败,请稍后重试"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, model.Success(gin.H{
|
|
"items": items,
|
|
}))
|
|
}
|
|
|
|
type updateSmokeLogRequest struct {
|
|
SmokeTime *string `json:"smoke_time"`
|
|
SmokeAt *string `json:"smoke_at"`
|
|
Remark *string `json:"remark"`
|
|
Level *int64 `json:"level"`
|
|
Num *int `json:"num"`
|
|
}
|
|
|
|
func (h *SmokeHandler) Update(c *gin.Context) {
|
|
user, ok := middleware.CurrentUser(c)
|
|
if !ok {
|
|
c.JSON(http.StatusUnauthorized, model.Error(http.StatusUnauthorized, "未登录或登录已过期"))
|
|
return
|
|
}
|
|
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil || id <= 0 {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "id 参数错误"))
|
|
return
|
|
}
|
|
|
|
var req updateSmokeLogRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误"))
|
|
return
|
|
}
|
|
|
|
if req.Level != nil && *req.Level < 0 {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "level 不能为负数"))
|
|
return
|
|
}
|
|
if req.Num != nil && *req.Num < 0 {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "num 不能为负数"))
|
|
return
|
|
}
|
|
|
|
smokeTimeProvided := req.SmokeTime != nil
|
|
var smokeTime *time.Time
|
|
if req.SmokeTime != nil {
|
|
if *req.SmokeTime == "" {
|
|
smokeTime = nil
|
|
} else {
|
|
parsed, err := time.ParseInLocation(dateLayout, *req.SmokeTime, time.Local)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "smoke_time 格式错误,应为 YYYY-MM-DD"))
|
|
return
|
|
}
|
|
smokeTime = &parsed
|
|
}
|
|
}
|
|
|
|
smokeAtProvided := req.SmokeAt != nil
|
|
var smokeAt *time.Time
|
|
if req.SmokeAt != nil {
|
|
if *req.SmokeAt == "" {
|
|
smokeAt = nil
|
|
} else {
|
|
parsed, err := time.ParseInLocation(dateTimeLayout, *req.SmokeAt, time.Local)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "smoke_at 格式错误,应为 YYYY-MM-DD HH:MM:SS"))
|
|
return
|
|
}
|
|
smokeAt = &parsed
|
|
}
|
|
}
|
|
|
|
record, err := h.smokeLogService.Update(c.Request.Context(), int(user.ID), id, smokeservice.UpdateSmokeLogRequest{
|
|
SmokeTimeProvided: smokeTimeProvided,
|
|
SmokeTime: smokeTime,
|
|
SmokeAtProvided: smokeAtProvided,
|
|
SmokeAt: smokeAt,
|
|
Remark: req.Remark,
|
|
Level: req.Level,
|
|
Num: req.Num,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, smokeservice.ErrSmokeLogNotFound) {
|
|
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "记录不存在"))
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "更新失败,请稍后重试"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, model.Success(record))
|
|
}
|
|
|
|
func (h *SmokeHandler) Delete(c *gin.Context) {
|
|
user, ok := middleware.CurrentUser(c)
|
|
if !ok {
|
|
c.JSON(http.StatusUnauthorized, model.Error(http.StatusUnauthorized, "未登录或登录已过期"))
|
|
return
|
|
}
|
|
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil || id <= 0 {
|
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "id 参数错误"))
|
|
return
|
|
}
|
|
|
|
if err := h.smokeLogService.Delete(c.Request.Context(), int(user.ID), id); err != nil {
|
|
if errors.Is(err, smokeservice.ErrSmokeLogNotFound) {
|
|
c.JSON(http.StatusNotFound, model.Error(http.StatusNotFound, "记录不存在"))
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "删除失败,请稍后重试"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, model.Success(gin.H{
|
|
"deleted": true,
|
|
}))
|
|
}
|
|
|
|
// defaultDashboardRange 返回“本周一到本周日”的日期范围,供看板默认使用。
|
|
func defaultDashboardRange(now time.Time) (time.Time, time.Time) {
|
|
local := now.In(time.Local)
|
|
weekday := local.Weekday()
|
|
// 转为以周一为 0
|
|
daysSinceMonday := int(weekday) - int(time.Monday)
|
|
if daysSinceMonday < 0 {
|
|
daysSinceMonday += 7
|
|
}
|
|
start := time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, time.Local).AddDate(0, 0, -daysSinceMonday)
|
|
end := start.AddDate(0, 0, 6)
|
|
return start, end
|
|
}
|