增加VIP资源返回
This commit is contained in:
parent
65b0558e03
commit
bf07c84dd0
@ -89,6 +89,21 @@ func (g *Gateways) GrantProps(ctx context.Context, req GrantPropsRequest) error
|
||||
return g.RewardDispatch.GrantProps(ctx, req)
|
||||
}
|
||||
|
||||
// GivePropsBackpack 透传到道具背包发放接口。
|
||||
func (g *Gateways) GivePropsBackpack(ctx context.Context, req GivePropsBackpackRequest) error {
|
||||
return g.RewardDispatch.GivePropsBackpack(ctx, req)
|
||||
}
|
||||
|
||||
// SwitchUseProps 透传到道具使用切换接口。
|
||||
func (g *Gateways) SwitchUseProps(ctx context.Context, userID int64, propsID int64) error {
|
||||
return g.RewardDispatch.SwitchUseProps(ctx, userID, propsID)
|
||||
}
|
||||
|
||||
// ActivateTemporaryBadge 透传到临时徽章激活接口。
|
||||
func (g *Gateways) ActivateTemporaryBadge(ctx context.Context, userID int64, badgeID int64, days int) error {
|
||||
return g.RewardDispatch.ActivateTemporaryBadge(ctx, userID, badgeID, days)
|
||||
}
|
||||
|
||||
// SendActivityReward 透传到奖励发放网关。
|
||||
func (g *Gateways) SendActivityReward(ctx context.Context, req SendActivityRewardRequest) error {
|
||||
return g.RewardDispatch.SendActivityReward(ctx, req)
|
||||
@ -124,6 +139,11 @@ func (g *Gateways) GetUserLanguage(ctx context.Context, userID int64) (string, e
|
||||
return g.Profile.GetUserLanguage(ctx, userID)
|
||||
}
|
||||
|
||||
// RemoveUserProfileCacheAll 透传到用户资料缓存清理接口。
|
||||
func (g *Gateways) RemoveUserProfileCacheAll(ctx context.Context, userID int64) error {
|
||||
return g.Profile.RemoveUserProfileCacheAll(ctx, userID)
|
||||
}
|
||||
|
||||
// ListCurrentWeekRoomContribution 透传到房间流水网关。
|
||||
func (g *Gateways) ListCurrentWeekRoomContribution(ctx context.Context, limit int) ([]RoomContributionActivityCount, error) {
|
||||
return g.Room.ListCurrentWeekRoomContribution(ctx, limit)
|
||||
@ -244,6 +264,21 @@ func (g *RewardDispatchGateway) GrantProps(ctx context.Context, req GrantPropsRe
|
||||
return g.client.GrantProps(ctx, req)
|
||||
}
|
||||
|
||||
// GivePropsBackpack 发放单个道具到用户背包。
|
||||
func (g *RewardDispatchGateway) GivePropsBackpack(ctx context.Context, req GivePropsBackpackRequest) error {
|
||||
return g.client.GivePropsBackpack(ctx, req)
|
||||
}
|
||||
|
||||
// SwitchUseProps 切换用户当前使用道具。
|
||||
func (g *RewardDispatchGateway) SwitchUseProps(ctx context.Context, userID int64, propsID int64) error {
|
||||
return g.client.SwitchUseProps(ctx, userID, propsID)
|
||||
}
|
||||
|
||||
// ActivateTemporaryBadge 激活临时徽章并佩戴。
|
||||
func (g *RewardDispatchGateway) ActivateTemporaryBadge(ctx context.Context, userID int64, badgeID int64, days int) error {
|
||||
return g.client.ActivateTemporaryBadge(ctx, userID, badgeID, days)
|
||||
}
|
||||
|
||||
// SendActivityReward 发放活动奖励组。
|
||||
func (g *RewardDispatchGateway) SendActivityReward(ctx context.Context, req SendActivityRewardRequest) error {
|
||||
return g.client.SendActivityReward(ctx, req)
|
||||
@ -289,6 +324,11 @@ func (g *ProfileGateway) GetUserLanguage(ctx context.Context, userID int64) (str
|
||||
return g.client.GetUserLanguage(ctx, userID)
|
||||
}
|
||||
|
||||
// RemoveUserProfileCacheAll 清理用户资料相关缓存。
|
||||
func (g *ProfileGateway) RemoveUserProfileCacheAll(ctx context.Context, userID int64) error {
|
||||
return g.client.RemoveUserProfileCacheAll(ctx, userID)
|
||||
}
|
||||
|
||||
// RoomGateway 负责房间资料和房间流水相关接口。
|
||||
type RoomGateway struct {
|
||||
client *Client
|
||||
|
||||
@ -126,6 +126,18 @@ type GrantPropsRequest struct {
|
||||
SourceGroupID int64 `json:"sourceGroupId"`
|
||||
}
|
||||
|
||||
type GivePropsBackpackRequest struct {
|
||||
AcceptUserID int64 `json:"acceptUserId"`
|
||||
PropsID int64 `json:"propsId"`
|
||||
Type string `json:"type"`
|
||||
Origin string `json:"origin"`
|
||||
OriginDesc string `json:"originDesc"`
|
||||
Days int `json:"days"`
|
||||
UseProps bool `json:"useProps"`
|
||||
AllowGive bool `json:"allowGive"`
|
||||
OpUser int64 `json:"opUser,omitempty"`
|
||||
}
|
||||
|
||||
type SendActivityRewardRequest struct {
|
||||
TrackID int64 `json:"trackId"`
|
||||
Origin string `json:"origin"`
|
||||
@ -486,6 +498,32 @@ func (c *Client) SendActivityReward(ctx context.Context, req SendActivityRewardR
|
||||
return c.postJSON(ctx, c.cfg.Java.OtherBaseURL+"/props-activity-cnf/client/sendActivityReward", req, nil, nil)
|
||||
}
|
||||
|
||||
func (c *Client) GivePropsBackpack(ctx context.Context, req GivePropsBackpackRequest) error {
|
||||
return c.postJSON(ctx, c.cfg.Java.OtherBaseURL+"/props/client/giveProps", req, nil, nil)
|
||||
}
|
||||
|
||||
func (c *Client) SwitchUseProps(ctx context.Context, userID int64, propsID int64) error {
|
||||
endpoint := c.cfg.Java.OtherBaseURL + "/props/client/switch/use-props?userId=" +
|
||||
url.QueryEscape(strconv.FormatInt(userID, 10)) +
|
||||
"&propsId=" + url.QueryEscape(strconv.FormatInt(propsID, 10))
|
||||
return c.getJSON(ctx, endpoint, nil, nil)
|
||||
}
|
||||
|
||||
func (c *Client) ActivateTemporaryBadge(ctx context.Context, userID int64, badgeID int64, days int) error {
|
||||
endpoint := c.cfg.Java.OtherBaseURL + "/user-badge/client/activationTemporaryAndUse?userId=" +
|
||||
url.QueryEscape(strconv.FormatInt(userID, 10)) +
|
||||
"&badgeId=" + url.QueryEscape(strconv.FormatInt(badgeID, 10)) +
|
||||
"&days=" + url.QueryEscape(strconv.Itoa(days))
|
||||
var resp resultResponse[bool]
|
||||
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
|
||||
return err
|
||||
}
|
||||
if !resp.Body {
|
||||
return fmt.Errorf("activate temporary badge returned false")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendOfficialNoticeCustomize 调用 external 服务发送自定义官方通知。
|
||||
func (c *Client) SendOfficialNoticeCustomize(ctx context.Context, req OfficialNoticeCustomizeRequest) error {
|
||||
return c.postJSON(ctx, c.cfg.Java.ExternalBaseURL+"/official-notice/client/send/customize/template", req, nil, nil)
|
||||
@ -685,6 +723,11 @@ func (c *Client) GetUserLanguage(ctx context.Context, userID int64) (string, err
|
||||
return strings.TrimSpace(resp.Body), nil
|
||||
}
|
||||
|
||||
func (c *Client) RemoveUserProfileCacheAll(ctx context.Context, userID int64) error {
|
||||
endpoint := c.cfg.Java.OtherBaseURL + "/user-profile/client/removeCacheAll?userId=" + url.QueryEscape(strconv.FormatInt(userID, 10))
|
||||
return c.getJSON(ctx, endpoint, nil, nil)
|
||||
}
|
||||
|
||||
func (c *Client) MapGoldBalance(ctx context.Context, userIDs []int64) (map[int64]int64, error) {
|
||||
endpoint := c.cfg.Java.WalletBaseURL + "/wallet/gold/client/mapBalance"
|
||||
var resp resultResponse[map[string]int64]
|
||||
|
||||
@ -156,6 +156,9 @@ func (s *Service) Purchase(ctx context.Context, user AuthUser, req PurchaseReque
|
||||
if stateErr != nil {
|
||||
return nil, stateErr
|
||||
}
|
||||
if err := s.ensureStateResourcesGranted(ctx, user.UserID, state, now); err != nil {
|
||||
return nil, NewAppError(http.StatusBadGateway, "vip_resource_grant_failed", err.Error())
|
||||
}
|
||||
statePayload := statePayloadFromModel(state, now)
|
||||
statePayload.UserID = user.UserID
|
||||
statePayload.SysOrigin = sysOrigin
|
||||
@ -268,6 +271,10 @@ func (s *Service) Purchase(ctx context.Context, user AuthUser, req PurchaseReque
|
||||
}
|
||||
committed = true
|
||||
|
||||
if err := s.ensurePlanResourcesGranted(ctx, user.UserID, plan, now); err != nil {
|
||||
return nil, NewAppError(http.StatusBadGateway, "vip_resource_grant_failed", err.Error())
|
||||
}
|
||||
|
||||
payload := statePayloadFromModel(state, now)
|
||||
return &PurchaseResponse{
|
||||
Success: true,
|
||||
@ -487,6 +494,176 @@ func expireState(state *model.UserVipState, now time.Time) {
|
||||
state.UpdateTime = now
|
||||
}
|
||||
|
||||
func (s *Service) ensurePlanResourcesGranted(ctx context.Context, userID int64, plan purchasePlan, now time.Time) error {
|
||||
return s.ensureVIPResourcesGranted(ctx, userID, vipGrantResourcesFromConfig(plan.TargetConfig), plan.ExpireAt, plan.DurationDays, now)
|
||||
}
|
||||
|
||||
func (s *Service) ensureStateResourcesGranted(ctx context.Context, userID int64, state *model.UserVipState, now time.Time) error {
|
||||
if !isActiveState(state, now) {
|
||||
return nil
|
||||
}
|
||||
return s.ensureVIPResourcesGranted(ctx, userID, vipGrantResourcesFromState(state), state.ExpireAt, vipGrantDaysUntil(now, state.ExpireAt), now)
|
||||
}
|
||||
|
||||
func (s *Service) ensureVIPResourcesGranted(ctx context.Context, userID int64, resources []vipGrantResource, expireAt time.Time, days int, now time.Time) error {
|
||||
if len(resources) == 0 {
|
||||
return nil
|
||||
}
|
||||
if s.java == nil {
|
||||
return fmt.Errorf("java gateway is unavailable")
|
||||
}
|
||||
if days <= 0 {
|
||||
days = 1
|
||||
}
|
||||
|
||||
for _, resource := range resources {
|
||||
if resource.ResourceID <= 0 {
|
||||
continue
|
||||
}
|
||||
exists, err := s.vipResourceExists(ctx, userID, resource, expireAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if err := s.grantVIPResource(ctx, userID, resource, days); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !resource.Badge {
|
||||
if err := s.java.SwitchUseProps(ctx, userID, resource.ResourceID); err != nil {
|
||||
return fmt.Errorf("switch vip resource %s:%d: %w", resource.PropsType, resource.ResourceID, err)
|
||||
}
|
||||
}
|
||||
if err := s.alignVIPResourceExpireAt(ctx, userID, resource, expireAt, now); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.java.RemoveUserProfileCacheAll(ctx, userID); err != nil {
|
||||
return fmt.Errorf("remove vip user profile cache: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) grantVIPResource(ctx context.Context, userID int64, resource vipGrantResource, days int) error {
|
||||
if resource.Badge {
|
||||
if err := s.java.ActivateTemporaryBadge(ctx, userID, resource.ResourceID, days); err != nil {
|
||||
return fmt.Errorf("grant vip badge %d: %w", resource.ResourceID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
req := integration.GivePropsBackpackRequest{
|
||||
AcceptUserID: userID,
|
||||
PropsID: resource.ResourceID,
|
||||
Type: resource.PropsType,
|
||||
Origin: walletOrigin,
|
||||
OriginDesc: walletOriginDesc,
|
||||
Days: days,
|
||||
UseProps: true,
|
||||
AllowGive: false,
|
||||
}
|
||||
if err := s.java.GivePropsBackpack(ctx, req); err != nil {
|
||||
return fmt.Errorf("grant vip resource %s:%d: %w", resource.PropsType, resource.ResourceID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) vipResourceExists(ctx context.Context, userID int64, resource vipGrantResource, expireAt time.Time) (bool, error) {
|
||||
var count int64
|
||||
threshold := expireAt.Add(-time.Second)
|
||||
query := s.db.WithContext(ctx)
|
||||
if resource.Badge {
|
||||
query = query.Table("user_badge_backpack").
|
||||
Where("user_id = ? AND badge_id = ? AND expire_time >= ?", userID, resource.ResourceID, threshold)
|
||||
} else {
|
||||
query = query.Table("user_props_backpack").
|
||||
Where("user_id = ? AND props_id = ? AND type = ? AND expire_time >= ?", userID, resource.ResourceID, resource.PropsType, threshold)
|
||||
}
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (s *Service) alignVIPResourceExpireAt(ctx context.Context, userID int64, resource vipGrantResource, expireAt time.Time, now time.Time) error {
|
||||
var result *gorm.DB
|
||||
if resource.Badge {
|
||||
result = s.db.WithContext(ctx).Exec(
|
||||
"UPDATE user_badge_backpack SET expire_type = ?, expire_time = ?, is_use_props = ?, update_time = ? WHERE user_id = ? AND badge_id = ?",
|
||||
"TEMPORARY", expireAt, true, now, userID, resource.ResourceID,
|
||||
)
|
||||
} else {
|
||||
result = s.db.WithContext(ctx).Exec(
|
||||
"UPDATE user_props_backpack SET expire_time = ?, is_use_props = ?, allow_give = ?, update_time = ? WHERE user_id = ? AND props_id = ? AND type = ?",
|
||||
expireAt, true, false, now, userID, resource.ResourceID, resource.PropsType,
|
||||
)
|
||||
}
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
exists, err := s.vipResourceExists(ctx, userID, resource, expireAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if resource.Badge {
|
||||
return fmt.Errorf("vip badge backpack row not found after grant: %d", resource.ResourceID)
|
||||
}
|
||||
return fmt.Errorf("vip props backpack row not found after grant: %s:%d", resource.PropsType, resource.ResourceID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func vipGrantResourcesFromConfig(config model.VipLevelConfig) []vipGrantResource {
|
||||
return compactVIPGrantResources([]vipGrantResource{
|
||||
{ResourceID: config.BadgeResourceID, Badge: true},
|
||||
{ResourceID: config.AvatarFrameResourceID, PropsType: vipPropsTypeAvatarFrame},
|
||||
{ResourceID: config.EntryEffectResourceID, PropsType: vipPropsTypeRide},
|
||||
{ResourceID: config.ChatBubbleResourceID, PropsType: vipPropsTypeChatBubble},
|
||||
{ResourceID: config.FloatPictureResourceID, PropsType: vipPropsTypeFloatPicture},
|
||||
})
|
||||
}
|
||||
|
||||
func vipGrantResourcesFromState(state *model.UserVipState) []vipGrantResource {
|
||||
if state == nil {
|
||||
return nil
|
||||
}
|
||||
return compactVIPGrantResources([]vipGrantResource{
|
||||
{ResourceID: state.BadgeResourceID, Badge: true},
|
||||
{ResourceID: state.AvatarFrameResourceID, PropsType: vipPropsTypeAvatarFrame},
|
||||
{ResourceID: state.EntryEffectResourceID, PropsType: vipPropsTypeRide},
|
||||
{ResourceID: state.ChatBubbleResourceID, PropsType: vipPropsTypeChatBubble},
|
||||
{ResourceID: state.FloatPictureResourceID, PropsType: vipPropsTypeFloatPicture},
|
||||
})
|
||||
}
|
||||
|
||||
func compactVIPGrantResources(resources []vipGrantResource) []vipGrantResource {
|
||||
compacted := make([]vipGrantResource, 0, len(resources))
|
||||
for _, resource := range resources {
|
||||
if resource.ResourceID > 0 {
|
||||
compacted = append(compacted, resource)
|
||||
}
|
||||
}
|
||||
return compacted
|
||||
}
|
||||
|
||||
func vipGrantDaysUntil(now time.Time, expireAt time.Time) int {
|
||||
if !expireAt.After(now) {
|
||||
return 1
|
||||
}
|
||||
duration := expireAt.Sub(now)
|
||||
days := int(duration / (24 * time.Hour))
|
||||
if duration%(24*time.Hour) != 0 {
|
||||
days++
|
||||
}
|
||||
if days < 1 {
|
||||
return 1
|
||||
}
|
||||
return days
|
||||
}
|
||||
|
||||
func mapWalletPurchaseError(err error) *AppError {
|
||||
message := err.Error()
|
||||
normalized := strings.ToLower(message)
|
||||
@ -509,7 +686,7 @@ func buildWalletCommand(userID int64, sysOrigin string, order model.VipOrderReco
|
||||
CloseDelayAsset: false,
|
||||
OpUserType: "APP",
|
||||
CustomizeOrigin: walletOrigin,
|
||||
CustomizeOriginDesc: "VIP purchase",
|
||||
CustomizeOriginDesc: walletOriginDesc,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -153,6 +153,45 @@ func TestMapWalletPurchaseErrorGatewayFailure(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVIPGrantResourcesFromConfigMapsBackpackTypes(t *testing.T) {
|
||||
config := model.VipLevelConfig{
|
||||
BadgeResourceID: 10,
|
||||
AvatarFrameResourceID: 11,
|
||||
EntryEffectResourceID: 12,
|
||||
ChatBubbleResourceID: 13,
|
||||
FloatPictureResourceID: 14,
|
||||
}
|
||||
|
||||
resources := vipGrantResourcesFromConfig(config)
|
||||
if len(resources) != 5 {
|
||||
t.Fatalf("resources len = %d, want 5", len(resources))
|
||||
}
|
||||
assertGrant := func(index int, resourceID int64, propsType string, badge bool) {
|
||||
t.Helper()
|
||||
got := resources[index]
|
||||
if got.ResourceID != resourceID || got.PropsType != propsType || got.Badge != badge {
|
||||
t.Fatalf("resource[%d] = %+v, want id=%d type=%s badge=%v", index, got, resourceID, propsType, badge)
|
||||
}
|
||||
}
|
||||
assertGrant(0, 10, "", true)
|
||||
assertGrant(1, 11, vipPropsTypeAvatarFrame, false)
|
||||
assertGrant(2, 12, vipPropsTypeRide, false)
|
||||
assertGrant(3, 13, vipPropsTypeChatBubble, false)
|
||||
assertGrant(4, 14, vipPropsTypeFloatPicture, false)
|
||||
}
|
||||
|
||||
func TestVIPGrantDaysUntilCeilsPartialDay(t *testing.T) {
|
||||
now := time.Date(2026, 4, 29, 10, 0, 0, 0, time.UTC)
|
||||
expireAt := now.Add((24 * time.Hour) + time.Second)
|
||||
|
||||
if got := vipGrantDaysUntil(now, expireAt); got != 2 {
|
||||
t.Fatalf("days = %d, want 2", got)
|
||||
}
|
||||
if got := vipGrantDaysUntil(now, now); got != 1 {
|
||||
t.Fatalf("expired days = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func vipTestConfigs() map[int]model.VipLevelConfig {
|
||||
configs := make(map[int]model.VipLevelConfig, levelCount)
|
||||
for level := 1; level <= levelCount; level++ {
|
||||
|
||||
@ -34,6 +34,12 @@ const (
|
||||
walletReceiptIncome = "INCOME"
|
||||
walletReceiptExpenditure = "EXPENDITURE"
|
||||
walletOrigin = "VIP_PURCHASE"
|
||||
walletOriginDesc = "VIP purchase"
|
||||
|
||||
vipPropsTypeAvatarFrame = "AVATAR_FRAME"
|
||||
vipPropsTypeRide = "RIDE"
|
||||
vipPropsTypeChatBubble = "CHAT_BUBBLE"
|
||||
vipPropsTypeFloatPicture = "FLOAT_PICTURE"
|
||||
)
|
||||
|
||||
type AppError = common.AppError
|
||||
@ -47,6 +53,10 @@ type vipDB interface {
|
||||
|
||||
type vipGateway interface {
|
||||
ChangeGoldBalance(ctx context.Context, cmd integration.GoldReceiptCommand) error
|
||||
GivePropsBackpack(ctx context.Context, req integration.GivePropsBackpackRequest) error
|
||||
SwitchUseProps(ctx context.Context, userID int64, propsID int64) error
|
||||
ActivateTemporaryBadge(ctx context.Context, userID int64, badgeID int64, days int) error
|
||||
RemoveUserProfileCacheAll(ctx context.Context, userID int64) error
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
@ -229,6 +239,12 @@ type purchasePlan struct {
|
||||
TargetConfig model.VipLevelConfig
|
||||
}
|
||||
|
||||
type vipGrantResource struct {
|
||||
ResourceID int64
|
||||
PropsType string
|
||||
Badge bool
|
||||
}
|
||||
|
||||
func normalizeSysOrigin(sysOrigin string) string {
|
||||
return strings.ToUpper(strings.TrimSpace(sysOrigin))
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user