Files
jiang13-bbs/backend/service/checkin.go
freefire b9ba8cb8c9 feat: 类型帖互动、积分经济与发帖体验
补齐问答采纳/重开、投票匿名与有票禁编、悬赏过期退回、抽奖回帖开奖,并接入积分账本与发帖附件可见性。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 03:17:07 +08:00

141 lines
3.5 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 service
import (
"errors"
"time"
"github.com/freefire/jiang13-bbs/model"
"gorm.io/gorm"
)
// DailyCheckinPoints 每日签到固定积分
const DailyCheckinPoints = 5
// maxStreakScan 连续天数最多向前回溯的天数
const maxStreakScan = 400
// ErrAlreadyCheckedIn 今日已签到
var ErrAlreadyCheckedIn = errors.New("今日已签到")
// CheckinStatus 签到状态(首页右栏与签到接口共用)
type CheckinStatus struct {
CheckedToday bool `json:"checked_today"`
Streak int `json:"streak"` // 连续签到天数
TotalPoints int `json:"total_points"` // 累计积分(可用余额)
TodayPoints int `json:"today_points"` // 今日签到可得积分
}
// CheckinService 每日签到
type CheckinService struct {
db *gorm.DB
}
func NewCheckinService(db *gorm.DB) *CheckinService {
return &CheckinService{db: db}
}
// localDay 取本地时区零点日期
func localDay(t time.Time) time.Time {
y, m, d := t.Date()
return time.Date(y, m, d, 0, 0, 0, 0, t.Location())
}
// streakFrom 从 anchor(含)起按日倒推连续签到天数
func streakFrom(dates map[string]bool, anchor time.Time) int {
streak := 0
for i := 0; i < maxStreakScan; i++ {
day := anchor.AddDate(0, 0, -i)
if !dates[day.Format("2006-01-02")] {
break
}
streak++
}
return streak
}
// Status 查询用户签到状态
func (s *CheckinService) Status(userID uint) (*CheckinStatus, error) {
today := localDay(time.Now())
var todayRow model.Checkin
err := s.db.Where("user_id = ? AND checkin_date::date = ?::date", userID, today).
First(&todayRow).Error
checkedToday := true
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
checkedToday = false
}
// 连续天数:今日已签从今天起算,未签从昨天起算(今日尚未签不打断连续)
anchor := today
if !checkedToday {
anchor = today.AddDate(0, 0, -1)
}
var dates []time.Time
if err := s.db.Model(&model.Checkin{}).
Where("user_id = ? AND checkin_date >= ?", userID, anchor.AddDate(0, 0, -maxStreakScan)).
Pluck("checkin_date", &dates).Error; err != nil {
return nil, err
}
dateSet := make(map[string]bool, len(dates))
for _, d := range dates {
dateSet[localDay(d).Format("2006-01-02")] = true
}
var total int64
if err := s.db.Model(&model.User{}).
Where("id = ?", userID).
Select("points").Scan(&total).Error; err != nil {
return nil, err
}
return &CheckinStatus{
CheckedToday: checkedToday,
Streak: streakFrom(dateSet, anchor),
TotalPoints: int(total),
TodayPoints: DailyCheckinPoints,
}, nil
}
// CheckIn 执行今日签到(每日一条,唯一索引兜底并发)
func (s *CheckinService) CheckIn(userID uint) (*CheckinStatus, error) {
today := localDay(time.Now())
var status *CheckinStatus
err := s.db.Transaction(func(tx *gorm.DB) error {
var count int64
if err := tx.Model(&model.Checkin{}).
Where("user_id = ? AND checkin_date::date = ?::date", userID, today).
Count(&count).Error; err != nil {
return err
}
if count > 0 {
return ErrAlreadyCheckedIn
}
row := model.Checkin{
UserID: userID,
CheckinDate: today,
Points: DailyCheckinPoints,
}
if err := tx.Create(&row).Error; err != nil {
return err
}
// 同步入账到用户积分余额
if _, err := CreditTx(tx, userID, DailyCheckinPoints, model.PointReasonCheckin, "checkin", row.ID, "每日签到"); err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
status, err = s.Status(userID)
if err != nil {
return nil, err
}
return status, nil
}