# 经验升级系统实现计划 ## Context 后端目前有 `FanProfile.Level` 和 `FanProfile.Experience` 两个字段,但**没有任何根据经验值自动计算等级的逻辑**。用户希望: 1. 等级阈值可配置(**数据库配置**) 2. 经验值增加后自动计算并更新等级 3. 在 `AddExperience` 的事务内部触发升级检查(保证原子性) 4. **10级满级,满级后溢出丢弃** 5. 等级变化需要返回给前端 ## 关键文件 - [backend/pkg/models/user.go](backend/pkg/models/user.go#L50-L77) — `FanProfile` 模型定义,包含 `Level` 和 `Experience` - [backend/services/userService/repository/fan_profile_repository.go](backend/services/userService/repository/fan_profile_repository.go#L396-L441) — `UpdateExperience` 方法,最佳插入点 - [backend/services/userService/repository/fan_profile_repository.go](backend/services/userService/repository/fan_profile_repository.go) — 新增 `GetLevelThreshold` 方法 ## 实现方案 ### 1. 数据库等级阈值表 新建 `backend/services/userService/repository/level_threshold.go`: ```go type LevelThreshold struct { Level int32 `gorm:"primaryKey;column:level"` ExpRequired int64 `gorm:"not null;column:exp_required"` } func (r *fanProfileRepository) GetLevelThresholds() (map[int32]int64, error) { var thresholds []LevelThreshold if err := r.db.Order("level ASC").Find(&thresholds).Error; err != nil { return nil, err } result := make(map[int32]int64) for _, t := range thresholds { result[t.Level] = t.ExpRequired } return result, nil } ``` **建表 SQL**(需执行): ```sql CREATE TABLE IF NOT EXISTS level_thresholds ( level INT PRIMARY KEY, exp_required BIGINT NOT NULL ); INSERT INTO level_thresholds (level, exp_required) VALUES (1, 0), (2, 100), (3, 300), (4, 600), (5, 1000), (6, 1500), (7, 2100), (8, 2800), (9, 3600), (10, 4500); ``` ### 2. 新增 `CalculateLevel` 函数 ```go const MaxLevel = 10 // 根据经验值计算当前等级(10级满级,溢出返回10) func CalculateLevel(experience int64, thresholds map[int32]int64) int32 { for level := MaxLevel; level >= 1; level-- { if exp >= thresholds[level] { return level } } return 1 } ``` ### 3. 修改 `UpdateExperience` 事务 在 [fan_profile_repository.go:396-441](backend/services/userService/repository/fan_profile_repository.go#L396-L441) 的事务末尾: ```go // 计算新等级 newLevel := CalculateLevel(newExperience, thresholds) // thresholds 从缓存或传入 // 如果等级有提升,更新 level 字段 if newLevel > profile.Level { tx.Model(&models.FanProfile{}). Where("user_id = ? AND star_id = ?", userID, starID). Updates(map[string]interface{}{ "experience": newExperience, "level": newLevel, }) } else { tx.Model(&models.FanProfile{}). Where("user_id = ? AND star_id = ?", userID, starID). Update("experience", newExperience) } ``` ### 4. 修改 `AddExperienceResponse` [proto/user.proto](backend/proto/user.proto#L214-L225) 的 `AddExperienceResponse` 增加 `new_level` 字段: ```protobuf message AddExperienceResponse { topfans.common.BaseResponse base = 1; int64 new_experience = 2; int32 new_level = 3; // 新增 } ``` 重新生成 `user.pb.go` 和 `user.triple.go`。 ### 5. 修改 `userService.AddExperience` [user_service.go:854-861](backend/services/userService/service/user_service.go#L854-L861) 返回 `newLevel`: ```go // 4. 计算新等级 newLevel := CalculateLevel(newExperience, thresholds) // 5. 构建响应 return &pb.AddExperienceResponse{ Base: &pbCommon.BaseResponse{...}, NewExperience: newExperience, NewLevel: newLevel, }, nil ``` ### 6. 修改 `user_rpc_client.go` [user_rpc_client.go:43-58](backend/services/taskService/client/user_rpc_client.go#L43-L58) 返回值增加 `newLevel`: ```go func (c *userServiceClient) AddExperience(ctx context.Context, userID, starID int64, delta int64) (int64, int32, error) { resp, err := c.client.AddExperience(ctx, &pbUser.AddExperienceRequest{ UserId: userID, StarId: starID, Delta: delta, }) if err != nil { return 0, 0, err } if resp.Base.Code != pbCommon.StatusCode_STATUS_OK { return 0, 0, fmt.Errorf("AddExperience failed with code: %d", resp.Base.Code) } return resp.NewExperience, resp.NewLevel, nil } ``` ### 7. 修改 `ClaimDailyTaskResponse` 和 `ClaimAllDailyTasksResponse` 在 [proto/task.proto](backend/proto/task.proto) 中两个 response 增加 `new_level` 字段,重新生成 pb 文件。 ## 验证方案 1. **编译验证**:`cd backend && go build ./...` 2. **数据库**:确认 `level_thresholds` 表存在且有数据 3. **手动测试**:调用 `/api/v1/tasks/daily/claim`,观察返回的 `experience` 和 `level` 是否正确 4. **边界测试**: - 经验刚好达到阈值时是否升级 - 满级后经验是否溢出丢弃 ## 涉及修改的文件 | 文件 | 修改内容 | |------|---------| | `backend/services/userService/repository/level_threshold.go` | **新建** — 等级阈值表模型和查询方法 | | `backend/services/userService/repository/fan_profile_repository.go` | 修改 `UpdateExperience` 事务,增加等级计算和更新 | | `backend/services/userService/service/user_service.go` | 修改 `AddExperience` 返回 `newLevel` | | `backend/services/userService/config/` | 可能需要新增缓存配置 | | `backend/proto/user.proto` | `AddExperienceResponse` 增加 `new_level` 字段 | | `backend/proto/task.proto` | `ClaimDailyTaskResponse` / `ClaimAllDailyTasksResponse` 增加 `new_level` | | `backend/pkg/proto/user/user.pb.go` | 重新生成 | | `backend/pkg/proto/user/user.triple.go` | 重新生成 | | `backend/pkg/proto/task/task.pb.go` | 重新生成 | | `backend/pkg/proto/task/task.triple.go` | 重新生成 | | `backend/services/taskService/client/user_rpc_client.go` | `AddExperience` 返回值增加 `newLevel` | | `backend/services/taskService/service/daily_task_service.go` | 调用处增加 `newLevel` 处理 |