171 lines
6.0 KiB
Markdown
171 lines
6.0 KiB
Markdown
# 经验升级系统实现计划
|
||
|
||
## 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` 处理 |
|