37 KiB
藏品升级系统 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 实现藏品等级系统,支持 N/R/SR/SSR/UR 五级体系,双轨升级条件(展出时长+点赞),赛季制降序机制
Architecture: 在 pkg/models 新建共享模型,在 assetService 下新建 repository、service、worker、provider;与现有 AssetService、AssetLikeService、RevenueService 集成
Tech Stack: Go (GORM), PostgreSQL, Worker/定时任务
一、文件结构概览
backend/pkg/models/
└── asset_level.go # 新增:AssetLevel, AssetLevelRecord, AssetLevelChangeLog, Season, SeasonDecayConfig 模型(共享)
backend/services/assetService/
├── repository/
│ ├── asset_level_repository.go # 新增
│ ├── season_repository.go # 新增
│ └── season_decay_config_repository.go # 新增
├── service/
│ └── asset_level_service.go # 新增
├── worker/
│ └── season_reset_worker.go # 新增
└── provider/
└── asset_level_provider.go # 新增:依赖注入
二、任务分解
Task 1: 创建数据库迁移文件
Files:
-
Create:
backend/docs/migrations/2026-05-25_create_asset_level_tables.sql -
Step 1: 创建迁移 SQL 文件
-- 藏品等级配置表
CREATE TABLE IF NOT EXISTS asset_levels (
id BIGSERIAL PRIMARY KEY,
level VARCHAR(10) NOT NULL UNIQUE,
level_order INT NOT NULL,
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
);
-- 藏品等级记录表
CREATE TABLE IF NOT EXISTS 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_id VARCHAR(50),
updated_at BIGINT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_asset_level_season ON asset_level_records(season_id);
CREATE INDEX IF NOT EXISTS idx_asset_level_current_level ON asset_level_records(current_level);
-- 藏品等级变化日志表
CREATE TABLE IF NOT EXISTS 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,
trigger_hours INT DEFAULT 0,
trigger_likes INT DEFAULT 0,
change_reason VARCHAR(255),
created_at BIGINT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_asset_level_log_asset ON asset_level_change_logs(asset_id);
CREATE INDEX IF NOT EXISTS idx_asset_level_log_created ON asset_level_change_logs(created_at DESC);
-- 赛季配置表
CREATE TABLE IF NOT EXISTS seasons (
id VARCHAR(50) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
duration_days INT NOT NULL DEFAULT 84,
start_time BIGINT NOT NULL,
end_time BIGINT NOT NULL,
reset_strategy VARCHAR(20) DEFAULT 'percentage_decay',
reset_level BOOLEAN DEFAULT TRUE,
status VARCHAR(20) DEFAULT 'active',
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL
);
-- 赛季降序百分比配置表
CREATE TABLE IF NOT EXISTS season_decay_config (
id BIGSERIAL PRIMARY KEY,
season_id VARCHAR(50) NOT NULL,
level VARCHAR(10) NOT NULL,
preserve_percent INT NOT NULL DEFAULT 100,
updated_at BIGINT NOT NULL,
CONSTRAINT uk_season_level UNIQUE (season_id, level)
);
-- 初始化藏品等级配置数据
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;
-- 初始化赛季配置数据
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;
-- 初始化赛季降序百分比配置
INSERT INTO season_decay_config (season_id, level, preserve_percent, updated_at) VALUES
('season_1', 'N', 100, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
('season_1', 'R', 80, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
('season_1', 'SR', 70, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
('season_1', 'SSR', 60, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
('season_1', 'UR', 50, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000))
ON CONFLICT (season_id, level) DO NOTHING;
- Step 2: 运行迁移
Run: psql -h $DB_HOST -U $DB_USER -d $DB_NAME -f backend/docs/migrations/2026-05-25_create_asset_level_tables.sql
Expected: 输出应包含 CREATE TABLE 和 INSERT 执行成功信息
- Step 3: 提交
git add backend/docs/migrations/2026-05-25_create_asset_level_tables.sql
git commit -m "feat: add asset level system database migration"
Task 2: 创建模型定义
Files:
-
Create:
backend/pkg/models/asset_level.go -
Step 1: 创建模型文件
package models
import (
"time"
"gorm.io/gorm"
)
// 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'"`
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" }
// CalculateEndTime 计算赛季结束时间
func (s *Season) CalculateEndTime() int64 {
return s.StartTime + int64(s.DurationDays)*86400000
}
// BeforeCreate 创建前钩子
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 更新前钩子
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"`
UpdatedAt int64 `gorm:"not null"`
}
func (SeasonDecayConfig) TableName() string { return "season_decay_config" }
// 等级常量
const (
LevelN = "N"
LevelR = "R"
LevelSR = "SR"
LevelSSR = "SSR"
LevelUR = "UR"
)
// LevelOrderMap 等级顺序映射
var LevelOrderMap = map[string]int{
LevelN: 1,
LevelR: 2,
LevelSR: 3,
LevelSSR: 4,
LevelUR: 5,
}
- Step 2: 提交
git add backend/pkg/models/asset_level.go
git commit -m "feat: add asset level models"
Task 3: 创建 Repository 层
Files:
-
Create:
backend/services/assetService/repository/asset_level_repository.go -
Create:
backend/services/assetService/repository/season_repository.go -
Create:
backend/services/assetService/repository/season_decay_config_repository.go -
Step 1: 创建 asset_level_repository.go
package repository
import (
"github.com/topfans/backend/pkg/models"
"gorm.io/gorm"
)
type AssetLevelRepository struct {
db *gorm.DB
}
func NewAssetLevelRepository(db *gorm.DB) *AssetLevelRepository {
return &AssetLevelRepository{db: db}
}
func (r *AssetLevelRepository) Create(record *models.AssetLevelRecord) error {
return r.db.Create(record).Error
}
func (r *AssetLevelRepository) Save(record *models.AssetLevelRecord) error {
return r.db.Save(record).Error
}
func (r *AssetLevelRepository) GetByAssetID(assetID int64) (*models.AssetLevelRecord, error) {
var record models.AssetLevelRecord
err := r.db.Where("asset_id = ?", assetID).First(&record).Error
if err != nil {
return nil, err
}
return &record, nil
}
func (r *AssetLevelRepository) GetBySeason(seasonID string) ([]*models.AssetLevelRecord, error) {
var records []*models.AssetLevelRecord
err := r.db.Where("season_id = ?", seasonID).Find(&records).Error
return records, err
}
func (r *AssetLevelRepository) GetAllLevels() ([]*models.AssetLevel, error) {
var levels []*models.AssetLevel
err := r.db.Order("level_order ASC").Find(&levels).Error
return levels, err
}
func (r *AssetLevelRepository) GetLevelConfig(level string) (*models.AssetLevel, error) {
var config models.AssetLevel
err := r.db.Where("level = ?", level).First(&config).Error
if err != nil {
return nil, err
}
return &config, nil
}
func (r *AssetLevelRepository) CreateChangeLog(log *models.AssetLevelChangeLog) error {
return r.db.Create(log).Error
}
func (r *AssetLevelRepository) GetChangeLogs(assetID int64, limit, offset int) ([]*models.AssetLevelChangeLog, error) {
var logs []*models.AssetLevelChangeLog
err := r.db.Where("asset_id = ?", assetID).
Order("created_at DESC").
Limit(limit).
Offset(offset).
Find(&logs).Error
return logs, err
}
- Step 2: 创建 season_repository.go
package repository
import (
"github.com/topfans/backend/pkg/models"
"gorm.io/gorm"
)
type SeasonRepository struct {
db *gorm.DB
}
func NewSeasonRepository(db *gorm.DB) *SeasonRepository {
return &SeasonRepository{db: db}
}
func (r *SeasonRepository) GetByID(seasonID string) (*models.Season, error) {
var season models.Season
err := r.db.Where("id = ?", seasonID).First(&season).Error
if err != nil {
return nil, err
}
return &season, nil
}
func (r *SeasonRepository) GetActiveSeason() (*models.Season, error) {
var season models.Season
err := r.db.Where("status = ?", "active").First(&season).Error
if err != nil {
return nil, err
}
return &season, nil
}
func (r *SeasonRepository) GetEndedSeasons() ([]*models.Season, error) {
var seasons []*models.Season
err := r.db.Where("status = ? AND end_time < ?", "active", gorm.Expr("NOW()")).Find(&seasons).Error
return seasons, err
}
func (r *SeasonRepository) Save(season *models.Season) error {
return r.db.Save(season).Error
}
func (r *SeasonRepository) Create(season *models.Season) error {
return r.db.Create(season).Error
}
func (r *SeasonRepository) GetOrCreate(seasonID string, durationDays int, resetStrategy string) (*models.Season, error) {
season, err := r.GetByID(seasonID)
if err == nil {
return season, nil
}
if err == gorm.ErrRecordNotFound {
season = &models.Season{
ID: seasonID,
DurationDays: durationDays,
ResetStrategy: resetStrategy,
ResetLevel: true,
Status: "active",
}
if err := r.Create(season); err != nil {
return nil, err
}
return season, nil
}
return nil, err
}
- Step 3: 创建 season_decay_config_repository.go
package repository
import (
"github.com/topfans/backend/pkg/models"
"gorm.io/gorm"
)
type SeasonDecayConfigRepository struct {
db *gorm.DB
}
func NewSeasonDecayConfigRepository(db *gorm.DB) *SeasonDecayConfigRepository {
return &SeasonDecayConfigRepository{db: db}
}
func (r *SeasonDecayConfigRepository) GetBySeason(seasonID string) ([]*models.SeasonDecayConfig, error) {
var configs []*models.SeasonDecayConfig
err := r.db.Where("season_id = ?", seasonID).Find(&configs).Error
return configs, err
}
func (r *SeasonDecayConfigRepository) GetBySeasonAndLevel(seasonID, level string) (*models.SeasonDecayConfig, error) {
var config models.SeasonDecayConfig
err := r.db.Where("season_id = ? AND level = ?", seasonID, level).First(&config).Error
if err != nil {
return nil, err
}
return &config, nil
}
- Step 4: 提交
git add backend/services/assetService/repository/asset_level_repository.go
git add backend/services/assetService/repository/season_repository.go
git add backend/services/assetService/repository/season_decay_config_repository.go
git commit -m "feat: add asset level repositories"
Task 4: 创建 Service 层
Files:
-
Create:
backend/services/assetService/service/asset_level_service.go -
Step 1: 创建 asset_level_service.go
package service
import (
"fmt"
"time"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/models"
"github.com/topfans/backend/services/assetService/repository"
"go.uber.org/zap"
"gorm.io/gorm"
)
type AssetLevelService interface {
GetOrCreateRecord(assetID int64) (*models.AssetLevelRecord, error)
GetRecordByAssetID(assetID int64) (*models.AssetLevelRecord, error)
GetLevelConfig(level string) (*models.AssetLevel, error)
GetAllLevels() ([]*models.AssetLevel, error)
AddExhibitionHours(assetID int64, hours int) (string, bool, error)
AddLikes(assetID int64, count int) (string, bool, error)
RemoveLikes(assetID int64, count int) (string, bool, error)
CalculateRevenue(assetID int64, likeCount int, startTime, endTime int64, revenueBoostBps int) (int64, error)
SeasonReset(seasonID string) error
GetCurrentSeason() (*models.Season, error)
GetChangeLogs(assetID int64, page, pageSize int) ([]*models.AssetLevelChangeLog, error)
}
type assetLevelService struct {
levelRepo *repository.AssetLevelRepository
seasonRepo *repository.SeasonRepository
decayConfigRepo *repository.SeasonDecayConfigRepository
}
func NewAssetLevelService(
levelRepo *repository.AssetLevelRepository,
seasonRepo *repository.SeasonRepository,
decayConfigRepo *repository.SeasonDecayConfigRepository,
) AssetLevelService {
return &assetLevelService{
levelRepo: levelRepo,
seasonRepo: seasonRepo,
decayConfigRepo: decayConfigRepo,
}
}
func (s *assetLevelService) GetOrCreateRecord(assetID int64) (*models.AssetLevelRecord, error) {
record, err := s.levelRepo.GetByAssetID(assetID)
if err == nil {
return record, nil
}
if err == gorm.ErrRecordNotFound {
record = &models.AssetLevelRecord{
AssetID: assetID,
CurrentLevel: models.LevelN,
}
if err := s.levelRepo.Create(record); err != nil {
return nil, err
}
return record, nil
}
return nil, err
}
func (s *assetLevelService) GetRecordByAssetID(assetID int64) (*models.AssetLevelRecord, error) {
return s.levelRepo.GetByAssetID(assetID)
}
func (s *assetLevelService) GetLevelConfig(level string) (*models.AssetLevel, error) {
return s.levelRepo.GetLevelConfig(level)
}
func (s *assetLevelService) GetAllLevels() ([]*models.AssetLevel, error) {
return s.levelRepo.GetAllLevels()
}
func (s *assetLevelService) GetCurrentSeason() (*models.Season, error) {
return s.seasonRepo.GetActiveSeason()
}
func (s *assetLevelService) getDefaultSeason() *models.Season {
return &models.Season{
ID: "season_1",
Name: "第一赛季",
DurationDays: 84,
ResetStrategy: "percentage_decay",
ResetLevel: true,
Status: "active",
}
}
// 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]
for _, level := range levels {
if level.LevelOrder != currentOrder {
continue
}
if record.SeasonExhibitionHours >= level.RequireHours &&
record.SeasonLikes >= level.RequireLikes {
return record.CurrentLevel, false
}
}
// 找到满足条件的最高等级
newLevel := models.LevelN
for _, level := range levels {
if record.SeasonExhibitionHours >= level.RequireHours &&
record.SeasonLikes >= level.RequireLikes {
newLevel = level.Level
}
}
return newLevel, newLevel != record.CurrentLevel
}
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
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.levelRepo.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小时", hours))
}
return newLevel, upgraded, nil
}
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
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.levelRepo.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
}
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.levelRepo.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
}
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 := 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)
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
}
// CalculateBuff 根据点赞数计算Buff百分比
func CalculateBuff(likeCount int) int {
switch {
case likeCount >= 30:
return 30
case likeCount >= 10:
return 20
case likeCount >= 5:
return 10
default:
return 0
}
}
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
}
decayConfigs, err := s.decayConfigRepo.GetBySeason(seasonID)
if err != nil {
return err
}
decayPercentMap := make(map[string]int)
for _, cfg := range decayConfigs {
decayPercentMap[cfg.Level] = cfg.PreservePercent
}
records, err := s.levelRepo.GetBySeason(seasonID)
if err != nil {
return err
}
nextSeasonID := s.calculateNextSeasonID(seasonID)
_, err = s.seasonRepo.GetOrCreate(nextSeasonID, season.DurationDays, season.ResetStrategy)
if err != nil {
return err
}
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
}
record.SeasonExhibitionHours = record.LifetimeExhibitionHours * preservePercent / 100
record.SeasonLikes = record.LifetimeLikes * preservePercent / 100
newLevel := s.recalculateLevelAfterDecay(record)
record.CurrentLevel = newLevel
record.SeasonID = nextSeasonID
if err := s.levelRepo.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%%", seasonID, preservePercent))
}
}
season.Status = "ended"
return s.seasonRepo.Save(season)
}
func (s *assetLevelService) calculateNextSeasonID(currentSeasonID string) string {
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"
}
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
}
func (s *assetLevelService) logLevelChange(assetID int64, fromLevel, toLevel, triggerType string, triggerHours, triggerLikes int, reason string) {
log := &models.AssetLevelChangeLog{
AssetID: assetID,
FromLevel: fromLevel,
ToLevel: toLevel,
TriggerType: triggerType,
TriggerHours: triggerHours,
TriggerLikes: triggerLikes,
ChangeReason: reason,
CreatedAt: time.Now().UnixMilli(),
}
if err := s.levelRepo.CreateChangeLog(log); err != nil {
logger.Logger.Error("Failed to create level change log",
zap.Int64("asset_id", assetID),
zap.Error(err))
}
}
func (s *assetLevelService) GetChangeLogs(assetID int64, page, pageSize int) ([]*models.AssetLevelChangeLog, error) {
offset := (page - 1) * pageSize
return s.levelRepo.GetChangeLogs(assetID, pageSize, offset)
}
- Step 2: 提交
git add backend/services/assetService/service/asset_level_service.go
git commit -m "feat: add asset level service with upgrade/downgrade logic"
Task 5: 创建 Season Reset Worker
Files:
-
Create:
backend/services/assetService/worker/season_reset_worker.go -
Step 1: 创建 season_reset_worker.go
package worker
import (
"time"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/services/assetService/repository"
"github.com/topfans/backend/services/assetService/service"
"go.uber.org/zap"
)
type SeasonResetWorker struct {
seasonRepo *repository.SeasonRepository
levelService service.AssetLevelService
}
func NewSeasonResetWorker(
seasonRepo *repository.SeasonRepository,
levelService service.AssetLevelService,
) *SeasonResetWorker {
return &SeasonResetWorker{
seasonRepo: seasonRepo,
levelService: levelService,
}
}
func (w *SeasonResetWorker) Run() {
now := time.Now().UnixMilli()
// 获取已结束的赛季
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 season.EndTime > now {
continue // 还未真正结束
}
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))
}
}
- Step 2: 提交
git add backend/services/assetService/worker/season_reset_worker.go
git commit -m "feat: add season reset worker"
Task 6: 创建 Provider(依赖注入)
Files:
-
Create:
backend/services/assetService/provider/asset_level_provider.go -
Step 1: 创建 provider
package provider
import (
"github.com/topfans/backend/services/assetService/repository"
"github.com/topfans/backend/services/assetService/service"
"github.com/topfans/backend/services/assetService/worker"
"gorm.io/gorm"
)
type AssetLevelProvider struct {
LevelService service.AssetLevelService
db *gorm.DB
}
func NewAssetLevelProvider(db *gorm.DB) *AssetLevelProvider {
levelRepo := repository.NewAssetLevelRepository(db)
seasonRepo := repository.NewSeasonRepository(db)
decayConfigRepo := repository.NewSeasonDecayConfigRepository(db)
levelService := service.NewAssetLevelService(levelRepo, seasonRepo, decayConfigRepo)
return &AssetLevelProvider{
LevelService: levelService,
db: db,
}
}
func (p *AssetLevelProvider) GetLevelService() service.AssetLevelService {
return p.LevelService
}
func (p *AssetLevelProvider) GetSeasonResetWorker() *worker.SeasonResetWorker {
seasonRepo := repository.NewSeasonRepository(p.db)
return worker.NewSeasonResetWorker(seasonRepo, p.LevelService)
}
- Step 2: 提交
git add backend/services/assetService/provider/asset_level_provider.go
git commit -m "feat: add asset level provider for dependency injection"
Task 7: 集成到现有服务
前置条件: Task 6 (Provider) 必须先完成,以便获取 AssetLevelService 实例
Files:
- Modify:
backend/services/assetService/service/asset_service.go- 注入 AssetLevelService,创建资产时初始化等级记录 - Modify:
backend/services/assetService/service/asset_like_service.go- 注入 AssetLevelService,点赞/取消点赞时更新等级 - Modify:
backend/services/taskService/service/revenue_service.go- 注入 AssetLevelService,展出完成时更新等级
集成说明:
-
assetService 集成: 在 assetService 的 main.go 或 provider 中创建 AssetLevelProvider,然后通过构造函数或字段注入到 AssetService 和 AssetLikeService
-
taskService 集成: 类似地,在 taskService 中创建并注入 AssetLevelService
- Step 1: 修改 asset_service.go
在 AssetService 结构体中添加 AssetLevelService 字段:
type assetService struct {
// ... existing fields ...
assetLevelService service.AssetLevelService
}
在 CreateAsset 方法中,资产创建成功后初始化等级记录:
// 在 CreateAsset 成功返回后添加
if s.assetLevelService != nil {
levelRecord := &models.AssetLevelRecord{
AssetID: asset.ID,
CurrentLevel: models.LevelN,
}
if err := s.assetLevelService.GetOrCreateRecord(asset.ID); err != nil {
logger.Logger.Warn("Failed to create asset level record",
zap.Int64("asset_id", asset.ID),
zap.Error(err))
}
}
- Step 2: 修改 asset_like_service.go
在 LikeAsset 成功后调用 AddLikes:
// 点赞成功后,更新藏品等级记录
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))
}
}
在 UnlikeAsset 成功后调用 RemoveLikes:
// 取消点赞成功后,减少藏品等级记录点赞数
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))
}
}
- Step 3: 修改 revenue_service.go 的 OnExhibitionCompleted
在展出完成时调用 AddExhibitionHours:
// 更新藏品等级记录(展出时长)
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))
}
}
}
- Step 4: 提交
git add backend/services/assetService/service/asset_service.go
git add backend/services/assetService/service/asset_like_service.go
git add backend/services/taskService/service/revenue_service.go
git commit -m "feat: integrate asset level system with existing services"
Task 8: 添加单元测试
Files:
-
Create:
backend/services/assetService/service/asset_level_service_test.go -
Step 1: 创建测试文件
package service
import (
"testing"
)
func TestCalculateBuff(t *testing.T) {
tests := []struct {
likeCount int
expected int
}{
{0, 0},
{4, 0},
{5, 10},
{9, 10},
{10, 20},
{29, 20},
{30, 30},
{100, 30},
}
for _, tt := range tests {
result := CalculateBuff(tt.likeCount)
if result != tt.expected {
t.Errorf("CalculateBuff(%d) = %d, want %d", tt.likeCount, result, tt.expected)
}
}
}
func TestCheckUpgrade(t *testing.T) {
// TODO: 实现完整的升级测试
}
- Step 2: 运行测试
Run: go test ./backend/services/assetService/service/... -v
Expected: PASS
- Step 3: 提交
git add backend/services/assetService/service/asset_level_service_test.go
git commit -m "test: add asset level service unit tests"
三、计划完成摘要
| Task | Description | Status |
|---|---|---|
| 1 | 数据库迁移 | Pending |
| 2 | 模型定义 | Pending |
| 3 | Repository 层 | Pending |
| 4 | Service 层 | Pending |
| 5 | Season Reset Worker | Pending |
| 6 | Provider | Pending |
| 7 | 集成到现有服务 | Pending |
| 8 | 单元测试 | Pending |
四、修正说明
本次计划基于以下修正:
- 导入路径修正: 使用正确的模块路径
github.com/topfans/backend/... - 模型位置修正: 模型放在共享的
pkg/models/目录下,而非 service 本地 - Provider 类型修正:
AssetLevelService是接口类型,直接存储接口而非指针 - 缺失 import 修正: season_decay_config_repository.go 添加了
gorm.io/gorm导入 - 依赖注入说明: Task 7 明确说明需要先完成 Task 6 才能进行集成
Plan complete. Two execution options:
1. Subagent-Driven (recommended) - I dispatch a fresh subagent per task, review between tasks, fast iteration
2. Inline Execution - Execute tasks in this session using executing-plans, batch execution with checkpoints
Which approach?