Compare commits

...

13 Commits

Author SHA1 Message Date
zerosaturation
e545f41c3b feat:修改类型为复合类型 2026-05-25 19:03:36 +08:00
zerosaturation
0852f96436 feat:去掉重复的点赞约束 2026-05-25 19:03:35 +08:00
zerosaturation
7c5d5a7275 feat:修改藏品等级硬编码改为根据数据库的配置来定义 2026-05-25 19:03:32 +08:00
zerosaturation
8dce6ae11a fix: add new models to autoMigrate and start season reset worker 2026-05-25 19:03:32 +08:00
zerosaturation
edfa5f1449 test: add asset level service unit tests
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:03:32 +08:00
zerosaturation
2a0db41dbf fix: wire AssetLevelService into existing service constructors 2026-05-25 19:03:32 +08:00
zerosaturation
bee30a1824 feat: integrate asset level system with existing services
- mintService: Add AssetLevelService field and call GetOrCreateRecord after asset creation
- assetLikeService: Add AssetLevelService and call AddLikes/RemoveLikes in LikeAsset/UnlikeAsset
- revenueService: Add AssetLevelService and call AddExhibitionHours in OnExhibitionCompleted

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:03:31 +08:00
zerosaturation
c6a8c66e39 feat: add asset level provider for dependency injection
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:03:31 +08:00
zerosaturation
d88e7ac504 feat: add season reset worker
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:03:31 +08:00
zerosaturation
11a59b0632 feat: add asset level service with upgrade/downgrade logic
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:03:31 +08:00
zerosaturation
0753f0b168 feat: add asset level repositories
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:03:31 +08:00
zerosaturation
6f21c294e9 feat: add asset level models
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:03:31 +08:00
zerosaturation
bd905e997c feat: add asset level system database migration
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:03:31 +08:00
36 changed files with 2559 additions and 98 deletions

View File

@ -408,6 +408,7 @@ start_watcher "gateway" "gateway:pkg/proto" "gateway/gateway
start_watcher "userService" "services/userService" "services/userService/userService" 20000 1
start_watcher "assetService" "services/assetService:pkg/proto/asset" "services/assetService/assetService" 20003 1
start_watcher "socialService" "services/socialService" "services/socialService/socialService" 20002 1
start_watcher "galleryService" "services/galleryService" "services/galleryService/galleryService" 20004 1
start_watcher "activityService" "services/activityService" "services/activityService/activityService" 20005 1
start_watcher "taskService" "services/taskService" "services/taskService/taskService" 20006 1
start_watcher "starbookService" "services/starbookService" "services/starbookService/starbookService" 20007 1

View File

@ -0,0 +1,91 @@
-- 藏品等级配置表
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;

View File

@ -121,7 +121,7 @@ type MintOrder struct {
MaterialURL *string `gorm:"type:varchar(500);column:material_url"` // 用户上传的素材URL阶段一写入
Name *string `gorm:"type:varchar(100);column:name"` // 藏品名称(阶段一写入,可选)
Description *string `gorm:"type:text;column:description"` // 藏品描述(阶段一写入,可选)
MaterialType *string `gorm:"type:varchar(50);column:material_type"` // 素材类型(可选
MaterialType *string `gorm:"type:varchar(100);column:material_type"` // 素材类型(可选,多个用逗号分隔
Event *string `gorm:"type:varchar(100);column:event"` // 藏品事件(可选)
Info *string `gorm:"type:text;column:info"` // 藏品信息(必填)
CreatedAt int64 `gorm:"not null;index:idx_mint_orders_created_at,sort:desc;column:created_at"`

View File

@ -0,0 +1,138 @@
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,
}
// LevelToGradeMap 等级字符串到前端Grade的映射1-5
var LevelToGradeMap = map[string]int{
LevelN: 1,
LevelR: 2,
LevelSR: 3,
LevelSSR: 4,
LevelUR: 5,
}
// LevelToGrade 将等级字符串转换为前端Grade
func LevelToGrade(level string) int {
if grade, ok := LevelToGradeMap[level]; ok {
return grade
}
return 1 // 默认返回1
}

View File

