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

@@ -4,6 +4,7 @@ import (
"errors"
"net/http"
"github.com/freefire/jiang13-bbs/model"
"github.com/freefire/jiang13-bbs/realtime"
"github.com/freefire/jiang13-bbs/service"
"github.com/gin-gonic/gin"
@@ -57,6 +58,18 @@ type updateSettingsRequest struct {
BrandLogoSize *string `json:"brand_logo_size"`
BrandLogoFit *string `json:"brand_logo_fit"`
FooterLinks *[]service.FooterLink `json:"footer_links"`
PointsCheckinBase *int `json:"points_checkin_base"`
PointsStreakEveryDays *int `json:"points_streak_every_days"`
PointsStreakBonus *int `json:"points_streak_bonus"`
PointsPostReward *int `json:"points_post_reward"`
PointsPostDailyCap *int `json:"points_post_daily_cap"`
PointsReplyReward *int `json:"points_reply_reward"`
PointsReplyDailyCap *int `json:"points_reply_daily_cap"`
PointsRecommendReward *int `json:"points_recommend_reward"`
Levels *[]model.LevelDef `json:"levels"`
LevelsFx *bool `json:"levels_fx"`
}
func settingsPayload(saved service.PublicSiteSettings) gin.H {
@@ -97,6 +110,18 @@ func settingsPayload(saved service.PublicSiteSettings) gin.H {
"brand_logo_size": saved.BrandLogoSize,
"brand_logo_fit": saved.BrandLogoFit,
"footer_links": saved.FooterLinks,
"points_checkin_base": saved.PointsCheckinBase,
"points_streak_every_days": saved.PointsStreakEveryDays,
"points_streak_bonus": saved.PointsStreakBonus,
"points_post_reward": saved.PointsPostReward,
"points_post_daily_cap": saved.PointsPostDailyCap,
"points_reply_reward": saved.PointsReplyReward,
"points_reply_daily_cap": saved.PointsReplyDailyCap,
"points_recommend_reward": saved.PointsRecommendReward,
"levels": saved.Levels,
"levels_fx": saved.LevelsFx,
}
}
@@ -113,7 +138,11 @@ func (req *updateSettingsRequest) hasAny() bool {
req.SiteWordmark != nil || req.SiteSlogan != nil || req.SiteKeywords != nil ||
req.LogoLightURL != nil || req.LogoDarkURL != nil || req.FaviconURL != nil ||
req.BrandMark != nil || req.BrandLogoSize != nil || req.BrandLogoFit != nil ||
req.FooterLinks != nil
req.FooterLinks != nil ||
req.PointsCheckinBase != nil || req.PointsStreakEveryDays != nil || req.PointsStreakBonus != nil ||
req.PointsPostReward != nil || req.PointsPostDailyCap != nil ||
req.PointsReplyReward != nil || req.PointsReplyDailyCap != nil ||
req.PointsRecommendReward != nil || req.Levels != nil || req.LevelsFx != nil
}
// AdminGetSettings 超管读取站点设置(与公开 payload 字段一致)
@@ -218,6 +247,49 @@ func (h *Handlers) UpdateSettings(c *gin.Context) {
return
}
}
pointsRules := []struct {
req *int
set func(int) error
hint string
}{
{req.PointsCheckinBase, h.Setting.SetPointsCheckinBase, "签到基础分须为 0–100"},
{req.PointsStreakEveryDays, h.Setting.SetPointsStreakEveryDays, "连续签到触发天数须为 0–100"},
{req.PointsStreakBonus, h.Setting.SetPointsStreakBonus, "连续签到加成分须为 0–100"},
{req.PointsPostReward, h.Setting.SetPointsPostReward, "发帖奖励须为 0–100"},
{req.PointsPostDailyCap, h.Setting.SetPointsPostDailyCap, "发帖奖励每日上限须为 0–100"},
{req.PointsReplyReward, h.Setting.SetPointsReplyReward, "回复奖励须为 0–100"},
{req.PointsReplyDailyCap, h.Setting.SetPointsReplyDailyCap, "回复奖励每日上限须为 0–100"},
{req.PointsRecommendReward, h.Setting.SetPointsRecommendReward, "推荐奖励须为 0–100"},
}
for _, r := range pointsRules {
if r.req == nil {
continue
}
if err := r.set(*r.req); err != nil {
if errors.Is(err, service.ErrInvalidSiteSetting) {
c.JSON(http.StatusBadRequest, gin.H{"error": r.hint})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.Levels != nil {
if err := h.Setting.SetLevels(*req.Levels); err != nil {
if errors.Is(err, service.ErrInvalidSiteSetting) {
c.JSON(http.StatusBadRequest, gin.H{"error": "等级配置无效(2–12 级,阈值须非负且严格递增、首级为 0,颜色须在色板内)"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.LevelsFx != nil {
if err := h.Setting.SetLevelsFx(*req.LevelsFx); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.CodeBlockAutoFold != nil {
if err := h.Setting.SetCodeBlockAutoFold(*req.CodeBlockAutoFold); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})