1184 lines
39 KiB
Markdown
1184 lines
39 KiB
Markdown
# 数字藏品升级系统设计方案
|
||
|
||
> 文档版本:v1.0
|
||
> 创建日期:2026-05-21
|
||
> 状态:待评审
|
||
|
||
---
|
||
|
||
## 一、背景与目标
|
||
|
||
### 1.1 需求背景
|
||
|
||
当前系统中的"等级"体系是针对**用户**的等级(1-20级),基于用户累计展出时长进行升级。现需要新增一套针对**单个藏品**的等级体系,形成双轨制升级机制。
|
||
|
||
核心目标:
|
||
1. 通过藏品等级激励用户持续创作和"铸爱"
|
||
2. 高等级藏品享有更高的展出收益,形成经济正循环
|
||
3. 赛季制设计避免等级通货膨胀,保持玩家竞争动力
|
||
|
||
### 1.2 设计原则
|
||
|
||
- **双轨驱动**:升级需同时满足"累计展出时长"和"累计点赞数"两个条件
|
||
- **可配置性**:等级门槛、收益参数可通过数据库配置,无需硬编码
|
||
- **可追溯性**:所有等级变化记录完整留存,支持审计
|
||
- **赛季隔离**:赛季结束后执行等级回退,支持多种回退策略
|
||
|
||
---
|
||
|
||
## 二、等级体系设计
|
||
|
||
### 2.1 等级定义
|
||
|
||
藏品共分 5 个等级,初始等级为 **N**:
|
||
|
||
| 等级 | 名称 | 英文 | 每小时收益 | 升级所需展出总时长 | 升级所需累计点赞数 |
|
||
|:----:|:----:|:----:|:--------:|:----------------:|:----------------:|
|
||
| 1 | N | Normal | 5 水晶/小时 | 0 小时(初始) | 0(初始) |
|
||
| 2 | R | Rare | 7 水晶/小时 | 24 小时 | 20 赞 |
|
||
| 3 | SR | Super Rare | 12 水晶/小时 | 120 小时 | 500 赞 |
|
||
| 4 | SSR | Super² Rare | 18 水晶/小时 | 360 小时 | 10,000 赞 |
|
||
| 5 | UR | Ultimate Rare | 30 水晶/小时 | 720 小时 | 100,000 赞 |
|
||
|
||
> 注:升级条件为**同时满足**时长和点赞两个门槛。
|
||
|
||
### 2.2 升级路线图
|
||
|
||
```
|
||
N ─[24h + 20赞]─→ R ─[96h + 480赞]─→ SR ─[240h + 9500赞]─→ SSR ─[360h + 90000赞]─→ UR
|
||
```
|
||
|
||
### 2.3 收益对比(以展出 1 小时为例)
|
||
|
||
| 等级 | 基础收益 | 5赞Buff(+10%) | 10赞Buff(+20%) | 30赞Buff(+30%) |
|
||
|:----:|:-------:|:------------:|:--------------:|:--------------:|
|
||
| N | 5 | 5.5 | 6.0 | 6.5 |
|
||
| R | 7 | 7.7 | 8.4 | 9.1 |
|
||
| SR | 12 | 13.2 | 14.4 | 15.6 |
|
||
| SSR | 18 | 19.8 | 21.6 | 23.4 |
|
||
| UR | 30 | 33.0 | 36.0 | 39.0 |
|
||
|
||
> Buff 计算公式:`R = 基础收益 × T × (100% + Buff%)`,其中 T 为展出时长(小时)
|
||
|
||
---
|
||
|
||
## 三、数据模型设计
|
||
|
||
### 3.1 新增数据库表
|
||
|
||
#### 3.1.1 藏品等级配置表 `asset_levels`
|
||
|
||
```sql
|
||
CREATE TABLE asset_levels (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
level VARCHAR(10) NOT NULL UNIQUE, -- N, R, SR, SSR, UR
|
||
level_order INT NOT NULL, -- 等级顺序(1-5),用于排序和比较
|
||
hourly_revenue INT NOT NULL, -- 每小时基础收益(水晶)
|
||
require_hours INT NOT NULL, -- 升级到该等级所需累计展出时长(小时)
|
||
require_likes INT NOT NULL, -- 升级到该等级所需累计点赞数
|
||
is_initial BOOLEAN DEFAULT FALSE, -- 是否为初始等级
|
||
created_at BIGINT NOT NULL,
|
||
updated_at BIGINT NOT NULL
|
||
);
|
||
|
||
COMMENT ON TABLE asset_levels IS '藏品等级配置表,定义各等级的基础参数和升级条件';
|
||
```
|
||
|
||
#### 3.1.2 藏品等级记录表 `asset_level_records`
|
||
|
||
```sql
|
||
CREATE TABLE asset_level_records (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
asset_id BIGINT NOT NULL UNIQUE,
|
||
current_level VARCHAR(10) NOT NULL DEFAULT 'N',
|
||
-- 赛季内累计(用于等级计算,会在赛季降序时按百分比衰减)
|
||
season_exhibition_hours INT NOT NULL DEFAULT 0, -- 赛季内累计展出时长(小时)
|
||
season_likes INT NOT NULL DEFAULT 0, -- 赛季内累计点赞数
|
||
-- 历史累计(不受赛季影响,用于赛季降序计算)
|
||
lifetime_exhibition_hours INT NOT NULL DEFAULT 0, -- 历史累计展出时长(跨赛季保留)
|
||
lifetime_likes INT NOT NULL DEFAULT 0, -- 历史累计点赞数(跨赛季,按百分比衰减)
|
||
-- 降序计算用:基于历史累计计算降序后的赛季值
|
||
-- season_xxx = lifetime_xxx * preserve_percent / 100
|
||
season_id VARCHAR(50), -- 当前赛季ID
|
||
updated_at BIGINT NOT NULL,
|
||
CONSTRAINT uk_asset_level_asset FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE
|
||
);
|
||
|
||
CREATE INDEX idx_asset_level_season ON asset_level_records(season_id);
|
||
CREATE INDEX idx_asset_level_current_level ON asset_level_records(current_level);
|
||
|
||
COMMENT ON TABLE asset_level_records IS '藏品等级记录表,记录每个藏品的当前等级和成长值;
|
||
赛季内累计(season_xxx)用于等级计算,赛季降序时按百分比衰减;
|
||
历史累计(lifetime_xxx)跨赛季保留,赛季降序时按百分比继承到赛季内累计';
|
||
```
|
||
|
||
#### 3.1.3 藏品等级变化日志表 `asset_level_change_logs`
|
||
|
||
```sql
|
||
CREATE TABLE asset_level_change_logs (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
asset_id BIGINT NOT NULL,
|
||
from_level VARCHAR(10),
|
||
to_level VARCHAR(10) NOT NULL,
|
||
trigger_type VARCHAR(20) NOT NULL, -- exhibition_complete: 展出完成, like_update: 点赞更新, season_reset: 赛季重置
|
||
trigger_hours INT DEFAULT 0, -- 触发升级时的时长
|
||
trigger_likes INT DEFAULT 0, -- 触发升级时的点赞数
|
||
change_reason VARCHAR(255), -- 详细原因描述
|
||
created_at BIGINT NOT NULL
|
||
);
|
||
|
||
CREATE INDEX idx_asset_level_log_asset ON asset_level_change_logs(asset_id);
|
||
CREATE INDEX idx_asset_level_log_created ON asset_level_change_logs(created_at DESC);
|
||
|
||
COMMENT ON TABLE asset_level_change_logs IS '藏品等级变化日志表,记录所有等级变动历史';
|
||
```
|
||
|
||
#### 3.1.4 赛季配置表 `seasons`(新增)
|
||
|
||
```sql
|
||
CREATE TABLE seasons (
|
||
id VARCHAR(50) PRIMARY KEY,
|
||
name VARCHAR(100) NOT NULL, -- 赛季名称,如"第一赛季"
|
||
duration_days INT NOT NULL DEFAULT 84, -- 赛季持续天数(可配置,默认84天≈12周)
|
||
start_time BIGINT NOT NULL, -- 开始时间(毫秒时间戳)
|
||
end_time BIGINT NOT NULL, -- 结束时间(毫秒时间戳,由 start_time + duration_days 计算得出)
|
||
reset_strategy VARCHAR(20) DEFAULT 'percentage_decay', -- reset_all / percentage_decay / keep_all
|
||
reset_level BOOLEAN DEFAULT TRUE, -- 是否重置等级
|
||
status VARCHAR(20) DEFAULT 'active', -- active/ended
|
||
created_at BIGINT NOT NULL,
|
||
updated_at BIGINT NOT NULL
|
||
);
|
||
|
||
COMMENT ON TABLE seasons IS '赛季配置表,支持百分比降序策略;
|
||
注意:end_time 由 CalculateEndTime() 方法计算得出,或在创建/更新时同步写入';
|
||
```
|
||
|
||
#### 3.1.5 赛季降序百分比配置表 `season_decay_config`(新增)
|
||
|
||
```sql
|
||
CREATE TABLE season_decay_config (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
season_id VARCHAR(50) NOT NULL, -- 赛季ID
|
||
level VARCHAR(10) NOT NULL, -- 等级(N/R/SR/SSR/UR)
|
||
preserve_percent INT NOT NULL DEFAULT 100, -- 降序时保留百分比(0-100)
|
||
updated_at BIGINT NOT NULL,
|
||
CONSTRAINT uk_season_level UNIQUE (season_id, level)
|
||
);
|
||
|
||
COMMENT ON TABLE season_decay_config IS '赛季降序百分比配置,每个等级的保留比例(0-100);
|
||
注意:每个等级在每个赛季都有独立的保留比例配置';
|
||
|
||
-- 示例数据(可按需调整)
|
||
INSERT INTO season_decay_config (season_id, level, preserve_percent, updated_at) VALUES
|
||
('season_1', 'N', 100, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), -- N级保持100%(不降序)
|
||
('season_1', 'R', 80, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), -- R级保留80%
|
||
('season_1', 'SR', 70, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), -- SR级保留70%
|
||
('season_1', 'SSR', 60, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)), -- SSR级保留60%
|
||
('season_1', 'UR', 50, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)) -- UR级保留50%
|
||
ON CONFLICT (season_id, level) DO NOTHING;
|
||
```
|
||
```
|
||
|
||
### 3.2 模型定义
|
||
|
||
#### pkg/models/asset_level.go
|
||
|
||
```go
|
||
package models
|
||
|
||
import "time"
|
||
|
||
// AssetLevel 藏品等级配置
|
||
type AssetLevel struct {
|
||
ID int64 `gorm:"primaryKey;autoIncrement"`
|
||
Level string `gorm:"type:varchar(10);unique;not null"`
|
||
LevelOrder int `gorm:"not null"`
|
||
HourlyRevenue int `gorm:"not null"`
|
||
RequireHours int `gorm:"not null"`
|
||
RequireLikes int `gorm:"not null"`
|
||
IsInitial bool `gorm:"default:false"`
|
||
CreatedAt int64 `gorm:"not null"`
|
||
UpdatedAt int64 `gorm:"not null"`
|
||
}
|
||
|
||
func (AssetLevel) TableName() string { return "asset_levels" }
|
||
|
||
// AssetLevelRecord 藏品等级记录
|
||
type AssetLevelRecord struct {
|
||
ID int64 `gorm:"primaryKey;autoIncrement"`
|
||
AssetID int64 `gorm:"unique;not null"`
|
||
CurrentLevel string `gorm:"type:varchar(10);not null;default:'N'"`
|
||
// 赛季内累计(用于等级计算,赛季降序时按百分比衰减)
|
||
SeasonExhibitionHours int `gorm:"default:0;not null"`
|
||
SeasonLikes int `gorm:"default:0;not null"`
|
||
// 历史累计(跨赛季保留,赛季降序时按百分比继承到赛季内累计)
|
||
LifetimeExhibitionHours int `gorm:"default:0;not null"`
|
||
LifetimeLikes int `gorm:"default:0;not null"`
|
||
SeasonID string `gorm:"type:varchar(50)"`
|
||
UpdatedAt int64 `gorm:"not null"`
|
||
}
|
||
|
||
func (AssetLevelRecord) TableName() string { return "asset_level_records" }
|
||
|
||
// AssetLevelChangeLog 等级变化日志
|
||
type AssetLevelChangeLog struct {
|
||
ID int64 `gorm:"primaryKey;autoIncrement"`
|
||
AssetID int64 `gorm:"not null;index"`
|
||
FromLevel string `gorm:"type:varchar(10)"`
|
||
ToLevel string `gorm:"type:varchar(10);not null"`
|
||
TriggerType string `gorm:"type:varchar(20);not null"`
|
||
TriggerHours int `gorm:"default:0"`
|
||
TriggerLikes int `gorm:"default:0"`
|
||
ChangeReason string `gorm:"type:varchar(255)"`
|
||
CreatedAt int64 `gorm:"not null;index"`
|
||
}
|
||
|
||
func (AssetLevelChangeLog) TableName() string { return "asset_level_change_logs" }
|
||
|
||
// Season 赛季配置
|
||
type Season struct {
|
||
ID string `gorm:"primaryKey;type:varchar(50)"`
|
||
Name string `gorm:"type:varchar(100);not null"`
|
||
DurationDays int `gorm:"not null;default:84"` // 赛季持续天数(可配置)
|
||
StartTime int64 `gorm:"not null"`
|
||
EndTime int64 `gorm:"not null"`
|
||
ResetStrategy string `gorm:"type:varchar(20);default:'percentage_decay'"` // reset_all / percentage_decay / keep_all
|
||
ResetLevel bool `gorm:"default:true"`
|
||
Status string `gorm:"type:varchar(20);default:'active'"`
|
||
CreatedAt int64 `gorm:"not null"`
|
||
UpdatedAt int64 `gorm:"not null"`
|
||
}
|
||
|
||
func (Season) TableName() string { return "seasons" }
|
||
|
||
// 计算赛季结束时间
|
||
func (s *Season) CalculateEndTime() int64 {
|
||
return s.StartTime + int64(s.DurationDays)*86400000
|
||
}
|
||
|
||
// BeforeCreate 创建前钩子:自动计算 end_time
|
||
func (s *Season) BeforeCreate(tx *gorm.DB) error {
|
||
now := time.Now().UnixMilli()
|
||
s.CreatedAt = now
|
||
s.UpdatedAt = now
|
||
s.EndTime = s.CalculateEndTime()
|
||
if s.Status == "" {
|
||
s.Status = "active"
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// BeforeUpdate 更新前钩子:自动更新 end_time
|
||
func (s *Season) BeforeUpdate(tx *gorm.DB) error {
|
||
s.UpdatedAt = time.Now().UnixMilli()
|
||
s.EndTime = s.CalculateEndTime()
|
||
return nil
|
||
}
|
||
|
||
// SeasonDecayConfig 赛季降序百分比配置
|
||
type SeasonDecayConfig struct {
|
||
ID int64 `gorm:"primaryKey;autoIncrement"`
|
||
SeasonID string `gorm:"type:varchar(50);not null;uniqueIndex:uk_season_level"`
|
||
Level string `gorm:"type:varchar(10);not null;uniqueIndex:uk_season_level"`
|
||
PreservePercent int `gorm:"not null;default:100"` // 降序时保留百分比(0-100)
|
||
UpdatedAt int64 `gorm:"not null"`
|
||
}
|
||
|
||
func (SeasonDecayConfig) TableName() string { return "season_decay_config" }
|
||
|
||
// 等级常量
|
||
const (
|
||
LevelN = "N"
|
||
LevelR = "R"
|
||
LevelSR = "SR"
|
||
LevelSSR = "SSR"
|
||
LevelUR = "UR"
|
||
)
|
||
|
||
// 等级顺序映射
|
||
var LevelOrderMap = map[string]int{
|
||
LevelN: 1,
|
||
LevelR: 2,
|
||
LevelSR: 3,
|
||
LevelSSR: 4,
|
||
LevelUR: 5,
|
||
}
|
||
```
|
||
|
||
### 3.3 初始数据
|
||
|
||
```sql
|
||
-- 藏品等级配置初始数据
|
||
INSERT INTO asset_levels (level, level_order, hourly_revenue, require_hours, require_likes, is_initial, created_at, updated_at) VALUES
|
||
('N', 1, 5, 0, 0, TRUE, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000), ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
|
||
('R', 2, 7, 24, 20, FALSE, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000), ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
|
||
('SR', 3, 12, 120, 500, FALSE, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000), ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
|
||
('SSR', 4, 18, 360, 10000, FALSE, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000), ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
|
||
('UR', 5, 30, 720, 100000, FALSE, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000), ROUND(EXTRACT(EPOCH FROM NOW()) * 1000))
|
||
ON CONFLICT (level) DO NOTHING;
|
||
|
||
-- 赛季配置初始数据(duration_days 可按需调整)
|
||
INSERT INTO seasons (id, name, duration_days, start_time, end_time, reset_strategy, reset_level, status, created_at, updated_at) VALUES
|
||
('season_1', '第一赛季', 84, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000), ROUND(EXTRACT(EPOCH FROM NOW()) * 1000) + 86400000 * 84, 'percentage_decay', TRUE, 'active', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000), ROUND(EXTRACT(EPOCH FROM NOW()) * 1000))
|
||
ON CONFLICT (id) DO NOTHING;
|
||
COMMENT ON COLUMN seasons.reset_strategy IS '重置策略: reset_all(全部重置N) / percentage_decay(按配置百分比降序) / keep_all(保持不变)';
|
||
```
|
||
|
||
---
|
||
|
||
## 四、服务层设计
|
||
|
||
### 4.1 AssetLevelService 接口
|
||
|
||
```go
|
||
// service/asset_level_service.go
|
||
|
||
package service
|
||
|
||
// AssetLevelService 藏品等级服务接口
|
||
type AssetLevelService interface {
|
||
// GetOrCreateRecord 获取或创建藏品等级记录
|
||
GetOrCreateRecord(assetID int64) (*models.AssetLevelRecord, error)
|
||
|
||
// GetRecordByAssetID 根据资产ID获取等级记录
|
||
GetRecordByAssetID(assetID int64) (*models.AssetLevelRecord, error)
|
||
|
||
// GetLevelConfig 获取等级配置
|
||
GetLevelConfig(level string) (*models.AssetLevel, error)
|
||
|
||
// GetAllLevels 获取所有等级配置(按顺序)
|
||
GetAllLevels() ([]*models.AssetLevel, error)
|
||
|
||
// AddExhibitionHours 增加展出时长并检查升级
|
||
AddExhibitionHours(assetID int64, hours int) (string, bool, error)
|
||
|
||
// AddLikes 增加点赞数并检查升级
|
||
AddLikes(assetID int64, count int) (string, bool, error)
|
||
|
||
// RemoveLikes 减少点赞数(用于取消点赞)
|
||
RemoveLikes(assetID int64, count int) (string, bool, error)
|
||
|
||
// CalculateRevenue 计算单次展出收益(基于藏品等级)
|
||
CalculateRevenue(assetID int64, likeCount int, startTime, endTime int64, revenueBoostBps int) (int64, error)
|
||
|
||
// SeasonReset 赛季重置(赛季结束时调用)
|
||
SeasonReset(seasonID string) error
|
||
|
||
// GetCurrentSeason 获取当前赛季
|
||
GetCurrentSeason() (*models.Season, error)
|
||
|
||
// getDefaultSeason 获取默认赛季(当无活跃赛季时返回默认赛季)
|
||
getDefaultSeason() *models.Season
|
||
}
|
||
```
|
||
|
||
### 4.2 核心业务逻辑
|
||
|
||
#### 4.2.1 升级检查逻辑
|
||
|
||
```go
|
||
// service/asset_level_service.go
|
||
|
||
// CheckUpgrade 检查是否可以升级(使用赛季内累计)
|
||
// 升级条件:同时满足累计时长和累计点赞数门槛
|
||
func (s *assetLevelService) CheckUpgrade(record *models.AssetLevelRecord) (string, bool) {
|
||
levels, err := s.GetAllLevels()
|
||
if err != nil {
|
||
return record.CurrentLevel, false
|
||
}
|
||
|
||
currentOrder := models.LevelOrderMap[record.CurrentLevel]
|
||
|
||
// 从高到低检查(优先匹配最高可达到的等级)
|
||
// 确保升级目标等级比当前等级高
|
||
for i := len(levels) - 1; i >= 0; i-- {
|
||
level := levels[i]
|
||
|
||
// 必须是更高的等级
|
||
if level.LevelOrder <= currentOrder {
|
||
continue
|
||
}
|
||
|
||
// 检查是否满足该等级的升级条件(同时满足时长和点赞)
|
||
if record.SeasonExhibitionHours >= level.RequireHours &&
|
||
record.SeasonLikes >= level.RequireLikes {
|
||
return level.Level, true
|
||
}
|
||
}
|
||
|
||
return record.CurrentLevel, false
|
||
}
|
||
|
||
// CheckDowngrade 检查是否需要降级(使用赛季内累计)
|
||
func (s *assetLevelService) CheckDowngrade(record *models.AssetLevelRecord) (string, bool) {
|
||
levels, err := s.GetAllLevels()
|
||
if err != nil {
|
||
return record.CurrentLevel, false
|
||
}
|
||
|
||
currentOrder := models.LevelOrderMap[record.CurrentLevel]
|
||
|
||
// 找到当前等级的配置
|
||
currentLevelConfig := s.getLevelConfigByOrder(currentOrder)
|
||
|
||
// 检查当前等级条件是否还满足
|
||
if currentLevelConfig != nil &&
|
||
record.SeasonExhibitionHours >= currentLevelConfig.RequireHours &&
|
||
record.SeasonLikes >= currentLevelConfig.RequireLikes {
|
||
// 当前等级条件仍满足,不降级
|
||
return record.CurrentLevel, false
|
||
}
|
||
|
||
// 条件不满足,找到满足条件的最高等级
|
||
newLevel := models.LevelN
|
||
for _, level := range levels {
|
||
if record.SeasonExhibitionHours >= level.RequireHours &&
|
||
record.SeasonLikes >= level.RequireLikes {
|
||
newLevel = level.Level
|
||
}
|
||
}
|
||
|
||
if newLevel != record.CurrentLevel {
|
||
return newLevel, true
|
||
}
|
||
|
||
return record.CurrentLevel, false
|
||
}
|
||
|
||
// getLevelConfigByOrder 根据等级顺序获取等级配置
|
||
func (s *assetLevelService) getLevelConfigByOrder(order int) *models.AssetLevel {
|
||
levels, _ := s.GetAllLevels()
|
||
for _, level := range levels {
|
||
if level.LevelOrder == order {
|
||
return level
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// AddExhibitionHours 增加展出时长
|
||
// 注意:时长和点赞的衰减在赛季重置时处理,不在此处
|
||
func (s *assetLevelService) AddExhibitionHours(assetID int64, hours int) (string, bool, error) {
|
||
record, err := s.GetOrCreateRecord(assetID)
|
||
if err != nil {
|
||
return "", false, err
|
||
}
|
||
|
||
oldLevel := record.CurrentLevel
|
||
|
||
// 如果是首次展出(SeasonID为空),关联当前赛季
|
||
if record.SeasonID == "" {
|
||
season, err := s.GetCurrentSeason()
|
||
if err != nil {
|
||
season = s.getDefaultSeason()
|
||
}
|
||
record.SeasonID = season.ID
|
||
}
|
||
|
||
// 赛季内累计 += 本次时长
|
||
record.SeasonExhibitionHours += hours
|
||
// 历史累计永久保留
|
||
record.LifetimeExhibitionHours += hours
|
||
|
||
// 检查升级(使用赛季内累计)
|
||
newLevel, upgraded := s.CheckUpgrade(record)
|
||
if upgraded {
|
||
record.CurrentLevel = newLevel
|
||
}
|
||
|
||
if err := s.repo.Save(record); err != nil {
|
||
return "", false, err
|
||
}
|
||
|
||
// 记录日志
|
||
if upgraded && newLevel != oldLevel {
|
||
s.logLevelChange(record.AssetID, oldLevel, newLevel,
|
||
"exhibition_complete", record.SeasonExhibitionHours, record.SeasonLikes,
|
||
fmt.Sprintf("展出完成,时长+%d小时(赛季内:%d 历史:%d)", hours, record.SeasonExhibitionHours, record.LifetimeExhibitionHours))
|
||
}
|
||
|
||
return newLevel, upgraded, nil
|
||
}
|
||
```
|
||
|
||
#### 4.2.2 点赞更新逻辑
|
||
|
||
```go
|
||
// AddLikes 增加点赞数
|
||
func (s *assetLevelService) AddLikes(assetID int64, count int) (string, bool, error) {
|
||
record, err := s.GetOrCreateRecord(assetID)
|
||
if err != nil {
|
||
return "", false, err
|
||
}
|
||
|
||
oldLevel := record.CurrentLevel
|
||
|
||
// 如果是首次获赞(SeasonID为空),关联当前赛季
|
||
if record.SeasonID == "" {
|
||
season, err := s.GetCurrentSeason()
|
||
if err != nil {
|
||
season = s.getDefaultSeason()
|
||
}
|
||
record.SeasonID = season.ID
|
||
}
|
||
|
||
// 赛季内累计 += 本次点赞
|
||
record.SeasonLikes += count
|
||
// 历史累计(用于降序计算)
|
||
record.LifetimeLikes += count
|
||
|
||
// 检查升级(使用赛季内累计)
|
||
newLevel, upgraded := s.CheckUpgrade(record)
|
||
if upgraded {
|
||
record.CurrentLevel = newLevel
|
||
}
|
||
|
||
if err := s.repo.Save(record); err != nil {
|
||
return "", false, err
|
||
}
|
||
|
||
// 记录日志
|
||
if upgraded && newLevel != oldLevel {
|
||
s.logLevelChange(record.AssetID, oldLevel, newLevel,
|
||
"like_update", record.SeasonExhibitionHours, record.SeasonLikes,
|
||
fmt.Sprintf("点赞数达到%d触发升级", record.SeasonLikes))
|
||
}
|
||
|
||
return newLevel, upgraded, nil
|
||
}
|
||
|
||
// RemoveLikes 减少点赞数(用于取消点赞)
|
||
func (s *assetLevelService) RemoveLikes(assetID int64, count int) (string, bool, error) {
|
||
record, err := s.GetOrCreateRecord(assetID)
|
||
if err != nil {
|
||
return "", false, err
|
||
}
|
||
|
||
oldLevel := record.CurrentLevel
|
||
|
||
// 赛季内累计 -= 本次点赞
|
||
record.SeasonLikes -= count
|
||
if record.SeasonLikes < 0 {
|
||
record.SeasonLikes = 0
|
||
}
|
||
// 历史累计也减少(用于降序计算)
|
||
record.LifetimeLikes -= count
|
||
if record.LifetimeLikes < 0 {
|
||
record.LifetimeLikes = 0
|
||
}
|
||
|
||
// 检查是否需要降级(使用赛季内累计)
|
||
newLevel, downgraded := s.CheckDowngrade(record)
|
||
if downgraded {
|
||
record.CurrentLevel = newLevel
|
||
}
|
||
|
||
if err := s.repo.Save(record); err != nil {
|
||
return "", false, err
|
||
}
|
||
|
||
// 记录日志
|
||
if downgraded && newLevel != oldLevel {
|
||
s.logLevelChange(record.AssetID, oldLevel, newLevel,
|
||
"like_remove", record.SeasonExhibitionHours, record.SeasonLikes,
|
||
fmt.Sprintf("取消点赞,点赞数降至%d触发降级", record.SeasonLikes))
|
||
}
|
||
|
||
return newLevel, downgraded, nil
|
||
}
|
||
```
|
||
|
||
#### 4.2.3 收益计算逻辑
|
||
|
||
```go
|
||
// CalculateRevenue 基于藏品等级计算收益
|
||
func (s *assetLevelService) CalculateRevenue(assetID int64, likeCount int, startTime, endTime int64, revenueBoostBps int) (int64, error) {
|
||
record, err := s.GetRecordByAssetID(assetID)
|
||
if err != nil || record == nil {
|
||
// 无等级记录,使用默认配置
|
||
return s.calculateDefaultRevenue(likeCount, startTime, endTime, revenueBoostBps)
|
||
}
|
||
|
||
levelConfig, err := s.GetLevelConfig(record.CurrentLevel)
|
||
if err != nil {
|
||
return s.calculateDefaultRevenue(likeCount, startTime, endTime, revenueBoostBps)
|
||
}
|
||
|
||
// 计算展出时长(小时)
|
||
T := (endTime - startTime) / 3600000
|
||
if T <= 0 {
|
||
T = 1
|
||
}
|
||
|
||
// 基础收益 = 等级小时收益 × 时长
|
||
R0 := int64(levelConfig.HourlyRevenue)
|
||
baseRevenue := R0 * T
|
||
|
||
// 计算Buff加成
|
||
buff := CalculateBuff(likeCount)
|
||
buffedRevenue := baseRevenue * (100 + int64(buff)) / 100
|
||
|
||
// 应用永久收益提升
|
||
if revenueBoostBps > 0 {
|
||
boost := buffedRevenue * int64(revenueBoostBps) / 10000
|
||
buffedRevenue += boost
|
||
}
|
||
|
||
return buffedRevenue, nil
|
||
}
|
||
|
||
func (s *assetLevelService) calculateDefaultRevenue(likeCount int, startTime, endTime int64, revenueBoostBps int) (int64, error) {
|
||
R0 := int64(5) // 默认N级收益
|
||
T := (endTime - startTime) / 3600000
|
||
if T <= 0 {
|
||
T = 1
|
||
}
|
||
|
||
baseRevenue := R0 * T
|
||
buff := CalculateBuff(likeCount)
|
||
buffedRevenue := baseRevenue * (100 + int64(buff)) / 100
|
||
|
||
if revenueBoostBps > 0 {
|
||
boost := buffedRevenue * int64(revenueBoostBps) / 10000
|
||
buffedRevenue += boost
|
||
}
|
||
|
||
return buffedRevenue, nil
|
||
}
|
||
```
|
||
|
||
### 4.3 赛季重置逻辑(百分比降序:时长 + 点赞均按百分比衰减)
|
||
|
||
```go
|
||
// SeasonReset 赛季结束时执行百分比降序
|
||
// 规则:累计展出时长和点赞均按百分比衰减,然后继承到新赛季
|
||
// 注意:此方法在新赛季开始前调用,将旧赛季数据迁移到新赛季
|
||
func (s *assetLevelService) SeasonReset(seasonID string) error {
|
||
season, err := s.seasonRepo.GetByID(seasonID)
|
||
if err != nil || season == nil {
|
||
return fmt.Errorf("season not found: %s", seasonID)
|
||
}
|
||
|
||
if !season.ResetLevel {
|
||
return nil // 该赛季不重置等级
|
||
}
|
||
|
||
// 获取该赛季的降序百分比配置
|
||
decayConfig, err := s.seasonDecayConfigRepo.GetBySeason(seasonID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 构建等级→百分比映射
|
||
decayPercentMap := make(map[string]int)
|
||
for _, cfg := range decayConfig {
|
||
decayPercentMap[cfg.Level] = cfg.PreservePercent
|
||
}
|
||
|
||
// 获取当前赛季的所有藏品等级记录
|
||
records, err := s.repo.GetBySeason(seasonID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 计算新赛季ID(基于旧赛季ID递增)
|
||
nextSeasonID := s.calculateNextSeasonID(seasonID)
|
||
|
||
// 获取或创建新赛季配置(使用相同的降序配置模板)
|
||
nextSeason, err := s.seasonRepo.GetOrCreate(nextSeasonID, season.DurationDays, season.ResetStrategy)
|
||
|
||
for _, record := range records {
|
||
oldLevel := record.CurrentLevel
|
||
oldHours := record.SeasonExhibitionHours // 保存旧值用于日志
|
||
oldLikes := record.SeasonLikes // 保存旧值用于日志
|
||
|
||
// 获取该等级的降序保留百分比
|
||
preservePercent := decayPercentMap[record.CurrentLevel]
|
||
if preservePercent <= 0 {
|
||
preservePercent = 0
|
||
} else if preservePercent >= 100 {
|
||
preservePercent = 100
|
||
}
|
||
|
||
// 规则1:累计展出时长按百分比衰减(与点赞一致)
|
||
// season_exhibition_hours = lifetime_exhibition_hours * preserve_percent / 100
|
||
record.SeasonExhibitionHours = record.LifetimeExhibitionHours * preservePercent / 100
|
||
|
||
// 规则2:点赞按百分比衰减
|
||
// season_likes = lifetime_likes * preserve_percent / 100
|
||
record.SeasonLikes = record.LifetimeLikes * preservePercent / 100
|
||
|
||
// 重新计算等级(可能在降序后触发降级)
|
||
newLevel := s.recalculateLevelAfterDecay(record)
|
||
record.CurrentLevel = newLevel
|
||
|
||
// 更新为新赛季ID
|
||
record.SeasonID = nextSeasonID
|
||
|
||
if err := s.repo.Save(record); err != nil {
|
||
logger.Logger.Error("SeasonReset: failed to update record",
|
||
zap.Int64("asset_id", record.AssetID),
|
||
zap.Error(err))
|
||
continue
|
||
}
|
||
|
||
// 记录日志
|
||
if oldLevel != newLevel {
|
||
s.logLevelChange(record.AssetID, oldLevel, newLevel,
|
||
"season_decay", record.SeasonExhibitionHours, record.SeasonLikes,
|
||
fmt.Sprintf("赛季%s降序,保留%d%%(时长:%d→%d,点赞:%d→%d),原等级%s→%s",
|
||
seasonID, preservePercent,
|
||
oldHours, record.SeasonExhibitionHours,
|
||
oldLikes, record.SeasonLikes,
|
||
oldLevel, newLevel))
|
||
} else {
|
||
logger.Logger.Info("SeasonReset: asset preserved",
|
||
zap.Int64("asset_id", record.AssetID),
|
||
zap.String("level", oldLevel),
|
||
zap.Int("season_exhibition_hours", record.SeasonExhibitionHours),
|
||
zap.Int("season_likes", record.SeasonLikes),
|
||
zap.Int("lifetime_exhibition_hours", record.LifetimeExhibitionHours),
|
||
zap.Int("lifetime_likes", record.LifetimeLikes))
|
||
}
|
||
}
|
||
|
||
// 更新旧赛季状态为已结束
|
||
season.Status = "ended"
|
||
if err := s.seasonRepo.Save(season); err != nil {
|
||
logger.Logger.Error("SeasonReset: failed to update old season status",
|
||
zap.String("season_id", seasonID),
|
||
zap.Error(err))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// calculateNextSeasonID 计算下一个赛季ID
|
||
func (s *assetLevelService) calculateNextSeasonID(currentSeasonID string) string {
|
||
// 假设 season_1 -> season_2
|
||
if len(currentSeasonID) > 0 && currentSeasonID[:7] == "season_" {
|
||
num := 0
|
||
fmt.Sscanf(currentSeasonID[7:], "%d", &num)
|
||
return fmt.Sprintf("season_%d", num+1)
|
||
}
|
||
return currentSeasonID + "_next"
|
||
}
|
||
|
||
// recalculateLevelAfterDecay 降序后重新计算等级
|
||
// 等级由 season_exhibition_hours 和 season_likes 共同决定
|
||
func (s *assetLevelService) recalculateLevelAfterDecay(record *models.AssetLevelRecord) string {
|
||
levels, err := s.GetAllLevels()
|
||
if err != nil {
|
||
return record.CurrentLevel // 保持当前等级
|
||
}
|
||
|
||
// 从低到高检查,找到符合条件的最高等级
|
||
newLevel := models.LevelN
|
||
for _, level := range levels {
|
||
if record.SeasonExhibitionHours >= level.RequireHours &&
|
||
record.SeasonLikes >= level.RequireLikes {
|
||
newLevel = level.Level
|
||
}
|
||
}
|
||
|
||
return newLevel
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 五、集成点设计
|
||
|
||
### 5.1 与 AssetService 集成
|
||
|
||
```go
|
||
// services/assetService/service/asset_service.go
|
||
|
||
// CreateAsset 创建资产时初始化等级记录
|
||
func (s *assetService) CreateAsset(ctx context.Context, req *CreateAssetRequest) (*CreateAssetResponse, error) {
|
||
asset, err := s.assetRepo.Create(assetModel)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 初始化藏品等级记录(赛季ID在首次展出时更新)
|
||
levelRecord := &models.AssetLevelRecord{
|
||
AssetID: asset.ID,
|
||
CurrentLevel: models.LevelN,
|
||
// SeasonID 初始为空,在首次展出时由 AssetLevelService 更新
|
||
}
|
||
if err := s.assetLevelRepo.Create(levelRecord); err != nil {
|
||
logger.Logger.Error("Failed to create asset level record",
|
||
zap.Int64("asset_id", asset.ID),
|
||
zap.Error(err))
|
||
// 不阻断资产创建流程,等级记录失败不影响主流程
|
||
}
|
||
|
||
return &CreateAssetResponse{AssetID: asset.ID}, nil
|
||
}
|
||
```
|
||
|
||
### 5.2 与 AssetLikeService 集成
|
||
|
||
```go
|
||
// services/assetService/service/asset_like_service.go
|
||
|
||
// LikeAsset 点赞成功后更新藏品等级
|
||
func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starID int64) (int32, error) {
|
||
// ... 现有点赞逻辑 ...
|
||
|
||
// 点赞成功后,更新藏品等级记录
|
||
if s.assetLevelService != nil {
|
||
newLevel, upgraded, err := s.assetLevelService.AddLikes(assetID, 1)
|
||
if err != nil {
|
||
logger.Logger.Warn("Failed to update asset level on like",
|
||
zap.Int64("asset_id", assetID),
|
||
zap.Error(err))
|
||
} else if upgraded {
|
||
logger.Logger.Info("Asset upgraded due to likes",
|
||
zap.Int64("asset_id", assetID),
|
||
zap.String("new_level", newLevel))
|
||
// TODO: 可发送通知给藏品所有者
|
||
}
|
||
}
|
||
|
||
return asset.LikeCount, nil
|
||
}
|
||
|
||
// UnlikeAsset 取消点赞时更新藏品等级
|
||
func (s *AssetLikeService) UnlikeAsset(ctx context.Context, assetID, userID, starID int64) (int32, error) {
|
||
// ... 现有取消点赞逻辑 ...
|
||
|
||
// 取消点赞成功后,减少藏品等级记录点赞数
|
||
if s.assetLevelService != nil {
|
||
_, _, err := s.assetLevelService.RemoveLikes(assetID, 1)
|
||
if err != nil {
|
||
logger.Logger.Warn("Failed to update asset level on unlike",
|
||
zap.Int64("asset_id", assetID),
|
||
zap.Error(err))
|
||
}
|
||
}
|
||
|
||
return asset.LikeCount, nil
|
||
}
|
||
```
|
||
|
||
### 5.3 与 RevenueService 集成
|
||
|
||
```go
|
||
// services/taskService/service/revenue_service.go
|
||
|
||
// OnExhibitionCompleted 展出完成时更新藏品等级
|
||
func (s *revenueService) OnExhibitionCompleted(ctx context.Context, req *pb.OnExhibitionCompletedRequest) (*pb.OnExhibitionCompletedResponse, error) {
|
||
// ... 现有逻辑(创建收益记录等)...
|
||
|
||
// 更新藏品等级记录(展出时长)
|
||
if s.assetLevelService != nil {
|
||
exhibitionHours := (req.ExpireAt - req.StartTime) / 3600000
|
||
if exhibitionHours > 0 {
|
||
newLevel, upgraded, err := s.assetLevelService.AddExhibitionHours(req.AssetId, int(exhibitionHours))
|
||
if err != nil {
|
||
logger.Logger.Warn("Failed to update asset level on exhibition complete",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
zap.Error(err))
|
||
} else if upgraded {
|
||
logger.Logger.Info("Asset upgraded due to exhibition",
|
||
zap.Int64("asset_id", req.AssetId),
|
||
zap.String("new_level", newLevel))
|
||
}
|
||
}
|
||
}
|
||
|
||
return resp, nil
|
||
}
|
||
|
||
// CalculateExhibitionRevenue 修改为调用藏品等级服务
|
||
func CalculateAssetLevelRevenue(assetLevelService AssetLevelService, assetID int64, likeCount int, startTime, endTime int64, revenueBoostBps int) int64 {
|
||
revenue, _ := assetLevelService.CalculateRevenue(assetID, likeCount, startTime, endTime, revenueBoostBps)
|
||
return revenue
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 六、API 接口设计
|
||
|
||
### 6.1 HTTP API(通过 Gateway)
|
||
|
||
```protobuf
|
||
// proto/asset_level.proto
|
||
|
||
message GetAssetLevelRequest {
|
||
int64 asset_id = 1;
|
||
}
|
||
|
||
message GetAssetLevelResponse {
|
||
BaseResponse base = 1;
|
||
AssetLevelData data = 2;
|
||
}
|
||
|
||
message AssetLevelData {
|
||
int64 asset_id = 1;
|
||
string current_level = 2; // N, R, SR, SSR, UR
|
||
string season_id = 3; // 当前赛季ID
|
||
int season_exhibition_hours = 4; // 赛季内累计展出时长(用于等级计算)
|
||
int season_likes = 5; // 赛季内累计点赞数(用于等级计算)
|
||
int lifetime_exhibition_hours = 6; // 历史累计展出时长(永久保留)
|
||
int lifetime_likes = 7; // 历史累计点赞数(用于降序计算)
|
||
LevelConfig next_level = 8; // 下一等级信息(为nil表示已满级)
|
||
int hours_to_next_level = 9; // 距离下一等级还需要的时长(赛季内)
|
||
int likes_to_next_level = 10; // 距离下一等级还需要的点赞数(赛季内)
|
||
}
|
||
|
||
message LevelConfig {
|
||
string level = 1;
|
||
int hourly_revenue = 2;
|
||
int require_hours = 3;
|
||
int require_likes = 4;
|
||
}
|
||
|
||
message GetAssetLevelHistoryRequest {
|
||
int64 asset_id = 1;
|
||
int page = 2;
|
||
int page_size = 3;
|
||
}
|
||
|
||
message GetAssetLevelHistoryResponse {
|
||
BaseResponse base = 1;
|
||
repeated LevelChangeItem items = 2;
|
||
int64 total = 3;
|
||
}
|
||
|
||
message LevelChangeItem {
|
||
string from_level = 1;
|
||
string to_level = 2;
|
||
string trigger_type = 3;
|
||
int trigger_hours = 4;
|
||
int trigger_likes = 5;
|
||
string change_reason = 6;
|
||
int64 created_at = 7;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 七、赛季制设计
|
||
|
||
### 7.1 赛季流程
|
||
|
||
```
|
||
赛季开始
|
||
↓
|
||
藏品展出/获赞 → 累计成长值
|
||
↓
|
||
赛季结束触发器(定时任务)
|
||
↓
|
||
执行赛季重置逻辑
|
||
↓
|
||
新赛季开始
|
||
```
|
||
|
||
### 7.2 赛季结束策略:时长 + 点赞均按百分比衰减
|
||
|
||
| 字段 | 处理规则 |
|
||
|------|----------|
|
||
| 累计展出时长 | **按百分比衰减**,通过 `season_decay_config` 配置每个等级的保留比例 |
|
||
| 累计点赞数 | **按百分比衰减**,通过 `season_decay_config` 配置每个等级的保留比例 |
|
||
|
||
> 两个字段独立计算衰减,互不影响
|
||
|
||
#### 降序示例
|
||
|
||
假设某 UR 藏品历史累计:720 小时展出时长,100,000 点赞
|
||
|
||
| 等级 | 保留比例 | 降序后赛季时长 | 降序后赛季点赞 | 降序后等级 | 说明 |
|
||
|:----:|:-------:|:------------:|:------------:|:---------:|:-----|
|
||
| N | 100% | 720h | 100,000 | N | 时长720h<24h,保持N |
|
||
| R | 80% | 576h | 80,000 | R | 时长576h≥24h,点赞80,000≥20,仍为R |
|
||
| SR | 70% | 504h | 70,000 | SR | 时长504h≥120h,点赞70,000≥500,仍为SR |
|
||
| SSR | 60% | 432h | 60,000 | SR | 时长432h≥120h,但点赞60,000<10,000,降为SR |
|
||
| UR | 50% | 360h | 50,000 | SSR | 时长360h≥360h,但点赞50,000<100,000,降为SSR |
|
||
|
||
### 7.3 定时任务
|
||
|
||
```go
|
||
// services/taskService/worker/season_worker.go
|
||
|
||
package worker
|
||
|
||
// SeasonResetWorker 赛季重置定时任务
|
||
type SeasonResetWorker struct {
|
||
seasonRepo repository.SeasonRepository
|
||
levelService service.AssetLevelService
|
||
}
|
||
|
||
func (w *SeasonResetWorker) Run() {
|
||
// 每小时检查是否有赛季结束
|
||
seasons, err := w.seasonRepo.GetEndedSeasons()
|
||
if err != nil {
|
||
logger.Logger.Error("SeasonResetWorker: failed to get ended seasons", zap.Error(err))
|
||
return
|
||
}
|
||
|
||
for _, season := range seasons {
|
||
if err := w.levelService.SeasonReset(season.ID); err != nil {
|
||
logger.Logger.Error("SeasonResetWorker: failed to reset season",
|
||
zap.String("season_id", season.ID),
|
||
zap.Error(err))
|
||
continue
|
||
}
|
||
|
||
logger.Logger.Info("SeasonResetWorker: season reset completed",
|
||
zap.String("season_id", season.ID))
|
||
}
|
||
|
||
// 创建新赛季
|
||
w.createNextSeasonIfNeeded()
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 八、监控与运营
|
||
|
||
### 8.1 关键指标
|
||
|
||
| 指标 | 描述 | 告警阈值 |
|
||
|------|------|----------|
|
||
| asset_level_upgrade_total | 藏品升级总次数 | - |
|
||
| asset_level_distribution | 各等级藏品数量分布 | - |
|
||
| season_reset_success_rate | 赛季重置成功率 | < 99% |
|
||
| upgrade_trigger_exhibition | 展出触发升级数 | - |
|
||
| upgrade_trigger_like | 点赞触发升级数 | - |
|
||
|
||
### 8.2 日志规范
|
||
|
||
```go
|
||
// 等级升级日志
|
||
logger.Logger.Info("asset_level_upgraded",
|
||
zap.Int64("asset_id", assetID),
|
||
zap.String("from_level", fromLevel),
|
||
zap.String("to_level", toLevel),
|
||
zap.String("trigger_type", triggerType),
|
||
zap.Int("total_hours", totalHours),
|
||
zap.Int("total_likes", totalLikes))
|
||
|
||
// 赛季重置日志
|
||
logger.Logger.Info("season_reset_completed",
|
||
zap.String("season_id", seasonID),
|
||
zap.Int("total_assets", totalAssets),
|
||
zap.Int("preserved_count", preservedCount),
|
||
zap.Int("reset_count", resetCount))
|
||
```
|
||
|
||
---
|
||
|
||
## 九、与现有系统的区别
|
||
|
||
| 维度 | 现有用户等级系统 | 藏品等级系统(新增) |
|
||
|------|----------------|---------------------|
|
||
| 升级主体 | 用户 (FanProfile) | 藏品 (Asset) |
|
||
| 等级标识 | 1-20 数字 | N/R/SR/SSR/UR |
|
||
| 升级条件 | 仅累计时长 | 时长 + 点赞双轨 |
|
||
| 赛季制 | 无 | 支持 |
|
||
| 收益影响 | 用户水晶余额 | 藏品展出收益单价 |
|
||
| 等级回退 | 无 | 赛季结束回退 |
|
||
| 初始等级 | 1级 | N级 |
|
||
|
||
---
|
||
|
||
## 十、前端展示规范
|
||
|
||
### 10.1 等级标签样式
|
||
|
||
藏品等级采用**标签文字 + 不同颜色背景**的方式展示:
|
||
|
||
| 等级 | 标签文字 | 背景色 | 文字色 | 使用场景 |
|
||
|:----:|:--------:|:------:|:------:|:---------|
|
||
| N | N | #9CA3AF (灰色) | #FFFFFF | 普通藏品 |
|
||
| R | R | #3B82F6 (蓝色) | #FFFFFF | 稀有藏品 |
|
||
| SR | SR | #8B5CF6 (紫色) | #FFFFFF | 超稀有藏品 |
|
||
| SSR | SSR | #F59E0B (橙色) | #FFFFFF | 超超稀有藏品 |
|
||
| UR | UR | #EF4444 (红色) | #FFFFFF | 终极稀有藏品 |
|
||
|
||
### 10.2 展示位置
|
||
|
||
- 藏品卡片:右下角显示等级标签
|
||
- 藏品详情页:标题下方显示等级标签
|
||
- 藏品列表:支持按等级筛选
|
||
|
||
### 10.3 样式实现
|
||
|
||
```vue
|
||
<!-- 等级标签组件 -->
|
||
<template>
|
||
<view class="asset-level-badge" :class="`level-${level}`">
|
||
{{ level }}
|
||
</view>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.asset-level-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 4rpx 12rpx;
|
||
border-radius: 8rpx;
|
||
font-size: 24rpx;
|
||
font-weight: bold;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.level-N { background-color: #9CA3AF; }
|
||
.level-R { background-color: #3B82F6; }
|
||
.level-SR { background-color: #8B5CF6; }
|
||
.level-SSR { background-color: #F59E0B; }
|
||
.level-UR { background-color: #EF4444; }
|
||
</style>
|
||
```
|
||
|
||
### 10.4 等级名称映射
|
||
|
||
```javascript
|
||
const LEVEL_NAMES = {
|
||
'N': 'Normal',
|
||
'R': 'Rare',
|
||
'SR': 'Super Rare',
|
||
'SSR': 'Super² Rare',
|
||
'UR': 'Ultimate Rare'
|
||
}
|
||
|
||
const LEVEL_NAMES_CN = {
|
||
'N': '普通',
|
||
'R': '稀有',
|
||
'SR': '超稀有',
|
||
'SSR': '超超稀有',
|
||
'UR': '终极稀有'
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 附录 A:数据库变更汇总
|
||
|
||
```sql
|
||
-- 新增表
|
||
CREATE TABLE asset_levels (...);
|
||
CREATE TABLE asset_level_records (...);
|
||
CREATE TABLE asset_level_change_logs (...);
|
||
CREATE TABLE seasons (...);
|
||
|
||
-- 初始化数据
|
||
INSERT INTO asset_levels (...);
|
||
INSERT INTO seasons (...);
|
||
|
||
-- 无需修改现有表结构
|
||
```
|
||
|
||
---
|
||
|
||
## 附录 B:推荐阅读
|
||
|
||
- [资产服务设计方案](./资产服务设计文档.md)
|
||
- [展馆服务设计方案](./展馆服务设计方案.md)
|
||
- [资产点赞功能实现总结](./资产点赞功能完整实现总结.md) |