@ -2247,6 +2247,8 @@ func (x *UnlikeAssetResponse) GetLikeCount() int32 {
type CheckAssetLikeRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
AssetId int64 `protobuf:"varint,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` // 资产ID
UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 用户ID
StarId int64 `protobuf:"varint,3,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` // 明星ID
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -2288,6 +2290,20 @@ func (x *CheckAssetLikeRequest) GetAssetId() int64 {
return 0
}
func (x *CheckAssetLikeRequest) GetUserId() int64 {
if x != nil {
return x.UserId
}
return 0
}
func (x *CheckAssetLikeRequest) GetStarId() int64 {
if x != nil {
return x.StarId
}
return 0
}
// 检查是否已点赞响应
type CheckAssetLikeResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -3828,9 +3844,11 @@ const file_asset_proto_rawDesc = "" +
"\x13UnlikeAssetResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1d\n" +
"\n" +
"like_count\x18\x02 \x01(\x05R\tlikeCount\"2\n" +
"like_count\x18\x02 \x01(\x05R\tlikeCount\"d\n" +
"\x15CheckAssetLikeRequest\x12\x19\n" +
"\basset_id\x18\x01 \x01(\x03R\aassetId\"e\n" +
"\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x17\n" +
"\auser_id\x18\x02 \x01(\x03R\x06userId\x12\x17\n" +
"\astar_id\x18\x03 \x01(\x03R\x06starId\"e\n" +
"\x16CheckAssetLikeResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x19\n" +
"\bis_liked\x18\x02 \x01(\bR\aisLiked\"\x87\x01\n" +

View File

@ -1726,6 +1726,7 @@ type OnExhibitionCompletedRequest struct {
CrystalAmount int64 `protobuf:"varint,7,opt,name=crystal_amount,json=crystalAmount,proto3" json:"crystal_amount,omitempty"`
StartTime int64 `protobuf:"varint,8,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"`
ExpireAt int64 `protobuf:"varint,9,opt,name=expire_at,json=expireAt,proto3" json:"expire_at,omitempty"`
LikeCount int32 `protobuf:"varint,10,opt,name=like_count,json=likeCount,proto3" json:"like_count,omitempty"` // 点赞数用于在taskService中根据资产等级重新计算收益
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -1823,6 +1824,13 @@ func (x *OnExhibitionCompletedRequest) GetExpireAt() int64 {
return 0
}
func (x *OnExhibitionCompletedRequest) GetLikeCount() int32 {
if x != nil {
return x.LikeCount
}
return 0
}
type OnExhibitionCompletedResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
@ -2001,7 +2009,7 @@ const file_task_proto_rawDesc = "" +
"\astar_id\x18\x02 \x01(\x03R\x06starId\"c\n" +
"\x15InitUserTasksResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x18\n" +
"\asuccess\x18\x02 \x01(\bR\asuccess\"\xcd\x02\n" +
"\asuccess\x18\x02 \x01(\bR\asuccess\"\xec\x02\n" +
"\x1cOnExhibitionCompletedRequest\x12#\n" +
"\rexhibition_id\x18\x01 \x01(\x03R\fexhibitionId\x12\x19\n" +
"\basset_id\x18\x02 \x01(\x03R\aassetId\x12\x17\n" +
@ -2012,7 +2020,10 @@ const file_task_proto_rawDesc = "" +
"\x0ecrystal_amount\x18\a \x01(\x03R\rcrystalAmount\x12\x1d\n" +
"\n" +
"start_time\x18\b \x01(\x03R\tstartTime\x12\x1b\n" +
"\texpire_at\x18\t \x01(\x03R\bexpireAt\"}\n" +
"\texpire_at\x18\t \x01(\x03R\bexpireAt\x12\x1d\n" +
"\n" +
"like_count\x18\n" +
" \x01(\x05R\tlikeCount\"}\n" +
"\x1dOnExhibitionCompletedResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12*\n" +
"\x11revenue_record_id\x18\x02 \x01(\x03R\x0frevenueRecordId2\xc6\f\n" +

View File

@ -279,6 +279,8 @@ message UnlikeAssetResponse {
// RPCSocial Service调用
message CheckAssetLikeRequest {
int64 asset_id = 1; // ID
int64 user_id = 2; // ID
int64 star_id = 3; // ID
}
//

View File

@ -195,6 +195,7 @@ message OnExhibitionCompletedRequest {
int64 crystal_amount = 7;
int64 start_time = 8;
int64 expire_at = 9;
int32 like_count = 10; // taskService中根据资产等级重新计算收益
}
message OnExhibitionCompletedResponse {

View File

@ -0,0 +1,6 @@
-- Drop the overly strict unique constraint that prevents users from liking
-- the same asset across different exhibitions. The correct constraint is
-- (user_id, asset_id, exhibition_id), not (user_id, asset_id, star_id).
-- This allows a user to like the same asset once per exhibition.
ALTER TABLE asset_likes DROP CONSTRAINT IF EXISTS uk_asset_likes_user_asset_star;

View File

@ -7,6 +7,7 @@ import (
"os/signal"
"strconv"
"syscall"
"time"
"dubbo.apache.org/dubbo-go/v3/client"
_ "dubbo.apache.org/dubbo-go/v3/imports"
@ -132,11 +133,16 @@ func main() {
userClient := assetClient.NewUserServiceClient(userServiceClient)
logger.Logger.Info("User Service RPC client initialized")
// 创建 Provider 层实例(用于获取 AssetLevelService
assetLevelProvider := provider.NewAssetLevelProvider(database.GetDB())
assetLevelSvc := assetLevelProvider.GetLevelService()
logger.Logger.Info("AssetLevelProvider initialized")
// 创建 Service 层实例
registryRepo := starbookRepo.NewAssetRegistryRepository(database.GetDB())
assetService := service.NewAssetService(assetRepo, mintOrderRepo, assetLikeRepo, userClient, database.GetDB(), registryRepo)
mintService := service.NewMintService(assetRepo, mintOrderRepo, userClient, database.GetDB(), config.GlobalAssetConfig, registryRepo, mintCostRepo, userMintCountRepo)
assetLikeService := service.NewAssetLikeService(assetRepo, assetLikeRepo, database.GetDB())
mintService := service.NewMintService(assetRepo, mintOrderRepo, userClient, database.GetDB(), config.GlobalAssetConfig, registryRepo, mintCostRepo, userMintCountRepo, assetLevelSvc)
assetLikeService := service.NewAssetLikeService(assetRepo, assetLikeRepo, database.GetDB(), assetLevelSvc)
rankingService := service.NewRankingService(rankingRepo, userClient)
materialService := service.NewMaterialService(materialRepo, relationRepo)
logger.Logger.Info("Service layer initialized")
@ -146,6 +152,18 @@ func main() {
rankingProvider := provider.NewRankingProvider(rankingService)
logger.Logger.Info("Provider layer initialized")
// 启动赛季重置 Worker每小时检查一次
seasonResetWorker := assetLevelProvider.GetSeasonResetWorker()
go func() {
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for {
<-ticker.C
seasonResetWorker.Run()
}
}()
logger.Logger.Info("Season reset worker started")
// 创建 Dubbo 服务器
srv, err := server.NewServer(
server.WithServerProtocol(
@ -201,6 +219,10 @@ func autoMigrate() error {
&models.AssetLike{},
&models.Material{},
&models.AssetMaterialRelation{},
&models.AssetLevelRecord{},
&models.AssetLevelChangeLog{},
&models.Season{},
&models.SeasonDecayConfig{},
}
for _, table := range tables {

View File

@ -0,0 +1,35 @@
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)
}

View File

@ -392,13 +392,14 @@ func (p *AssetProvider) LikeAsset(ctx context.Context, req *pb.LikeAssetRequest)
zap.Int64("asset_id", req.AssetId),
zap.Error(err),
)
// 返回响应但不返回 error让客户端检查 Base.Code
return &pb.LikeAssetResponse{
Base: &pbCommon.BaseResponse{
Code: appErrors.ToStatusCode(err),
Message: err.Error(),
Timestamp: 0,
},
}, err
}, nil
}
logger.Logger.Info("LikeAsset successful",
@ -475,13 +476,18 @@ func (p *AssetProvider) UnlikeAsset(ctx context.Context, req *pb.UnlikeAssetRequ
func (p *AssetProvider) CheckAssetLike(ctx context.Context, req *pb.CheckAssetLikeRequest) (*pb.CheckAssetLikeResponse, error) {
logger.Logger.Info("Received CheckAssetLike request",
zap.Int64("asset_id", req.AssetId),
zap.Int64("user_id", req.UserId),
zap.Int64("star_id", req.StarId),
)
// 从 Dubbo attachments 获取用户信息
userID, starID, err := extractUserInfoFromDubboAttachments(ctx)
if err != nil {
logger.Logger.Error("Failed to extract user info from attachments",
zap.Error(err),
// 直接使用请求中的用户信息
userID := req.UserId
starID := req.StarId
if userID == 0 || starID == 0 {
logger.Logger.Error("Invalid user info in request",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
return &pb.CheckAssetLikeResponse{
Base: &pbCommon.BaseResponse{
@ -489,7 +495,7 @@ func (p *AssetProvider) CheckAssetLike(ctx context.Context, req *pb.CheckAssetLi
Message: "user authentication required",
Timestamp: 0,
},
}, err
}, fmt.Errorf("invalid user_id or star_id")
}
// 调用Service层

View File

@ -0,0 +1,70 @@
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) GetDB() *gorm.DB {
return r.db
}
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
}

View File

@ -48,6 +48,9 @@ type AssetRepository interface {
// DecrementLikeCount 减少点赞数
DecrementLikeCount(assetID int64) error
// UpdateGradeByAssetID 根据asset_id更新藏品等级用于同步AssetLevelRecord.CurrentLevel到AssetRegistry.Grade
UpdateGradeByAssetID(assetID int64, grade int32) error
// UpdateMaterialTypeByLikes 根据点赞数和创建时间更新素材类型
UpdateMaterialTypeByLikes(assetID int64, likes int32, createdAt int64) error
@ -154,6 +157,17 @@ func (r *assetRepository) GetGradeByAssetID(assetID int64) (int32, error) {
return 0, nil
}
// UpdateGradeByAssetID 根据asset_id更新藏品等级用于同步AssetLevelRecord.CurrentLevel到AssetRegistry.Grade
func (r *assetRepository) UpdateGradeByAssetID(assetID int64, grade int32) error {
if assetID <= 0 {
return errors.New("asset_id must be greater than 0")
}
return r.db.Table("public.asset_registry").
Where("asset_id = ?", assetID).
Update("grade", grade).Error
}
// GetByIDs 批量查询资产
func (r *assetRepository) GetByIDs(assetIDs []int64) ([]*models.Asset, error) {
if len(assetIDs) == 0 {

View File

@ -0,0 +1,29 @@
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
}

View File

@ -0,0 +1,67 @@
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
}

View File

@ -0,0 +1,478 @@
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
assetRepo repository.AssetRepository // 用于同步等级到AssetRegistry.Grade可选
}
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))
// 同步等级到AssetRegistry.Grade
s.syncGradeToAssetRegistry(record.AssetID, newLevel)
}
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))
// 同步等级到AssetRegistry.Grade
s.syncGradeToAssetRegistry(record.AssetID, newLevel)
}
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))
// 同步等级到AssetRegistry.Grade
s.syncGradeToAssetRegistry(record.AssetID, newLevel)
}
return newLevel, downgraded, nil
}
// syncGradeToAssetRegistry 将等级同步到AssetRegistry.Grade
// 直接使用gorm.DB更新不依赖repository层
func (s *assetLevelService) syncGradeToAssetRegistry(assetID int64, level string) {
if s.assetRepo == nil {
// 直接使用levelRepo的db进行更新
grade := models.LevelToGrade(level)
db := s.levelRepo.GetDB()
if db != nil {
if err := db.Table("public.asset_registry").
Where("asset_id = ?", assetID).
Update("grade", grade).Error; err != nil {
logger.Logger.Warn("syncGradeToAssetRegistry failed",
zap.Int64("asset_id", assetID),
zap.String("level", level),
zap.Int("grade", grade),
zap.Error(err))
} else {
logger.Logger.Info("syncGradeToAssetRegistry success",
zap.Int64("asset_id", assetID),
zap.String("level", level),
zap.Int("grade", grade))
}
}
return
}
grade := models.LevelToGrade(level)
if err := s.assetRepo.UpdateGradeByAssetID(assetID, int32(grade)); err != nil {
logger.Logger.Warn("syncGradeToAssetRegistry failed",
zap.Int64("asset_id", assetID),
zap.String("level", level),
zap.Int("grade", grade),
zap.Error(err))
} else {
logger.Logger.Info("syncGradeToAssetRegistry success",
zap.Int64("asset_id", assetID),
zap.String("level", level),
zap.Int("grade", grade))
}
}
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
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))
// 同步等级到AssetRegistry.Grade
s.syncGradeToAssetRegistry(record.AssetID, newLevel)
}
}
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)
}

