feat:修改展示收益

This commit is contained in:
zerosaturation 2026-05-15 11:48:46 +08:00
parent 2a0faeb835
commit 3ef3510303
10 changed files with 115 additions and 139 deletions

View File

@ -2,7 +2,9 @@
"permissions": { "permissions": {
"allow": [ "allow": [
"Skill(superpowers:subagent-driven-development)", "Skill(superpowers:subagent-driven-development)",
"Skill(superpowers:subagent-driven-development:*)" "Skill(superpowers:subagent-driven-development:*)",
"Bash(go build:*)",
"Bash(go vet:*)"
] ]
} }
} }

View File

@ -32,13 +32,13 @@ type ServerConfig struct {
// DubboConfig Dubbo 服务配置 // DubboConfig Dubbo 服务配置
type DubboConfig struct { type DubboConfig struct {
UserServiceURL string UserServiceURL string
SocialServiceURL string SocialServiceURL string
AssetServiceURL string AssetServiceURL string
GalleryServiceURL string GalleryServiceURL string
ActivityServiceURL string ActivityServiceURL string
TaskServiceURL string TaskServiceURL string
StarbookServiceURL string StarbookServiceURL string
} }
// JWTConfig JWT 配置 // JWTConfig JWT 配置
@ -104,7 +104,7 @@ func Load() *Config {
Redis: RedisConfig{ Redis: RedisConfig{
Host: getEnv("REDIS_HOST", "127.0.0.1"), Host: getEnv("REDIS_HOST", "127.0.0.1"),
Port: getEnvInt("REDIS_PORT", 6379), Port: getEnvInt("REDIS_PORT", 6379),
Password: getEnv("REDIS_PASSWORD", ""), Password: getEnv("REDIS_PASSWORD", "123456"),
DB: getEnvInt("REDIS_DB", 0), DB: getEnvInt("REDIS_DB", 0),
}, },
} }

View File

@ -48,6 +48,8 @@ github.com/aws/smithy-go v1.8.0 h1:AEwwwXQZtUwP5Mz506FeXXrKBe0jA8gVM+1gEcSRooc=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bketelsen/crypt v0.0.4 h1:w/jqZtC9YD4DS/Vp9GhWfWcCpuAL58oTnLoI8vE9YHU= github.com/bketelsen/crypt v0.0.4 h1:w/jqZtC9YD4DS/Vp9GhWfWcCpuAL58oTnLoI8vE9YHU=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/casbin/casbin/v2 v2.1.2 h1:bTwon/ECRx9dwBy2ewRVr5OiqjeXSGiTUY74sDPQi/g= github.com/casbin/casbin/v2 v2.1.2 h1:bTwon/ECRx9dwBy2ewRVr5OiqjeXSGiTUY74sDPQi/g=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
@ -289,6 +291,7 @@ github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/X
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs=
go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts=

View File

