- 新增站长/超级管理员/管理员/板块管理员四级角色体系 - 实现实时权限快照加载与细粒度权限校验 - 新增内容审核队列与前后端页面 - 重构用户权限管理与站点管理逻辑 - 新增评论/帖子审核状态与通知推送 - 优化前端权限控制与角色徽章展示 - 修正数据库迁移与默认值问题
137 lines
3.4 KiB
Go
137 lines
3.4 KiB
Go
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.Checkin{}).
|
||
Where("user_id = ?", userID).
|
||
Select("COALESCE(SUM(points), 0)").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
|
||
}
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
status, err = s.Status(userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return status, nil
|
||
}
|