Files
healthflow/server/internal/handler/epoch.go
2026-01-17 18:21:40 +08:00

500 lines
15 KiB
Go

package handler
import (
"database/sql"
"net/http"
"strconv"
"time"
"healthflow/internal/config"
"healthflow/internal/middleware"
"healthflow/internal/model"
"github.com/gin-gonic/gin"
)
type EpochHandler struct {
db *sql.DB
cfg *config.Config
}
func NewEpochHandler(db *sql.DB, cfg *config.Config) *EpochHandler {
return &EpochHandler{db: db, cfg: cfg}
}
// 创建纪元
func (h *EpochHandler) CreateEpoch(c *gin.Context) {
userID := middleware.GetUserID(c)
var req model.CreateEpochRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 将之前的活跃纪元设为非活跃
h.db.Exec("UPDATE weight_epochs SET is_active = 0 WHERE user_id = ? AND is_active = 1", userID)
// 创建新纪元
result, err := h.db.Exec(`
INSERT INTO weight_epochs (user_id, name, initial_weight, target_weight, start_date, end_date, is_active)
VALUES (?, ?, ?, ?, ?, ?, 1)
`, userID, req.Name, req.InitialWeight, req.TargetWeight, req.StartDate, req.EndDate)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create epoch"})
return
}
epochID, _ := result.LastInsertId()
// 生成每周计划
h.generateWeeklyPlans(epochID, userID, req.StartDate, req.EndDate, req.InitialWeight, req.TargetWeight)
c.JSON(http.StatusOK, gin.H{"id": epochID, "message": "epoch created"})
}
// 获取当前活跃纪元
func (h *EpochHandler) GetActiveEpoch(c *gin.Context) {
userID := middleware.GetUserID(c)
var epoch model.WeightEpoch
var finalWeight sql.NullFloat64
err := h.db.QueryRow(`
SELECT id, user_id, name, initial_weight, target_weight, start_date, end_date,
final_weight, is_active, is_completed, created_at
FROM weight_epochs WHERE user_id = ? AND is_active = 1
`, userID).Scan(
&epoch.ID, &epoch.UserID, &epoch.Name, &epoch.InitialWeight, &epoch.TargetWeight,
&epoch.StartDate, &epoch.EndDate, &finalWeight, &epoch.IsActive, &epoch.IsCompleted, &epoch.CreatedAt,
)
if err == sql.ErrNoRows {
c.JSON(http.StatusOK, nil)
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
return
}
if finalWeight.Valid {
epoch.FinalWeight = &finalWeight.Float64
}
c.JSON(http.StatusOK, epoch)
}
// 获取纪元详情
func (h *EpochHandler) GetEpochDetail(c *gin.Context) {
userID := middleware.GetUserID(c)
epochID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
var epoch model.WeightEpoch
var finalWeight sql.NullFloat64
err := h.db.QueryRow(`
SELECT id, user_id, name, initial_weight, target_weight, start_date, end_date,
final_weight, is_active, is_completed, created_at
FROM weight_epochs WHERE id = ? AND user_id = ?
`, epochID, userID).Scan(
&epoch.ID, &epoch.UserID, &epoch.Name, &epoch.InitialWeight, &epoch.TargetWeight,
&epoch.StartDate, &epoch.EndDate, &finalWeight, &epoch.IsActive, &epoch.IsCompleted, &epoch.CreatedAt,
)
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "epoch not found"})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
return
}
if finalWeight.Valid {
epoch.FinalWeight = &finalWeight.Float64
}
// 计算统计数据
detail := h.calculateEpochDetail(&epoch)
c.JSON(http.StatusOK, detail)
}
// 获取纪元列表
func (h *EpochHandler) GetEpochList(c *gin.Context) {
userID := middleware.GetUserID(c)
rows, err := h.db.Query(`
SELECT id, user_id, name, initial_weight, target_weight, start_date, end_date,
final_weight, is_active, is_completed, created_at
FROM weight_epochs WHERE user_id = ? ORDER BY created_at DESC
`, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
return
}
defer rows.Close()
var epochs []model.WeightEpoch
for rows.Next() {
var e model.WeightEpoch
var finalWeight sql.NullFloat64
rows.Scan(
&e.ID, &e.UserID, &e.Name, &e.InitialWeight, &e.TargetWeight,
&e.StartDate, &e.EndDate, &finalWeight, &e.IsActive, &e.IsCompleted, &e.CreatedAt,
)
if finalWeight.Valid {
e.FinalWeight = &finalWeight.Float64
}
epochs = append(epochs, e)
}
c.JSON(http.StatusOK, epochs)
}
// 更新纪元
func (h *EpochHandler) UpdateEpoch(c *gin.Context) {
userID := middleware.GetUserID(c)
epochID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
var req model.UpdateEpochRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 检查纪元是否存在且属于当前用户
var exists int
err := h.db.QueryRow("SELECT 1 FROM weight_epochs WHERE id = ? AND user_id = ?", epochID, userID).Scan(&exists)
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "epoch not found"})
return
}
// 更新纪元
_, err = h.db.Exec(`
UPDATE weight_epochs
SET name = COALESCE(?, name),
initial_weight = COALESCE(?, initial_weight),
target_weight = COALESCE(?, target_weight),
start_date = COALESCE(?, start_date),
end_date = COALESCE(?, end_date)
WHERE id = ? AND user_id = ?
`, req.Name, req.InitialWeight, req.TargetWeight, req.StartDate, req.EndDate, epochID, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update epoch"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "epoch updated"})
}
// 删除纪元
func (h *EpochHandler) DeleteEpoch(c *gin.Context) {
userID := middleware.GetUserID(c)
epochID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
// 检查纪元是否存在且属于当前用户
var exists int
err := h.db.QueryRow("SELECT 1 FROM weight_epochs WHERE id = ? AND user_id = ?", epochID, userID).Scan(&exists)
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "epoch not found"})
return
}
// 先删除关联的周计划
_, err = h.db.Exec("DELETE FROM weekly_plans WHERE epoch_id = ? AND user_id = ?", epochID, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete weekly plans"})
return
}
// 删除纪元
_, err = h.db.Exec("DELETE FROM weight_epochs WHERE id = ? AND user_id = ?", epochID, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete epoch"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "epoch deleted"})
}
// 获取纪元的每周计划列表
func (h *EpochHandler) GetWeeklyPlans(c *gin.Context) {
userID := middleware.GetUserID(c)
epochID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
rows, err := h.db.Query(`
SELECT id, epoch_id, user_id, year, week, start_date, end_date,
initial_weight, target_weight, final_weight, note, created_at
FROM weekly_plans WHERE epoch_id = ? AND user_id = ? ORDER BY year, week
`, epochID, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
return
}
defer rows.Close()
var plans []model.WeeklyPlanDetail
now := time.Now()
currentYear, currentWeek := now.ISOWeek()
// 先收集所有计划
var allPlans []*model.WeeklyPlan
for rows.Next() {
var p model.WeeklyPlan
var initialWeight, targetWeight, finalWeight sql.NullFloat64
rows.Scan(
&p.ID, &p.EpochID, &p.UserID, &p.Year, &p.Week, &p.StartDate, &p.EndDate,
&initialWeight, &targetWeight, &finalWeight, &p.Note, &p.CreatedAt,
)
if initialWeight.Valid {
p.InitialWeight = &initialWeight.Float64
}
if targetWeight.Valid {
p.TargetWeight = &targetWeight.Float64
}
if finalWeight.Valid {
p.FinalWeight = &finalWeight.Float64
}
allPlans = append(allPlans, &p)
}
// 处理每个计划,设置初始体重(从上一周的最终体重获取)
for i, p := range allPlans {
// 检查上一周是否有最终体重记录
hasPrevFinalWeight := false
if i > 0 {
prevPlan := allPlans[i-1]
if prevPlan.FinalWeight != nil {
// 上一周有最终体重,作为本周初始体重
p.InitialWeight = prevPlan.FinalWeight
hasPrevFinalWeight = true
}
}
detail := model.WeeklyPlanDetail{
Plan: p,
IsPast: p.Year < currentYear || (p.Year == currentYear && p.Week < currentWeek),
InitialWeightEditable: !hasPrevFinalWeight, // 上一周没有记录时才可编辑
}
// 计算本周减重
if p.InitialWeight != nil && p.FinalWeight != nil {
change := *p.InitialWeight - *p.FinalWeight
detail.WeightChange = &change
}
// 判断是否达标
if p.TargetWeight != nil && p.FinalWeight != nil {
detail.IsQualified = *p.FinalWeight <= *p.TargetWeight
}
plans = append(plans, detail)
}
c.JSON(http.StatusOK, plans)
}
// 更新每周计划
func (h *EpochHandler) UpdateWeeklyPlan(c *gin.Context) {
userID := middleware.GetUserID(c)
epochID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
planID, _ := strconv.ParseInt(c.Param("planId"), 10, 64)
var req model.UpdateWeeklyPlanRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 获取当前计划信息
var year, week int
err := h.db.QueryRow("SELECT year, week FROM weekly_plans WHERE id = ? AND user_id = ?", planID, userID).Scan(&year, &week)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "plan not found"})
return
}
// 检查是否允许修改初始体重
// 如果上一周有最终体重记录,则不允许修改本周初始体重
if req.InitialWeight != nil {
var prevFinalWeight sql.NullFloat64
h.db.QueryRow(`
SELECT final_weight FROM weekly_plans
WHERE epoch_id = ? AND user_id = ? AND (year < ? OR (year = ? AND week < ?))
ORDER BY year DESC, week DESC LIMIT 1
`, epochID, userID, year, year, week).Scan(&prevFinalWeight)
if prevFinalWeight.Valid {
// 上一周有记录,不允许修改初始体重,使用上一周的最终体重
req.InitialWeight = nil
}
}
// 更新计划
_, err = h.db.Exec(`
UPDATE weekly_plans
SET initial_weight = COALESCE(?, initial_weight),
target_weight = COALESCE(?, target_weight),
final_weight = COALESCE(?, final_weight),
note = ?
WHERE id = ? AND user_id = ?
`, req.InitialWeight, req.TargetWeight, req.FinalWeight, req.Note, planID, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update plan"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "plan updated"})
}
// 生成每周计划 - 不自动设置目标,初始体重从上周最终体重获取
func (h *EpochHandler) generateWeeklyPlans(epochID, userID int64, startDate, endDate string, initialWeight, targetWeight float64) {
start, _ := time.Parse("2006-01-02", startDate)
end, _ := time.Parse("2006-01-02", endDate)
currentDate := start
for currentDate.Before(end) || currentDate.Equal(end) {
year, week := currentDate.ISOWeek()
weekStart := getWeekStart(currentDate)
weekEnd := weekStart.AddDate(0, 0, 6)
// 不设置初始体重和目标体重,由用户手动设置
h.db.Exec(`
INSERT OR IGNORE INTO weekly_plans (epoch_id, user_id, year, week, start_date, end_date)
VALUES (?, ?, ?, ?, ?, ?)
`, epochID, userID, year, week, weekStart.Format("2006-01-02"), weekEnd.Format("2006-01-02"))
currentDate = currentDate.AddDate(0, 0, 7)
}
}
// 计算纪元详情
func (h *EpochHandler) calculateEpochDetail(epoch *model.WeightEpoch) *model.EpochDetail {
now := time.Now()
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.Local)
// 解析日期,支持多种格式
epochStart := parseDate(epoch.StartDate)
epochEnd := parseDate(epoch.EndDate)
detail := &model.EpochDetail{
Epoch: epoch,
YearDaysPassed: int(now.Sub(startOfYear).Hours() / 24),
EpochDaysPassed: int(now.Sub(epochStart).Hours() / 24),
DaysRemaining: int(epochEnd.Sub(now).Hours() / 24),
}
if detail.EpochDaysPassed < 0 {
detail.EpochDaysPassed = 0
}
if detail.DaysRemaining < 0 {
detail.DaysRemaining = 0
}
// 获取最新体重记录
var latestWeight float64
err := h.db.QueryRow(`
SELECT final_weight FROM weekly_plans
WHERE epoch_id = ? AND final_weight IS NOT NULL
ORDER BY year DESC, week DESC LIMIT 1
`, epoch.ID).Scan(&latestWeight)
if err == nil {
detail.CurrentWeight = &latestWeight
actualChange := epoch.InitialWeight - latestWeight
detail.ActualChange = &actualChange
distanceToGoal := latestWeight - epoch.TargetWeight
detail.DistanceToGoal = &distanceToGoal
detail.IsQualified = latestWeight <= epoch.TargetWeight
// 本纪元总减重
epochLoss := epoch.InitialWeight - latestWeight
detail.EpochTotalLoss = &epochLoss
}
// 计算本年度总减重
// 策略:找到年初的体重(今年第一周或去年最后一周)- 今年最新体重
var yearStartWeight, yearLatestWeight sql.NullFloat64
// 1. 先获取今年最新体重
err = h.db.QueryRow(`
SELECT final_weight
FROM weekly_plans
WHERE user_id = ? AND year = ? AND final_weight IS NOT NULL
ORDER BY week DESC LIMIT 1
`, epoch.UserID, now.Year()).Scan(&yearLatestWeight)
if err == nil && yearLatestWeight.Valid {
// 2. 获取年初体重:优先用今年第一周,如果没有则用去年最后一周
err = h.db.QueryRow(`
SELECT final_weight
FROM weekly_plans
WHERE user_id = ? AND year = ? AND final_weight IS NOT NULL
ORDER BY week ASC LIMIT 1
`, epoch.UserID, now.Year()).Scan(&yearStartWeight)
// 如果今年第一周没有,用去年最后一周
if err != nil || !yearStartWeight.Valid {
h.db.QueryRow(`
SELECT final_weight
FROM weekly_plans
WHERE user_id = ? AND year = ? AND final_weight IS NOT NULL
ORDER BY week DESC LIMIT 1
`, epoch.UserID, now.Year()-1).Scan(&yearStartWeight)
}
// 如果有年初体重,计算减重
if yearStartWeight.Valid && yearStartWeight.Float64 > 0 {
yearLoss := yearStartWeight.Float64 - yearLatestWeight.Float64
detail.YearTotalLoss = &yearLoss
}
}
// 计算累计总减重(所有纪元的减重总和)
var allTimeLoss float64
h.db.QueryRow(`
SELECT COALESCE(SUM(e.initial_weight - COALESCE(
(SELECT wp.final_weight FROM weekly_plans wp
WHERE wp.epoch_id = e.id AND wp.final_weight IS NOT NULL
ORDER BY wp.year DESC, wp.week DESC LIMIT 1),
e.initial_weight
)), 0)
FROM weight_epochs e
WHERE e.user_id = ?
`, epoch.UserID).Scan(&allTimeLoss)
if allTimeLoss != 0 {
detail.AllTimeTotalLoss = &allTimeLoss
}
return detail
}
// 解析日期,支持多种格式
func parseDate(dateStr string) time.Time {
// 尝试多种格式
formats := []string{
"2006-01-02",
"2006-01-02T15:04:05Z",
"2006-01-02T15:04:05Z07:00",
"2006-01-02 15:04:05",
time.RFC3339,
}
for _, format := range formats {
if t, err := time.Parse(format, dateStr); err == nil {
return t
}
}
// 默认返回当前时间
return time.Now()
}
func getWeekStart(t time.Time) time.Time {
weekday := int(t.Weekday())
if weekday == 0 {
weekday = 7
}
return t.AddDate(0, 0, -(weekday - 1))
}