269 lines
10 KiB
Go
269 lines
10 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"google.golang.org/protobuf/proto"
|
||
luckygiftv1 "hyapp.local/api/proto/luckygift/v1"
|
||
roomv1 "hyapp.local/api/proto/room/v1"
|
||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/pkg/xerr"
|
||
"hyapp/services/room-service/internal/room/command"
|
||
)
|
||
|
||
func (s *Service) executeLuckyGiftDraws(ctx context.Context, meta *roomv1.RequestMeta, cmd command.SendGift, roomMeta RoomMeta, targetBillings []giftTargetBilling, now time.Time) ([]*roomv1.LuckyGiftDrawResult, error) {
|
||
if len(targetBillings) == 0 {
|
||
return nil, nil
|
||
}
|
||
if len(targetBillings) == 1 {
|
||
result, err := s.executeLuckyGiftDraw(ctx, meta, cmd, roomMeta, targetBillings[0], now)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return []*roomv1.LuckyGiftDrawResult{result}, nil
|
||
}
|
||
|
||
return s.executeLuckyGiftDrawBatch(ctx, meta, cmd, roomMeta, targetBillings, now)
|
||
}
|
||
|
||
func (s *Service) executeLuckyGiftDraw(ctx context.Context, meta *roomv1.RequestMeta, cmd command.SendGift, roomMeta RoomMeta, targetBilling giftTargetBilling, now time.Time) (*roomv1.LuckyGiftDrawResult, error) {
|
||
if targetBilling.Billing == nil {
|
||
return nil, xerr.New(xerr.Unavailable, "wallet target debit response is empty")
|
||
}
|
||
// 抽奖必须按每个收礼目标独立落事实;command_id、target_user_id 和 coin_spent 都来自该目标的 wallet 子交易。
|
||
drawResp, err := s.luckyGift.ExecuteLuckyGiftDraw(ctx, &luckygiftv1.ExecuteLuckyGiftDrawRequest{
|
||
LuckyGift: luckyGiftMetaForTarget(ctx, meta, cmd, roomMeta, targetBilling, now),
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if drawResp == nil || drawResp.GetResult() == nil {
|
||
return nil, xerr.New(xerr.Unavailable, "lucky gift draw response is empty")
|
||
}
|
||
// 同步返回只承载抽奖表现事实;返奖后的余额由 activity 在中奖落库后发送钱包私有 IM,避免 HTTP 响应领先账务通知链路。
|
||
return luckyGiftResultFromProto(drawResp.GetResult(), targetBilling.TargetUserID), nil
|
||
}
|
||
|
||
func (s *Service) executeLuckyGiftDrawBatch(ctx context.Context, meta *roomv1.RequestMeta, cmd command.SendGift, roomMeta RoomMeta, targetBillings []giftTargetBilling, now time.Time) ([]*roomv1.LuckyGiftDrawResult, error) {
|
||
req := &luckygiftv1.BatchExecuteLuckyGiftDrawRequest{LuckyGifts: make([]*luckygiftv1.LuckyGiftMeta, 0, len(targetBillings))}
|
||
for _, targetBilling := range targetBillings {
|
||
if targetBilling.Billing == nil {
|
||
return nil, xerr.New(xerr.Unavailable, "wallet target debit response is empty")
|
||
}
|
||
// 多人送礼只跨服务调用一次;activity-service 在一个事务内按这个顺序推进锁和抽奖事实。
|
||
req.LuckyGifts = append(req.LuckyGifts, luckyGiftMetaForTarget(ctx, meta, cmd, roomMeta, targetBilling, now))
|
||
}
|
||
resp, err := s.luckyGift.BatchExecuteLuckyGiftDraw(ctx, req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if resp == nil || len(resp.GetResults()) != len(targetBillings) {
|
||
return nil, xerr.New(xerr.Unavailable, "lucky gift batch draw response is incomplete")
|
||
}
|
||
results := make([]*roomv1.LuckyGiftDrawResult, 0, len(targetBillings))
|
||
for index, result := range resp.GetResults() {
|
||
if result == nil {
|
||
return nil, xerr.New(xerr.Unavailable, "lucky gift batch draw result is empty")
|
||
}
|
||
results = append(results, luckyGiftResultFromProto(result, targetBillings[index].TargetUserID))
|
||
}
|
||
return results, nil
|
||
}
|
||
|
||
func luckyGiftMetaForTarget(ctx context.Context, meta *roomv1.RequestMeta, cmd command.SendGift, roomMeta RoomMeta, targetBilling giftTargetBilling, now time.Time) *luckygiftv1.LuckyGiftMeta {
|
||
return &luckygiftv1.LuckyGiftMeta{
|
||
Meta: luckyGiftMetaFromRoom(ctx, meta),
|
||
CommandId: targetBilling.CommandID,
|
||
UserId: cmd.ActorUserID(),
|
||
TargetUserId: targetBilling.TargetUserID,
|
||
// 目前没有独立设备 ID 字段,优先用 session_id;没有时退回目标子 command_id,保证每次抽奖 scope 不为空。
|
||
DeviceId: luckyGiftDeviceID(cmd, targetBilling.CommandID),
|
||
RoomId: cmd.RoomID(),
|
||
AnchorId: luckyGiftAnchorID(roomMeta),
|
||
GiftId: cmd.GiftID,
|
||
GiftCount: cmd.GiftCount,
|
||
CoinSpent: targetBilling.Billing.GetCoinSpent(),
|
||
// 房间链路统一使用 UTC epoch ms;不能把本地时区时间传给幸运礼物 RTP 窗口。
|
||
PaidAtMs: now.UTC().UnixMilli(),
|
||
PoolId: cmd.PoolID,
|
||
VisibleRegionId: roomMeta.VisibleRegionID,
|
||
CountryId: cmd.SenderCountryID,
|
||
}
|
||
}
|
||
|
||
func (s *Service) shouldDrawLuckyGift(poolID string, giftTypeCode string) bool {
|
||
if s.luckyGift == nil {
|
||
return false
|
||
}
|
||
switch strings.TrimSpace(giftTypeCode) {
|
||
case "lucky", "super_lucky":
|
||
return true
|
||
default:
|
||
return strings.TrimSpace(poolID) != ""
|
||
}
|
||
}
|
||
|
||
func (s *Service) withLuckyGiftSendLock(ctx context.Context, enabled bool, userID int64, fn func() error) error {
|
||
if !enabled || s == nil || s.luckyGiftSendLocker == nil {
|
||
return fn()
|
||
}
|
||
release, err := s.luckyGiftSendLocker.AcquireLuckyGiftSendLock(ctx, appcode.FromContext(ctx), userID, s.luckyGiftSendLockTTL)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if release != nil {
|
||
defer func() {
|
||
releaseCtx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||
defer cancel()
|
||
release(releaseCtx)
|
||
}()
|
||
}
|
||
return fn()
|
||
}
|
||
|
||
func luckyGiftMetaFromRoom(ctx context.Context, meta *roomv1.RequestMeta) *luckygiftv1.RequestMeta {
|
||
return &luckygiftv1.RequestMeta{
|
||
RequestId: meta.GetRequestId(),
|
||
Caller: "room-service",
|
||
GatewayNodeId: meta.GetGatewayNodeId(),
|
||
SentAtMs: time.Now().UTC().UnixMilli(),
|
||
AppCode: appcode.FromContext(ctx),
|
||
}
|
||
}
|
||
|
||
func luckyGiftDeviceID(cmd command.SendGift, fallbackCommandID string) string {
|
||
if value := strings.TrimSpace(cmd.SessionID); value != "" {
|
||
return value
|
||
}
|
||
if value := strings.TrimSpace(fallbackCommandID); value != "" {
|
||
return value
|
||
}
|
||
return cmd.ID()
|
||
}
|
||
|
||
func luckyGiftAnchorID(roomMeta RoomMeta) string {
|
||
if roomMeta.OwnerUserID <= 0 {
|
||
return roomMeta.RoomID
|
||
}
|
||
return fmt.Sprintf("%d", roomMeta.OwnerUserID)
|
||
}
|
||
|
||
func luckyGiftResultFromProto(result *luckygiftv1.LuckyGiftDrawResult, targetUserID int64) *roomv1.LuckyGiftDrawResult {
|
||
if result == nil {
|
||
return nil
|
||
}
|
||
return &roomv1.LuckyGiftDrawResult{
|
||
Enabled: true,
|
||
DrawId: result.GetDrawId(),
|
||
CommandId: result.GetCommandId(),
|
||
PoolId: result.GetPoolId(),
|
||
GiftId: result.GetGiftId(),
|
||
RuleVersion: result.GetRuleVersion(),
|
||
ExperiencePool: result.GetExperiencePool(),
|
||
SelectedTierId: result.GetSelectedTierId(),
|
||
MultiplierPpm: result.GetMultiplierPpm(),
|
||
BaseRewardCoins: result.GetBaseRewardCoins(),
|
||
EffectiveRewardCoins: result.GetEffectiveRewardCoins(),
|
||
RewardStatus: result.GetRewardStatus(),
|
||
StageFeedback: result.GetStageFeedback(),
|
||
HighMultiplier: result.GetHighMultiplier(),
|
||
CreatedAtMs: result.GetCreatedAtMs(),
|
||
WalletTransactionId: result.GetWalletTransactionId(),
|
||
// 这里故意不透出 activity 返回的返奖后余额。SendGift HTTP 只承诺扣费后的真实余额,中奖返奖后的余额必须等钱包落账后由 WalletBalanceChanged 私有 IM 覆盖。
|
||
CoinBalanceAfter: 0,
|
||
TargetUserId: targetUserID,
|
||
}
|
||
}
|
||
|
||
func aggregateLuckyGiftResults(commandID string, results []*roomv1.LuckyGiftDrawResult) *roomv1.LuckyGiftDrawResult {
|
||
if len(results) == 0 {
|
||
return nil
|
||
}
|
||
if len(results) == 1 {
|
||
return results[0]
|
||
}
|
||
aggregate := proto.Clone(results[0]).(*roomv1.LuckyGiftDrawResult)
|
||
aggregate.CommandId = commandID
|
||
aggregate.SelectedTierId = "batch"
|
||
aggregate.TargetUserId = 0
|
||
aggregate.WalletTransactionId = ""
|
||
aggregate.MultiplierPpm = 0
|
||
aggregate.BaseRewardCoins = 0
|
||
aggregate.EffectiveRewardCoins = 0
|
||
aggregate.RewardStatus = "granted"
|
||
aggregate.StageFeedback = false
|
||
aggregate.HighMultiplier = false
|
||
aggregate.CoinBalanceAfter = 0
|
||
for _, result := range results {
|
||
if result == nil {
|
||
continue
|
||
}
|
||
aggregate.RuleVersion = result.GetRuleVersion()
|
||
aggregate.ExperiencePool = result.GetExperiencePool()
|
||
aggregate.CreatedAtMs = result.GetCreatedAtMs()
|
||
aggregate.MultiplierPpm += result.GetMultiplierPpm()
|
||
aggregate.BaseRewardCoins += result.GetBaseRewardCoins()
|
||
aggregate.EffectiveRewardCoins += result.GetEffectiveRewardCoins()
|
||
aggregate.StageFeedback = aggregate.StageFeedback || result.GetStageFeedback()
|
||
aggregate.HighMultiplier = aggregate.HighMultiplier || result.GetHighMultiplier()
|
||
switch result.GetRewardStatus() {
|
||
case "pending":
|
||
aggregate.RewardStatus = "pending"
|
||
case "failed":
|
||
if aggregate.RewardStatus != "pending" {
|
||
aggregate.RewardStatus = "failed"
|
||
}
|
||
case "granted":
|
||
default:
|
||
if aggregate.RewardStatus == "" {
|
||
aggregate.RewardStatus = result.GetRewardStatus()
|
||
}
|
||
}
|
||
}
|
||
return aggregate
|
||
}
|
||
|
||
func syntheticLuckyGiftResult(cmd command.SendGift, targetBillings []giftTargetBilling, now time.Time) *roomv1.LuckyGiftDrawResult {
|
||
if len(targetBillings) == 0 || targetBillings[0].Billing == nil {
|
||
return nil
|
||
}
|
||
rewardCoins := firstPositiveInt64(cmd.SyntheticLuckyRewardCoins, targetBillings[0].Billing.GetCoinSpent()*5)
|
||
multiplier := firstPositiveInt64(cmd.SyntheticLuckyMultiplierPPM, 5000000)
|
||
return &roomv1.LuckyGiftDrawResult{
|
||
Enabled: true,
|
||
DrawId: fmt.Sprintf("robot_lucky_%s_%d", cmd.ID(), now.UnixMilli()),
|
||
CommandId: cmd.ID(),
|
||
PoolId: cmd.PoolID,
|
||
GiftId: cmd.GiftID,
|
||
MultiplierPpm: multiplier,
|
||
BaseRewardCoins: rewardCoins,
|
||
EffectiveRewardCoins: rewardCoins,
|
||
RewardStatus: "granted",
|
||
StageFeedback: true,
|
||
HighMultiplier: multiplier >= 5000000,
|
||
CreatedAtMs: now.UnixMilli(),
|
||
TargetUserId: cmd.TargetUserID,
|
||
}
|
||
}
|
||
|
||
func giftFinalCoinBalanceAfter(billing *walletv1.DebitGiftResponse) int64 {
|
||
if billing == nil {
|
||
return 0
|
||
}
|
||
// SendGift 的 HTTP 余额只表示本次送礼扣费完成后的 COIN 余额;lucky/super_lucky 返奖属于后续入账事实,由钱包余额 IM 更新客户端。
|
||
return billing.GetBalanceAfter()
|
||
}
|
||
|
||
func firstPositiveInt64(values ...int64) int64 {
|
||
for _, value := range values {
|
||
if value > 0 {
|
||
return value
|
||
}
|
||
}
|
||
return 0
|
||
}
|