feat: 新增经济系统

This commit is contained in:
zerosaturation 2026-05-14 15:58:01 +08:00
parent 1af0a0527a
commit 74182ad662
36 changed files with 1415 additions and 1896 deletions

File diff suppressed because it is too large Load Diff

View File

@ -211,7 +211,6 @@ func (ctrl *TaskController) ClaimDailyTask(c *gin.Context) {
response.Success(c, map[string]interface{}{
"success": resp.Success,
"crystal_balance": resp.CrystalBalance,
"experience": resp.Experience,
})
}
@ -270,7 +269,6 @@ func (ctrl *TaskController) ClaimAllDailyTasks(c *gin.Context) {
response.Success(c, map[string]interface{}{
"claimed_count": resp.ClaimedCount,
"crystal_balance": resp.CrystalBalance,
"experience": resp.Experience,
"claimed_task_keys": resp.ClaimedTaskKeys,
})
}
@ -481,7 +479,6 @@ func (ctrl *TaskController) ClaimOnboardingReward(c *gin.Context) {
response.Success(c, map[string]interface{}{
"success": resp.Success,
"crystal_balance": resp.CrystalBalance,
"experience": resp.Experience,
})
}
@ -677,7 +674,6 @@ func convertDailyTasksResponse(resp *pbTask.GetDailyTasksResponse) map[string]in
"name": task.Name,
"description": task.Description,
"crystal_reward": task.CrystalReward,
"exp_reward": task.ExpReward,
"status": task.Status,
"can_claim": task.CanClaim,
})
@ -696,7 +692,6 @@ func convertOnboardingStatusResponse(resp *pbTask.GetOnboardingStatusResponse) m
"name": stage.Name,
"required_task_keys": stage.RequiredTaskKeys,
"crystal_reward": stage.CrystalReward,
"exp_reward": stage.ExpReward,
"status": stage.Status,
"is_current": stage.IsCurrent,
})
@ -717,7 +712,6 @@ func convertAdvanceStageResponse(resp *pbTask.AdvanceStageResponse) map[string]i
"name": stage.Name,
"required_task_keys": stage.RequiredTaskKeys,
"crystal_reward": stage.CrystalReward,
"exp_reward": stage.ExpReward,
"status": stage.Status,
"is_current": stage.IsCurrent,
})
@ -737,7 +731,6 @@ func convertCompleteGuideResponse(resp *pbTask.CompleteGuideResponse) map[string
"name": stage.Name,
"required_task_keys": stage.RequiredTaskKeys,
"crystal_reward": stage.CrystalReward,
"exp_reward": stage.ExpReward,
"status": stage.Status,
"is_current": stage.IsCurrent,
})

View File

@ -17,7 +17,6 @@ type CurrentIdentityDTO struct {
IdentityName string `json:"identity_name"` // "王一博"
Tag string `json:"tag"` // "小摩托"
Level int32 `json:"level"`
Experience int64 `json:"experience"`
CrystalBalance int64 `json:"crystal_balance"`
}
@ -109,7 +108,6 @@ type MyFanIdentityItemDTO struct {
StarTag string `json:"star_tag"` // 明星标签
Nickname string `json:"nickname"` // 用户昵称
Level int32 `json:"level"` // 等级
Experience int64 `json:"experience"` // 经验值
CrystalBalance int64 `json:"crystal_balance"` // 水晶余额
IsActive bool `json:"is_active"` // 是否激活(当前使用)
}

View File

@ -24,7 +24,6 @@ func ToCurrentIdentityDTO(profile *pb.FanProfile, star *pb.Star) CurrentIdentity
IdentityName: star.Name, // "王一博"
Tag: star.Tag, // "小摩托"
Level: profile.Level,
Experience: profile.Experience,
CrystalBalance: profile.CrystalBalance,
}
}
@ -199,7 +198,6 @@ func ToMyFanIdentitiesResponseDTO(items []*pb.MyFanIdentityItem, currentStarID i
StarTag: star.Tag,
Nickname: profile.Nickname,
Level: profile.Level,
Experience: profile.Experience,
CrystalBalance: profile.CrystalBalance,
IsActive: profile.StarId == currentStarID,
})

132
backend/pkg/models/level.go Normal file
View File

@ -0,0 +1,132 @@
package models
import (
"time"
"gorm.io/gorm"
)
// LevelThreshold 等级阈值配置表
type LevelThreshold struct {
Level int32 `gorm:"primaryKey;column:level"` // 等级 1-20
MaxExhibitionHours int64 `gorm:"not null;column:max_exhibition_hours"` // 升级到该等级需要的累计上架时长(小时)
LikeBetCount int32 `gorm:"not null;column:like_bet_count"` // 升级后解锁的点赞押注次数
Description string `gorm:"type:varchar(100);column:description"` // 描述
}
// TableName 指定表名
func (LevelThreshold) TableName() string {
return "level_thresholds"
}
// LevelUpgradeCondition 等级升级条件配置表21级+
type LevelUpgradeCondition struct {
Level int32 `gorm:"primaryKey;column:level"` // 主等级如21级
RequireTotalHours int64 `gorm:"not null;column:require_total_hours"` // 需要的总上架时长
RequireDaziLevel int32 `gorm:"default:0;column:require_dazi_level"` // 需要的搭子等级0=不需要)
Description string `gorm:"type:varchar(100);column:description"` // 描述
UpdatedAt int64 `gorm:"not null;column:updated_at"`
}
// TableName 指定表名
func (LevelUpgradeCondition) TableName() string {
return "level_upgrade_conditions"
}
// BeforeUpdate 更新前钩子
func (l *LevelUpgradeCondition) BeforeUpdate(tx *gorm.DB) error {
l.UpdatedAt = time.Now().UnixMilli()
return nil
}
// LevelCapConfig 等级上限配置表
type LevelCapConfig struct {
ID int64 `gorm:"primaryKey;column:id"`
MaxLevel int32 `gorm:"not null;column:max_level"` // 当前等级上限
UpdatedAt int64 `gorm:"not null;column:updated_at"`
}
// TableName 指定表名
func (LevelCapConfig) TableName() string {
return "level_cap_config"
}
// BeforeUpdate 更新前钩子
func (l *LevelCapConfig) BeforeUpdate(tx *gorm.DB) error {
l.UpdatedAt = time.Now().UnixMilli()
return nil
}
// UserExhibitionHours 用户累计上架时长表
type UserExhibitionHours struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
UserID int64 `gorm:"uniqueIndex:uk_exhibition_user_star;not null;column:user_id"`
StarID int64 `gorm:"uniqueIndex:uk_exhibition_user_star;not null;column:star_id"`
TotalExhibitionHours int64 `gorm:"default:0;not null;column:total_exhibition_hours"` // 累计上架时长(小时)
UpdatedAt int64 `gorm:"not null;column:updated_at"`
}
// TableName 指定表名
func (UserExhibitionHours) TableName() string {
return "user_exhibition_hours"
}
// BeforeCreate 创建前钩子
func (u *UserExhibitionHours) BeforeCreate(tx *gorm.DB) error {
u.UpdatedAt = time.Now().UnixMilli()
return nil
}
// BeforeUpdate 更新前钩子
func (u *UserExhibitionHours) BeforeUpdate(tx *gorm.DB) error {
u.UpdatedAt = time.Now().UnixMilli()
return nil
}
// LevelUpRewardConfig 升级奖励配置表
type LevelUpRewardConfig struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
Level int32 `gorm:"uniqueIndex:uk_level_reward_type;not null;column:level"` // 等级
RewardType string `gorm:"uniqueIndex:uk_level_reward_type;type:varchar(50);not null;column:reward_type"` // 奖励类型crystal/like_bet_count
RewardValue int64 `gorm:"default:0;column:reward_value"` // 奖励值
IsEnabled bool `gorm:"default:true;column:is_enabled"` // 功能开关
UpdatedAt int64 `gorm:"not null;column:updated_at"`
}
// TableName 指定表名
func (LevelUpRewardConfig) TableName() string {
return "level_up_reward_config"
}
// BeforeUpdate 更新前钩子
func (l *LevelUpRewardConfig) BeforeUpdate(tx *gorm.DB) error {
l.UpdatedAt = time.Now().UnixMilli()
return nil
}
// DaziLevelThreshold 搭子等级阈值配置表(预留)
type DaziLevelThreshold struct {
Level int32 `gorm:"primaryKey;column:level"` // 搭子等级 1-N
UpgradeCondition string `gorm:"type:varchar(100);column:upgrade_condition"` // 升级条件描述
ConditionParam int `gorm:"default:0;column:condition_param"` // 条件参数
Description string `gorm:"type:varchar(100);column:description"`
}
// TableName 指定表名
func (DaziLevelThreshold) TableName() string {
return "dazi_level_thresholds"
}
// UserDaziLevel 用户搭子等级表(预留)
type UserDaziLevel struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
UserID int64 `gorm:"uniqueIndex:uk_dazi_user_star;not null;column:user_id"`
StarID int64 `gorm:"uniqueIndex:uk_dazi_user_star;not null;column:star_id"`
DaziLevel int32 `gorm:"default:1;not null;column:dazi_level"` // 搭子等级
UpdatedAt int64 `gorm:"not null;column:updated_at"`
}
// TableName 指定表名
func (UserDaziLevel) TableName() string {
return "user_dazi_level"
}

View File

@ -0,0 +1,85 @@
package models
import (
"time"
"gorm.io/gorm"
)
// MintCostConfig 铸造消耗配置表
type MintCostConfig struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
MintCount int32 `gorm:"uniqueIndex;not null;column:mint_count"` // 铸爱次数 1-10
CostCrystal int64 `gorm:"not null;column:cost_crystal"` // 消耗水晶数
Probability int64 `gorm:"default:0;column:probability"` // 保底触发概率 0-100
RewardType *string `gorm:"type:varchar(50);column:reward_type"` // 奖励类型
RewardValue int64 `gorm:"default:0;column:reward_value"` // 奖励值bps
Description *string `gorm:"type:varchar(255);column:description"` // 描述
UpdatedAt int64 `gorm:"not null;column:updated_at"`
}
// TableName 指定表名
func (MintCostConfig) TableName() string {
return "mint_cost_config"
}
// BeforeUpdate 更新前钩子
func (m *MintCostConfig) BeforeUpdate(tx *gorm.DB) error {
m.UpdatedAt = time.Now().UnixMilli()
return nil
}
// UserMintCount 用户铸爱累计表
type UserMintCount struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
UserID int64 `gorm:"uniqueIndex:uk_user_mint_star;not null;column:user_id"`
StarID int64 `gorm:"uniqueIndex:uk_user_mint_star;not null;column:star_id"`
MintCount int32 `gorm:"default:0;not null;column:mint_count"` // 累计铸造次数
RevenueBoostBps int32 `gorm:"default:0;not null;column:revenue_boost_bps"` // 永久收益提升基点500=+5%
UpdatedAt int64 `gorm:"not null;column:updated_at"`
}
// TableName 指定表名
func (UserMintCount) TableName() string {
return "user_mint_count"
}
// BeforeCreate 创建前钩子
func (u *UserMintCount) BeforeCreate(tx *gorm.DB) error {
u.UpdatedAt = time.Now().UnixMilli()
return nil
}
// BeforeUpdate 更新前钩子
func (u *UserMintCount) BeforeUpdate(tx *gorm.DB) error {
u.UpdatedAt = time.Now().UnixMilli()
return nil
}
// MintRewardConfig 铸造奖励配置表(预留)
type MintRewardConfig struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
StarID int64 `gorm:"uniqueIndex;not null;column:star_id"` // 偶像ID0=全服默认
BaseReward int64 `gorm:"default:0;column:base_reward"` // 每次铸造基础返还水晶数
IsEnabled bool `gorm:"default:true;column:is_enabled"` // 功能开关
UpdatedAt int64 `gorm:"not null;column:updated_at"`
}
// TableName 指定表名
func (MintRewardConfig) TableName() string {
return "mint_reward_config"
}
// MintMilestoneConfig 铸造阶梯奖励配置表(预留)
type MintMilestoneConfig struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
StarID int64 `gorm:"uniqueIndex:uk_milestone_star_count;not null;column:star_id"`
MilestoneCount int32 `gorm:"uniqueIndex:uk_milestone_star_count;not null;column:milestone_count"` // 累计次数阈值
BonusReward int64 `gorm:"not null;column:bonus_reward"` // 达到该阶梯时额外奖励水晶
CreatedAt int64 `gorm:"not null;column:created_at"`
}
// TableName 指定表名
func (MintMilestoneConfig) TableName() string {
return "mint_milestone_config"
}

View File

