feat:修改展示收益
This commit is contained in:
parent
2a0faeb835
commit
3ef3510303
@ -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:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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=
|
||||||
|
|||||||
@ -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" +
|
||||||
|
|||||||
@ -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,用于升级奖励流水的溯源
|
||||||
}
|
}
|
||||||
|
|
||||||
// 增加累计上架时长响应
|
// 增加累计上架时长响应
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user