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

240 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"database/sql"
"net/http"
"strconv"
"time"
"healthflow/internal/config"
"healthflow/internal/model"
"github.com/gin-gonic/gin"
)
type ExerciseHandler struct {
db *sql.DB
cfg *config.Config
}
func NewExerciseHandler(db *sql.DB, cfg *config.Config) *ExerciseHandler {
return &ExerciseHandler{db: db, cfg: cfg}
}
// CreateCheckin 创建运动打卡
func (h *ExerciseHandler) CreateCheckin(c *gin.Context) {
userID := c.GetInt64("user_id")
var req model.CreateExerciseCheckinRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 每次都创建新记录,支持一天多次打卡
result, err := h.db.Exec(`
INSERT INTO exercise_checkins (user_id, checkin_date, exercise_type, body_part, duration, note)
VALUES (?, ?, ?, ?, ?, ?)
`, userID, req.CheckinDate, req.ExerciseType, req.BodyPart, req.Duration, req.Note)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "打卡失败", "detail": err.Error()})
return
}
id, _ := result.LastInsertId()
c.JSON(http.StatusOK, gin.H{"message": "打卡成功", "id": id, "date": req.CheckinDate})
}
// GetCheckins 获取打卡记录列表
func (h *ExerciseHandler) GetCheckins(c *gin.Context) {
userID := c.GetInt64("user_id")
query := `
SELECT id, user_id, checkin_date, exercise_type, body_part, duration, note, created_at
FROM exercise_checkins
WHERE user_id = ?
ORDER BY checkin_date DESC, created_at DESC
LIMIT 50
`
rows, err := h.db.Query(query, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败"})
return
}
defer rows.Close()
var checkins []model.ExerciseCheckin
for rows.Next() {
var checkin model.ExerciseCheckin
if err := rows.Scan(&checkin.ID, &checkin.UserID, &checkin.CheckinDate,
&checkin.ExerciseType, &checkin.BodyPart, &checkin.Duration, &checkin.Note, &checkin.CreatedAt); err != nil {
continue
}
checkins = append(checkins, checkin)
}
if checkins == nil {
checkins = []model.ExerciseCheckin{}
}
c.JSON(http.StatusOK, checkins)
}
// GetHeatmap 获取热力图数据
func (h *ExerciseHandler) GetHeatmap(c *gin.Context) {
userID := c.GetInt64("user_id")
year := c.Query("year")
if year == "" {
year = strconv.Itoa(time.Now().Year())
}
// 使用 substr 提取日期的年份部分进行匹配
// 同时只取日期部分前10个字符作为分组键
query := `
SELECT substr(checkin_date, 1, 10) as date, COUNT(*) as count
FROM exercise_checkins
WHERE user_id = ? AND substr(checkin_date, 1, 4) = ?
GROUP BY substr(checkin_date, 1, 10)
ORDER BY date
`
rows, err := h.db.Query(query, userID, year)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败", "detail": err.Error()})
return
}
defer rows.Close()
var data []model.ExerciseHeatmapData
for rows.Next() {
var item model.ExerciseHeatmapData
if err := rows.Scan(&item.Date, &item.Count); err != nil {
continue
}
data = append(data, item)
}
if data == nil {
data = []model.ExerciseHeatmapData{}
}
c.JSON(http.StatusOK, data)
}
// GetStats 获取运动统计
func (h *ExerciseHandler) GetStats(c *gin.Context) {
userID := c.GetInt64("user_id")
now := time.Now()
stats := model.ExerciseStats{}
// 总打卡次数
h.db.QueryRow(`
SELECT COUNT(*) FROM exercise_checkins WHERE user_id = ?
`, userID).Scan(&stats.TotalCheckins)
// 本月打卡
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
h.db.QueryRow(`
SELECT COUNT(*) FROM exercise_checkins
WHERE user_id = ? AND checkin_date >= ?
`, userID, monthStart.Format("2006-01-02")).Scan(&stats.ThisMonth)
// 本周打卡
weekday := int(now.Weekday())
if weekday == 0 {
weekday = 7
}
weekStart := now.AddDate(0, 0, -(weekday - 1))
h.db.QueryRow(`
SELECT COUNT(*) FROM exercise_checkins
WHERE user_id = ? AND checkin_date >= ?
`, userID, weekStart.Format("2006-01-02")).Scan(&stats.ThisWeek)
// 今年运动天数(去重)
yearStart := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, now.Location())
h.db.QueryRow(`
SELECT COUNT(DISTINCT checkin_date) FROM exercise_checkins
WHERE user_id = ? AND checkin_date >= ?
`, userID, yearStart.Format("2006-01-02")).Scan(&stats.ThisYearDays)
// 计算连续打卡天数
stats.CurrentStreak = h.calculateStreak(userID)
c.JSON(http.StatusOK, stats)
}
// calculateStreak 计算连续打卡天数
func (h *ExerciseHandler) calculateStreak(userID int64) int {
rows, err := h.db.Query(`
SELECT DISTINCT checkin_date FROM exercise_checkins
WHERE user_id = ?
ORDER BY checkin_date DESC
`, userID)
if err != nil {
return 0
}
defer rows.Close()
var dates []string
for rows.Next() {
var date string
if err := rows.Scan(&date); err == nil {
dates = append(dates, date)
}
}
if len(dates) == 0 {
return 0
}
streak := 0
today := time.Now().Format("2006-01-02")
yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02")
// 检查今天或昨天是否有打卡
if dates[0] != today && dates[0] != yesterday {
return 0
}
expectedDate := dates[0]
for _, date := range dates {
if date == expectedDate {
streak++
t, _ := time.Parse("2006-01-02", expectedDate)
expectedDate = t.AddDate(0, 0, -1).Format("2006-01-02")
} else {
break
}
}
return streak
}
// DeleteCheckin 删除打卡记录
func (h *ExerciseHandler) DeleteCheckin(c *gin.Context) {
userID := c.GetInt64("user_id")
idStr := c.Param("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
result, err := h.db.Exec(`
DELETE FROM exercise_checkins WHERE id = ? AND user_id = ?
`, id, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"})
return
}
affected, _ := result.RowsAffected()
if affected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "记录不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}