@ -55,17 +55,17 @@ type FanProfile struct {
Level int32 `gorm:"default:1;not null;column:level"`
Times int32 `gorm:"default:1;not null;column:times"` // 剩余铸造次数
Social int32 `gorm:"default:0;not null;column:social"` // 好友个数
Experience int64 `gorm:"default:0;not null;column:experience"`
CoinBalance int64 `gorm:"default:0;not null;column:coin_balance"`
CrystalBalance int64 `gorm:"default:0;not null;column:crystal_balance"`
Tags StringArray `gorm:"type:jsonb;column:tags"`
AvatarURL *string `gorm:"type:varchar(500);column:avatar_url"`
// 新增字段
StarbookLimit int32 `gorm:"default:3;not null;column:starbook_limit"`
SlotLimit int32 `gorm:"default:3;not null;column:slot_limit"`
AssetsCount int32 `gorm:"default:0;not null;column:assets_count"`
ChainAddress *string `gorm:"type:varchar(100);column:chain_address"`
StarbookLimit int32 `gorm:"default:3;not null;column:starbook_limit"`
SlotLimit int32 `gorm:"default:3;not null;column:slot_limit"`
AssetsCount int32 `gorm:"default:0;not null;column:assets_count"`
LikeBetCount int32 `gorm:"default:0;not null;column:like_bet_count"` // 点赞押注次数
ChainAddress *string `gorm:"type:varchar(100);column:chain_address"`
IsActive bool `gorm:"default:true;not null;column:is_active"`
CreatedAt int64 `gorm:"not null;column:created_at"`
@ -170,3 +170,41 @@ func (sa *StringArray) Scan(value interface{}) error {
return json.Unmarshal(bytes, sa)
}
// CrystalTransactionRecord 水晶交易流水表
type CrystalTransactionRecord struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
UserID int64 `gorm:"not null;index:ix_crystal_tx_user_star;column:user_id"`
StarID int64 `gorm:"not null;index:ix_crystal_tx_user_star;column:star_id"`
ChangeType string `gorm:"type:varchar(30);not null;index:ix_crystal_tx_change_type;column:change_type"` // task_reward/mint_cost/mint_reward/exhibition_revenue/level_up_bonus/manual_adjust
Delta int64 `gorm:"not null;column:delta"` // 正数=收入,负数=消耗
BalanceBefore int64 `gorm:"not null;column:balance_before"` // 变化前余额快照
BalanceAfter int64 `gorm:"not null;column:balance_after"` // 变化后余额快照
SourceID string `gorm:"type:varchar(100);column:source_id"` // 关联业务ID
Description string `gorm:"type:varchar(255);column:description"` // 可读描述
CreatedAt int64 `gorm:"not null;index:ix_crystal_tx_created;column:created_at"`
}
// TableName 指定表名
func (CrystalTransactionRecord) TableName() string {
return "crystal_transaction_records"
}
// CoinTransactionRecord 游戏币交易流水表(预留)
type CoinTransactionRecord struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
UserID int64 `gorm:"not null;index:ix_coin_tx_user_star;column:user_id"`
StarID int64 `gorm:"not null;index:ix_coin_tx_user_star;column:star_id"`
ChangeType string `gorm:"type:varchar(30);not null;column:change_type"`
Delta int64 `gorm:"not null;column:delta"`
BalanceBefore int64 `gorm:"not null;column:balance_before"`
BalanceAfter int64 `gorm:"not null;column:balance_after"`
SourceID string `gorm:"type:varchar(100);column:source_id"`
Description string `gorm:"type:varchar(255);column:description"`
CreatedAt int64 `gorm:"not null;index:ix_coin_tx_created;column:created_at"`
}
// TableName 指定表名
func (CoinTransactionRecord) TableName() string {
return "coin_transaction_records"
}

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-triple. DO NOT EDIT.
//
// Source: proto/asset.proto
// Source: asset.proto
package asset
import (

View File

@ -1,17 +0,0 @@
// Code generated by protoc-gen-triple. DO NOT EDIT.
//
// Source: proto/common.proto
package common
import (
"dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol"
)
// This is a compile-time assertion to ensure that this generated file and the Triple package
// are compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of Triple newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of Triple or updating the Triple
// version compiled into your binary.
const _ = triple_protocol.IsAtLeastVersion0_1_0
var ()

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-triple. DO NOT EDIT.
//
// Source: proto/gallery.proto
// Source: gallery.proto
package gallery
import (

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-triple. DO NOT EDIT.
//
// Source: proto/ranking.proto
// Source: ranking.proto
package ranking
import (

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-triple. DO NOT EDIT.
//
// Source: proto/social.proto
// Source: social.proto
package social
import (
@ -66,6 +66,12 @@ const (
SocialServiceCheckAssetLikeProcedure = "/topfans.social.SocialService/CheckAssetLike"
// SocialServiceGetMyLikedAssetsProcedure is the fully-qualified name of the SocialService's GetMyLikedAssets RPC.
SocialServiceGetMyLikedAssetsProcedure = "/topfans.social.SocialService/GetMyLikedAssets"
// SocialServiceGetMyTodayLikedAssetsProcedure is the fully-qualified name of the SocialService's GetMyTodayLikedAssets RPC.
SocialServiceGetMyTodayLikedAssetsProcedure = "/topfans.social.SocialService/GetMyTodayLikedAssets"
// SocialServiceGetMyWeekLikedAssetsProcedure is the fully-qualified name of the SocialService's GetMyWeekLikedAssets RPC.
SocialServiceGetMyWeekLikedAssetsProcedure = "/topfans.social.SocialService/GetMyWeekLikedAssets"
// SocialServiceGetUserLikedAssetsProcedure is the fully-qualified name of the SocialService's GetUserLikedAssets RPC.
SocialServiceGetUserLikedAssetsProcedure = "/topfans.social.SocialService/GetUserLikedAssets"
)
var (

View File

@ -30,9 +30,8 @@ type DailyTaskItem struct {
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"`
CrystalReward int64 `protobuf:"varint,5,opt,name=crystal_reward,json=crystalReward,proto3" json:"crystal_reward,omitempty"`
ExpReward int64 `protobuf:"varint,6,opt,name=exp_reward,json=expReward,proto3" json:"exp_reward,omitempty"`
Status string `protobuf:"bytes,7,opt,name=status,proto3" json:"status,omitempty"` // pending/completed/claimed
CanClaim bool `protobuf:"varint,8,opt,name=can_claim,json=canClaim,proto3" json:"can_claim,omitempty"`
Status string `protobuf:"bytes,6,opt,name=status,proto3" json:"status,omitempty"` // pending/completed/claimed
CanClaim bool `protobuf:"varint,7,opt,name=can_claim,json=canClaim,proto3" json:"can_claim,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -102,13 +101,6 @@ func (x *DailyTaskItem) GetCrystalReward() int64 {
return 0
}
func (x *DailyTaskItem) GetExpReward() int64 {
if x != nil {
return x.ExpReward
}
return 0
}
func (x *DailyTaskItem) GetStatus() string {
if x != nil {
return x.Status
@ -412,7 +404,6 @@ type ClaimDailyTaskResponse struct {
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"`
CrystalBalance int64 `protobuf:"varint,3,opt,name=crystal_balance,json=crystalBalance,proto3" json:"crystal_balance,omitempty"`
Experience int64 `protobuf:"varint,4,opt,name=experience,proto3" json:"experience,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -468,13 +459,6 @@ func (x *ClaimDailyTaskResponse) GetCrystalBalance() int64 {
return 0
}
func (x *ClaimDailyTaskResponse) GetExperience() int64 {
if x != nil {
return x.Experience
}
return 0
}
type ClaimAllDailyTasksRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
StarId int64 `protobuf:"varint,1,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"`
@ -524,8 +508,7 @@ type ClaimAllDailyTasksResponse struct {
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
ClaimedCount int32 `protobuf:"varint,2,opt,name=claimed_count,json=claimedCount,proto3" json:"claimed_count,omitempty"`
CrystalBalance int64 `protobuf:"varint,3,opt,name=crystal_balance,json=crystalBalance,proto3" json:"crystal_balance,omitempty"`
Experience int64 `protobuf:"varint,4,opt,name=experience,proto3" json:"experience,omitempty"`
ClaimedTaskKeys []string `protobuf:"bytes,5,rep,name=claimed_task_keys,json=claimedTaskKeys,proto3" json:"claimed_task_keys,omitempty"`
ClaimedTaskKeys []string `protobuf:"bytes,4,rep,name=claimed_task_keys,json=claimedTaskKeys,proto3" json:"claimed_task_keys,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -581,13 +564,6 @@ func (x *ClaimAllDailyTasksResponse) GetCrystalBalance() int64 {
return 0
}
func (x *ClaimAllDailyTasksResponse) GetExperience() int64 {
if x != nil {
return x.Experience
}
return 0
}
func (x *ClaimAllDailyTasksResponse) GetClaimedTaskKeys() []string {
if x != nil {
return x.ClaimedTaskKeys
@ -601,11 +577,10 @@ type OnboardingStage struct {
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
RequiredTaskKeys []string `protobuf:"bytes,3,rep,name=required_task_keys,json=requiredTaskKeys,proto3" json:"required_task_keys,omitempty"`
CrystalReward int64 `protobuf:"varint,4,opt,name=crystal_reward,json=crystalReward,proto3" json:"crystal_reward,omitempty"`
ExpReward int64 `protobuf:"varint,5,opt,name=exp_reward,json=expReward,proto3" json:"exp_reward,omitempty"`
Status string `protobuf:"bytes,6,opt,name=status,proto3" json:"status,omitempty"` // pending/completed/in_progress
IsCurrent bool `protobuf:"varint,7,opt,name=is_current,json=isCurrent,proto3" json:"is_current,omitempty"`
AllTasksCompleted bool `protobuf:"varint,8,opt,name=all_tasks_completed,json=allTasksCompleted,proto3" json:"all_tasks_completed,omitempty"` // 该阶段所有任务是否完成
IsRewardClaimed bool `protobuf:"varint,9,opt,name=is_reward_claimed,json=isRewardClaimed,proto3" json:"is_reward_claimed,omitempty"` // 该阶段奖励是否已领取
Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` // pending/completed/in_progress
IsCurrent bool `protobuf:"varint,6,opt,name=is_current,json=isCurrent,proto3" json:"is_current,omitempty"`
AllTasksCompleted bool `protobuf:"varint,7,opt,name=all_tasks_completed,json=allTasksCompleted,proto3" json:"all_tasks_completed,omitempty"` // 该阶段所有任务是否完成
IsRewardClaimed bool `protobuf:"varint,8,opt,name=is_reward_claimed,json=isRewardClaimed,proto3" json:"is_reward_claimed,omitempty"` // 该阶段奖励是否已领取
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -668,13 +643,6 @@ func (x *OnboardingStage) GetCrystalReward() int64 {
return 0
}
func (x *OnboardingStage) GetExpReward() int64 {
if x != nil {
return x.ExpReward
}
return 0
}
func (x *OnboardingStage) GetStatus() string {
if x != nil {
return x.Status
@ -1104,7 +1072,6 @@ type ClaimOnboardingRewardResponse struct {
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"`
CrystalBalance string `protobuf:"bytes,3,opt,name=crystal_balance,json=crystalBalance,proto3" json:"crystal_balance,omitempty"` // 使用 string 避免 Dubbo int64 序列化 bug
Experience string `protobuf:"bytes,4,opt,name=experience,proto3" json:"experience,omitempty"` // 使用 string 避免 Dubbo int64 序列化 bug
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -1160,13 +1127,6 @@ func (x *ClaimOnboardingRewardResponse) GetCrystalBalance() string {
return ""
}
func (x *ClaimOnboardingRewardResponse) GetExperience() string {
if x != nil {
return x.Experience
}
return ""
}
type ExhibitionRevenueItem struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
@ -1904,17 +1864,15 @@ var File_task_proto protoreflect.FileDescriptor
const file_task_proto_rawDesc = "" +
"\n" +
"\n" +
"task.proto\x12\ftopfans.task\x1a\x12proto/common.proto\x1a\x1cgoogle/api/annotations.proto\"\xf4\x01\n" +
"task.proto\x12\ftopfans.task\x1a\x12proto/common.proto\x1a\x1cgoogle/api/annotations.proto\"\xd5\x01\n" +
"\rDailyTaskItem\x12\x19\n" +
"\btask_key\x18\x01 \x01(\tR\ataskKey\x12\x17\n" +
"\astar_id\x18\x02 \x01(\x03R\x06starId\x12\x12\n" +
"\x04name\x18\x03 \x01(\tR\x04name\x12 \n" +
"\vdescription\x18\x04 \x01(\tR\vdescription\x12%\n" +
"\x0ecrystal_reward\x18\x05 \x01(\x03R\rcrystalReward\x12\x1d\n" +
"\n" +
"exp_reward\x18\x06 \x01(\x03R\texpReward\x12\x16\n" +
"\x06status\x18\a \x01(\tR\x06status\x12\x1b\n" +
"\tcan_claim\x18\b \x01(\bR\bcanClaim\"/\n" +
"\x0ecrystal_reward\x18\x05 \x01(\x03R\rcrystalReward\x12\x16\n" +
"\x06status\x18\x06 \x01(\tR\x06status\x12\x1b\n" +
"\tcan_claim\x18\a \x01(\bR\bcanClaim\"/\n" +
"\x14GetDailyTasksRequest\x12\x17\n" +
"\astar_id\x18\x01 \x01(\x03R\x06starId\"\x95\x01\n" +
"\x15GetDailyTasksResponse\x120\n" +
@ -1933,36 +1891,28 @@ const file_task_proto_rawDesc = "" +
"\amessage\x18\x05 \x01(\tR\amessage\"K\n" +
"\x15ClaimDailyTaskRequest\x12\x19\n" +
"\btask_key\x18\x01 \x01(\tR\ataskKey\x12\x17\n" +
"\astar_id\x18\x02 \x01(\x03R\x06starId\"\xad\x01\n" +
"\astar_id\x18\x02 \x01(\x03R\x06starId\"\x8d\x01\n" +
"\x16ClaimDailyTaskResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x18\n" +
"\asuccess\x18\x02 \x01(\bR\asuccess\x12'\n" +
"\x0fcrystal_balance\x18\x03 \x01(\x03R\x0ecrystalBalance\x12\x1e\n" +
"\n" +
"experience\x18\x04 \x01(\x03R\n" +
"experience\"4\n" +
"\x0fcrystal_balance\x18\x03 \x01(\x03R\x0ecrystalBalance\"4\n" +
"\x19ClaimAllDailyTasksRequest\x12\x17\n" +
"\astar_id\x18\x01 \x01(\x03R\x06starId\"\xe8\x01\n" +
"\astar_id\x18\x01 \x01(\x03R\x06starId\"\xc8\x01\n" +
"\x1aClaimAllDailyTasksResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12#\n" +
"\rclaimed_count\x18\x02 \x01(\x05R\fclaimedCount\x12'\n" +
"\x0fcrystal_balance\x18\x03 \x01(\x03R\x0ecrystalBalance\x12\x1e\n" +
"\n" +
"experience\x18\x04 \x01(\x03R\n" +
"experience\x12*\n" +
"\x11claimed_task_keys\x18\x05 \x03(\tR\x0fclaimedTaskKeys\"\xc2\x02\n" +
"\x0fcrystal_balance\x18\x03 \x01(\x03R\x0ecrystalBalance\x12*\n" +
"\x11claimed_task_keys\x18\x04 \x03(\tR\x0fclaimedTaskKeys\"\xa3\x02\n" +
"\x0fOnboardingStage\x12\x14\n" +
"\x05stage\x18\x01 \x01(\x05R\x05stage\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12,\n" +
"\x12required_task_keys\x18\x03 \x03(\tR\x10requiredTaskKeys\x12%\n" +
"\x0ecrystal_reward\x18\x04 \x01(\x03R\rcrystalReward\x12\x1d\n" +
"\x0ecrystal_reward\x18\x04 \x01(\x03R\rcrystalReward\x12\x16\n" +
"\x06status\x18\x05 \x01(\tR\x06status\x12\x1d\n" +
"\n" +
"exp_reward\x18\x05 \x01(\x03R\texpReward\x12\x16\n" +
"\x06status\x18\x06 \x01(\tR\x06status\x12\x1d\n" +
"\n" +
"is_current\x18\a \x01(\bR\tisCurrent\x12.\n" +
"\x13all_tasks_completed\x18\b \x01(\bR\x11allTasksCompleted\x12*\n" +
"\x11is_reward_claimed\x18\t \x01(\bR\x0fisRewardClaimed\"h\n" +
"is_current\x18\x06 \x01(\bR\tisCurrent\x12.\n" +
"\x13all_tasks_completed\x18\a \x01(\bR\x11allTasksCompleted\x12*\n" +
"\x11is_reward_claimed\x18\b \x01(\bR\x0fisRewardClaimed\"h\n" +
"\x14CompleteGuideRequest\x12\x19\n" +
"\btask_key\x18\x01 \x01(\tR\ataskKey\x125\n" +
"\x06stages\x18\x02 \x03(\v2\x1d.topfans.task.OnboardingStageR\x06stages\"\xd6\x01\n" +
@ -1987,14 +1937,11 @@ const file_task_proto_rawDesc = "" +
"\x06status\x18\x03 \x01(\tR\x06status\x125\n" +
"\x06stages\x18\x04 \x03(\v2\x1d.topfans.task.OnboardingStageR\x06stages\"4\n" +
"\x1cClaimOnboardingRewardRequest\x12\x14\n" +
"\x05stage\x18\x01 \x01(\x05R\x05stage\"\xb4\x01\n" +
"\x05stage\x18\x01 \x01(\x05R\x05stage\"\x94\x01\n" +
"\x1dClaimOnboardingRewardResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x18\n" +
"\asuccess\x18\x02 \x01(\bR\asuccess\x12'\n" +
"\x0fcrystal_balance\x18\x03 \x01(\tR\x0ecrystalBalance\x12\x1e\n" +
"\n" +
"experience\x18\x04 \x01(\tR\n" +
"experience\"\xe2\x02\n" +
"\x0fcrystal_balance\x18\x03 \x01(\tR\x0ecrystalBalance\"\xe2\x02\n" +
"\x15ExhibitionRevenueItem\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x17\n" +
"\astar_id\x18\x02 \x01(\x03R\x06starId\x12#\n" +

View File

@ -113,21 +113,20 @@ type FanProfile struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
StarId int64 `protobuf:"varint,3,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` // 核心隔离键
Nickname string `protobuf:"bytes,4,opt,name=nickname,proto3" json:"nickname,omitempty"` // 星球专属昵称
Level int32 `protobuf:"varint,5,opt,name=level,proto3" json:"level,omitempty"` // 等级
Times int32 `protobuf:"varint,6,opt,name=times,proto3" json:"times,omitempty"` // 剩余铸造次数
Social int32 `protobuf:"varint,7,opt,name=social,proto3" json:"social,omitempty"` // 好友个数
Experience int64 `protobuf:"varint,8,opt,name=experience,proto3" json:"experience,omitempty"` // 经验值
CoinBalance int64 `protobuf:"varint,9,opt,name=coin_balance,json=coinBalance,proto3" json:"coin_balance,omitempty"` // 游戏币余额
CrystalBalance int64 `protobuf:"varint,10,opt,name=crystal_balance,json=crystalBalance,proto3" json:"crystal_balance,omitempty"` // 顶粉水晶余额
Tags []string `protobuf:"bytes,11,rep,name=tags,proto3" json:"tags,omitempty"` // 标签数组
AvatarUrl string `protobuf:"bytes,17,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"` // 头像URL星球专属
CreatedAt int64 `protobuf:"varint,12,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
StarbookLimit int32 `protobuf:"varint,13,opt,name=starbook_limit,json=starbookLimit,proto3" json:"starbook_limit,omitempty"` // 星书限制
SlotLimit int32 `protobuf:"varint,14,opt,name=slot_limit,json=slotLimit,proto3" json:"slot_limit,omitempty"` // 槽位限制
AssetsCount int32 `protobuf:"varint,15,opt,name=assets_count,json=assetsCount,proto3" json:"assets_count,omitempty"` // 资产数量
ChainAddress string `protobuf:"bytes,16,opt,name=chain_address,json=chainAddress,proto3" json:"chain_address,omitempty"` // 链地址
StarId int64 `protobuf:"varint,3,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` // 核心隔离键
Nickname string `protobuf:"bytes,4,opt,name=nickname,proto3" json:"nickname,omitempty"` // 星球专属昵称
Level int32 `protobuf:"varint,5,opt,name=level,proto3" json:"level,omitempty"` // 等级
Times int32 `protobuf:"varint,6,opt,name=times,proto3" json:"times,omitempty"` // 剩余铸造次数
Social int32 `protobuf:"varint,7,opt,name=social,proto3" json:"social,omitempty"` // 好友个数
CoinBalance int64 `protobuf:"varint,8,opt,name=coin_balance,json=coinBalance,proto3" json:"coin_balance,omitempty"` // 游戏币余额
CrystalBalance int64 `protobuf:"varint,9,opt,name=crystal_balance,json=crystalBalance,proto3" json:"crystal_balance,omitempty"` // 顶粉水晶余额
Tags []string `protobuf:"bytes,10,rep,name=tags,proto3" json:"tags,omitempty"` // 标签数组
AvatarUrl string `protobuf:"bytes,17,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"` // 头像URL星球专属
CreatedAt int64 `protobuf:"varint,11,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
StarbookLimit int32 `protobuf:"varint,12,opt,name=starbook_limit,json=starbookLimit,proto3" json:"starbook_limit,omitempty"` // 星书限制
SlotLimit int32 `protobuf:"varint,13,opt,name=slot_limit,json=slotLimit,proto3" json:"slot_limit,omitempty"` // 槽位限制
AssetsCount int32 `protobuf:"varint,14,opt,name=assets_count,json=assetsCount,proto3" json:"assets_count,omitempty"` // 资产数量
ChainAddress string `protobuf:"bytes,15,opt,name=chain_address,json=chainAddress,proto3" json:"chain_address,omitempty"` // 链地址
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -211,13 +210,6 @@ func (x *FanProfile) GetSocial() int32 {
return 0
}
func (x *FanProfile) GetExperience() int64 {
if x != nil {
return x.Experience
}
return 0
}
func (x *FanProfile) GetCoinBalance() int64 {
if x != nil {
return x.CoinBalance
@ -1501,9 +1493,12 @@ func (x *UpdateFanProfileSocialResponse) GetNewSocial() int32 {
// 更新水晶余额请求内部RPC调用用于资产服务扣除/退款水晶)
type UpdateCrystalBalanceRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 用户ID
StarId int64 `protobuf:"varint,2,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` // 明星ID
Delta int64 `protobuf:"varint,3,opt,name=delta,proto3" json:"delta,omitempty"` // 变化量(正数增加,负数减少)
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 用户ID
StarId int64 `protobuf:"varint,2,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` // 明星ID
Delta int64 `protobuf:"varint,3,opt,name=delta,proto3" json:"delta,omitempty"` // 变化量(正数增加,负数减少)
ChangeType string `protobuf:"bytes,4,opt,name=change_type,json=changeType,proto3" json:"change_type,omitempty"` // 变化类型,如 task_reward/mint_cost/mint_reward/exhibition_revenue/level_up_bonus/manual_adjust
SourceId string `protobuf:"bytes,5,opt,name=source_id,json=sourceId,proto3" json:"source_id,omitempty"` // 关联业务ID
Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"` // 可读描述
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -1559,6 +1554,27 @@ func (x *UpdateCrystalBalanceRequest) GetDelta() int64 {
return 0
}
func (x *UpdateCrystalBalanceRequest) GetChangeType() string {
if x != nil {
return x.ChangeType
}
return ""
}
func (x *UpdateCrystalBalanceRequest) GetSourceId() string {
if x != nil {
return x.SourceId
}
return ""
}
func (x *UpdateCrystalBalanceRequest) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
// 更新水晶余额响应
type UpdateCrystalBalanceResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -1726,30 +1742,30 @@ func (x *UpdateAssetsCountResponse) GetNewCount() int32 {
return 0
}
// 增加经验值请求内部RPC调用用于taskService增加经验
type AddExperienceRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 用户ID
StarId int64 `protobuf:"varint,2,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` // 明星ID
Delta int64 `protobuf:"varint,3,opt,name=delta,proto3" json:"delta,omitempty"` // 变化量(正数增加,负数减少
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
// 增加累计上架时长请求内部RPC调用用于galleryService展品下架时累加时长
type AddExhibitionHoursRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 用户ID
StarId int64 `protobuf:"varint,2,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"` // 明星ID
ExhibitionHours int64 `protobuf:"varint,3,opt,name=exhibition_hours,json=exhibitionHours,proto3" json:"exhibition_hours,omitempty"` // 本次展出的时长(小时
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AddExperienceRequest) Reset() {
*x = AddExperienceRequest{}
func (x *AddExhibitionHoursRequest) Reset() {
*x = AddExhibitionHoursRequest{}
mi := &file_user_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AddExperienceRequest) String() string {
func (x *AddExhibitionHoursRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AddExperienceRequest) ProtoMessage() {}
func (*AddExhibitionHoursRequest) ProtoMessage() {}
func (x *AddExperienceRequest) ProtoReflect() protoreflect.Message {
func (x *AddExhibitionHoursRequest) ProtoReflect() protoreflect.Message {
mi := &file_user_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@ -1761,55 +1777,57 @@ func (x *AddExperienceRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use AddExperienceRequest.ProtoReflect.Descriptor instead.
func (*AddExperienceRequest) Descriptor() ([]byte, []int) {
// Deprecated: Use AddExhibitionHoursRequest.ProtoReflect.Descriptor instead.
func (*AddExhibitionHoursRequest) Descriptor() ([]byte, []int) {
return file_user_proto_rawDescGZIP(), []int{27}
}
func (x *AddExperienceRequest) GetUserId() int64 {
func (x *AddExhibitionHoursRequest) GetUserId() int64 {
if x != nil {
return x.UserId
}
return 0
}
func (x *AddExperienceRequest) GetStarId() int64 {
func (x *AddExhibitionHoursRequest) GetStarId() int64 {
if x != nil {
return x.StarId
}
return 0
}
func (x *AddExperienceRequest) GetDelta() int64 {
func (x *AddExhibitionHoursRequest) GetExhibitionHours() int64 {
if x != nil {
return x.Delta
return x.ExhibitionHours
}
return 0
}
// 增加经验值响应
type AddExperienceResponse struct {
// 增加累计上架时长响应
type AddExhibitionHoursResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
NewExperience int64 `protobuf:"varint,2,opt,name=new_experience,json=newExperience,proto3" json:"new_experience,omitempty"` // 更新后的经验值
NewLevel int32 `protobuf:"varint,2,opt,name=new_level,json=newLevel,proto3" json:"new_level,omitempty"` // 新的等级
LevelDelta int32 `protobuf:"varint,3,opt,name=level_delta,json=levelDelta,proto3" json:"level_delta,omitempty"` // 等级变化量(正数=升级0=无变化)
CrystalReward int64 `protobuf:"varint,4,opt,name=crystal_reward,json=crystalReward,proto3" json:"crystal_reward,omitempty"` // 升级水晶奖励无升级时为0
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AddExperienceResponse) Reset() {
*x = AddExperienceResponse{}
func (x *AddExhibitionHoursResponse) Reset() {
*x = AddExhibitionHoursResponse{}
mi := &file_user_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AddExperienceResponse) String() string {
func (x *AddExhibitionHoursResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AddExperienceResponse) ProtoMessage() {}
func (*AddExhibitionHoursResponse) ProtoMessage() {}
func (x *AddExperienceResponse) ProtoReflect() protoreflect.Message {
func (x *AddExhibitionHoursResponse) ProtoReflect() protoreflect.Message {
mi := &file_user_proto_msgTypes[28]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@ -1821,21 +1839,35 @@ func (x *AddExperienceResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use AddExperienceResponse.ProtoReflect.Descriptor instead.
func (*AddExperienceResponse) Descriptor() ([]byte, []int) {
// Deprecated: Use AddExhibitionHoursResponse.ProtoReflect.Descriptor instead.
func (*AddExhibitionHoursResponse) Descriptor() ([]byte, []int) {
return file_user_proto_rawDescGZIP(), []int{28}
}
func (x *AddExperienceResponse) GetBase() *common.BaseResponse {
func (x *AddExhibitionHoursResponse) GetBase() *common.BaseResponse {
if x != nil {
return x.Base
}
return nil
}
func (x *AddExperienceResponse) GetNewExperience() int64 {
func (x *AddExhibitionHoursResponse) GetNewLevel() int32 {
if x != nil {
return x.NewExperience
return x.NewLevel
}
return 0
}
func (x *AddExhibitionHoursResponse) GetLevelDelta() int32 {
if x != nil {
return x.LevelDelta
}
return 0
}
func (x *AddExhibitionHoursResponse) GetCrystalReward() int64 {
if x != nil {
return x.CrystalReward
}
return 0
}
@ -2829,7 +2861,7 @@ const file_user_proto_rawDesc = "" +
"\x15global_wallet_address\x18\x04 \x01(\tR\x13globalWalletAddress\x12\x1b\n" +
"\tis_active\x18\x05 \x01(\bR\bisActive\x12\x1d\n" +
"\n" +
"created_at\x18\x06 \x01(\x03R\tcreatedAt\"\xfa\x03\n" +
"created_at\x18\x06 \x01(\x03R\tcreatedAt\"\xda\x03\n" +
"\n" +
"FanProfile\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x17\n" +
@ -2838,23 +2870,20 @@ const file_user_proto_rawDesc = "" +
"\bnickname\x18\x04 \x01(\tR\bnickname\x12\x14\n" +
"\x05level\x18\x05 \x01(\x05R\x05level\x12\x14\n" +
"\x05times\x18\x06 \x01(\x05R\x05times\x12\x16\n" +
"\x06social\x18\a \x01(\x05R\x06social\x12\x1e\n" +
"\n" +
"experience\x18\b \x01(\x03R\n" +
"experience\x12!\n" +
"\fcoin_balance\x18\t \x01(\x03R\vcoinBalance\x12'\n" +
"\x0fcrystal_balance\x18\n" +
" \x01(\x03R\x0ecrystalBalance\x12\x12\n" +
"\x04tags\x18\v \x03(\tR\x04tags\x12\x1d\n" +
"\x06social\x18\a \x01(\x05R\x06social\x12!\n" +
"\fcoin_balance\x18\b \x01(\x03R\vcoinBalance\x12'\n" +
"\x0fcrystal_balance\x18\t \x01(\x03R\x0ecrystalBalance\x12\x12\n" +
"\x04tags\x18\n" +
" \x03(\tR\x04tags\x12\x1d\n" +
"\n" +
"avatar_url\x18\x11 \x01(\tR\tavatarUrl\x12\x1d\n" +
"\n" +
"created_at\x18\f \x01(\x03R\tcreatedAt\x12%\n" +
"\x0estarbook_limit\x18\r \x01(\x05R\rstarbookLimit\x12\x1d\n" +
"created_at\x18\v \x01(\x03R\tcreatedAt\x12%\n" +
"\x0estarbook_limit\x18\f \x01(\x05R\rstarbookLimit\x12\x1d\n" +
"\n" +
"slot_limit\x18\x0e \x01(\x05R\tslotLimit\x12!\n" +
"\fassets_count\x18\x0f \x01(\x05R\vassetsCount\x12#\n" +
"\rchain_address\x18\x10 \x01(\tR\fchainAddress\"\xf6\x01\n" +
"slot_limit\x18\r \x01(\x05R\tslotLimit\x12!\n" +
"\fassets_count\x18\x0e \x01(\x05R\vassetsCount\x12#\n" +
"\rchain_address\x18\x0f \x01(\tR\fchainAddress\"\xf6\x01\n" +
"\x04Star\x12\x17\n" +
"\astar_id\x18\x01 \x01(\x03R\x06starId\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x17\n" +
@ -2939,11 +2968,15 @@ const file_user_proto_rawDesc = "" +
"\x1eUpdateFanProfileSocialResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1d\n" +
"\n" +
"new_social\x18\x02 \x01(\x05R\tnewSocial\"e\n" +
"new_social\x18\x02 \x01(\x05R\tnewSocial\"\xc5\x01\n" +
"\x1bUpdateCrystalBalanceRequest\x12\x17\n" +
"\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x17\n" +
"\astar_id\x18\x02 \x01(\x03R\x06starId\x12\x14\n" +
"\x05delta\x18\x03 \x01(\x03R\x05delta\"q\n" +
"\x05delta\x18\x03 \x01(\x03R\x05delta\x12\x1f\n" +
"\vchange_type\x18\x04 \x01(\tR\n" +
"changeType\x12\x1b\n" +
"\tsource_id\x18\x05 \x01(\tR\bsourceId\x12 \n" +
"\vdescription\x18\x06 \x01(\tR\vdescription\"q\n" +
"\x1cUpdateCrystalBalanceResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1f\n" +
"\vnew_balance\x18\x02 \x01(\x03R\n" +
@ -2954,14 +2987,17 @@ const file_user_proto_rawDesc = "" +
"\x05delta\x18\x03 \x01(\x05R\x05delta\"j\n" +
"\x19UpdateAssetsCountResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1b\n" +
"\tnew_count\x18\x02 \x01(\x05R\bnewCount\"^\n" +
"\x14AddExperienceRequest\x12\x17\n" +
"\tnew_count\x18\x02 \x01(\x05R\bnewCount\"x\n" +
"\x19AddExhibitionHoursRequest\x12\x17\n" +
"\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x17\n" +
"\astar_id\x18\x02 \x01(\x03R\x06starId\x12\x14\n" +
"\x05delta\x18\x03 \x01(\x03R\x05delta\"p\n" +
"\x15AddExperienceResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12%\n" +
"\x0enew_experience\x18\x02 \x01(\x03R\rnewExperience\"\x17\n" +
"\astar_id\x18\x02 \x01(\x03R\x06starId\x12)\n" +
"\x10exhibition_hours\x18\x03 \x01(\x03R\x0fexhibitionHours\"\xb3\x01\n" +
"\x1aAddExhibitionHoursResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1b\n" +
"\tnew_level\x18\x02 \x01(\x05R\bnewLevel\x12\x1f\n" +
"\vlevel_delta\x18\x03 \x01(\x05R\n" +
"levelDelta\x12%\n" +
"\x0ecrystal_reward\x18\x04 \x01(\x03R\rcrystalReward\"\x17\n" +
"\x15GetCurrentUserRequest\"\xea\x01\n" +
"\x16GetCurrentUserResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12&\n" +
@ -3023,7 +3059,7 @@ const file_user_proto_rawDesc = "" +
"\n" +
"expires_in\x18\x03 \x01(\x03R\texpiresIn\x129\n" +
"\vfan_profile\x18\x04 \x01(\v2\x18.topfans.user.FanProfileR\n" +
"fanProfile2\xca\x14\n" +
"fanProfile2\xd9\x14\n" +
"\x11UserSocialService\x12k\n" +
"\bRegister\x12\x1d.topfans.user.RegisterRequest\x1a\x1e.topfans.user.RegisterResponse\" \x82\xd3\xe4\x93\x02\x1a:\x01*\"\x15/api/v1/auth/register\x12_\n" +
"\x05Login\x12\x1a.topfans.user.LoginRequest\x1a\x1b.topfans.user.LoginResponse\"\x1d\x82\xd3\xe4\x93\x02\x17:\x01*\"\x12/api/v1/auth/login\x12v\n" +
@ -3036,8 +3072,8 @@ const file_user_proto_rawDesc = "" +
"\rGetFanProfile\x12\".topfans.user.GetFanProfileRequest\x1a#.topfans.user.GetFanProfileResponse\"6\x82\xd3\xe4\x93\x020\x12./api/v1/users/{user_id}/fan-profiles/{star_id}\x12s\n" +
"\x16UpdateFanProfileSocial\x12+.topfans.user.UpdateFanProfileSocialRequest\x1a,.topfans.user.UpdateFanProfileSocialResponse\x12m\n" +
"\x14UpdateCrystalBalance\x12).topfans.user.UpdateCrystalBalanceRequest\x1a*.topfans.user.UpdateCrystalBalanceResponse\x12d\n" +
"\x11UpdateAssetsCount\x12&.topfans.user.UpdateAssetsCountRequest\x1a'.topfans.user.UpdateAssetsCountResponse\x12X\n" +
"\rAddExperience\x12\".topfans.user.AddExperienceRequest\x1a#.topfans.user.AddExperienceResponse\x12t\n" +
"\x11UpdateAssetsCount\x12&.topfans.user.UpdateAssetsCountRequest\x1a'.topfans.user.UpdateAssetsCountResponse\x12g\n" +
"\x12AddExhibitionHours\x12'.topfans.user.AddExhibitionHoursRequest\x1a(.topfans.user.AddExhibitionHoursResponse\x12t\n" +
"\x0eGetCurrentUser\x12#.topfans.user.GetCurrentUserRequest\x1a$.topfans.user.GetCurrentUserResponse\"\x17\x82\xd3\xe4\x93\x02\x11\x12\x0f/api/v1/auth/me\x12q\n" +
"\fGetMyProfile\x12!.topfans.user.GetMyProfileRequest\x1a\".topfans.user.GetMyProfileResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\x12\x12/api/v1/me/profile\x12z\n" +
"\x0eUpdateNickname\x12#.topfans.user.UpdateNicknameRequest\x1a$.topfans.user.UpdateNicknameResponse\"\x1d\x82\xd3\xe4\x93\x02\x17:\x01*\"\x12/api/v1/me/profile\x12\x80\x01\n" +
@ -3089,8 +3125,8 @@ var file_user_proto_goTypes = []any{
(*UpdateCrystalBalanceResponse)(nil), // 24: topfans.user.UpdateCrystalBalanceResponse
(*UpdateAssetsCountRequest)(nil), // 25: topfans.user.UpdateAssetsCountRequest
(*UpdateAssetsCountResponse)(nil), // 26: topfans.user.UpdateAssetsCountResponse
(*AddExperienceRequest)(nil), // 27: topfans.user.AddExperienceRequest
(*AddExperienceResponse)(nil), // 28: topfans.user.AddExperienceResponse
(*AddExhibitionHoursRequest)(nil), // 27: topfans.user.AddExhibitionHoursRequest
(*AddExhibitionHoursResponse)(nil), // 28: topfans.user.AddExhibitionHoursResponse
(*GetCurrentUserRequest)(nil), // 29: topfans.user.GetCurrentUserRequest
(*GetCurrentUserResponse)(nil), // 30: topfans.user.GetCurrentUserResponse
(*GetMyProfileRequest)(nil), // 31: topfans.user.GetMyProfileRequest
@ -3132,7 +3168,7 @@ var file_user_proto_depIdxs = []int32{
48, // 16: topfans.user.UpdateFanProfileSocialResponse.base:type_name -> topfans.common.BaseResponse
48, // 17: topfans.user.UpdateCrystalBalanceResponse.base:type_name -> topfans.common.BaseResponse
48, // 18: topfans.user.UpdateAssetsCountResponse.base:type_name -> topfans.common.BaseResponse
48, // 19: topfans.user.AddExperienceResponse.base:type_name -> topfans.common.BaseResponse
48, // 19: topfans.user.AddExhibitionHoursResponse.base:type_name -> topfans.common.BaseResponse
48, // 20: topfans.user.GetCurrentUserResponse.base:type_name -> topfans.common.BaseResponse
0, // 21: topfans.user.GetCurrentUserResponse.user:type_name -> topfans.user.User
1, // 22: topfans.user.GetCurrentUserResponse.fan_profile:type_name -> topfans.user.FanProfile
@ -3167,7 +3203,7 @@ var file_user_proto_depIdxs = []int32{
21, // 51: topfans.user.UserSocialService.UpdateFanProfileSocial:input_type -> topfans.user.UpdateFanProfileSocialRequest
23, // 52: topfans.user.UserSocialService.UpdateCrystalBalance:input_type -> topfans.user.UpdateCrystalBalanceRequest
25, // 53: topfans.user.UserSocialService.UpdateAssetsCount:input_type -> topfans.user.UpdateAssetsCountRequest
27, // 54: topfans.user.UserSocialService.AddExperience:input_type -> topfans.user.AddExperienceRequest
27, // 54: topfans.user.UserSocialService.AddExhibitionHours:input_type -> topfans.user.AddExhibitionHoursRequest
29, // 55: topfans.user.UserSocialService.GetCurrentUser:input_type -> topfans.user.GetCurrentUserRequest
31, // 56: topfans.user.UserSocialService.GetMyProfile:input_type -> topfans.user.GetMyProfileRequest
33, // 57: topfans.user.UserSocialService.UpdateNickname:input_type -> topfans.user.UpdateNicknameRequest
@ -3189,7 +3225,7 @@ var file_user_proto_depIdxs = []int32{
22, // 73: topfans.user.UserSocialService.UpdateFanProfileSocial:output_type -> topfans.user.UpdateFanProfileSocialResponse
24, // 74: topfans.user.UserSocialService.UpdateCrystalBalance:output_type -> topfans.user.UpdateCrystalBalanceResponse
26, // 75: topfans.user.UserSocialService.UpdateAssetsCount:output_type -> topfans.user.UpdateAssetsCountResponse
28, // 76: topfans.user.UserSocialService.AddExperience:output_type -> topfans.user.AddExperienceResponse
28, // 76: topfans.user.UserSocialService.AddExhibitionHours:output_type -> topfans.user.AddExhibitionHoursResponse
30, // 77: topfans.user.UserSocialService.GetCurrentUser:output_type -> topfans.user.GetCurrentUserResponse
32, // 78: topfans.user.UserSocialService.GetMyProfile:output_type -> topfans.user.GetMyProfileResponse
34, // 79: topfans.user.UserSocialService.UpdateNickname:output_type -> topfans.user.UpdateNicknameResponse

View File

@ -60,8 +60,8 @@ const (
UserSocialServiceUpdateCrystalBalanceProcedure = "/topfans.user.UserSocialService/UpdateCrystalBalance"
// UserSocialServiceUpdateAssetsCountProcedure is the fully-qualified name of the UserSocialService's UpdateAssetsCount RPC.
UserSocialServiceUpdateAssetsCountProcedure = "/topfans.user.UserSocialService/UpdateAssetsCount"
// UserSocialServiceAddExperienceProcedure is the fully-qualified name of the UserSocialService's AddExperience RPC.
UserSocialServiceAddExperienceProcedure = "/topfans.user.UserSocialService/AddExperience"
// UserSocialServiceAddExhibitionHoursProcedure is the fully-qualified name of the UserSocialService's AddExhibitionHours RPC.
UserSocialServiceAddExhibitionHoursProcedure = "/topfans.user.UserSocialService/AddExhibitionHours"
// UserSocialServiceGetCurrentUserProcedure is the fully-qualified name of the UserSocialService's GetCurrentUser RPC.
UserSocialServiceGetCurrentUserProcedure = "/topfans.user.UserSocialService/GetCurrentUser"
// UserSocialServiceGetMyProfileProcedure is the fully-qualified name of the UserSocialService's GetMyProfile RPC.
@ -100,7 +100,7 @@ type UserSocialService interface {
UpdateFanProfileSocial(ctx context.Context, req *UpdateFanProfileSocialRequest, opts ...client.CallOption) (*UpdateFanProfileSocialResponse, error)
UpdateCrystalBalance(ctx context.Context, req *UpdateCrystalBalanceRequest, opts ...client.CallOption) (*UpdateCrystalBalanceResponse, error)
UpdateAssetsCount(ctx context.Context, req *UpdateAssetsCountRequest, opts ...client.CallOption) (*UpdateAssetsCountResponse, error)
AddExperience(ctx context.Context, req *AddExperienceRequest, opts ...client.CallOption) (*AddExperienceResponse, error)
AddExhibitionHours(ctx context.Context, req *AddExhibitionHoursRequest, opts ...client.CallOption) (*AddExhibitionHoursResponse, error)
GetCurrentUser(ctx context.Context, req *GetCurrentUserRequest, opts ...client.CallOption) (*GetCurrentUserResponse, error)
GetMyProfile(ctx context.Context, req *GetMyProfileRequest, opts ...client.CallOption) (*GetMyProfileResponse, error)
UpdateNickname(ctx context.Context, req *UpdateNicknameRequest, opts ...client.CallOption) (*UpdateNicknameResponse, error)
@ -228,9 +228,9 @@ func (c *UserSocialServiceImpl) UpdateAssetsCount(ctx context.Context, req *Upda
return resp, nil
}
func (c *UserSocialServiceImpl) AddExperience(ctx context.Context, req *AddExperienceRequest, opts ...client.CallOption) (*AddExperienceResponse, error) {
resp := new(AddExperienceResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "AddExperience", opts...); err != nil {
func (c *UserSocialServiceImpl) AddExhibitionHours(ctx context.Context, req *AddExhibitionHoursRequest, opts ...client.CallOption) (*AddExhibitionHoursResponse, error) {
resp := new(AddExhibitionHoursResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "AddExhibitionHours", opts...); err != nil {
return nil, err
}
return resp, nil
@ -310,7 +310,7 @@ func (c *UserSocialServiceImpl) SwitchIdentity(ctx context.Context, req *SwitchI
var UserSocialService_ClientInfo = client.ClientInfo{
InterfaceName: "topfans.user.UserSocialService",
MethodNames: []string{"Register", "Login", "RefreshToken", "ValidateToken", "Logout", "CheckNickname", "CheckMobile", "GetUser", "GetFanProfile", "UpdateFanProfileSocial", "UpdateCrystalBalance", "UpdateAssetsCount", "AddExperience", "GetCurrentUser", "GetMyProfile", "UpdateNickname", "UpdatePassword", "UpdateAvatar", "GetFanIdentities", "GetMyFanIdentities", "AddIdentity", "SwitchIdentity"},
MethodNames: []string{"Register", "Login", "RefreshToken", "ValidateToken", "Logout", "CheckNickname", "CheckMobile", "GetUser", "GetFanProfile", "UpdateFanProfileSocial", "UpdateCrystalBalance", "UpdateAssetsCount", "AddExhibitionHours", "GetCurrentUser", "GetMyProfile", "UpdateNickname", "UpdatePassword", "UpdateAvatar", "GetFanIdentities", "GetMyFanIdentities", "AddIdentity", "SwitchIdentity"},
ConnectionInjectFunc: func(dubboCliRaw interface{}, conn *client.Connection) {
dubboCli := dubboCliRaw.(*UserSocialServiceImpl)
dubboCli.conn = conn
@ -331,7 +331,7 @@ type UserSocialServiceHandler interface {
UpdateFanProfileSocial(context.Context, *UpdateFanProfileSocialRequest) (*UpdateFanProfileSocialResponse, error)
UpdateCrystalBalance(context.Context, *UpdateCrystalBalanceRequest) (*UpdateCrystalBalanceResponse, error)
UpdateAssetsCount(context.Context, *UpdateAssetsCountRequest) (*UpdateAssetsCountResponse, error)
AddExperience(context.Context, *AddExperienceRequest) (*AddExperienceResponse, error)
AddExhibitionHours(context.Context, *AddExhibitionHoursRequest) (*AddExhibitionHoursResponse, error)
GetCurrentUser(context.Context, *GetCurrentUserRequest) (*GetCurrentUserResponse, error)
GetMyProfile(context.Context, *GetMyProfileRequest) (*GetMyProfileResponse, error)
UpdateNickname(context.Context, *UpdateNicknameRequest) (*UpdateNicknameResponse, error)
@ -536,14 +536,14 @@ var UserSocialService_ServiceInfo = server.ServiceInfo{
},
},
{
Name: "AddExperience",
Name: "AddExhibitionHours",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(AddExperienceRequest)
return new(AddExhibitionHoursRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*AddExperienceRequest)
res, err := handler.(UserSocialServiceHandler).AddExperience(ctx, req)
req := args[0].(*AddExhibitionHoursRequest)
res, err := handler.(UserSocialServiceHandler).AddExhibitionHours(ctx, req)
if err != nil {
return nil, err
}

View File

@ -15,9 +15,8 @@ message DailyTaskItem {
string name = 3;
string description = 4;
int64 crystal_reward = 5;
int64 exp_reward = 6;
string status = 7; // pending/completed/claimed
bool can_claim = 8;
string status = 6; // pending/completed/claimed
bool can_claim = 7;
}
message GetDailyTasksRequest {
@ -52,7 +51,6 @@ message ClaimDailyTaskResponse {
topfans.common.BaseResponse base = 1;
bool success = 2;
int64 crystal_balance = 3;
int64 experience = 4;
}
message ClaimAllDailyTasksRequest {
@ -63,8 +61,7 @@ message ClaimAllDailyTasksResponse {
topfans.common.BaseResponse base = 1;
int32 claimed_count = 2;
int64 crystal_balance = 3;
int64 experience = 4;
repeated string claimed_task_keys = 5;
repeated string claimed_task_keys = 4;
}
// ==================== ====================
@ -74,11 +71,10 @@ message OnboardingStage {
string name = 2;
repeated string required_task_keys = 3;
int64 crystal_reward = 4;
int64 exp_reward = 5;
string status = 6; // pending/completed/in_progress
bool is_current = 7;
bool all_tasks_completed = 8; //
bool is_reward_claimed = 9; //
string status = 5; // pending/completed/in_progress
bool is_current = 6;
bool all_tasks_completed = 7; //
bool is_reward_claimed = 8; //
}
message CompleteGuideRequest {
@ -123,7 +119,6 @@ message ClaimOnboardingRewardResponse {
topfans.common.BaseResponse base = 1;
bool success = 2;
string crystal_balance = 3; // 使 string Dubbo int64 bug
string experience = 4; // 使 string Dubbo int64 bug
}
// ==================== ====================

View File

@ -28,16 +28,15 @@ message FanProfile {
int32 level = 5; //
int32 times = 6; //
int32 social = 7; //
int64 experience = 8; //
int64 coin_balance = 9; //
int64 crystal_balance = 10; //
repeated string tags = 11; //
int64 coin_balance = 8; //
int64 crystal_balance = 9; //
repeated string tags = 10; //
string avatar_url = 17; // URL
int64 created_at = 12;
int32 starbook_limit = 13; //
int32 slot_limit = 14; //
int32 assets_count = 15; //
string chain_address = 16; //
int64 created_at = 11;
int32 starbook_limit = 12; //
int32 slot_limit = 13; //
int32 assets_count = 14; //
string chain_address = 15; //
}
//
@ -190,6 +189,9 @@ message UpdateCrystalBalanceRequest {
int64 user_id = 1; // ID
int64 star_id = 2; // ID
int64 delta = 3; //
string change_type = 4; // task_reward/mint_cost/mint_reward/exhibition_revenue/level_up_bonus/manual_adjust
string source_id = 5; // ID
string description = 6; //
}
//
@ -211,17 +213,19 @@ message UpdateAssetsCountResponse {
int32 new_count = 2; //
}
// RPC调用taskService增加经验
message AddExperienceRequest {
// RPC调用galleryService展品下架时累加时长
message AddExhibitionHoursRequest {
int64 user_id = 1; // ID
int64 star_id = 2; // ID
int64 delta = 3; //
int64 exhibition_hours = 3; //
}
//
message AddExperienceResponse {
//
message AddExhibitionHoursResponse {
topfans.common.BaseResponse base = 1;
int64 new_experience = 2; //
int32 new_level = 2; //
int32 level_delta = 3; // =0=
int64 crystal_reward = 4; // 0
}
//
@ -414,8 +418,8 @@ service UserSocialService {
// RPCassetService调用
rpc UpdateAssetsCount(UpdateAssetsCountRequest) returns (UpdateAssetsCountResponse);
// RPCtaskService调用
rpc AddExperience(AddExperienceRequest) returns (AddExperienceResponse);
// RPCgalleryService调用
rpc AddExhibitionHours(AddExhibitionHoursRequest) returns (AddExhibitionHoursResponse);
rpc GetCurrentUser(GetCurrentUserRequest) returns (GetCurrentUserResponse) {
option (google.api.http) = {

View File

@ -0,0 +1,278 @@
-- ============================================================
-- 经济系统建表脚本
-- 执行方式: psql -h <host> -U <user> -d <db> -f backend/scripts/20260513_economic_system.sql
-- 创建日期: 2026-05-13
-- 说明: 本脚本用于初始化经济系统相关的表和配置
-- - fan_profiles 表新增字段: revenue_boost_bps, like_bet_count
-- - 铸造消耗配置、用户铸爱累计
-- - 等级阈值、升级奖励配置
-- - 水晶/游戏币流水表
-- ============================================================
-- 1. Add revenue_boost_bps column to fan_profiles (if not exists)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'fan_profiles' AND column_name = 'revenue_boost_bps') THEN
ALTER TABLE fan_profiles ADD COLUMN revenue_boost_bps INT NOT NULL DEFAULT 0;
END IF;
END $$;
-- 1.2 Add like_bet_count column to fan_profiles (if not exists)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'fan_profiles' AND column_name = 'like_bet_count') THEN
ALTER TABLE fan_profiles ADD COLUMN like_bet_count INT NOT NULL DEFAULT 0;
END IF;
END $$;
-- 2. 铸造消耗配置表
CREATE TABLE IF NOT EXISTS mint_cost_config (
id BIGSERIAL PRIMARY KEY,
mint_count INT NOT NULL UNIQUE,
cost_crystal BIGINT NOT NULL,
probability BIGINT DEFAULT 0,
reward_type VARCHAR(50) DEFAULT NULL,
reward_value BIGINT DEFAULT 0,
description VARCHAR(255),
updated_at BIGINT NOT NULL
);
COMMENT ON TABLE mint_cost_config IS '铸造消耗配置表,记录每次铸造的水晶消耗和保底概率';
-- 初始数据
INSERT INTO mint_cost_config (mint_count, cost_crystal, probability, reward_type, reward_value, description, updated_at) VALUES
(1, 2, 0, NULL, 0, '第1次铸造', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(2, 4, 0, NULL, 0, '第2次铸造', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(3, 8, 0, NULL, 0, '第3次铸造', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(4, 16, 0, NULL, 0, '第4次铸造', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(5, 32, 0, NULL, 0, '第5次铸造', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(6, 64, 0, NULL, 0, '第6次铸造', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(7, 128, 0, NULL, 0, '第7次铸造', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(8, 256, 0, NULL, 0, '第8次铸造', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(9, 512, 20, '收益提升', 500, '20%概率获得500 bps+5%)永久收益提升(小保底)', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(10, 1024, 100, '收益提升', 500, '100%概率获得500 bps+5%)永久收益提升(大保底)', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000))
ON CONFLICT (mint_count) DO NOTHING;
-- 3. 用户铸爱累计表
CREATE TABLE IF NOT EXISTS user_mint_count (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
star_id BIGINT NOT NULL,
mint_count INT NOT NULL DEFAULT 0,
revenue_boost_bps INT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL,
CONSTRAINT uk_user_mint_star UNIQUE (user_id, star_id)
);
COMMENT ON TABLE user_mint_count IS '用户铸爱累计表,记录用户累计铸造次数和永久收益提升基点';
-- 4. 等级阈值配置表
CREATE TABLE IF NOT EXISTS level_thresholds (
level INT PRIMARY KEY,
max_exhibition_hours BIGINT NOT NULL,
like_bet_count INT NOT NULL,
description VARCHAR(100)
);
COMMENT ON TABLE level_thresholds IS '等级阈值配置表,记录升级到每个等级需要的累计上架时长和点赞押注次数';
-- 初始数据20级满级
INSERT INTO level_thresholds (level, max_exhibition_hours, like_bet_count, description) VALUES
(1, 0, 0, '1级新手'),
(2, 6, 6, '2级粉丝'),
(3, 12, 7, '3级真爱'),
(4, 18, 8, '4级铁粉'),
(5, 24, 9, '5级钻石粉'),
(6, 30, 9, '6级钻石粉'),
(7, 36, 10, '7级钻石粉'),
(8, 42, 11, '8级钻石粉'),
(9, 48, 12, '9级钻石粉'),
(10, 54, 13, '10级钻石粉'),
(11, 60, 13, '11级钻石粉'),
(12, 66, 13, '12级钻石粉'),
(13, 72, 14, '13级钻石粉'),
(14, 78, 15, '14级钻石粉'),
(15, 84, 16, '15级钻石粉'),
(16, 90, 16, '16级钻石粉'),
(17, 96, 17, '17级钻石粉'),
(18, 102, 18, '18级钻石粉'),
(19, 108, 19, '19级钻石粉'),
(20, 114, 20, '20级终极粉')
ON CONFLICT (level) DO NOTHING;
-- 5. 用户累计上架时长表
CREATE TABLE IF NOT EXISTS user_exhibition_hours (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
star_id BIGINT NOT NULL,
total_exhibition_hours BIGINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL,
CONSTRAINT uk_exhibition_user_star UNIQUE (user_id, star_id)
);
COMMENT ON TABLE user_exhibition_hours IS '用户累计上架时长表,记录用户累计上架时长';
-- 6. 等级上限配置表
CREATE TABLE IF NOT EXISTS level_cap_config (
id BIGSERIAL PRIMARY KEY,
max_level INT NOT NULL DEFAULT 20,
updated_at BIGINT NOT NULL
);
COMMENT ON TABLE level_cap_config IS '等级上限配置表,记录最高等级';
INSERT INTO level_cap_config (max_level, updated_at) VALUES (20, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000))
ON CONFLICT DO NOTHING;
-- 7. 等级升级条件配置表21级+
CREATE TABLE IF NOT EXISTS level_upgrade_conditions (
level INT PRIMARY KEY,
require_total_hours BIGINT NOT NULL,
require_dazi_level INT DEFAULT 0,
description VARCHAR(100),
updated_at BIGINT NOT NULL
);
COMMENT ON TABLE level_upgrade_conditions IS '等级升级条件配置表记录21级及以上的升级条件';
-- 8. 升级奖励配置表
CREATE TABLE IF NOT EXISTS level_up_reward_config (
id BIGSERIAL PRIMARY KEY,
level INT NOT NULL,
reward_type VARCHAR(50) NOT NULL,
reward_value BIGINT NOT NULL DEFAULT 0,
is_enabled BOOLEAN DEFAULT true,
updated_at BIGINT NOT NULL,
CONSTRAINT uk_level_reward_type UNIQUE (level, reward_type)
);
COMMENT ON TABLE level_up_reward_config IS '升级奖励配置表,记录升级时发放的奖励类型和数值';
-- 初始数据21级示例后续可扩展更多等级
INSERT INTO level_upgrade_conditions (level, require_total_hours, require_dazi_level, description, updated_at) VALUES
(21, 120, 21, '21级总时长120h + 搭子21级', ROUND(EXTRACT(EPOCH FROM NOW()) * 1000))
ON CONFLICT (level) DO NOTHING;
-- 8. 升级奖励配置表
CREATE TABLE IF NOT EXISTS level_up_reward_config (
id BIGSERIAL PRIMARY KEY,
level INT NOT NULL,
reward_type VARCHAR(50) NOT NULL,
reward_value BIGINT NOT NULL DEFAULT 0,
is_enabled BOOLEAN DEFAULT true,
updated_at BIGINT NOT NULL,
CONSTRAINT uk_level_reward_type UNIQUE (level, reward_type)
);
-- 初始数据示例
INSERT INTO level_up_reward_config (level, reward_type, reward_value, is_enabled, updated_at) VALUES
(2, 'crystal', 10, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(2, 'like_bet_count', 1, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(3, 'crystal', 20, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(3, 'like_bet_count', 1, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(4, 'crystal', 30, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(4, 'like_bet_count', 1, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(5, 'crystal', 50, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(5, 'like_bet_count', 1, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(6, 'crystal', 80, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(7, 'crystal', 120, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(8, 'crystal', 180, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(9, 'crystal', 280, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(10, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(11, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(12, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(13, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(14, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(15, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(16, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(17, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(18, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(19, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(20, 'crystal', 500, true, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000))
ON CONFLICT (level, reward_type) DO NOTHING;
-- 9. 搭子等级阈值配置表(预留)
CREATE TABLE IF NOT EXISTS dazi_level_thresholds (
level INT PRIMARY KEY,
upgrade_condition VARCHAR(100),
condition_param INT DEFAULT 0,
description VARCHAR(100)
);
COMMENT ON TABLE dazi_level_thresholds IS '搭子等级阈值配置表(预留)';
-- 10. 用户搭子等级表(预留)
CREATE TABLE IF NOT EXISTS user_dazi_level (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
star_id BIGINT NOT NULL,
dazi_level INT NOT NULL DEFAULT 1,
updated_at BIGINT NOT NULL,
CONSTRAINT uk_dazi_user_star UNIQUE (user_id, star_id)
);
COMMENT ON TABLE user_dazi_level IS '用户搭子等级表(预留)';
-- 11. 水晶交易流水表(预留)
CREATE TABLE IF NOT EXISTS crystal_transaction_records (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
star_id BIGINT NOT NULL,
change_type VARCHAR(30) NOT NULL,
delta BIGINT NOT NULL,
balance_before BIGINT NOT NULL,
balance_after BIGINT NOT NULL,
source_id VARCHAR(100),
description VARCHAR(255),
created_at BIGINT NOT NULL
);
COMMENT ON TABLE crystal_transaction_records IS '水晶交易流水表,记录水晶的收支明细(复式记账)';
CREATE INDEX IF NOT EXISTS ix_crystal_tx_user_star ON crystal_transaction_records(user_id, star_id);
CREATE INDEX IF NOT EXISTS ix_crystal_tx_created ON crystal_transaction_records(created_at DESC);
CREATE INDEX IF NOT EXISTS ix_crystal_tx_change_type ON crystal_transaction_records(change_type);
-- 12. 游戏币交易流水表(预留)
CREATE TABLE IF NOT EXISTS coin_transaction_records (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
star_id BIGINT NOT NULL,
change_type VARCHAR(30) NOT NULL,
delta BIGINT NOT NULL,
balance_before BIGINT NOT NULL,
balance_after BIGINT NOT NULL,
source_id VARCHAR(100),
description VARCHAR(255),
created_at BIGINT NOT NULL
);
COMMENT ON TABLE coin_transaction_records IS '游戏币交易流水表(预留)';
CREATE INDEX IF NOT EXISTS ix_coin_tx_user_star ON coin_transaction_records(user_id, star_id);
CREATE INDEX IF NOT EXISTS ix_coin_tx_created ON coin_transaction_records(created_at DESC);
-- 13. 铸造奖励配置表(预留)
CREATE TABLE IF NOT EXISTS mint_reward_config (
id BIGSERIAL PRIMARY KEY,
star_id BIGINT NOT NULL UNIQUE,
base_reward BIGINT NOT NULL DEFAULT 0,
is_enabled BOOLEAN DEFAULT true,
updated_at BIGINT NOT NULL
);
COMMENT ON TABLE mint_reward_config IS '铸造奖励配置表(预留),记录每个明星的铸造基础奖励';
-- 初始数据0=全服默认)
INSERT INTO mint_reward_config (star_id, base_reward, is_enabled, updated_at) VALUES (0, 0, false, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000))
ON CONFLICT (star_id) DO NOTHING;
-- 14. 铸造阶梯奖励配置表(预留)
CREATE TABLE IF NOT EXISTS mint_milestone_config (
id BIGSERIAL PRIMARY KEY,
star_id BIGINT NOT NULL,
milestone_count INT NOT NULL,
bonus_reward BIGINT NOT NULL,
created_at BIGINT NOT NULL,
CONSTRAINT uk_milestone_star_count UNIQUE (star_id, milestone_count)
);
COMMENT ON TABLE mint_milestone_config IS '铸造阶梯奖励配置表(预留),记录铸造里程碑奖励';
-- 初始数据示例
INSERT INTO mint_milestone_config (star_id, milestone_count, bonus_reward, created_at) VALUES
(0, 10, 5, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(0, 30, 15, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000)),
(0, 100, 50, ROUND(EXTRACT(EPOCH FROM NOW()) * 1000))
ON CONFLICT (star_id, milestone_count) DO NOTHING;
-- ============================================================
-- 完成
-- ============================================================

View File

@ -63,8 +63,8 @@ ON CONFLICT (mobile) DO UPDATE SET password_hash = EXCLUDED.password_hash;
fmt.Println()
// 2. 插入粉丝档案
fmt.Printf(`INSERT INTO fan_profiles (user_id, star_id, nickname, level, times, social, experience, coin_balance, crystal_balance, tags, starbook_limit, slot_limit, assets_count, is_active, created_at, updated_at)
VALUES (%d, %d, '%s', 1, 1, 0, 0, 0, 1000, '[]'::jsonb, 3, 3, 2, true, %d, %d)
fmt.Printf(`INSERT INTO fan_profiles (user_id, star_id, nickname, level, times, social, coin_balance, crystal_balance, tags, starbook_limit, slot_limit, assets_count, is_active, created_at, updated_at)
VALUES (%d, %d, '%s', 1, 1, 0, 0, 1000, '[]'::jsonb, 3, 3, 2, true, %d, %d)
ON CONFLICT (user_id, star_id) DO UPDATE SET nickname = EXCLUDED.nickname, crystal_balance = EXCLUDED.crystal_balance;
`, user1ID, star1ID, user1Nickname, now, now)
fmt.Println()
@ -114,8 +114,8 @@ ON CONFLICT (mobile) DO UPDATE SET password_hash = EXCLUDED.password_hash;
fmt.Println()
// 2. 插入粉丝档案
fmt.Printf(`INSERT INTO fan_profiles (user_id, star_id, nickname, level, times, social, experience, coin_balance, crystal_balance, tags, starbook_limit, slot_limit, assets_count, is_active, created_at, updated_at)
VALUES (%d, %d, '%s', 1, 1, 0, 0, 0, 1000, '[]'::jsonb, 3, 3, 2, true, %d, %d)
fmt.Printf(`INSERT INTO fan_profiles (user_id, star_id, nickname, level, times, social, coin_balance, crystal_balance, tags, starbook_limit, slot_limit, assets_count, is_active, created_at, updated_at)
VALUES (%d, %d, '%s', 1, 1, 0, 0, 1000, '[]'::jsonb, 3, 3, 2, true, %d, %d)
ON CONFLICT (user_id, star_id) DO UPDATE SET nickname = EXCLUDED.nickname, crystal_balance = EXCLUDED.crystal_balance;
`, user2ID, star2ID, user2Nickname, now, now)
fmt.Println()

View File

@ -13,7 +13,7 @@ import (
// UserServiceClient User Service RPC 客户端接口
type UserServiceClient interface {
// UpdateCrystalBalance 更新水晶余额
UpdateCrystalBalance(ctx context.Context, userID, starID int64, delta int64) (int64, error)
UpdateCrystalBalance(ctx context.Context, userID, starID int64, delta int64, changeType string, sourceID string, description string) (int64, error)
// UpdateAssetsCount 更新资产数量
UpdateAssetsCount(ctx context.Context, userID, starID int64, delta int32) (int32, error)
@ -35,7 +35,7 @@ func NewUserServiceClient(client pbUser.UserSocialService) UserServiceClient {
}
// UpdateCrystalBalance 更新水晶余额
func (c *userServiceClient) UpdateCrystalBalance(ctx context.Context, userID, starID int64, delta int64) (int64, error) {
func (c *userServiceClient) UpdateCrystalBalance(ctx context.Context, userID, starID int64, delta int64, changeType string, sourceID string, description string) (int64, error) {
logger.Logger.Debug("Calling UserService.UpdateCrystalBalance",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
@ -46,6 +46,9 @@ func (c *userServiceClient) UpdateCrystalBalance(ctx context.Context, userID, st
UserId: userID,
StarId: starID,
Delta: delta,
ChangeType: changeType,
SourceId: sourceID,
Description: description,
})
if err != nil {

View File

@ -110,6 +110,8 @@ func main() {
mintOrderRepo := repository.NewMintOrderRepository(database.GetDB())
assetLikeRepo := repository.NewAssetLikeRepository(database.GetDB())
rankingRepo := repository.NewRankingRepository(database.GetDB())
mintCostRepo := repository.NewMintCostRepository()
userMintCountRepo := repository.NewUserMintCountRepository()
logger.Logger.Info("Repository layer initialized")
// 创建 Dubbo 客户端
@ -131,7 +133,7 @@ func main() {
// 创建 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)
mintService := service.NewMintService(assetRepo, mintOrderRepo, userClient, database.GetDB(), config.GlobalAssetConfig, registryRepo, mintCostRepo, userMintCountRepo)
assetLikeService := service.NewAssetLikeService(assetRepo, assetLikeRepo, database.GetDB())
rankingService := service.NewRankingService(rankingRepo, userClient)
logger.Logger.Info("Service layer initialized")

View File

@ -0,0 +1,45 @@
package repository
import (
"github.com/topfans/backend/pkg/database"
"github.com/topfans/backend/pkg/models"
"gorm.io/gorm"
)
// MintCostRepository 铸造消耗配置仓库
type MintCostRepository interface {
// GetByMintCount 根据铸爱次数获取配置
GetByMintCount(mintCount int32) (*models.MintCostConfig, error)
// GetAll 获取所有配置
GetAll() ([]*models.MintCostConfig, error)
}
type mintCostRepository struct {
db *gorm.DB
}
// NewMintCostRepository 创建铸造消耗配置仓库
func NewMintCostRepository() MintCostRepository {
return &mintCostRepository{
db: database.GetDB(),
}
}
// GetByMintCount 根据铸爱次数获取配置
func (r *mintCostRepository) GetByMintCount(mintCount int32) (*models.MintCostConfig, error) {
var config models.MintCostConfig
if err := r.db.Where("mint_count = ?", mintCount).First(&config).Error; err != nil {
return nil, err
}
return &config, nil
}
// GetAll 获取所有配置
func (r *mintCostRepository) GetAll() ([]*models.MintCostConfig, error) {
var configs []*models.MintCostConfig
if err := r.db.Order("mint_count ASC").Find(&configs).Error; err != nil {
return nil, err
}
return configs, nil
}

View File

@ -0,0 +1,115 @@
package repository
import (
"errors"
"time"
"github.com/topfans/backend/pkg/database"
"github.com/topfans/backend/pkg/models"
"gorm.io/gorm"
)
// UserMintCountRepository 用户铸爱累计仓库
type UserMintCountRepository interface {
// GetOrCreate 获取或创建用户铸爱累计记录
GetOrCreate(tx *gorm.DB, userID, starID int64) (*models.UserMintCount, bool, error)
// Get 获取用户铸爱累计记录
Get(userID, starID int64) (*models.UserMintCount, error)
// IncrementMintCount 累加铸爱次数
IncrementMintCount(tx *gorm.DB, userID, starID int64, delta int32) (*models.UserMintCount, error)
// UpdateRevenueBoost 更新永久收益提升
UpdateRevenueBoost(tx *gorm.DB, userID, starID int64, addBps int32) (*models.UserMintCount, error)
}
type userMintCountRepository struct {
db *gorm.DB
}
// NewUserMintCountRepository 创建用户铸爱累计仓库
func NewUserMintCountRepository() UserMintCountRepository {
return &userMintCountRepository{
db: database.GetDB(),
}
}
// GetOrCreate 获取或创建用户铸爱累计记录
func (r *userMintCountRepository) GetOrCreate(tx *gorm.DB, userID, starID int64) (*models.UserMintCount, bool, error) {
if tx == nil {
tx = r.db
}
var record models.UserMintCount
err := tx.Where("user_id = ? AND star_id = ?", userID, starID).First(&record).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
// 创建新记录
record = models.UserMintCount{
UserID: userID,
StarID: starID,
MintCount: 0,
RevenueBoostBps: 0,
UpdatedAt: time.Now().UnixMilli(),
}
if err := tx.Create(&record).Error; err != nil {
return nil, false, err
}
return &record, true, nil
}
if err != nil {
return nil, false, err
}
return &record, false, nil
}
// Get 获取用户铸爱累计记录
func (r *userMintCountRepository) Get(userID, starID int64) (*models.UserMintCount, error) {
var record models.UserMintCount
if err := r.db.Where("user_id = ? AND star_id = ?", userID, starID).First(&record).Error; err != nil {
return nil, err
}
return &record, nil
}
// IncrementMintCount 累加铸爱次数
func (r *userMintCountRepository) IncrementMintCount(tx *gorm.DB, userID, starID int64, delta int32) (*models.UserMintCount, error) {
if tx == nil {
tx = r.db
}
// 先查询当前记录
var record models.UserMintCount
if err := tx.Where("user_id = ? AND star_id = ?", userID, starID).First(&record).Error; err != nil {
return nil, err
}
// 更新铸爱次数
record.MintCount += delta
record.UpdatedAt = time.Now().UnixMilli()
if err := tx.Save(&record).Error; err != nil {
return nil, err
}
return &record, nil
}
// UpdateRevenueBoost 更新永久收益提升
func (r *userMintCountRepository) UpdateRevenueBoost(tx *gorm.DB, userID, starID int64, addBps int32) (*models.UserMintCount, error) {
if tx == nil {
tx = r.db
}
// 先查询当前记录
var record models.UserMintCount
if err := tx.Where("user_id = ? AND star_id = ?", userID, starID).First(&record).Error; err != nil {
return nil, err
}
// 更新收益提升
record.RevenueBoostBps += addBps
record.UpdatedAt = time.Now().UnixMilli()
if err := tx.Save(&record).Error; err != nil {
return nil, err
}
return &record, nil
}

View File

@ -31,6 +31,9 @@ type AssetRPCClient interface {
// GetAssetInfo 获取资产信息(简化版,用于展示)
GetAssetInfo(assetID, userID, starID int64) (*AssetInfo, error)
// GetAssetLikeCount 获取资产的点赞数
GetAssetLikeCount(assetID int64) int
// ClearAssetLikeRecords 清除资产点赞记录(不修改 like_count在藏品下架时调用
ClearAssetLikeRecords(assetID int64) error
}
@ -220,3 +223,26 @@ func (c *assetRPCClient) ClearAssetLikeRecords(assetID int64) error {
return nil
}
// GetAssetLikeCount 获取资产的点赞数(不验证用户)
func (c *assetRPCClient) GetAssetLikeCount(assetID int64) int {
ctx := context.Background()
resp, err := c.client.GetAsset(ctx, &pbAsset.GetAssetRequest{
AssetId: assetID,
})
if err != nil {
logger.Logger.Error("Failed to call AssetService.GetAsset for like count",
zap.Int64("asset_id", assetID),
zap.Error(err))
return 0
}
if resp == nil || resp.Base == nil || resp.Base.Code != pbCommon.StatusCode_STATUS_OK {
logger.Logger.Warn("AssetService.GetAsset returned error for like count",
zap.Int64("asset_id", assetID))
return 0
}
return int(resp.Asset.LikeCount)
}

View File

@ -0,0 +1,100 @@
package client
import (
"context"
"errors"
"fmt"
"github.com/topfans/backend/pkg/logger"
pbCommon "github.com/topfans/backend/pkg/proto/common"
pbTask "github.com/topfans/backend/pkg/proto/task"
"go.uber.org/zap"
)
// TaskRPCClient Task Service RPC客户端接口
type TaskRPCClient interface {
// OnExhibitionCompleted 当展位到期完成时调用,创建展示收益记录
OnExhibitionCompleted(ctx context.Context, req *OnExhibitionCompletedRequest) (*OnExhibitionCompletedResponse, error)
}
// OnExhibitionCompletedRequest 展位完成请求
type OnExhibitionCompletedRequest struct {
ExhibitionId int64
AssetId int64
SlotId int64
OccupierUid int64
OccupierStarId int64
SlotOwnerUid int64
StartTime int64
ExpireAt int64
CrystalAmount int64
}
// OnExhibitionCompletedResponse 展位完成响应
type OnExhibitionCompletedResponse struct {
RevenueRecordId int64
}
// taskRPCClient Task Service RPC客户端实现
type taskRPCClient struct {
client pbTask.TaskInternalService
}
// NewTaskRPCClient 创建Task Service RPC客户端
func NewTaskRPCClient(client pbTask.TaskInternalService) TaskRPCClient {
return &taskRPCClient{
client: client,
}
}
// OnExhibitionCompleted 当展位到期完成时调用,创建展示收益记录
func (c *taskRPCClient) OnExhibitionCompleted(ctx context.Context, req *OnExhibitionCompletedRequest) (*OnExhibitionCompletedResponse, error) {
logger.Logger.Debug("Calling TaskService.OnExhibitionCompleted",
zap.Int64("exhibition_id", req.ExhibitionId),
zap.Int64("asset_id", req.AssetId),
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))
if c.client == nil {
logger.Logger.Error("TaskRPCClient: client is nil")
return nil, errors.New("task client is nil")
}
pbReq := &pbTask.OnExhibitionCompletedRequest{
ExhibitionId: req.ExhibitionId,
AssetId: req.AssetId,
SlotId: req.SlotId,
OccupierUid: req.OccupierUid,
OccupierStarId: req.OccupierStarId,
SlotOwnerUid: req.SlotOwnerUid,
StartTime: req.StartTime,
ExpireAt: req.ExpireAt,
CrystalAmount: req.CrystalAmount,
}
resp, err := c.client.OnExhibitionCompleted(ctx, pbReq)
if err != nil {
logger.Logger.Error("Failed to call TaskService.OnExhibitionCompleted",
zap.Int64("exhibition_id", req.ExhibitionId),
zap.Error(err))
return nil, fmt.Errorf("call TaskService.OnExhibitionCompleted failed: %w", err)
}
if resp.Base.Code != pbCommon.StatusCode_STATUS_OK {
logger.Logger.Warn("TaskService.OnExhibitionCompleted returned error",
zap.Int64("exhibition_id", req.ExhibitionId),
zap.Int32("code", int32(resp.Base.Code)),
zap.String("message", resp.Base.Message))
return nil, fmt.Errorf("TaskService.OnExhibitionCompleted error: %s", resp.Base.Message)
}
logger.Logger.Info("TaskService.OnExhibitionCompleted successful",
zap.Int64("exhibition_id", req.ExhibitionId),
zap.Int64("revenue_record_id", resp.RevenueRecordId))
return &OnExhibitionCompletedResponse{
RevenueRecordId: resp.RevenueRecordId,
}, nil
}

View File

@ -28,6 +28,10 @@ type UserRPCClient interface {
// UpdateCrystalBalance 更新水晶余额(返回更新后的余额)
UpdateCrystalBalance(userID, starID int64, delta int64) (int64, error)
// AddExhibitionHours 增加用户累计上架时长
// 返回: newLevel, levelDelta, crystalReward, error
AddExhibitionHours(userID, starID int64, hours int64) (int32, int32, int64, error)
}
// userRPCClient User Service RPC客户端实现
@ -144,3 +148,54 @@ func (c *userRPCClient) UpdateCrystalBalance(userID, starID int64, delta int64)
return resp.NewBalance, nil
}
// AddExhibitionHours 增加用户累计上架时长
func (c *userRPCClient) AddExhibitionHours(userID, starID int64, hours int64) (int32, int32, int64, error) {
logger.Logger.Debug("Calling UserService.AddExhibitionHours",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("hours", hours),
)
ctx := context.Background()
resp, err := c.client.AddExhibitionHours(ctx, &pbUser.AddExhibitionHoursRequest{
UserId: userID,
StarId: starID,
ExhibitionHours: hours,
})
if err != nil {
logger.Logger.Error("Failed to call UserService.AddExhibitionHours",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("hours", hours),
zap.Error(err),
)
return 0, 0, 0, err
}
if resp.Base.Code != pbCommon.StatusCode_STATUS_OK {
errorMsg := resp.Base.Message
if errorMsg == "" {
errorMsg = fmt.Sprintf("UserService返回错误码: %d", resp.Base.Code)
}
logger.Logger.Warn("UserService.AddExhibitionHours returned error",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int32("code", int32(resp.Base.Code)),
zap.String("message", errorMsg),
)
return 0, 0, 0, errors.New(errorMsg)
}
logger.Logger.Debug("UserService.AddExhibitionHours successful",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("hours", hours),
zap.Int32("new_level", resp.NewLevel),
zap.Int32("level_delta", resp.LevelDelta),
zap.Int64("crystal_reward", resp.CrystalReward),
)
return resp.NewLevel, resp.LevelDelta, resp.CrystalReward, nil
}

View File

@ -19,6 +19,7 @@ import (
"github.com/topfans/backend/pkg/models"
pbAsset "github.com/topfans/backend/pkg/proto/asset"
pbGallery "github.com/topfans/backend/pkg/proto/gallery"
pbTask "github.com/topfans/backend/pkg/proto/task"
pbUser "github.com/topfans/backend/pkg/proto/user"
rpcclient "github.com/topfans/backend/services/galleryService/client"
"github.com/topfans/backend/services/galleryService/provider"
@ -35,6 +36,7 @@ var (
dbName = flag.String("db-name", getEnv("DB_NAME", "top-fans"), "Database name")
assetServiceURL = flag.String("asset-service-url", getEnv("ASSET_SERVICE_URL", "tri://localhost:20003"), "Asset service URL")
userServiceURL = flag.String("user-service-url", getEnv("USER_SERVICE_URL", "tri://localhost:20000"), "User service URL")
taskServiceURL = flag.String("task-service-url", getEnv("TASK_SERVICE_URL", "tri://localhost:20002"), "Task service URL")
healthHandler *health.Handler
)
@ -136,14 +138,30 @@ func main() {
userRPCClient := rpcclient.NewUserRPCClient(userServiceClient)
logger.Logger.Info("User Service RPC client initialized")
// 创建 Task Service Dubbo 客户端
taskCli, err := dubboclient.NewClient(
dubboclient.WithClientURL(*taskServiceURL),
)
if err != nil {
logger.Logger.Fatal(fmt.Sprintf("Failed to create Task Service Dubbo client: %v", err))
}
// 获取 Task Service RPC 客户端
taskServiceClient, err := pbTask.NewTaskInternalService(taskCli)
if err != nil {
logger.Logger.Fatal(fmt.Sprintf("Failed to create Task Service RPC client: %v", err))
}
taskRPCClient := rpcclient.NewTaskRPCClient(taskServiceClient)
logger.Logger.Info("Task Service RPC client initialized")
// 创建 Service 层实例
galleryService := service.NewGalleryService(galleryRepo, assetRPCClient, userRPCClient)
slotService := service.NewSlotService(galleryRepo, userRPCClient)
exhibitionService := service.NewExhibitionService(galleryRepo, assetRPCClient)
logger.Logger.Info("Service layer initialized")
// 创建并启动清理 Worker注入 assetRPCClient 以便下架时清除点赞记录)
cleanupWorker := service.NewCleanupWorker(galleryRepo, assetRPCClient)
// 创建并启动清理 Worker注入 assetRPCClient, userClient 和 taskClient
cleanupWorker := service.NewCleanupWorker(galleryRepo, assetRPCClient, userRPCClient, taskRPCClient)
go cleanupWorker.Start()
logger.Logger.Info("Cleanup worker started")

View File

@ -70,6 +70,9 @@ type GalleryRepository interface {
// limit: 返回数量
// offset: 偏移量(随机生成)
GetRandomExhibitions(starID int64, materialType string, excludeIDs []int64, limit, offset int) ([]*InspirationFlowItem, error)
// GetSlotOwnerUserID 获取展位所有者的用户ID
GetSlotOwnerUserID(slotID int64) (int64, error)
}
// InspirationFlowItem 灵感瀑布展品项
@ -555,6 +558,15 @@ func calcSpanByLikes(likes int32) int32 {
return 4
}
// GetSlotOwnerUserID 获取展位所有者的用户ID
func (r *galleryRepository) GetSlotOwnerUserID(slotID int64) (int64, error) {
var slot models.BoothSlot
if err := r.db.Where("slot_id = ?", slotID).First(&slot).Error; err != nil {
return 0, err
}
return slot.UserID, nil
}
// ==================== 辅助函数 ====================
// generateHostProfileID 生成 host_profile_id

View File

@ -10,8 +10,7 @@ import (
)
type UserServiceClient interface {
UpdateCrystalBalance(ctx context.Context, userID, starID int64, delta int64) (int64, error)
AddExperience(ctx context.Context, userID, starID int64, delta int64) (int64, error)
UpdateCrystalBalance(ctx context.Context, userID, starID int64, delta int64, changeType string, sourceID string, description string) (int64, error)
GetFanProfile(ctx context.Context, userID, starID int64) (*pbUser.FanProfile, error)
}
@ -23,11 +22,11 @@ func NewUserServiceClient(client pbUser.UserSocialService) UserServiceClient {
return &userServiceClient{client: client}
}
func (c *userServiceClient) UpdateCrystalBalance(ctx context.Context, userID, starID int64, delta int64) (int64, error) {
func (c *userServiceClient) UpdateCrystalBalance(ctx context.Context, userID, starID int64, delta int64, changeType string, sourceID string, description string) (int64, error) {
logger.Logger.Debug("Calling UserService.UpdateCrystalBalance",
zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Int64("delta", delta))
resp, err := c.client.UpdateCrystalBalance(ctx, &pbUser.UpdateCrystalBalanceRequest{
UserId: userID, StarId: starID, Delta: delta,
UserId: userID, StarId: starID, Delta: delta, ChangeType: changeType, SourceId: sourceID, Description: description,
})
if err != nil {
logger.Logger.Error("UserService.UpdateCrystalBalance failed", zap.Error(err))
@ -40,23 +39,6 @@ func (c *userServiceClient) UpdateCrystalBalance(ctx context.Context, userID, st
return resp.NewBalance, nil
}
func (c *userServiceClient) AddExperience(ctx context.Context, userID, starID int64, delta int64) (int64, error) {
logger.Logger.Debug("Calling UserService.AddExperience",
zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Int64("delta", delta))
resp, err := c.client.AddExperience(ctx, &pbUser.AddExperienceRequest{
UserId: userID, StarId: starID, Delta: delta,
})
if err != nil {
logger.Logger.Error("UserService.AddExperience failed", zap.Error(err))
return 0, err
}
if resp.Base.Code != pbCommon.StatusCode_STATUS_OK {
logger.Logger.Warn("AddExperience non-zero code", zap.Int32("code", int32(resp.Base.Code)))
return 0, fmt.Errorf("AddExperience failed with code: %d", resp.Base.Code)
}
return resp.NewExperience, nil
}
func (c *userServiceClient) GetFanProfile(ctx context.Context, userID, starID int64) (*pbUser.FanProfile, error) {
logger.Logger.Debug("Calling UserService.GetFanProfile",
zap.Int64("user_id", userID), zap.Int64("star_id", starID))

View File

@ -9,7 +9,6 @@ type TaskDefinition struct {
Name string `gorm:"column:name;size:100;not null"`
Description string `gorm:"column:description;type:text"`
CrystalReward int64 `gorm:"column:crystal_reward;default:0"`
ExpReward int64 `gorm:"column:exp_reward;default:0"`
SortOrder int `gorm:"column:sort_order;default:0"`
IsActive bool `gorm:"column:is_active;default:true"`
CreatedAt int64 `gorm:"column:created_at"`
@ -77,7 +76,6 @@ type OnboardingStageConfig struct {
Description string `gorm:"column:description;type:text"`
RequiredTaskKeys []string `gorm:"column:required_task_keys;type:text;serializer:json"` // 存储为 JSON 字符串
CrystalReward int64 `gorm:"column:crystal_reward;default:0"`
ExpReward int64 `gorm:"column:exp_reward;default:0"`
SortOrder int `gorm:"column:sort_order;default:0"`
IsActive bool `gorm:"column:is_active;default:true"`
CreatedAt int64 `gorm:"column:created_at"`

View File

@ -183,15 +183,14 @@ func (r *onboardingRepository) SaveStageConfigs(configs []*model.OnboardingStage
zap.Int("stage", cfg.Stage),
zap.String("name", cfg.Name),
zap.Strings("required_task_keys", cfg.RequiredTaskKeys),
zap.Int64("crystal_reward", cfg.CrystalReward),
zap.Int64("exp_reward", cfg.ExpReward))
zap.Int64("crystal_reward", cfg.CrystalReward))
cfg.UpdatedAt = now
// Use upsert via ON CONFLICT to properly handle JSON serialization
upsert := clause.OnConflict{
Columns: []clause.Column{{Name: "stage"}},
DoUpdates: clause.AssignmentColumns([]string{
"name", "required_task_keys", "crystal_reward", "exp_reward", "is_active", "updated_at",
"name", "required_task_keys", "crystal_reward", "is_active", "updated_at",
}),
}
if err := r.db.Clauses(upsert).Create(cfg).Error; err != nil {

View File

@ -120,7 +120,8 @@ func (w *DailyResetWorker) autoClaimExhibitionRevenue() {
for _, record := range records {
var lastErr error
for attempt := 0; attempt < maxRetries; attempt++ {
_, err := w.userClient.UpdateCrystalBalance(context.Background(), record.UserID, record.StarID, record.CrystalAmount)
_, err := w.userClient.UpdateCrystalBalance(context.Background(), record.UserID, record.StarID, record.CrystalAmount,
"exhibition_revenue", fmt.Sprintf("%d", record.ID), fmt.Sprintf("展示收益 #%d", record.ID))
if err == nil {
if err := w.revenueRepo.UpdateRevenueStatus(record.ID, "claimed"); err != nil {
logger.Logger.Error("DailyResetWorker: failed to update status to claimed",

View File

@ -145,7 +145,7 @@ func (p *UnifiedProvider) UpdateAssetsCount(ctx context.Context, req *pb.UpdateA
return p.userProvider.UpdateAssetsCount(ctx, req)
}
// AddExperience 增加经验值内部RPC调用
func (p *UnifiedProvider) AddExperience(ctx context.Context, req *pb.AddExperienceRequest) (*pb.AddExperienceResponse, error) {
return p.userProvider.AddExperience(ctx, req)
// AddExhibitionHours 增加用户累计上架时长内部RPC调用
func (p *UnifiedProvider) AddExhibitionHours(ctx context.Context, req *pb.AddExhibitionHoursRequest) (*pb.AddExhibitionHoursResponse, error) {
return p.userProvider.AddExhibitionHours(ctx, req)
}

View File

@ -852,27 +852,27 @@ func (p *UserProvider) UpdateAssetsCount(ctx context.Context, req *pb.UpdateAsse
return resp, nil
}
// AddExperience 增加经验值内部RPC调用
func (p *UserProvider) AddExperience(ctx context.Context, req *pb.AddExperienceRequest) (*pb.AddExperienceResponse, error) {
logger.Logger.Info("Received AddExperience request",
// AddExhibitionHours 增加用户累计上架时长内部RPC调用
func (p *UserProvider) AddExhibitionHours(ctx context.Context, req *pb.AddExhibitionHoursRequest) (*pb.AddExhibitionHoursResponse, error) {
logger.Logger.Info("Received AddExhibitionHours request",
zap.Int64("user_id", req.UserId),
zap.Int64("star_id", req.StarId),
zap.Int64("delta", req.Delta),
zap.Int64("exhibition_hours", req.ExhibitionHours),
)
// 调用Service层
resp, err := p.userService.AddExperience(req)
resp, err := p.userService.AddExhibitionHours(req)
if err != nil {
logger.Logger.Error("AddExperience failed",
logger.Logger.Error("AddExhibitionHours failed",
zap.Int64("user_id", req.UserId),
zap.Int64("star_id", req.StarId),
zap.Int64("delta", req.Delta),
zap.Int64("exhibition_hours", req.ExhibitionHours),
zap.Error(err),
)
// 如果响应为空,构建错误响应
if resp == nil {
resp = &pb.AddExperienceResponse{
resp = &pb.AddExhibitionHoursResponse{
Base: &pbCommon.BaseResponse{
Code: appErrors.ToStatusCode(err),
Message: err.Error(),
@ -884,11 +884,13 @@ func (p *UserProvider) AddExperience(ctx context.Context, req *pb.AddExperienceR
return resp, err
}
logger.Logger.Info("AddExperience successful",
logger.Logger.Info("AddExhibitionHours successful",
zap.Int64("user_id", req.UserId),
zap.Int64("star_id", req.StarId),
zap.Int64("delta", req.Delta),
zap.Int64("new_experience", resp.NewExperience),
zap.Int64("exhibition_hours", req.ExhibitionHours),
zap.Int32("new_level", resp.NewLevel),
zap.Int32("level_delta", resp.LevelDelta),
zap.Int64("crystal_reward", resp.CrystalReward),
)
return resp, nil

View File

@ -2,13 +2,18 @@ package repository
import (
"errors"
"fmt"
"math"
"strings"
"time"
"github.com/topfans/backend/pkg/database"
appErrors "github.com/topfans/backend/pkg/errors"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/models"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// contains 检查字符串是否包含子串(不区分大小写)
@ -79,8 +84,15 @@ type FanProfileRepository interface {
// UpdateSocial 更新好友数量social字段
UpdateSocial(userID, starID int64, delta int32) (int32, error)
// UpdateCrystalBalance 更新水晶余额
UpdateCrystalBalance(userID, starID int64, delta int64) (int64, error)
// UpdateCrystalBalance 更新水晶余额(支持流水记录)
// changeType: 变化类型,如 task_reward/mint_cost/mint_reward/exhibition_revenue/level_up_bonus/manual_adjust
// sourceID: 关联业务ID
// description: 可读描述
UpdateCrystalBalance(userID, starID int64, delta int64, changeType string, sourceID string, description string) (int64, error)
// AddExhibitionHours 增加用户累计上架时长并同步等级(事务性)
// 返回: newLevel, levelDelta, crystalReward, error
AddExhibitionHours(userID, starID int64, hours int64) (int32, int32, int64, error)
// UpdateExperience 更新经验值
UpdateExperience(userID, starID int64, delta int64) (int64, error)
@ -369,10 +381,13 @@ func (r *fanProfileRepository) UpdateSocial(userID, starID int64, delta int32) (
return newSocial, nil
}
// UpdateCrystalBalance 更新水晶余额
// UpdateCrystalBalance 更新水晶余额(支持流水记录)
// delta: 变化量,正数表示增加,负数表示减少
// changeType: 变化类型,如 task_reward/mint_cost/mint_reward/exhibition_revenue/level_up_bonus/manual_adjust
// sourceID: 关联业务ID
// description: 可读描述
// 返回: 更新后的水晶余额
func (r *fanProfileRepository) UpdateCrystalBalance(userID, starID int64, delta int64) (int64, error) {
func (r *fanProfileRepository) UpdateCrystalBalance(userID, starID int64, delta int64, changeType string, sourceID string, description string) (int64, error) {
if userID <= 0 {
return 0, errors.New("user_id must be greater than 0")
}
@ -384,9 +399,10 @@ func (r *fanProfileRepository) UpdateCrystalBalance(userID, starID int64, delta
// 使用事务确保原子性
var newBalance int64
err := r.db.Transaction(func(tx *gorm.DB) error {
// 先查询当前的 crystal_balance 值
// 1. SELECT FOR UPDATE 加行锁(悲观锁策略)
var profile models.FanProfile
if err := tx.Where("user_id = ? AND star_id = ?", userID, starID).
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("user_id = ? AND star_id = ?", userID, starID).
First(&profile).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return appErrors.ErrFanProfileNotFound
@ -394,15 +410,32 @@ func (r *fanProfileRepository) UpdateCrystalBalance(userID, starID int64, delta
return err
}
// 计算新值
newBalance = profile.CrystalBalance + delta
// 2. 计算余额变化
balanceBefore := profile.CrystalBalance
newBalance = balanceBefore + delta
// 确保不会小于 0
if newBalance < 0 {
newBalance = 0
}
// 更新 crystal_balance 字段
// 3. 写入水晶流水(复式记账,包含余额快照)
crystalRecord := &models.CrystalTransactionRecord{
UserID: userID,
StarID: starID,
ChangeType: changeType,
Delta: delta,
BalanceBefore: balanceBefore,
BalanceAfter: newBalance,
SourceID: sourceID,
Description: description,
CreatedAt: time.Now().UnixMilli(),
}
if err := tx.Create(crystalRecord).Error; err != nil {
return err
}
// 4. 更新 crystal_balance 字段
if err := tx.Model(&models.FanProfile{}).
Where("user_id = ? AND star_id = ?", userID, starID).
Update("crystal_balance", newBalance).Error; err != nil {
@ -416,9 +449,222 @@ func (r *fanProfileRepository) UpdateCrystalBalance(userID, starID int64, delta
return 0, err
}
return newBalance, nil
return newBalance, nil
}
// GetOrCreateExhibitionHours 获取或创建用户累计上架时长记录
func (r *fanProfileRepository) GetOrCreateExhibitionHours(tx *gorm.DB, userID, starID int64) (*models.UserExhibitionHours, error) {
var existing models.UserExhibitionHours
err := tx.Where("user_id = ? AND star_id = ?", userID, starID).First(&existing).Error
if err == nil {
return &existing, nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
// 创建新记录
now := time.Now().UnixMilli()
newRecord := &models.UserExhibitionHours{
UserID: userID,
StarID: starID,
TotalExhibitionHours: 0,
UpdatedAt: now,
}
if err := tx.Create(newRecord).Error; err != nil {
return nil, err
}
return newRecord, nil
}
// CalculateLevelFromExhibitionHours 根据累计上架时长计算等级
func CalculateLevelFromExhibitionHours(totalHours int64) int32 {
db := database.GetDB()
if db == nil {
return 1
}
var threshold models.LevelThreshold
err := db.Where("max_exhibition_hours <= ?", totalHours).
Order("level DESC").
First(&threshold).Error
if err != nil || threshold.Level == 0 {
return 1
}
return threshold.Level
}
// GetLevelCap 获取当前等级上限
func GetLevelCap() int32 {
db := database.GetDB()
if db == nil {
return 20
}
var config models.LevelCapConfig
err := db.First(&config).Error
if err != nil {
return 20 // 默认20级
}
return config.MaxLevel
}
// AddExhibitionHours 增加用户累计上架时长并同步等级(事务性)
// 返回: newLevel, levelDelta, crystalReward, error
func (r *fanProfileRepository) AddExhibitionHours(userID, starID int64, hours int64) (int32, int32, int64, error) {
var result struct {
OldLevel int32
NewLevel int32
CrystalReward int64
}
err := r.db.Transaction(func(tx *gorm.DB) error {
// 1. 获取或创建累计时长记录
exhibitionHours, err := r.GetOrCreateExhibitionHours(tx, userID, starID)
if err != nil {
return err
}
// 2. 原子性累加时长(避免竞态条件)
now := time.Now().UnixMilli()
if err := tx.Model(&models.UserExhibitionHours{}).
Where("user_id = ? AND star_id = ?", userID, starID).
Updates(map[string]interface{}{
"total_exhibition_hours": gorm.Expr("total_exhibition_hours + ?", hours),
"updated_at": now,
}).Error; err != nil {
return err
}
// 重新查询更新后的时长
if err := tx.Where("user_id = ? AND star_id = ?", userID, starID).First(exhibitionHours).Error; err != nil {
return err
}
// 3. 获取当前等级上限
maxLevel := GetLevelCap()
// 4. 计算新等级(基于累计时长)
newLevel := CalculateLevelFromExhibitionHours(exhibitionHours.TotalExhibitionHours)
if newLevel > maxLevel {
newLevel = maxLevel
}
// 5. SELECT FOR UPDATE 加行锁获取粉丝档案当前等级
var profile models.FanProfile
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("user_id = ? AND star_id = ?", userID, starID).First(&profile).Error; err != nil {
return err
}
result.OldLevel = profile.Level
result.NewLevel = newLevel
// 6. 如有升级,发放奖励
if newLevel > profile.Level {
// 查询升级奖励
rewards, err := r.getLevelUpRewards(tx, newLevel)
if err != nil {
logger.Logger.Warn("Failed to get level up rewards",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int32("level", newLevel),
zap.Error(err))
}
// 计算水晶奖励总额
var crystalReward int64 = 0
var likeBetCountReward int32 = 0
for _, reward := range rewards {
if reward.RewardType == "crystal" && reward.IsEnabled {
crystalReward += reward.RewardValue
}
if reward.RewardType == "like_bet_count" && reward.IsEnabled {
likeBetCountReward += int32(reward.RewardValue)
}
}
result.CrystalReward = crystalReward
// 发放升级奖励(水晶 + 点赞押注次数)
if crystalReward > 0 || likeBetCountReward > 0 {
balanceBefore := profile.CrystalBalance
balanceAfter := balanceBefore + crystalReward
// 写入水晶流水(只有水晶有流水)
if crystalReward > 0 {
crystalRecord := &models.CrystalTransactionRecord{
UserID: userID,
StarID: starID,
ChangeType: "level_up_bonus",
Delta: crystalReward,
BalanceBefore: balanceBefore,
BalanceAfter: balanceAfter,
SourceID: "",
Description: fmt.Sprintf("升级到%d级奖励", newLevel),
CreatedAt: time.Now().UnixMilli(),
}
if err := tx.Create(crystalRecord).Error; err != nil {
return err
}
}
// 更新 FanProfile等级 + 水晶余额 + 点赞押注次数)
updates := map[string]interface{}{
"level": newLevel,
}
if crystalReward > 0 {
updates["crystal_balance"] = balanceAfter
}
if likeBetCountReward > 0 {
updates["like_bet_count"] = gorm.Expr("like_bet_count + ?", likeBetCountReward)
}
if err := tx.Model(&profile).Updates(updates).Error; err != nil {
return err
}
} else {
// 只更新等级
if err := tx.Model(&profile).Update("level", newLevel).Error; err != nil {
return err
}
}
logger.Logger.Info("Level up from exhibition hours",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int32("old_level", profile.Level),
zap.Int32("new_level", newLevel),
zap.Int64("crystal_reward", crystalReward),
zap.Int32("like_bet_count_reward", likeBetCountReward),
zap.Int64("total_hours", exhibitionHours.TotalExhibitionHours))
}
return nil
})
if err != nil {
return 0, 0, 0, err
}
levelDelta := result.NewLevel - result.OldLevel
return result.NewLevel, levelDelta, result.CrystalReward, nil
}
// getLevelUpRewards 获取指定等级的升级奖励
func (r *fanProfileRepository) getLevelUpRewards(tx *gorm.DB, level int32) ([]*models.LevelUpRewardConfig, error) {
var rewards []*models.LevelUpRewardConfig
err := tx.Where("level = ? AND is_enabled = ?", level, true).Find(&rewards).Error
return rewards, err
}
// UpdateExperience 更新经验值(同时自动更新等级)
// UpdateExperience 更新经验值(同时自动更新等级)
// delta: 变化量,正数表示增加,负数表示减少
// 返回: 更新后的经验值

View File

@ -987,13 +987,12 @@ ON CONFLICT (level) DO NOTHING;
-- 升级奖励配置表
CREATE TABLE IF NOT EXISTS level_up_reward_config (
id BIGSERIAL PRIMARY KEY,
level INT NOT NULL UNIQUE,
level INT NOT NULL,
reward_type VARCHAR(50) NOT NULL,
reward_value BIGINT NOT NULL DEFAULT 0,
is_enabled BOOLEAN DEFAULT true,
level INT NOT NULL, -- 等级 2-20+
reward_type VARCHAR(50) NOT NULL, -- 奖励类型crystal(水晶) / like_bet_count(点赞押注数) / exhibition_count(上架藏品数) / badge(勋章) / 其他
reward_value BIGINT NOT NULL DEFAULT 0, -- 奖励值
is_enabled BOOLEAN DEFAULT true, -- 功能开关
updated_at BIGINT NOT NULL,
UNIQUE(level, reward_type)
UNIQUE(level, reward_type) -- 同一等级同类型奖励唯一
);
-- 插入初始数据示例(可由运营后台动态调整)
@ -1080,7 +1079,9 @@ CREATE TABLE IF NOT EXISTS user_dazi_level (
### 13.1 铸爱次数与消耗水晶
用户铸造藏品时,每次铸造消耗水晶数根据累计铸爱次数动态计算。**达到第10次消耗1024水晶后重置铸爱次数重新从第1次开始。**
用户铸造藏品时,每次铸造消耗水晶数根据累计铸爱次数动态计算。**达到第10次消耗1024水晶后重置铸爱次数为0重新从第1次开始累积。**
> **重要:** 铸爱次数重置仅清除计数,**永久收益提升revenue_boost_bps不清除**会持续累加。例如用户第9次触发小保底+500 bps = +5%第10次触发大保底+500 bps = +5%),则 `revenue_boost_bps = 1000`(即 +10% 永久收益提升铸爱次数重置为0但收益提升保留。
| 铸爱次数 | 消耗水晶数 | 备注 |
|---------|-----------|------|
@ -1092,10 +1093,10 @@ CREATE TABLE IF NOT EXISTS user_dazi_level (
| 6 | 64 | |
| 7 | 128 | |
| 8 | 256 | 接近1天收益 |
| 9 | 512 | 20%概率获得5%永久收益提升(俗称小保底) |
| 10 | 1024 | 接近3天收益建议为最高值100%概率获得5%永久收益提升(俗称大保底) |
| 9 | 512 | 20%概率获得500 bps+5%永久收益提升(俗称小保底) |
| 10 | 1024 | 接近3天收益建议为最高值100%概率获得500 bps+5%永久收益提升(俗称大保底) |
> **注:** 消耗水晶数上限为1024达到第10次后重置铸爱次数。
> **注:** 消耗水晶数上限为1024达到第10次后铸爱次数重置为0永久收益提升保留
### 13.2 数据库设计
@ -1107,7 +1108,7 @@ CREATE TABLE IF NOT EXISTS user_dazi_level (
- `cost_crystal`消耗水晶数上限1024
- `probability`保底触发概率0-1000=不触发
- `reward_type`:触发奖励类型(收益提升等)
- `reward_value`:奖励值如5代表5%
- `reward_value`:奖励值,单位为 bpsbasis points如 500 代表 +5% 永久收益提升
```sql
CREATE TABLE mint_cost_config (
@ -1116,7 +1117,7 @@ CREATE TABLE mint_cost_config (
cost_crystal BIGINT NOT NULL,
probability BIGINT DEFAULT 0,
reward_type VARCHAR(50) DEFAULT NULL,
reward_value BIGINT DEFAULT 0,
reward_value BIGINT DEFAULT 0, -- 单位bps基点500=+5%
description VARCHAR(255),
updated_at BIGINT NOT NULL
);
@ -1131,8 +1132,8 @@ INSERT INTO mint_cost_config (mint_count, cost_crystal, probability, reward_type
(6, 64, 0, NULL, 0, '', UNIX_MILLIS()),
(7, 128, 0, NULL, 0, '', UNIX_MILLIS()),
(8, 256, 0, NULL, 0, '接近1天收益', UNIX_MILLIS()),
(9, 512, 20, '收益提升', 5, '20%概率获得5%永久收益提升(俗称小保底)', UNIX_MILLIS()),
(10, 1024, 100, '收益提升', 5, '接近3天收益建议为最高值100%概率获得5%永久收益提升(俗称大保底)', UNIX_MILLIS());
(9, 512, 20, '收益提升', 500, '20%概率获得500 bps+5%永久收益提升(俗称小保底)', UNIX_MILLIS()),
(10, 1024, 100, '收益提升', 500, '接近3天收益建议为最高值100%概率获得500 bps+5%永久收益提升(俗称大保底)', UNIX_MILLIS());
```
#### 13.2.2 用户累计铸爱次数表 (user_mint_count)
@ -1162,7 +1163,7 @@ func (s *MintService) CreateMintOrder(userID, starID int64, ...) (orderID string
// 3. 检查是否触发保底
var boost int64 = 0
if cost.Probability > 0 && rand.Intn(100) < cost.Probability {
boost = cost.RewardValue // 如5代表5%
boost = cost.RewardValue // 单位bps 500 代表 +5%
}
// 4. 扣水晶