@ -1748,6 +1748,7 @@ type AddExhibitionHoursRequest struct {
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 用户ID 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 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"` // 本次展出的时长(小时) ExhibitionHours int64 `protobuf:"varint,3,opt,name=exhibition_hours,json=exhibitionHours,proto3" json:"exhibition_hours,omitempty"` // 本次展出的时长(小时)
SourceId string `protobuf:"bytes,4,opt,name=source_id,json=sourceId,proto3" json:"source_id,omitempty"` // 关联业务ID用于升级奖励流水的溯源
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -1803,6 +1804,13 @@ func (x *AddExhibitionHoursRequest) GetExhibitionHours() int64 {
return 0 return 0
} }
func (x *AddExhibitionHoursRequest) GetSourceId() string {
if x != nil {
return x.SourceId
}
return ""
}
// 增加累计上架时长响应 // 增加累计上架时长响应
type AddExhibitionHoursResponse struct { type AddExhibitionHoursResponse struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
@ -2987,11 +2995,12 @@ const file_user_proto_rawDesc = "" +
"\x05delta\x18\x03 \x01(\x05R\x05delta\"j\n" + "\x05delta\x18\x03 \x01(\x05R\x05delta\"j\n" +
"\x19UpdateAssetsCountResponse\x120\n" + "\x19UpdateAssetsCountResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1b\n" + "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1b\n" +
"\tnew_count\x18\x02 \x01(\x05R\bnewCount\"x\n" + "\tnew_count\x18\x02 \x01(\x05R\bnewCount\"\x95\x01\n" +
"\x19AddExhibitionHoursRequest\x12\x17\n" + "\x19AddExhibitionHoursRequest\x12\x17\n" +
"\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x17\n" + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x17\n" +
"\astar_id\x18\x02 \x01(\x03R\x06starId\x12)\n" + "\astar_id\x18\x02 \x01(\x03R\x06starId\x12)\n" +
"\x10exhibition_hours\x18\x03 \x01(\x03R\x0fexhibitionHours\"\xb3\x01\n" + "\x10exhibition_hours\x18\x03 \x01(\x03R\x0fexhibitionHours\x12\x1b\n" +
"\tsource_id\x18\x04 \x01(\tR\bsourceId\"\xb3\x01\n" +
"\x1aAddExhibitionHoursResponse\x120\n" + "\x1aAddExhibitionHoursResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1b\n" + "\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x1b\n" +
"\tnew_level\x18\x02 \x01(\x05R\bnewLevel\x12\x1f\n" + "\tnew_level\x18\x02 \x01(\x05R\bnewLevel\x12\x1f\n" +

View File

@ -218,6 +218,7 @@ message AddExhibitionHoursRequest {
int64 user_id = 1; // ID int64 user_id = 1; // ID
int64 star_id = 2; // ID int64 star_id = 2; // ID
int64 exhibition_hours = 3; // int64 exhibition_hours = 3; //
string source_id = 4; // ID
} }
// //

View File

