1260 lines
37 KiB
Markdown
1260 lines
37 KiB
Markdown
# 藏品升级系统 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 文件**
|
||
|
||
```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: 提交**
|
||
|
||
```bash
|
||
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: 创建模型文件**
|
||
|
||
```go
|
||
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: 提交**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```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**
|
||
|
||
```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**
|
||
|
||
```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: 提交**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```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: 提交**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```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: 提交**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```go
|
||
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: 提交**
|
||
|
||
```bash
|
||
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,展出完成时更新等级
|
||
|
||
**集成说明:**
|
||
|
||
1. **assetService 集成**: 在 assetService 的 main.go 或 provider 中创建 AssetLevelProvider,然后通过构造函数或字段注入到 AssetService 和 AssetLikeService
|
||
|
||
2. **taskService 集成**: 类似地,在 taskService 中创建并注入 AssetLevelService
|
||
|
||
- [ ] **Step 1: 修改 asset_service.go**
|
||
|
||
在 AssetService 结构体中添加 AssetLevelService 字段:
|
||
|
||
```go
|
||
type assetService struct {
|
||
// ... existing fields ...
|
||
assetLevelService service.AssetLevelService
|
||
}
|
||
```
|
||
|
||
在 CreateAsset 方法中,资产创建成功后初始化等级记录:
|
||
|
||
```go
|
||
// 在 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:
|
||
|
||
```go
|
||
// 点赞成功后,更新藏品等级记录
|
||
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:
|
||
|
||
```go
|
||
// 取消点赞成功后,减少藏品等级记录点赞数
|
||
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:
|
||
|
||
```go
|
||
// 更新藏品等级记录(展出时长)
|
||
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: 提交**
|
||
|
||
```bash
|
||
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: 创建测试文件**
|
||
|
||
```go
|
||
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: 提交**
|
||
|
||
```bash
|
||
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 |
|
||
|
||
---
|
||
|
||
## 四、修正说明
|
||
|
||
本次计划基于以下修正:
|
||
1. **导入路径修正**: 使用正确的模块路径 `github.com/topfans/backend/...`
|
||
2. **模型位置修正**: 模型放在共享的 `pkg/models/` 目录下,而非 service 本地
|
||
3. **Provider 类型修正**: `AssetLevelService` 是接口类型,直接存储接口而非指针
|
||
4. **缺失 import 修正**: season_decay_config_repository.go 添加了 `gorm.io/gorm` 导入
|
||
5. **依赖注入说明**: 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?**
|