feat: 用户徽章与等级体系,积分经济管理重构

- 新增徽章系统:badge 服务与 handler、管理端配置页、BadgeChip/LevelBadge 展示组件
- 新增等级模型:model/level 及前端 levels 元数据
- 管理端经济模块拆分为 points/badges/levels 独立页面,EconomySettings 迁入设置
- 积分、签到、评论等服务适配等级与徽章逻辑
This commit is contained in:
2026-09-25 16:38:29 +08:00
parent 7b327bc8cc
commit 4ae57d9392
57 changed files with 3939 additions and 147 deletions

View File

@@ -61,6 +61,7 @@ func InitDB(dsn string) error {
&Announcement{}, &SitePage{}, &SiteSetting{}, &SiteDailyStats{}, &SiteDailyVisitor{}, &VisitEvent{}, &Attachment{}, &UserBoard{}, &LoginLog{},
&ChatRoom{}, &ChatRoomMember{}, &ChatMessage{},
&PointLedger{}, &PostContentUnlock{}, &PostAttachment{}, &PostAttachmentUnlock{},
&Badge{}, &UserBadge{},
&PostPollVote{}, &PostLotteryEntry{}, &Ad{}, &SidebarWidget{},
&ImportRecord{},
); err != nil {
@@ -73,6 +74,10 @@ func InitDB(dsn string) error {
return fmt.Errorf("站长角色迁移失败: %w", err)
}
// 用户等级:按积分流水回填累计获得积分(经验值,LevelOf 定级依据)。
// 每次启动执行,幂等自愈,保证与 CreditTx 增量口径一致(均排除 bounty_refund)
backfillUserTotalPoints(db)
// login_logs.success 早期 default=true 与 GORM 零值省略叠加,
// 会把失败登录错存为成功;AutoMigrate 不会改列默认值,这里幂等修正
if err := db.Exec(`ALTER TABLE login_logs ALTER COLUMN success SET DEFAULT false`).Error; err != nil {
@@ -334,6 +339,35 @@ func ensureOwnerRole(db *gorm.DB) error {
return nil
}
// backfillUserTotalPoints 按积分流水回填累计获得积分(经验值)。
// 口径与 CreditTx 增量一致:仅计正入账,排除悬赏托管退回(bounty_refund)。
// 每次启动执行,幂等自愈;失败仅记日志不阻断启动(等级纯展示,可下次启动修复)。
func backfillUserTotalPoints(db *gorm.DB) {
var tableCount int64
if err := db.Raw(`SELECT count(1) FROM information_schema.tables WHERE table_name = 'point_ledgers'`).
Scan(&tableCount).Error; err != nil {
log.Printf("[model] 回填累计获得积分前置检查失败: %v", err)
return
}
if tableCount == 0 {
return
}
res := db.Exec(`
UPDATE users SET total_points = COALESCE((
SELECT SUM(delta) FROM point_ledgers
WHERE point_ledgers.user_id = users.id AND delta > 0 AND reason <> 'bounty_refund'
), 0)
WHERE deleted_at IS NULL
`)
if res.Error != nil {
log.Printf("[model] 回填累计获得积分失败: %v", res.Error)
return
}
if res.RowsAffected > 0 {
log.Printf("[model] 已按积分流水回填累计获得积分 %d 行", res.RowsAffected)
}
}
// backfillBoardDefaults 为升级前已存在的板块补齐新字段默认值。
// 以 post_policy 为空作为「迁移前旧行」的判定标志:仅对这些行一次性回填
// post_policy=all、visible=true;后续已被显式设置过的行(含站长隐藏的板块)不会被覆盖。