fix: 修复自动升级和展览藏品下架的索引bug

This commit is contained in:
zerosaturation 2026-04-23 14:31:40 +08:00
parent 1ebc23b832
commit e761bde30b
8 changed files with 268 additions and 7 deletions

View File

@ -140,7 +140,7 @@ func CleanErrorMessage(err error) string {
"invalid token": "Token无效",
"token expired": "Token已过期",
"user is inactive": "用户已被禁用",
"maximum number of identities reached": "已达到最大身份数量限制最多2个",
"maximum number of identities reached": "已达到最大身份数量限制",
"invalid star_id": "明星ID无效",
"invalid user_id": "用户ID无效",
"invalid nickname": "昵称格式不正确",
@ -148,6 +148,7 @@ func CleanErrorMessage(err error) string {
"nickname too short": "昵称过短至少需要2个字符",
"user info not found in Dubbo attachments": "请先登录",
"missing or invalid authorization token": "Token缺失或无效",
"已达到最大好友数量限制": "已达到最大好友数量限制",
}
msgLower := strings.ToLower(msg)

View File

@ -43,6 +43,7 @@ var (
ErrRequestExpired = errors.New("好友请求已过期")
ErrInvalidAction = errors.New("无效的操作")
ErrNotFriends = errors.New("你们不是好友")
ErrMaxFriendsReached = errors.New("已达到最大好友数量限制")
// 资产服务相关错误
ErrAssetNotFound = errors.New("资产不存在")

View File

@ -0,0 +1,17 @@
package models
// SystemConfig 系统配置表模型
type SystemConfig struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
ConfigKey string `gorm:"type:varchar(100);uniqueIndex;not null;column:config_key"`
ConfigValue int `gorm:"not null;column:config_value"`
Description string `gorm:"type:varchar(500);column:description"`
IsActive bool `gorm:"default:true;column:is_active"`
CreatedAt int64 `gorm:"column:created_at"`
UpdatedAt int64 `gorm:"column:updated_at"`
}
// TableName 指定表名
func (SystemConfig) TableName() string {
return "system_configs"
}

View File

@ -3,6 +3,11 @@ package config
import (
"flag"
"log"
"github.com/topfans/backend/pkg/database"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/models"
"go.uber.org/zap"
)
// GalleryRulesConfig 展馆规则配置(硬编码)
@ -107,3 +112,28 @@ func InitConfig() {
log.Printf(" User Service: %s", ServiceURLsConfig.UserService)
log.Printf(" Task Service: %s", ServiceURLsConfig.TaskService)
}
// GetExhibitionDuration 获取展览时长(秒),从数据库配置读取
func GetExhibitionDuration() int64 {
defer func() {
if r := recover(); r != nil {
logger.Logger.Error("Panic in GetExhibitionDuration, using default=14400",
zap.Any("error", r))
}
}()
db := database.GetDB()
if db == nil {
logger.Logger.Warn("Database not initialized, using default GrabSlotDuration=14400")
return 14400
}
var config models.SystemConfig
err := db.Where("config_key = ? AND is_active = ?", "exhibition_duration", true).First(&config).Error
if err != nil {
logger.Logger.Warn("Failed to get exhibition_duration config, using default=14400",
zap.Error(err))
return 14400
}
// 数据库存储的是小时,转换为秒
return int64(config.ConfigValue) * 3600
}

View File

@ -2,6 +2,11 @@ package config
import (
"time"
"github.com/topfans/backend/pkg/database"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/models"
"go.uber.org/zap"
)
// SocialConfig 社交服务配置
@ -149,3 +154,20 @@ func (c *SocialConfig) UpdateFriendLimit(enabled bool, defaultLimit int) {
c.FriendLimit.Enabled = enabled
c.FriendLimit.DefaultLimit = defaultLimit
}
// GetMaxFriends 获取最大好友数量限制(从数据库配置)
func GetMaxFriends() int {
db := database.GetDB()
if db == nil {
logger.Logger.Warn("Database not initialized, using default MaxFriends=50")
return 50
}
var config models.SystemConfig
err := db.Where("config_key = ? AND is_active = ?", "max_friends", true).First(&config).Error
if err != nil {
logger.Logger.Warn("Failed to get max_friends config, using default=50",
zap.Error(err))
return 50
}
return config.ConfigValue
}

View File

@ -2,6 +2,7 @@ package repository
import (
"errors"
"math"
"strings"
"github.com/topfans/backend/pkg/database"
@ -15,6 +16,31 @@ func contains(s, substr string) bool {
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
}
// CalculateLevel 根据经验值计算等级
// 公式: 升级到等级L需要的累计经验 = (L-1) * L * 50
// Level 1: 0经验, Level 2: 100经验, Level 3: 300经验, Level 4: 600经验...
func CalculateLevel(experience int64) int32 {
if experience < 0 {
return 1
}
// 使用公式: (L-1) * L * 50 <= experience
// 解方程: L^2 - L - experience/50 <= 0
// L = (1 + sqrt(1 + 4*experience/50)) / 2
level := int32((1 + math.Sqrt(1+4*float64(experience)/50)) / 2)
if level < 1 {
level = 1
}
return level
}
// GetExperienceForLevel 获取指定等级需要的经验值
func GetExperienceForLevel(level int32) int64 {
if level <= 1 {
return 0
}
return int64((level-1) * level * 50)
}
// FanProfileRepository 粉丝档案Repository接口
type FanProfileRepository interface {
// Create 创建粉丝档案
@ -41,6 +67,9 @@ type FanProfileRepository interface {
// IncrementAssetsCount 增加资产计数
IncrementAssetsCount(userID, starID int64, delta int32) error
// SyncLevelFromExperience 根据经验值同步等级(只升级不降级)
SyncLevelFromExperience(userID, starID int64) (int32, error)
// DecrementAssetsCount 减少资产计数
DecrementAssetsCount(userID, starID int64, delta int32) error
@ -390,7 +419,7 @@ func (r *fanProfileRepository) UpdateCrystalBalance(userID, starID int64, delta
return newBalance, nil
}
// UpdateExperience 更新经验值
// UpdateExperience 更新经验值(同时自动更新等级)
// delta: 变化量,正数表示增加,负数表示减少
// 返回: 更新后的经验值
func (r *fanProfileRepository) UpdateExperience(userID, starID int64, delta int64) (int64, error) {
@ -415,7 +444,7 @@ func (r *fanProfileRepository) UpdateExperience(userID, starID int64, delta int6
return err
}
// 计算新
// 计算新经验
newExperience = profile.Experience + delta
// 确保不会小于 0
@ -423,10 +452,16 @@ func (r *fanProfileRepository) UpdateExperience(userID, starID int64, delta int6
newExperience = 0
}
// 更新 experience 字段
// 根据新经验值计算新等级
newLevel := CalculateLevel(newExperience)
// 更新 experience 和 level 字段
if err := tx.Model(&models.FanProfile{}).
Where("user_id = ? AND star_id = ?", userID, starID).
Update("experience", newExperience).Error; err != nil {
Updates(map[string]interface{}{
"experience": newExperience,
"level": newLevel,
}).Error; err != nil {
return err
}
@ -468,3 +503,24 @@ func (r *fanProfileRepository) UpdateAvatar(userID, starID int64, avatarURL stri
return nil
}
// SyncLevelFromExperience 根据经验值同步等级(只升级不降级)
// 在获取用户信息时调用,确保等级与经验值匹配
func (r *fanProfileRepository) SyncLevelFromExperience(userID, starID int64) (int32, error) {
var profile models.FanProfile
if err := r.db.Where("user_id = ? AND star_id = ?", userID, starID).First(&profile).Error; err != nil {
return 0, err
}
newLevel := CalculateLevel(profile.Experience)
// 只升级,不降级
if newLevel > profile.Level {
if err := r.db.Model(&profile).Update("level", newLevel).Error; err != nil {
return profile.Level, err
}
return newLevel, nil
}
return profile.Level, nil
}

View File

@ -0,0 +1,119 @@
# 硬编码常量汇总
> 生成时间: 2026-04-23
> 用途: 供团队审批,确认这些硬编码值是否需要改为可配置
---
## 一、前端硬编码
### 1. API Base URL
| 文件 | 行号 | 硬编码值 | 风险等级 |
|------|------|----------|----------|
| `frontend/utils/api.js` | 4 | `http://localhost:8080` | 高 |
### 2. 性能配置
| 文件 | 行号 | 硬编码值 | 说明 |
|------|------|----------|------|
| `frontend/utils/performance-config.js:36` | 36 | `30000` | 数据过时阈值30秒 |
| `frontend/utils/performance-config.js:41` | 41 | `3000` | 关键资源加载超时(毫秒) |
### 3. UI/动画配置
| 文件 | 行号 | 硬编码值 | 说明 |
|------|------|----------|------|
| `frontend/pages/support-activity/components/FloatingBubbles.vue:337` | 337 | `30000` | 气泡刷新间隔30秒 |
| `frontend/pages/components/WelcomeAnimation.vue:704` | 704 | `3000` | 欢迎动画延迟3秒 |
| `frontend/pages/components/WelcomeAnimationWebview.vue:83` | 83 | `3000` | Webview动画延迟3秒 |
| `frontend/pages/support-activity/components/ActionBar.vue:97` | 97 | `3000` | 行动条动画延迟3秒 |
| `frontend/pages/components/CastloveContent.vue:19` | 19 | `interval="3000"` | 轮播间隔3秒 |
---
## 二、后端硬编码
### 1. JWT 密钥(安全风险)
| 文件 | 行号 | 硬编码值 | 风险等级 |
|------|------|----------|----------|
| `backend/pkg/jwt/jwt.go` | 18 | `your-secret-key-change-in-production` | **高** |
| `backend/gateway/config/config.go` | 83 | `topfans-secret-key-please-change-in-production` | **高** |
### 2. 展馆相关 (`galleryService/config/gallery_config.go`)
| 配置项 | 硬编码值 | 说明 |
|--------|----------|------|
| `InitialSlotCount` | **6** | 初始展位数量3公开+3私有 |
| `GrabSlotDuration` | **14400秒4小时** | 抢展位后未放置的过期时间 |
| `MaxSlotCount` | **10** | 最大展位数量 |
| `UnlockLevelBySlot` | 第4-10展位需等级5-11 | 解锁展位需要的等级 |
| `UnlockValueBySlot` | 水晶100-700 | 解锁展位需要的水晶 |
### 3. 社交相关 (`socialService/config/social_config.go`)
| 配置项 | 硬编码值 | 说明 |
|--------|----------|------|
| `RejectionCooldownDays` | **7天** | 拒绝好友冷却期 |
| `RequestExpiryDays` | **30天** | 好友请求过期时间 |
| `MaxLimit` | **0未启用** | 好友数量上限(预留) |
### 4. 用户/身份相关
| 配置项 | 值 | 文件 |
|--------|-----|------|
| `MaxIdentities` | **2个** | `userService/service/identity_service.go:23` |
| `MaxPasswordLength` | **50** | `pkg/validator/validator.go:14` |
| `MaxNicknameLength` | **20** | `pkg/validator/validator.go:18` |
### 5. 任务相关 (`taskService/config/task_config.go`)
| 配置项 | 硬编码值 | 说明 |
|--------|----------|------|
| `ResetHour` | **5** | 每日重置小时Asia/Shanghai |
| `ResetMinute` | **0** | 每日重置分钟 |
| `RevenueBatchSize` | **100** | 收益自动发放批次大小 |
| `RevenueMaxRetries` | **3** | 收益自动发放最大重试次数 |
### 6. 铸造相关 (`assetService/config/asset_config.go`)
| 配置项 | 硬编码值 | 说明 |
|--------|----------|------|
| `MaxRetryCount` | **3次** | 铸造失败最大重试次数 |
| `MintSimulationDelaySeconds` | **3秒** | 模拟上链延迟 |
| `GetByOwner limit` | **1000** | 单次获取藏品上限 |
### 7. Token/认证相关
| 配置项 | 硬编码值 | 文件 |
|--------|----------|------|
| `TokenExpiration` | **7天** | `pkg/jwt/jwt.go:13` |
### 8. 预签名URL相关 (`gateway/controller/asset_controller.go`)
| 配置项 | 硬编码值 | 说明 |
|--------|----------|------|
| 默认过期时间 | **3600秒1小时** | 预签名URL默认 |
| 最大过期时间 | **86400秒24小时** | 预签名URL最大 |
| STS最大 | **3600秒1小时** | STS Token最大 |
| STS最小 | **900秒15分钟** | STS Token最小 |
### 9. 数据库/服务连接(开发环境默认值)
| 服务 | 默认值 |
|------|--------|
| DB_HOST | `localhost` |
| Dubbo服务 | `tri://127.0.0.1:20000` ~ `20007` |
---
## 三、审批选项
请确认以下处理方式:
| 类别 | 选项A保留硬编码 | 选项B改为环境变量 |
|------|---------------------|----------------------|
| API Base URL | 继续使用localhost开发 | 改为环境变量切换dev/prod |
| JWT 密钥 | 生产部署时手动修改 | 改为环境变量,强制必须配置 |
| 展馆规则(时间/等级/水晶) | 保留硬编码 | 改为配置文件 |
| 社交规则(冷却期/过期) | 保留硬编码 | 改为配置文件 |
| 任务配置(重置时间) | 保留硬编码 | 改为环境变量 |
| 铸造配置(重试次数) | 保留硬编码 | 改为配置文件 |
| UI配置动画时长 | 保留硬编码 | 改为样式变量 |
---
## 四、建议
1. **高风险**: JWT密钥硬编码存在安全隐患建议改为强制环境变量配置
2. **中风险**: API Base URL建议改为环境变量方便切换开发/生产环境
3. **低风险**: 业务规则类硬编码(如展馆规则、社交规则)可根据团队需求决定是否配置化

View File

@ -352,7 +352,14 @@ async function handleClaim(task) {
try {
claimingTask.value = task.task_key
const res = await claimDailyTask(task.task_key, starId.value)
await loadTasks()
//
const index = tasks.value.findIndex(t => t.task_key === task.task_key)
if (index !== -1) {
tasks.value[index].status = 'claimed'
tasks.value[index].can_claim = false
}
emit('updated')
uni.$emit('balanceUpdated', { crystal_balance: res.data?.crystal_balance, experience: res.data?.experience })
uni.showToast({ title: '领取成功', icon: 'success' })
@ -369,7 +376,15 @@ async function handleClaimAll() {
try {
claimingAll.value = true
const res = await claimAllDailyTasks(starId.value)
await loadTasks()
// claimed
tasks.value.forEach(task => {
if (task.can_claim) {
task.status = 'claimed'
task.can_claim = false
}
})
emit('updated')
uni.$emit('balanceUpdated', { crystal_balance: res.data?.crystal_balance, experience: res.data?.experience })
uni.showToast({ title: '领取成功', icon: 'success' })