feat: 添加每日签到功能,新增帖子排序与社区面板优化
本次提交完成了多个核心功能迭代: 1. 新增用户每日签到系统,包含签到状态查询、连续天数统计、积分累计逻辑 2. 新增"新帖子"排序选项,替换原有的latest排序为新评论排序 3. 重构首页信息流布局,优化移动端与桌面端展示样式 4. 新增最新评论、最新注册用户模块,完善社区面板数据展示 5. 新增静态友情链接配置模块 6. 提取通用时间格式化工具函数,统一全站相对时间展示 7. 补充数据库签到模型与后端API接口,完善前后端交互链路
This commit is contained in:
136
backend/service/checkin.go
Normal file
136
backend/service/checkin.go
Normal file
@@ -0,0 +1,136 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user