500 lines
15 KiB
Go
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))
|
|
}
|