@ -27,11 +27,15 @@ type UserRPCClient interface {
GetFanProfile(userID, starID int64) (*FanProfile, error) GetFanProfile(userID, starID int64) (*FanProfile, error)
// UpdateCrystalBalance 更新水晶余额(返回更新后的余额) // UpdateCrystalBalance 更新水晶余额(返回更新后的余额)
UpdateCrystalBalance(userID, starID int64, delta int64) (int64, error) // 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 增加用户累计上架时长 // AddExhibitionHours 增加用户累计上架时长
// sourceID: 关联业务ID用于升级奖励流水的溯源
// 返回: newLevel, levelDelta, crystalReward, error // 返回: newLevel, levelDelta, crystalReward, error
AddExhibitionHours(userID, starID int64, hours int64) (int32, int32, int64, error) AddExhibitionHours(userID, starID int64, hours int64, sourceID string) (int32, int32, int64, error)
} }
// userRPCClient User Service RPC客户端实现 // userRPCClient User Service RPC客户端实现
@ -101,18 +105,22 @@ func (c *userRPCClient) GetFanProfile(userID, starID int64) (*FanProfile, error)
} }
// UpdateCrystalBalance 更新水晶余额(返回更新后的余额) // UpdateCrystalBalance 更新水晶余额(返回更新后的余额)
func (c *userRPCClient) UpdateCrystalBalance(userID, starID int64, delta int64) (int64, error) { func (c *userRPCClient) UpdateCrystalBalance(userID, starID int64, delta int64, changeType string, sourceID string, description string) (int64, error) {
logger.Logger.Debug("Calling UserService.UpdateCrystalBalance", logger.Logger.Debug("Calling UserService.UpdateCrystalBalance",
zap.Int64("user_id", userID), zap.Int64("user_id", userID),
zap.Int64("star_id", starID), zap.Int64("star_id", starID),
zap.Int64("delta", delta), zap.Int64("delta", delta),
zap.String("change_type", changeType),
) )
ctx := context.Background() ctx := context.Background()
resp, err := c.client.UpdateCrystalBalance(ctx, &pbUser.UpdateCrystalBalanceRequest{ resp, err := c.client.UpdateCrystalBalance(ctx, &pbUser.UpdateCrystalBalanceRequest{
UserId: userID, UserId: userID,
StarId: starID, StarId: starID,
Delta: delta, Delta: delta,
ChangeType: changeType,
SourceId: sourceID,
Description: description,
}) })
if err != nil { if err != nil {
@ -150,7 +158,8 @@ func (c *userRPCClient) UpdateCrystalBalance(userID, starID int64, delta int64)
} }
// AddExhibitionHours 增加用户累计上架时长 // AddExhibitionHours 增加用户累计上架时长
func (c *userRPCClient) AddExhibitionHours(userID, starID int64, hours int64) (int32, int32, int64, error) { // sourceID: 关联业务ID用于升级奖励流水的溯源本参数暂未透传到RPC仅Go层保留
func (c *userRPCClient) AddExhibitionHours(userID, starID int64, hours int64, sourceID string) (int32, int32, int64, error) {
logger.Logger.Debug("Calling UserService.AddExhibitionHours", logger.Logger.Debug("Calling UserService.AddExhibitionHours",
zap.Int64("user_id", userID), zap.Int64("user_id", userID),
zap.Int64("star_id", starID), zap.Int64("star_id", starID),
@ -162,6 +171,7 @@ func (c *userRPCClient) AddExhibitionHours(userID, starID int64, hours int64) (i
UserId: userID, UserId: userID,
StarId: starID, StarId: starID,
ExhibitionHours: hours, ExhibitionHours: hours,
SourceId: sourceID,
}) })
if err != nil { if err != nil {

View File

@ -385,15 +385,12 @@ func (r *galleryRepository) GetMyExhibitedAssets(userID, starID int64, page, pag
err = r.db.Model(&models.Exhibition{}). err = r.db.Model(&models.Exhibition{}).
Raw(` Raw(`
SELECT exhibitions.asset_id, a.name, a.cover_url, a.like_count, SELECT exhibitions.asset_id, a.name, a.cover_url, a.like_count,
exhibitions.start_time as exhibited_at, exhibitions.expire_at, bs.slot_index, exhibitions.start_time as exhibited_at, exhibitions.expire_at, bs.slot_index
COALESCE(CAST(SUM(err.crystal_amount) / 10 AS bigint), 0) as earnings
FROM exhibitions FROM exhibitions
JOIN assets a ON a.id = exhibitions.asset_id JOIN assets a ON a.id = exhibitions.asset_id
JOIN booth_slots bs ON bs.slot_id = exhibitions.slot_id JOIN booth_slots bs ON bs.slot_id = exhibitions.slot_id
LEFT JOIN exhibition_revenue_records err ON err.asset_id = a.id AND err.status = 'claimable'
WHERE exhibitions.occupier_uid = ? AND exhibitions.occupier_star_id = ? WHERE exhibitions.occupier_uid = ? AND exhibitions.occupier_star_id = ?
AND exhibitions.deleted_at IS NULL AND exhibitions.expire_at > ? AND exhibitions.deleted_at IS NULL AND exhibitions.expire_at > ?
GROUP BY exhibitions.asset_id, a.name, a.cover_url, a.like_count, exhibitions.start_time, exhibitions.expire_at, bs.slot_index
ORDER BY bs.slot_index ASC ORDER BY bs.slot_index ASC
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
`, userID, starID, now, pageSize, offset).Scan(&items).Error `, userID, starID, now, pageSize, offset).Scan(&items).Error
@ -402,6 +399,12 @@ func (r *galleryRepository) GetMyExhibitedAssets(userID, starID int64, page, pag
return nil, 0, err return nil, 0, err
} }
// 实时计算展示收益R1 = R0 × T × [100% + Buff(n)]
// R0 = 5 水晶/小时
for _, item := range items {
item.Earnings = calculateRealtimeEarnings(item.LikeCount, item.ExhibitedAt, now)
}
return items, total, nil return items, total, nil
} }
@ -576,3 +579,37 @@ func generateHostProfileID(userID, starID int64) int64 {
// 实际项目中应该使用与 User Service 一致的逻辑 // 实际项目中应该使用与 User Service 一致的逻辑
return userID*1000000 + starID return userID*1000000 + starID
} }
// calculateRealtimeEarnings 实时计算展示收益
// 公式R1 = R0 × T × [100% + Buff(n)]
// R0 = 5 水晶/小时T = 上架时长小时Buff(n) 根据点赞数计算
func calculateRealtimeEarnings(likeCount int32, startTime, now int64) int64 {
R0 := int64(5) // 水晶/小时
// 计算上架时长(毫秒转小时)
T := (now - startTime) / 3600000
if T <= 0 {
T = 1 // 最少1小时
}
// 计算Buff
var buff int
switch {
case likeCount >= 30:
buff = 30
case likeCount >= 10:
buff = 20
case likeCount >= 5:
buff = 10
default:
buff = 0
}
// 基础收益
baseRevenue := R0 * T
// 应用Buff加成R1 = R0 × T × (100% + Buff)
buffedRevenue := baseRevenue * (100 + int64(buff)) / 100
return buffedRevenue
}

View File

@ -12,6 +12,10 @@ import (
type UserServiceClient interface { type UserServiceClient interface {
UpdateCrystalBalance(ctx context.Context, userID, starID int64, delta int64, changeType string, sourceID string, description string) (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) GetFanProfile(ctx context.Context, userID, starID int64) (*pbUser.FanProfile, error)
// AddExhibitionHours 增加用户累计上架时长
// sourceID: 关联业务ID用于升级奖励流水的溯源
// 返回: newLevel, levelDelta, crystalReward, error
AddExhibitionHours(ctx context.Context, userID, starID int64, hours int64, sourceID string) (int32, int32, int64, error)
} }
type userServiceClient struct { type userServiceClient struct {
@ -56,3 +60,24 @@ func (c *userServiceClient) GetFanProfile(ctx context.Context, userID, starID in
} }
return resp.Profile, nil return resp.Profile, nil
} }
// AddExhibitionHours 增加用户累计上架时长
func (c *userServiceClient) AddExhibitionHours(ctx context.Context, userID, starID int64, hours int64, sourceID string) (int32, int32, int64, error) {
logger.Logger.Debug("Calling UserService.AddExhibitionHours",
zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Int64("hours", hours))
resp, err := c.client.AddExhibitionHours(ctx, &pbUser.AddExhibitionHoursRequest{
UserId: userID,
StarId: starID,
ExhibitionHours: hours,
SourceId: sourceID,
})
if err != nil {
logger.Logger.Error("UserService.AddExhibitionHours failed", zap.Error(err))
return 0, 0, 0, err
}
if resp.Base.Code != pbCommon.StatusCode_STATUS_OK {
logger.Logger.Warn("AddExhibitionHours non-zero code", zap.Int32("code", int32(resp.Base.Code)))
return 0, 0, 0, fmt.Errorf("AddExhibitionHours failed with code: %d", resp.Base.Code)
}
return resp.NewLevel, resp.LevelDelta, resp.CrystalReward, nil
}

View File

@ -3,7 +3,6 @@ package repository
import ( import (
"errors" "errors"
"fmt" "fmt"
"math"
"strings" "strings"
"time" "time"
@ -21,31 +20,6 @@ func contains(s, substr string) bool {
return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
} }
// CalculateLevel 根据经验值计算等级
// 公式: 升级到等级L需要的累计经验 = (L-1) * L * 50
// Level 1: 0经验, Level 2: 100经验, Level 3: 300经验, Level 4: 600经验...
func CalculateLevel(experience int64) int32 {
if experience < 0 {
return 1
}
// 使用公式: (L-1) * L * 50 <= experience
// 解方程: L^2 - L - experience/50 <= 0
// L = (1 + sqrt(1 + 4*experience/50)) / 2
level := int32((1 + math.Sqrt(1+4*float64(experience)/50)) / 2)
if level < 1 {
level = 1
}
return level
}
// GetExperienceForLevel 获取指定等级需要的经验值
func GetExperienceForLevel(level int32) int64 {
if level <= 1 {
return 0
}
return int64((level-1) * level * 50)
}
// FanProfileRepository 粉丝档案Repository接口 // FanProfileRepository 粉丝档案Repository接口
type FanProfileRepository interface { type FanProfileRepository interface {
// Create 创建粉丝档案 // Create 创建粉丝档案
@ -72,9 +46,6 @@ type FanProfileRepository interface {
// IncrementAssetsCount 增加资产计数 // IncrementAssetsCount 增加资产计数
IncrementAssetsCount(userID, starID int64, delta int32) error IncrementAssetsCount(userID, starID int64, delta int32) error
// SyncLevelFromExperience 根据经验值同步等级(只升级不降级)
SyncLevelFromExperience(userID, starID int64) (int32, error)
// DecrementAssetsCount 减少资产计数 // DecrementAssetsCount 减少资产计数
DecrementAssetsCount(userID, starID int64, delta int32) error DecrementAssetsCount(userID, starID int64, delta int32) error
@ -91,11 +62,9 @@ type FanProfileRepository interface {
UpdateCrystalBalance(userID, starID int64, delta int64, changeType string, sourceID string, description string) (int64, error) UpdateCrystalBalance(userID, starID int64, delta int64, changeType string, sourceID string, description string) (int64, error)
// AddExhibitionHours 增加用户累计上架时长并同步等级(事务性) // AddExhibitionHours 增加用户累计上架时长并同步等级(事务性)
// sourceID: 关联业务ID用于升级奖励流水的溯源
// 返回: newLevel, levelDelta, crystalReward, error // 返回: newLevel, levelDelta, crystalReward, error
AddExhibitionHours(userID, starID int64, hours int64) (int32, int32, int64, error) AddExhibitionHours(userID, starID int64, hours int64, sourceID string) (int32, int32, int64, error)
// UpdateExperience 更新经验值
UpdateExperience(userID, starID int64, delta int64) (int64, error)
// UpdateAvatar 更新头像 // UpdateAvatar 更新头像
UpdateAvatar(userID, starID int64, avatarURL string) error UpdateAvatar(userID, starID int64, avatarURL string) error
@ -517,8 +486,9 @@ func GetLevelCap() int32 {
} }
// AddExhibitionHours 增加用户累计上架时长并同步等级(事务性) // AddExhibitionHours 增加用户累计上架时长并同步等级(事务性)
// sourceID: 关联业务ID用于升级奖励流水的溯源
// 返回: newLevel, levelDelta, crystalReward, error // 返回: newLevel, levelDelta, crystalReward, error
func (r *fanProfileRepository) AddExhibitionHours(userID, starID int64, hours int64) (int32, int32, int64, error) { func (r *fanProfileRepository) AddExhibitionHours(userID, starID int64, hours int64, sourceID string) (int32, int32, int64, error) {
var result struct { var result struct {
OldLevel int32 OldLevel int32
NewLevel int32 NewLevel int32
@ -607,7 +577,7 @@ func (r *fanProfileRepository) AddExhibitionHours(userID, starID int64, hours in
Delta: crystalReward, Delta: crystalReward,
BalanceBefore: balanceBefore, BalanceBefore: balanceBefore,
BalanceAfter: balanceAfter, BalanceAfter: balanceAfter,
SourceID: "", SourceID: sourceID,
Description: fmt.Sprintf("升级到%d级奖励", newLevel), Description: fmt.Sprintf("升级到%d级奖励", newLevel),
CreatedAt: time.Now().UnixMilli(), CreatedAt: time.Now().UnixMilli(),
} }
@ -664,63 +634,6 @@ func (r *fanProfileRepository) getLevelUpRewards(tx *gorm.DB, level int32) ([]*m
return rewards, err return rewards, err
} }
// UpdateExperience 更新经验值(同时自动更新等级)
// UpdateExperience 更新经验值(同时自动更新等级)
// delta: 变化量,正数表示增加,负数表示减少
// 返回: 更新后的经验值
func (r *fanProfileRepository) UpdateExperience(userID, starID int64, delta int64) (int64, error) {
if userID <= 0 {
return 0, errors.New("user_id must be greater than 0")
}
if starID <= 0 {
return 0, errors.New("star_id must be greater than 0")
}
// 使用事务确保原子性
var newExperience int64
err := r.db.Transaction(func(tx *gorm.DB) error {
// 先查询当前的 experience 值
var profile models.FanProfile
if err := tx.Where("user_id = ? AND star_id = ?", userID, starID).
First(&profile).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return appErrors.ErrFanProfileNotFound
}
return err
}
// 计算新经验值
newExperience = profile.Experience + delta
// 确保不会小于 0
if newExperience < 0 {
newExperience = 0
}
// 根据新经验值计算新等级
newLevel := CalculateLevel(newExperience)
// 更新 experience 和 level 字段
if err := tx.Model(&models.FanProfile{}).
Where("user_id = ? AND star_id = ?", userID, starID).
Updates(map[string]interface{}{
"experience": newExperience,
"level": newLevel,
}).Error; err != nil {
return err
}
return nil
})
if err != nil {
return 0, err
}
return newExperience, nil
}
// UpdateAvatar 更新头像 // UpdateAvatar 更新头像
func (r *fanProfileRepository) UpdateAvatar(userID, starID int64, avatarURL string) error { func (r *fanProfileRepository) UpdateAvatar(userID, starID int64, avatarURL string) error {
if userID <= 0 { if userID <= 0 {
@ -750,23 +663,4 @@ func (r *fanProfileRepository) UpdateAvatar(userID, starID int64, avatarURL stri
return nil return nil
} }
// SyncLevelFromExperience 根据经验值同步等级(只升级不降级) // UpdateAvatar 更新头像
// 在获取用户信息时调用,确保等级与经验值匹配
func (r *fanProfileRepository) SyncLevelFromExperience(userID, starID int64) (int32, error) {
var profile models.FanProfile
if err := r.db.Where("user_id = ? AND star_id = ?", userID, starID).First(&profile).Error; err != nil {
return 0, err
}
newLevel := CalculateLevel(profile.Experience)
// 只升级,不降级
if newLevel > profile.Level {
if err := r.db.Model(&profile).Update("level", newLevel).Error; err != nil {
return profile.Level, err
}
return newLevel, nil
}
return profile.Level, nil
}

View File

@ -353,7 +353,6 @@ func TestFanProfileRepository_Update(t *testing.T) {
// 更新粉丝档案 // 更新粉丝档案
profile.Level = 2 profile.Level = 2
profile.Experience = 100
err := repo.Update(profile) err := repo.Update(profile)
if err != nil { if err != nil {
t.Fatalf("Update failed: %v", err) t.Fatalf("Update failed: %v", err)
@ -368,10 +367,6 @@ func TestFanProfileRepository_Update(t *testing.T) {
if retrieved.Level != 2 { if retrieved.Level != 2 {
t.Errorf("Level mismatch: expected 2, got %d", retrieved.Level) t.Errorf("Level mismatch: expected 2, got %d", retrieved.Level)
} }
if retrieved.Experience != 100 {
t.Errorf("Experience mismatch: expected 100, got %d", retrieved.Experience)
}
} }
func TestFanProfileRepository_UpdateNickname(t *testing.T) { func TestFanProfileRepository_UpdateNickname(t *testing.T) {