View File

@ -0,0 +1,28 @@
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)
}
}
}

View File

@ -15,9 +15,10 @@ import (
// AssetLikeService 资产点赞业务逻辑层
type AssetLikeService struct {
assetRepo repository.AssetRepository
assetLikeRepo repository.AssetLikeRepository
db *gorm.DB
assetRepo repository.AssetRepository
assetLikeRepo repository.AssetLikeRepository
db *gorm.DB
assetLevelService AssetLevelService // 资产等级服务
}
// NewAssetLikeService 创建资产点赞Service实例
@ -25,11 +26,13 @@ func NewAssetLikeService(
assetRepo repository.AssetRepository,
assetLikeRepo repository.AssetLikeRepository,
db *gorm.DB,
assetLevelService AssetLevelService,
) *AssetLikeService {
return &AssetLikeService{
assetRepo: assetRepo,
assetLikeRepo: assetLikeRepo,
db: db,
assetRepo: assetRepo,
assetLikeRepo: assetLikeRepo,
db: db,
assetLevelService: assetLevelService,
}
}
@ -44,6 +47,10 @@ func (s *AssetLikeService) isAssetExhibiting(assetID int64) (int64, error) {
Where("asset_id = ? AND expire_at > ?", assetID, nowMs).
First(&exhibition).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
// 资产不在任何展出中,返回 0 表示未展出
return 0, nil
}
return 0, fmt.Errorf("failed to check exhibition status: %w", err)
}
return exhibition.ID, nil
@ -67,37 +74,18 @@ func shouldUpdateMaterialType(oldLikes, newLikes int32, createdAt int64, isIncre
// 点赞增加时的临界点
if isIncrement {
// new → hot (超过20)
if oldLikes <= 20 && newLikes > 20 {
// 超过20 → 添加 hot
if newLikes > 20 {
return true, "hot"
}
// new → potential (达到10且在1小时内)
if oldLikes < 10 && newLikes >= 10 && isWithin1Hour {
// 1小时内达到10 → 添加 potential
if newLikes >= 10 && isWithin1Hour {
return true, "potential"
}
}
// 点赞减少时的临界点(降级检测)
if !isIncrement {
// hot → new/potential (降到20以下)
if oldLikes > 20 && newLikes <= 20 {
// 重新计算
if newLikes >= 10 && isWithin1Hour {
return true, "potential"
}
return true, "new"
}
// potential → new (降到10以下)
if oldLikes >= 10 && newLikes < 10 {
return true, "new"
}
// hot 降到 21-20 区间
if oldLikes > 20 && newLikes > 20 {
return false, "" // 仍在 hot 区间
}
}
// 点赞减少时不降级(保留原类型)
return false, ""
}
@ -184,15 +172,13 @@ func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starI
return fmt.Errorf("failed to increment like count: %w", err)
}
// 3.3 更新素材类型(只在临界点更新
// 3.3 更新素材类型(追加类型,不重复
oldLikes := asset.LikeCount
newLikes := oldLikes + 1
shouldUpdate, newMaterialType := shouldUpdateMaterialType(oldLikes, newLikes, asset.CreatedAt, true)
if shouldUpdate {
if err := tx.Model(&models.Asset{}).
Where("id = ?", assetID).
UpdateColumn("material_type", newMaterialType).
Error; err != nil {
if shouldUpdate && newMaterialType != "" {
// 使用 PostgreSQL CONCAT 函数追加类型
if err := tx.Raw("UPDATE assets SET material_type = CONCAT(COALESCE(material_type, ''), ',', ?) WHERE id = ?", newMaterialType, assetID).Error; err != nil {
logger.Logger.Warn("Failed to update material type",
zap.Error(err),
zap.Int64("asset_id", assetID),
@ -223,6 +209,19 @@ func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starI
zap.Int32("like_count", asset.LikeCount),
)
// 5. 更新资产等级记录(点赞数变化)
if s.assetLevelService != nil {
if newLevel, upgraded, err := s.assetLevelService.AddLikes(assetID, 1); err != nil {
logger.Logger.Warn("Failed to add likes to asset level",
zap.Int64("asset_id", assetID),
zap.Error(err))
} else if upgraded {
logger.Logger.Info("Asset leveled up due to likes",
zap.Int64("asset_id", assetID),
zap.String("new_level", newLevel))
}
}
return asset.LikeCount, nil
}
@ -338,6 +337,19 @@ func (s *AssetLikeService) UnlikeAsset(ctx context.Context, assetID, userID, sta
zap.Int32("like_count", asset.LikeCount),
)
// 4. 更新资产等级记录(取消点赞)
if s.assetLevelService != nil {
if newLevel, downgraded, err := s.assetLevelService.RemoveLikes(assetID, 1); err != nil {
logger.Logger.Warn("Failed to remove likes from asset level",
zap.Int64("asset_id", assetID),
zap.Error(err))
} else if downgraded {
logger.Logger.Info("Asset downgraded due to like removal",
zap.Int64("asset_id", assetID),
zap.String("new_level", newLevel))
}
}
return asset.LikeCount, nil
}

View File

@ -441,13 +441,13 @@ func (s *assetService) GetAsset(req *pb.GetAssetRequest, userID, starID int64) (
var ownerNickname string
profile, err := s.userClient.GetFanProfile(context.Background(), asset.OwnerUID, asset.StarID)
if err != nil {
logger.Logger.Warn("Failed to get owner fan profile, will return without nickname",
logger.Logger.Warn("Failed to get owner fan profile, using fallback nickname",
zap.Int64("owner_uid", asset.OwnerUID),
zap.Int64("star_id", asset.StarID),
zap.Error(err),
)
// 获取失败时,使用空字符串
ownerNickname = ""
// 获取失败时,使用 User{uid} 作为 fallback
ownerNickname = fmt.Sprintf("User%d", asset.OwnerUID)
} else {
ownerNickname = profile.Nickname
}

View File

@ -67,6 +67,7 @@ type mintService struct {
registryRepo starbookRepo.AssetRegistryRepository // 资产索引仓库(用于星册体系)
localMintCostRepo repository.MintCostRepository // 铸造消耗配置仓库
userMintCountRepo repository.UserMintCountRepository // 用户铸爱累计仓库
assetLevelService AssetLevelService // 资产等级服务
}
// NewMintService 创建铸造服务实例
@ -79,6 +80,7 @@ func NewMintService(
registryRepo starbookRepo.AssetRegistryRepository,
localMintCostRepo repository.MintCostRepository,
userMintCountRepo repository.UserMintCountRepository,
assetLevelService AssetLevelService,
) MintService {
return &mintService{
assetRepo: assetRepo,
@ -89,6 +91,7 @@ func NewMintService(
registryRepo: registryRepo,
localMintCostRepo: localMintCostRepo,
userMintCountRepo: userMintCountRepo,
assetLevelService: assetLevelService,
}
}
@ -439,7 +442,16 @@ func (s *mintService) CreateMintOrder(req *pb.CreateMintOrderRequest, userID, st
return nil, err
}
// 4. 无需异步 AI 处理cover_url 已在步骤 3.2 中直接设置
// 4. 初始化资产等级记录
if s.assetLevelService != nil && asset != nil {
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))
}
}
// 5. 无需异步 AI 处理cover_url 已在步骤 3.2 中直接设置
// 5. 获取所有者的昵称(创建时所有者就是当前用户)
var ownerNickname string

View File

@ -0,0 +1,52 @@
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))
}
}

