258 lines
7.8 KiB
Go
258 lines
7.8 KiB
Go
package services
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/inchstep/server/internal/models"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type CheckInService struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewCheckInService(db *gorm.DB) *CheckInService {
|
|
return &CheckInService{db: db}
|
|
}
|
|
|
|
// CreateItem 创建打卡项
|
|
func (s *CheckInService) CreateItem(userID string, item *models.CheckInItem) error {
|
|
item.ID = generateID()
|
|
item.UserID = userID
|
|
item.Status = models.StatusActive
|
|
item.CreatedAt = time.Now()
|
|
return s.db.Create(item).Error
|
|
}
|
|
|
|
// GetUserItems 获取用户打卡项列表
|
|
func (s *CheckInService) GetUserItems(userID string) ([]models.CheckInItem, error) {
|
|
var items []models.CheckInItem
|
|
err := s.db.Where("user_id = ? AND status != ?", userID, models.StatusDeleted).
|
|
Order("created_at desc").Find(&items).Error
|
|
return items, err
|
|
}
|
|
|
|
// GetItemByID 获取打卡项详情
|
|
func (s *CheckInService) GetItemByID(id, userID string) (*models.CheckInItem, error) {
|
|
var item models.CheckInItem
|
|
err := s.db.Where("id = ? AND user_id = ?", id, userID).First(&item).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &item, nil
|
|
}
|
|
|
|
// UpdateItem 更新打卡项
|
|
func (s *CheckInService) UpdateItem(id, userID string, updates map[string]interface{}) (*models.CheckInItem, error) {
|
|
allowed := map[string]bool{"name": true, "icon": true, "category": true, "daily_goal": true,
|
|
"cycle_type": true, "privacy": true}
|
|
filtered := make(map[string]interface{})
|
|
for k, v := range updates {
|
|
if allowed[k] {
|
|
filtered[k] = v
|
|
}
|
|
}
|
|
|
|
result := s.db.Model(&models.CheckInItem{}).Where("id = ? AND user_id = ?", id, userID).Updates(filtered)
|
|
if result.RowsAffected == 0 {
|
|
return nil, errors.New("打卡项不存在或无权限")
|
|
}
|
|
return s.GetItemByID(id, userID)
|
|
}
|
|
|
|
// DeleteItem 软删除打卡项
|
|
func (s *CheckInService) DeleteItem(id, userID string) error {
|
|
return s.db.Model(&models.CheckInItem{}).
|
|
Where("id = ? AND user_id = ?", id, userID).
|
|
Update("status", models.StatusDeleted).Error
|
|
}
|
|
|
|
// PauseItem 暂停打卡项
|
|
func (s *CheckInService) PauseItem(id, userID string) error {
|
|
return s.db.Model(&models.CheckInItem{}).
|
|
Where("id = ? AND user_id = ?", id, userID).
|
|
Update("status", models.StatusPaused).Error
|
|
}
|
|
|
|
// ResumeItem 恢复打卡项
|
|
func (s *CheckInService) ResumeItem(id, userID string) error {
|
|
return s.db.Model(&models.CheckInItem{}).
|
|
Where("id = ? AND user_id = ?", id, userID).
|
|
Update("status", models.StatusActive).Error
|
|
}
|
|
|
|
// CreateRecord 创建打卡记录
|
|
func (s *CheckInService) CreateRecord(userID, itemID string, checkDate time.Time, note, images *string) (*models.CheckInRecord, error) {
|
|
// 验证打卡项
|
|
item, err := s.GetItemByID(itemID, userID)
|
|
if err != nil {
|
|
return nil, errors.New("打卡项不存在或无权限")
|
|
}
|
|
if item.Status != models.StatusActive {
|
|
return nil, errors.New("打卡项当前不可用(已暂停或归档)")
|
|
}
|
|
|
|
// 检查当日是否已打卡
|
|
startOfDay := time.Date(checkDate.Year(), checkDate.Month(), checkDate.Day(), 0, 0, 0, 0, checkDate.Location())
|
|
endOfDay := startOfDay.Add(24 * time.Hour)
|
|
|
|
var count int64
|
|
s.db.Model(&models.CheckInRecord{}).
|
|
Where("item_id = ? AND user_id = ? AND check_date >= ? AND check_date < ?",
|
|
itemID, userID, startOfDay, endOfDay).Count(&count)
|
|
if count > 0 {
|
|
return nil, errors.New("今日已打卡")
|
|
}
|
|
|
|
record := &models.CheckInRecord{
|
|
ID: generateID(),
|
|
ItemID: itemID,
|
|
UserID: userID,
|
|
CheckDate: checkDate,
|
|
Status: models.RecordNormal,
|
|
Note: note,
|
|
Images: images,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
if err := s.db.Create(record).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return record, nil
|
|
}
|
|
|
|
// MakeupRecord 补卡(7 天内,每月 3 次免费,超出扣积分)
|
|
func (s *CheckInService) MakeupRecord(userID, itemID string, checkDate time.Time, costPoints int) (*models.CheckInRecord, error) {
|
|
now := time.Now()
|
|
diff := now.Sub(checkDate)
|
|
if diff.Hours() > 7*24 || diff.Hours() < 0 {
|
|
return nil, errors.New("只能补签最近 7 天的记录")
|
|
}
|
|
|
|
// 检查本月已用补卡次数
|
|
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
|
var makeupCount int64
|
|
s.db.Model(&models.CheckInRecord{}).
|
|
Where("user_id = ? AND status = ? AND created_at >= ?",
|
|
userID, models.RecordMakeup, currentMonthStart).Count(&makeupCount)
|
|
|
|
// 计算需要消耗的积分
|
|
freeCount := int64(3)
|
|
if makeupCount >= freeCount {
|
|
var user models.User
|
|
if err := s.db.First(&user, "id = ?", userID).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
if user.Points < costPoints {
|
|
return nil, errors.New("积分不足,补卡需要 5 积分")
|
|
}
|
|
s.db.Model(&user).Update("points", user.Points-costPoints)
|
|
}
|
|
|
|
record := &models.CheckInRecord{
|
|
ID: generateID(),
|
|
ItemID: itemID,
|
|
UserID: userID,
|
|
CheckDate: checkDate,
|
|
Status: models.RecordMakeup,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
return record, s.db.Create(record).Error
|
|
}
|
|
|
|
// GetRecords 获取打卡记录列表
|
|
func (s *CheckInService) GetRecords(userID, itemID string, start, end time.Time) ([]models.CheckInRecord, error) {
|
|
var records []models.CheckInRecord
|
|
query := s.db.Where("user_id = ?", userID)
|
|
if itemID != "" {
|
|
query = query.Where("item_id = ?", itemID)
|
|
}
|
|
err := query.Where("check_date >= ? AND check_date < ?", start, end).
|
|
Order("check_date desc").Find(&records).Error
|
|
return records, err
|
|
}
|
|
|
|
// GetStreak 计算连续打卡天数(时区感知)
|
|
func (s *CheckInService) GetStreak(itemID, userID string) (int, error) {
|
|
var records []models.CheckInRecord
|
|
if err := s.db.Where("item_id = ? AND user_id = ?", itemID, userID).
|
|
Order("check_date desc").Find(&records).Error; err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if len(records) == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
streak := 0
|
|
today := time.Now().Truncate(24 * time.Hour)
|
|
|
|
for i, r := range records {
|
|
recordDate := r.CheckDate.Truncate(24 * time.Hour)
|
|
if i == 0 {
|
|
if recordDate.Before(today.Add(-48 * time.Hour)) {
|
|
return 0, nil // 最近一次打卡超过 2 天前,连续已断
|
|
}
|
|
streak = 1
|
|
continue
|
|
}
|
|
|
|
prevDate := records[i-1].CheckDate.Truncate(24 * time.Hour)
|
|
if prevDate.Sub(recordDate).Hours() == 24 {
|
|
streak++
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
return streak, nil
|
|
}
|
|
|
|
// GetMonthlyStats 获取月度统计
|
|
func (s *CheckInService) GetMonthlyStats(userID string, year, month int) (map[string]interface{}, error) {
|
|
startOfMonth := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.Local)
|
|
endOfMonth := startOfMonth.AddDate(0, 1, 0)
|
|
|
|
var totalItems int64
|
|
s.db.Model(&models.CheckInItem{}).Where("user_id = ? AND status = ?", userID, models.StatusActive).Count(&totalItems)
|
|
|
|
var totalRecords int64
|
|
s.db.Model(&models.CheckInRecord{}).
|
|
Where("user_id = ? AND check_date >= ? AND check_date < ?", userID, startOfMonth, endOfMonth).
|
|
Count(&totalRecords)
|
|
|
|
// 按分类统计
|
|
type CategoryCount struct {
|
|
Category string
|
|
Count int64
|
|
}
|
|
var catStats []CategoryCount
|
|
s.db.Model(&models.CheckInItem{}).
|
|
Select("category, count(*) as count").
|
|
Where("user_id = ? AND status = ?", userID, models.StatusActive).
|
|
Group("category").Find(&catStats)
|
|
|
|
return map[string]interface{}{
|
|
"totalItems": totalItems,
|
|
"totalRecords": totalRecords,
|
|
"byCategory": catStats,
|
|
}, nil
|
|
}
|
|
|
|
|
|
// GetTodayStatus 获取用户今天已打卡的打卡项 ID 列表
|
|
func (s *CheckInService) GetTodayStatus(userID string) (map[string]bool, error) {
|
|
today := time.Now().Truncate(24 * time.Hour)
|
|
endOfToday := today.Add(24 * time.Hour)
|
|
|
|
var records []models.CheckInRecord
|
|
if err := s.db.Where("user_id = ? AND check_date >= ? AND check_date < ?",
|
|
userID, today, endOfToday).Find(&records).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make(map[string]bool)
|
|
for _, r := range records {
|
|
result[r.ItemID] = true
|
|
}
|
|
return result, nil
|
|
} |