topfans/docs/generic-moseying-bird.md
2026-04-15 17:04:42 +08:00

171 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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