From e761bde30bf83d37ae371379390f5998af1711bc Mon Sep 17 00:00:00 2001 From: zerosaturation Date: Thu, 23 Apr 2026 14:31:40 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=E5=92=8C=E5=B1=95=E8=A7=88=E8=97=8F=E5=93=81?= =?UTF-8?q?=E4=B8=8B=E6=9E=B6=E7=9A=84=E7=B4=A2=E5=BC=95bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/gateway/pkg/response/response.go | 3 +- backend/pkg/errors/errors.go | 1 + backend/pkg/models/system_config.go | 17 +++ .../galleryService/config/gallery_config.go | 30 +++++ .../socialService/config/social_config.go | 22 ++++ .../repository/fan_profile_repository.go | 64 +++++++++- docs/硬编码常量审批文档.md | 119 ++++++++++++++++++ frontend/pages/tasks/daily-tasks.vue | 19 ++- 8 files changed, 268 insertions(+), 7 deletions(-) create mode 100644 backend/pkg/models/system_config.go create mode 100644 docs/硬编码常量审批文档.md diff --git a/backend/gateway/pkg/response/response.go b/backend/gateway/pkg/response/response.go index 34ba8fa..383fd9e 100644 --- a/backend/gateway/pkg/response/response.go +++ b/backend/gateway/pkg/response/response.go @@ -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) diff --git a/backend/pkg/errors/errors.go b/backend/pkg/errors/errors.go index b366b78..983e3e2 100644 --- a/backend/pkg/errors/errors.go +++ b/backend/pkg/errors/errors.go @@ -43,6 +43,7 @@ var ( ErrRequestExpired = errors.New("好友请求已过期") ErrInvalidAction = errors.New("无效的操作") ErrNotFriends = errors.New("你们不是好友") + ErrMaxFriendsReached = errors.New("已达到最大好友数量限制") // 资产服务相关错误 ErrAssetNotFound = errors.New("资产不存在") diff --git a/backend/pkg/models/system_config.go b/backend/pkg/models/system_config.go new file mode 100644 index 0000000..61d205c --- /dev/null +++ b/backend/pkg/models/system_config.go @@ -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" +} \ No newline at end of file diff --git a/backend/services/galleryService/config/gallery_config.go b/backend/services/galleryService/config/gallery_config.go index 193cd5c..c282846 100644 --- a/backend/services/galleryService/config/gallery_config.go +++ b/backend/services/galleryService/config/gallery_config.go @@ -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 +} diff --git a/backend/services/socialService/config/social_config.go b/backend/services/socialService/config/social_config.go index 9db3342..f04b66a 100644 --- a/backend/services/socialService/config/social_config.go +++ b/backend/services/socialService/config/social_config.go @@ -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 +} diff --git a/backend/services/userService/repository/fan_profile_repository.go b/backend/services/userService/repository/fan_profile_repository.go index 25ed663..b1a9a31 100644 --- a/backend/services/userService/repository/fan_profile_repository.go +++ b/backend/services/userService/repository/fan_profile_repository.go @@ -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 +} diff --git a/docs/硬编码常量审批文档.md b/docs/硬编码常量审批文档.md new file mode 100644 index 0000000..d913daf --- /dev/null +++ b/docs/硬编码常量审批文档.md @@ -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. **低风险**: 业务规则类硬编码(如展馆规则、社交规则)可根据团队需求决定是否配置化 diff --git a/frontend/pages/tasks/daily-tasks.vue b/frontend/pages/tasks/daily-tasks.vue index 1853486..b1656c4 100644 --- a/frontend/pages/tasks/daily-tasks.vue +++ b/frontend/pages/tasks/daily-tasks.vue @@ -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' })