View File

@ -28,6 +28,7 @@ type OnExhibitionCompletedRequest struct {
StartTime int64
ExpireAt int64
CrystalAmount int64
LikeCount int32 // 点赞数用于taskService根据资产等级重新计算收益
}
// OnExhibitionCompletedResponse 展位完成响应
@ -72,6 +73,7 @@ func (c *taskRPCClient) OnExhibitionCompleted(ctx context.Context, req *OnExhibi
StartTime: req.StartTime,
ExpireAt: req.ExpireAt,
CrystalAmount: req.CrystalAmount,
LikeCount: req.LikeCount,
}
resp, err := c.client.OnExhibitionCompleted(ctx, pbReq)

View File

@ -2,6 +2,7 @@ package repository
import (
"errors"
"strings"
"time"
"github.com/topfans/backend/pkg/models"
@ -514,7 +515,7 @@ func (r *galleryRepository) CountValidExhibitions(starID int64, materialType str
if materialType != "" && materialType != "all" && materialType != "random" {
query = query.Joins("JOIN assets a ON a.id = exhibitions.asset_id").
Where("a.material_type = ?", materialType)
Where("a.material_type LIKE ?", "%"+materialType+"%")
}
err := query.Count(&count).Error
@ -532,7 +533,7 @@ func (r *galleryRepository) GetRandomExhibitions(starID int64, materialType stri
if materialType != "" && materialType != "all" && materialType != "random" {
baseQuery = baseQuery.Joins("JOIN assets a ON a.id = exhibitions.asset_id").
Where("a.material_type = ?", materialType)
Where("a.material_type LIKE ?", "%"+materialType+"%")
}
// 排除已展示的ID
@ -568,26 +569,42 @@ func (r *galleryRepository) GetRandomExhibitions(starID int64, materialType stri
return nil, err
}
// Read-repair: 检查并修正 material_type查询时自动修正数据不一致
// Read-repair: 检查并补充 material_type多类型共存不降级
for _, item := range items {
item.Span = calcSpanByLikes(item.LikeCount)
correctType := calcMaterialType(item.LikeCount, item.CreatedAt)
if item.MaterialType != correctType {
// 异步修正,不阻塞返回(使用 goroutine 避免影响响应时间)
go func(assetID int64, correct string) {
correctTypes := calcMaterialTypes(item.LikeCount, item.CreatedAt)
// 如果缺少任何类型,追加(不覆盖原有类型)
missingTypes := []string{}
for _, ct := range strings.Split(correctTypes, ",") {
ct = strings.TrimSpace(ct)
if ct != "" && !strings.Contains(item.MaterialType, ct) {
missingTypes = append(missingTypes, ct)
}
}
if len(missingTypes) > 0 {
newType := strings.Join(missingTypes, ",")
// 异步修正
go func(assetID int64, newType string) {
r.db.Model(&models.Asset{}).
Where("id = ?", assetID).
Update("material_type", correct)
}(item.AssetID, correctType)
// 同时修正当前对象的值,保证返回给前端的是正确的
item.MaterialType = correctType
Update("material_type", gorm.Expr("CONCAT(COALESCE(material_type, ''), ',', ?)", newType))
}(item.AssetID, newType)
// 修正当前返回对象
if item.MaterialType == "" {
item.MaterialType = correctTypes
} else {
item.MaterialType = item.MaterialType + "," + strings.Join(missingTypes, ",")
}
}
}
return items, nil
}
// calcMaterialType 根据点赞数计算素材类型
// calcMaterialType 根据点赞数计算素材类型(单类型,互斥)
func calcMaterialType(likes int32, createdAt int64) string {
now := time.Now().UnixMilli()
isWithin1Hour := createdAt > 0 && now-createdAt < 3600*1000
@ -602,6 +619,27 @@ func calcMaterialType(likes int32, createdAt int64) string {
return "new"
}
// calcMaterialTypes 根据点赞数计算素材类型(多类型共存)
func calcMaterialTypes(likes int32, createdAt int64) string {
now := time.Now().UnixMilli()
isWithin1Hour := createdAt > 0 && now-createdAt < 3600*1000
types := []string{}
// 超过20 → hot
if likes > 20 {
types = append(types, "hot")
}
// 1小时内达到10 → potential
if isWithin1Hour && likes >= 10 {
types = append(types, "potential")
}
// new 默认存在
types = append(types, "new")
return strings.Join(types, ",")
}
// calcSpanByLikes 根据点赞数计算卡片大小
// 0-30 → span 1, 31-100 → span 2, 101-200 → span 3, 200+ → span 4
func calcSpanByLikes(likes int32) int32 {

View File

@ -132,6 +132,7 @@ func (w *CleanupWorker) cleanupExpiredExhibitions(now int64) {
StartTime: e.StartTime,
ExpireAt: now,
CrystalAmount: revenue,
LikeCount: int32(likeCount),
})
if err != nil {
logger.Logger.Error("调用TaskService记录收益失败跳过标记已处理以便重试",

View File

@ -62,6 +62,8 @@ func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starI
// 2. 检查是否已点赞
checkLikeReq := &assetPb.CheckAssetLikeRequest{
AssetId: assetID,
UserId: userID,
StarId: starID,
}
checkLikeResp, err := s.assetClient.CheckAssetLike(ctx, checkLikeReq)
if err != nil {
@ -125,6 +127,8 @@ func (s *AssetLikeService) UnlikeAsset(ctx context.Context, assetID, userID, sta
// 1. 检查是否已点赞
checkLikeReq := &assetPb.CheckAssetLikeRequest{
AssetId: assetID,
UserId: userID,
StarId: starID,
}
checkLikeResp, err := s.assetClient.CheckAssetLike(ctx, checkLikeReq)
if err != nil {
@ -187,6 +191,8 @@ func (s *AssetLikeService) CheckAssetLike(ctx context.Context, assetID, userID,
checkLikeReq := &assetPb.CheckAssetLikeRequest{
AssetId: assetID,
UserId: userID,
StarId: starID,
}
checkLikeResp, err := s.assetClient.CheckAssetLike(ctx, checkLikeReq)
if err != nil {

View File

@ -17,6 +17,8 @@ import (
pb "github.com/topfans/backend/pkg/proto/task"
pbGallery "github.com/topfans/backend/pkg/proto/gallery"
pbUser "github.com/topfans/backend/pkg/proto/user"
assetLevelRepo "github.com/topfans/backend/services/assetService/repository"
assetLevelSvc "github.com/topfans/backend/services/assetService/service"
"github.com/topfans/backend/services/taskService/client"
"github.com/topfans/backend/services/taskService/config"
"github.com/topfans/backend/services/taskService/model"
@ -105,9 +107,15 @@ func main() {
logger.Logger.Info("Gallery RPC client initialized")
// 6. Init services
// Create AssetLevelService for revenue calculations
assetLevelRepository := assetLevelRepo.NewAssetLevelRepository(db)
seasonRepository := assetLevelRepo.NewSeasonRepository(db)
seasonDecayConfigRepository := assetLevelRepo.NewSeasonDecayConfigRepository(db)
assetLevelService := assetLevelSvc.NewAssetLevelService(assetLevelRepository, seasonRepository, seasonDecayConfigRepository)
dailySvc := service.NewDailyTaskService(dailyRepo, userRPCClient)
onboardingSvc := service.NewOnboardingService(onboardingRepo, dailyRepo, userRPCClient)
revenueSvc := service.NewRevenueService(revenueRepo, userRPCClient, galleryRPCClient)
revenueSvc := service.NewRevenueService(revenueRepo, userRPCClient, galleryRPCClient, assetLevelService)
logger.Logger.Info("Services initialized")
// 7. Init workergoroutine 中启动)

View File

@ -7,6 +7,7 @@ import (
pbCommon "github.com/topfans/backend/pkg/proto/common"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/models"
pb "github.com/topfans/backend/pkg/proto/task"
"github.com/topfans/backend/services/taskService/client"
"github.com/topfans/backend/services/taskService/model"
@ -20,24 +21,39 @@ type RevenueService interface {
ClaimExhibitionRevenue(ctx context.Context, userID, starID int64, revenueID int64) (*pb.ClaimExhibitionRevenueResponse, error)
ClaimAllExhibitionRevenue(ctx context.Context, userID, starID int64) (*pb.ClaimAllExhibitionRevenueResponse, error)
OnExhibitionCompleted(ctx context.Context, req *pb.OnExhibitionCompletedRequest) (*pb.OnExhibitionCompletedResponse, error)
SetAssetLevelService(svc AssetLevelService)
}
// AssetLevelService 资产等级服务接口定义在assetService
type AssetLevelService interface {
GetOrCreateRecord(assetID int64) (*models.AssetLevelRecord, error)
AddExhibitionHours(assetID int64, hours int) (string, bool, error)
CalculateRevenue(assetID int64, likeCount int, startTime, endTime int64, revenueBoostBps int) (int64, error)
}
// revenueService 展示收益Service实现
type revenueService struct {
revenueRepo repository.RevenueRepository
userRPCClient client.UserServiceClient
revenueRepo repository.RevenueRepository
userRPCClient client.UserServiceClient
galleryRPCClient client.GalleryServiceClient
assetLevelService AssetLevelService // 资产等级服务
}
// NewRevenueService 创建收益Service实例
func NewRevenueService(revenueRepo repository.RevenueRepository, userRPCClient client.UserServiceClient, galleryRPCClient client.GalleryServiceClient) RevenueService {
func NewRevenueService(revenueRepo repository.RevenueRepository, userRPCClient client.UserServiceClient, galleryRPCClient client.GalleryServiceClient, assetLevelService AssetLevelService) RevenueService {
return &revenueService{
revenueRepo: revenueRepo,
userRPCClient: userRPCClient,
revenueRepo: revenueRepo,
userRPCClient: userRPCClient,
galleryRPCClient: galleryRPCClient,
assetLevelService: assetLevelService,
}
}
// SetAssetLevelService 设置资产等级服务
func (s *revenueService) SetAssetLevelService(svc AssetLevelService) {
s.assetLevelService = svc
}
// GetExhibitionRevenue 获取展示收益列表
func (s *revenueService) GetExhibitionRevenue(ctx context.Context, userID, starID int64, status string, page, pageSize int32) (*pb.GetExhibitionRevenueResponse, error) {
logger.Logger.Debug("GetExhibitionRevenue",
@ -307,7 +323,33 @@ func (s *revenueService) OnExhibitionCompleted(ctx context.Context, req *pb.OnEx
zap.Int64("slot_id", req.SlotId),
zap.Int64("occupier_uid", req.OccupierUid),
zap.Int64("slot_owner_uid", req.SlotOwnerUid),
zap.Int64("crystal_amount", req.CrystalAmount))
zap.Int64("crystal_amount", req.CrystalAmount),
zap.Int32("like_count", req.LikeCount))
// 计算实际上架时长(毫秒转小时)
startTime := req.StartTime
expireAt := req.ExpireAt
actualHours := (expireAt - startTime) / 3600000
// 重新计算收益使用资产等级对应的R0值而非galleryService传来的硬编码R0=5
var finalRevenue int64
if s.assetLevelService != nil && req.AssetId > 0 {
if calculatedRevenue, err := s.assetLevelService.CalculateRevenue(req.AssetId, int(req.LikeCount), startTime, expireAt, 0); err == nil {
finalRevenue = calculatedRevenue
logger.Logger.Info("OnExhibitionCompleted: recalculated revenue using asset level",
zap.Int64("asset_id", req.AssetId),
zap.Int64("original_revenue", req.CrystalAmount),
zap.Int64("recalculated_revenue", finalRevenue))
} else {
// 计算失败,使用传来的值
finalRevenue = req.CrystalAmount
logger.Logger.Warn("OnExhibitionCompleted: failed to calculate revenue with asset level, using original",
zap.Int64("asset_id", req.AssetId),
zap.Error(err))
}
} else {
finalRevenue = req.CrystalAmount
}
// 收益归属资产主人(铸爱用户),无论展位是否为自己
now := time.Now().UnixMilli()
@ -319,7 +361,7 @@ func (s *revenueService) OnExhibitionCompleted(ctx context.Context, req *pb.OnEx
SlotID: req.SlotId,
SlotOwnerUID: req.SlotOwnerUid, // 记录展位所有者信息(仅供参考)
SlotType: "exhibition", // 上架展示收益
CrystalAmount: req.CrystalAmount,
CrystalAmount: finalRevenue, // 使用重新计算的收益
CycleStartTime: req.StartTime,
CycleEndTime: req.ExpireAt,
Status: "claimable",
@ -334,12 +376,6 @@ func (s *revenueService) OnExhibitionCompleted(ctx context.Context, req *pb.OnEx
return &pb.OnExhibitionCompletedResponse{Base: &pbCommon.BaseResponse{Code: pbCommon.StatusCode_STATUS_INTERNAL_ERROR}}, err
}
// 增加用户累计上架时长(展位主人获得上架时长累计)
// 计算实际上架时长(毫秒转小时)
startTime := req.StartTime
expireAt := req.ExpireAt
actualHours := (expireAt - startTime) / 3600000
// sourceID 用于去重,避免重复累计
sourceID := fmt.Sprintf("exhibition_%d", req.ExhibitionId)
@ -367,6 +403,21 @@ func (s *revenueService) OnExhibitionCompleted(ctx context.Context, req *pb.OnEx
zap.Int64("crystal_reward", crystalReward))
}
// 增加资产累计展出时长(资产等级系统)
if s.assetLevelService != nil && req.AssetId > 0 && actualHours > 0 {
if newLevel, upgraded, err := s.assetLevelService.AddExhibitionHours(req.AssetId, int(actualHours)); err != nil {
logger.Logger.Warn("OnExhibitionCompleted: failed to add exhibition hours to asset level",
zap.Int64("asset_id", req.AssetId),
zap.Int64("hours", actualHours),
zap.Error(err))
} else if upgraded {
logger.Logger.Info("OnExhibitionCompleted: asset leveled up due to exhibition",
zap.Int64("asset_id", req.AssetId),
zap.String("new_level", newLevel),
zap.Int64("hours", actualHours))
}
}
logger.Logger.Info("OnExhibitionCompleted: success",
zap.Int64("exhibition_id", req.ExhibitionId),
zap.Int64("revenue_record_id", createdRecord.ID))
@ -393,15 +444,17 @@ func CalculateBuff(likeCount int) int {
}
}
// CalculateExhibitionRevenue 计算单次上架收益
// CalculateExhibitionRevenue 计算单次上架收益(参考实现,未被调用)
// 注意:实际收益计算在 OnExhibitionCompleted 中通过 AssetLevelService.CalculateRevenue 实现
// 此函数保留用于参考和测试场景
// 设计文档公式:
// R1 = R0 × T × [100% + Buff(n)]
// R0 = 5 水晶/小时
// R0 = 5 水晶/小时(默认,仅作参考)
// T = 上架时长(小时)
// Buff(n) 根据点赞数计算
// 应用永久收益提升revenueBoostBps (bps),如 500 = +5%
func CalculateExhibitionRevenue(likeCount int, startTime, endTime int64, revenueBoostBps int) int64 {
R0 := int64(5) // 水晶/小时
R0 := int64(5) // 水晶/小时(默认参考值)
// 计算上架时长(毫秒转小时)
T := (endTime - startTime) / 3600000

BIN
backend/socialService Executable file

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -1012,7 +1012,7 @@ onUnmounted(() => {
width: 352rpx;
height: 520rpx;
margin-bottom: 32rpx;
/* animation: card-3d-flip 15s ease-in-out infinite; */
animation: card-3d-flip 15s ease-in-out infinite;
transform-origin: center center;
}

View File

@ -383,13 +383,13 @@ const handleAvatarClick = () => {
if (pages.length > 0) {
const currentPage = pages[pages.length - 1];
//
if (currentPage.route === 'pages/profile/myWorks') {
if (currentPage.route === 'pages/profile/profile') {
//
return;
}
}
uni.navigateTo({
url: '/pages/profile/myWorks'
url: '/pages/profile/profile'
});
};

View File

@ -10,9 +10,9 @@
</view>
<!-- <text class="nav-title">我的作品</text> -->
<view class="nav-placeholder"></view>
<view class="nav-settings" @tap="goToSettings">
<!-- <view class="nav-settings" @tap="goToSettings">
<image class="nav-settings-icon" src="/static/icon/settings.png" mode="aspectFit"></image>
</view>
</view> -->
</view>
<view class="scroll-content">

View File

@ -1296,8 +1296,8 @@ onShow(() => {
}
.close-icon-img {
width: 80rpx;
height: 80rpx;
width: 48rpx;
height: 48rpx;
position: relative;
top: 96rpx;
left: 32rpx;
@ -1372,15 +1372,11 @@ onShow(() => {
}
.uid-value {}
.toggle-icon {
width: 32rpx;
height: 32rpx;
}
.address-value {}
.info-btn {
font-size: 20rpx;
color: #FFD700;

View File

@ -33,7 +33,7 @@
<script setup>
defineProps({
modelValue: { type: String, default: 'random' }
modelValue: { type: String, default: 'hot' }
})
defineEmits(['update:modelValue'])
@ -42,7 +42,7 @@ const tabs = [
{ key: 'hot', label: '人气王者', emoji: null, icon: '/static/square/3.png',iconWidth: 80, iconHeight: 80 },
{ key: 'new', label: '新鲜上架', emoji: null, icon: '/static/square/2.png',iconWidth: 80, iconHeight: 80 },
{ key: 'potential', label: '潜力之星', emoji: null, icon: '/static/square/1.png',iconWidth: 96, iconHeight: 96 },
{ key: 'random', label: '随机寻宝', emoji: null, icon: '/static/square/4.png',iconWidth: 80, iconHeight: 80 },
{ key: 'myworks', label: '我的', emoji: null, icon: '/static/square/4.png',iconWidth: 80, iconHeight: 80 },
]
</script>

View File

@ -89,14 +89,18 @@ const currentUserNickname = computed(() => store.state.user?.userInfo?.nickname
const currentStarId = ref(uni.getStorageSync('star_id') || null)
// ========== UI State ==========
const activeContentTab = ref('random')
const activeContentTab = ref('hot')
const waterfallKey = ref(0) // WaterfallGrid
const navExpanded = ref(false)
const showRankingModal = ref(false)
const isActive = ref(true)
// ========== Watch activeContentTab ==========
watch(activeContentTab, () => {
watch(activeContentTab, (newTab) => {
if (newTab === 'myworks') {
uni.navigateTo({ url: '/pages/profile/myWorks' })
return
}
// WaterfallGridiOS CSS
waterfallKey.value++
})
@ -191,6 +195,7 @@ onMounted(() => {
onShow(() => {
isActive.value = true
activeContentTab.value = 'hot'
//
// if (shouldShowGuideStartModal()) {
// uni.navigateTo({