2026-06-28 15:11:56 +08:00

731 lines
26 KiB
Go

package mysql
import (
"context"
"database/sql"
"encoding/json"
"strings"
"time"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
"hyapp/services/wallet-service/internal/domain/ledger"
)
// WalletOutboxRecord is a committed wallet fact ready for external fanout.
type WalletOutboxRecord struct {
AppCode string
EventID string
EventType string
TransactionID string
CommandID string
UserID int64
AssetType string
AvailableDelta int64
FrozenDelta int64
PayloadJSON string
CreatedAtMS int64
RetryCount int
LastError string
WorkerID string
LockUntilMS int64
}
// WalletOutboxClaimFilter narrows one MQ fanout lane by event type.
type WalletOutboxClaimFilter struct {
// IncludeEventTypes lets a realtime worker own only the configured UI event facts.
IncludeEventTypes []string
// ExcludeEventTypes lets the generic wallet worker skip facts owned by the realtime lane.
ExcludeEventTypes []string
}
type walletOutboxEvent struct {
AppCode string
EventID string
EventType string
TransactionID string
CommandID string
UserID int64
AssetType string
AvailableDelta int64
FrozenDelta int64
Payload any
CreatedAtMS int64
}
func (r *Repository) insertWalletOutbox(ctx context.Context, tx *sql.Tx, events []walletOutboxEvent) error {
for _, event := range events {
payloadJSON, err := json.Marshal(event.Payload)
if err != nil {
return err
}
if _, err := tx.ExecContext(ctx,
`INSERT INTO wallet_outbox (
app_code, event_id, event_type, transaction_id, command_id, user_id, asset_type,
available_delta, frozen_delta, payload, status, retry_count, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)`,
normalizedEntryAppCode(ctx, event.AppCode),
event.EventID,
event.EventType,
event.TransactionID,
event.CommandID,
event.UserID,
event.AssetType,
event.AvailableDelta,
event.FrozenDelta,
string(payloadJSON),
outboxStatusPending,
event.CreatedAtMS,
event.CreatedAtMS,
); err != nil {
return err
}
}
return nil
}
// ClaimPendingWalletOutbox atomically claims committed wallet facts for MQ fanout.
func (r *Repository) ClaimPendingWalletOutbox(ctx context.Context, workerID string, limit int, lockUntilMS int64) ([]WalletOutboxRecord, error) {
return r.ClaimPendingWalletOutboxFiltered(ctx, workerID, limit, lockUntilMS, WalletOutboxClaimFilter{})
}
// ClaimPendingWalletOutboxFiltered atomically claims committed wallet facts for one MQ fanout lane.
func (r *Repository) ClaimPendingWalletOutboxFiltered(ctx context.Context, workerID string, limit int, lockUntilMS int64, filter WalletOutboxClaimFilter) ([]WalletOutboxRecord, error) {
if r == nil || r.db == nil {
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
workerID = strings.TrimSpace(workerID)
if workerID == "" {
return nil, xerr.New(xerr.InvalidArgument, "worker_id is required")
}
if limit <= 0 {
limit = 100
}
if limit > 500 {
limit = 500
}
nowMS := time.Now().UTC().UnixMilli()
if lockUntilMS <= nowMS {
lockUntilMS = time.Now().UTC().Add(30 * time.Second).UnixMilli()
}
appCode := appcode.FromContext(ctx)
eventFilterSQL, eventFilterArgs := walletOutboxEventFilterSQL(filter)
pendingIndex, claimIndex := walletOutboxClaimIndexes(filter)
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer func() { _ = tx.Rollback() }()
records := make([]WalletOutboxRecord, 0, limit)
claimBranches := []struct {
query string
args []any
}{
{
query: walletOutboxClaimQuery(pendingIndex, `
AND next_retry_at_ms IS NULL
AND lock_until_ms IS NULL`, eventFilterSQL),
args: append([]any{appCode, outboxStatusPending}, eventFilterArgs...),
},
{
query: walletOutboxClaimQuery(pendingIndex, `
AND next_retry_at_ms <= ?
AND lock_until_ms IS NULL`, eventFilterSQL),
args: append([]any{appCode, outboxStatusRetryable, nowMS}, eventFilterArgs...),
},
{
query: walletOutboxClaimQuery(claimIndex, `
AND lock_until_ms <= ?`, eventFilterSQL),
args: append([]any{appCode, outboxStatusDelivering, nowMS}, eventFilterArgs...),
},
}
for _, branch := range claimBranches {
remaining := limit - len(records)
if remaining <= 0 {
break
}
args := append(append([]any{}, branch.args...), remaining)
branchRecords, err := queryWalletOutboxRecords(ctx, tx, branch.query, args...)
if err != nil {
return nil, err
}
records = append(records, branchRecords...)
}
for _, record := range records {
if _, err := tx.ExecContext(ctx, `
UPDATE wallet_outbox
SET status = ?, worker_id = ?, lock_until_ms = ?, updated_at_ms = ?
WHERE app_code = ? AND event_id = ?`,
outboxStatusDelivering,
workerID,
lockUntilMS,
nowMS,
record.AppCode,
record.EventID,
); err != nil {
return nil, err
}
}
if err := tx.Commit(); err != nil {
return nil, err
}
for index := range records {
records[index].WorkerID = workerID
records[index].LockUntilMS = lockUntilMS
}
return records, nil
}
func walletOutboxClaimQuery(indexName string, statusCondition string, eventFilterSQL string) string {
return `
SELECT app_code, event_id, event_type, transaction_id, command_id, user_id, asset_type,
available_delta, frozen_delta, COALESCE(CAST(payload AS CHAR), '{}'), created_at_ms,
retry_count, COALESCE(last_error, ''), COALESCE(worker_id, ''), COALESCE(lock_until_ms, 0)
FROM wallet_outbox FORCE INDEX (` + indexName + `)
WHERE app_code = ? AND status = ?` + statusCondition + eventFilterSQL + `
ORDER BY created_at_ms ASC, event_id ASC
LIMIT ?
FOR UPDATE SKIP LOCKED`
}
func walletOutboxEventFilterSQL(filter WalletOutboxClaimFilter) (string, []any) {
include := normalizeWalletOutboxEventTypes(filter.IncludeEventTypes)
if len(include) > 0 {
return " AND event_type IN (" + walletOutboxPlaceholders(len(include)) + ")", walletOutboxEventArgs(include)
}
exclude := normalizeWalletOutboxEventTypes(filter.ExcludeEventTypes)
if len(exclude) > 0 {
return " AND event_type NOT IN (" + walletOutboxPlaceholders(len(exclude)) + ")", walletOutboxEventArgs(exclude)
}
return "", nil
}
func walletOutboxClaimIndexes(filter WalletOutboxClaimFilter) (string, string) {
if len(normalizeWalletOutboxEventTypes(filter.IncludeEventTypes)) > 0 {
return "idx_wallet_outbox_event_pending", "idx_wallet_outbox_event_claim"
}
return "idx_wallet_outbox_pending", "idx_wallet_outbox_claim"
}
func normalizeWalletOutboxEventTypes(values []string) []string {
normalized := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
normalized = append(normalized, value)
}
return normalized
}
func walletOutboxPlaceholders(count int) string {
if count <= 0 {
return ""
}
placeholders := make([]string, count)
for index := range placeholders {
placeholders[index] = "?"
}
return strings.Join(placeholders, ",")
}
func walletOutboxEventArgs(values []string) []any {
args := make([]any, 0, len(values))
for _, value := range values {
args = append(args, value)
}
return args
}
func queryWalletOutboxRecords(ctx context.Context, tx *sql.Tx, query string, args ...any) ([]WalletOutboxRecord, error) {
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
records := make([]WalletOutboxRecord, 0)
for rows.Next() {
var record WalletOutboxRecord
if err := rows.Scan(
&record.AppCode,
&record.EventID,
&record.EventType,
&record.TransactionID,
&record.CommandID,
&record.UserID,
&record.AssetType,
&record.AvailableDelta,
&record.FrozenDelta,
&record.PayloadJSON,
&record.CreatedAtMS,
&record.RetryCount,
&record.LastError,
&record.WorkerID,
&record.LockUntilMS,
); err != nil {
return nil, err
}
records = append(records, record)
}
if err := rows.Err(); err != nil {
return nil, err
}
return records, nil
}
// MarkWalletOutboxDelivered marks one wallet fact as accepted by the MQ broker.
func (r *Repository) MarkWalletOutboxDelivered(ctx context.Context, eventID string) error {
if r == nil || r.db == nil {
return xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
_, err := r.db.ExecContext(ctx, `
UPDATE wallet_outbox
SET status = ?, worker_id = '', lock_until_ms = NULL, next_retry_at_ms = NULL, last_error = NULL, updated_at_ms = ?
WHERE app_code = ? AND event_id = ?`,
outboxStatusDelivered,
time.Now().UTC().UnixMilli(),
appcode.FromContext(ctx),
eventID,
)
return err
}
// MarkWalletOutboxRetryable releases one failed MQ publish with a bounded retry schedule.
func (r *Repository) MarkWalletOutboxRetryable(ctx context.Context, eventID string, lastErr string, nextRetryAtMS int64) error {
if r == nil || r.db == nil {
return xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
_, err := r.db.ExecContext(ctx, `
UPDATE wallet_outbox
SET status = ?, worker_id = '', lock_until_ms = NULL, retry_count = retry_count + 1, next_retry_at_ms = ?, last_error = ?, updated_at_ms = ?
WHERE app_code = ? AND event_id = ?`,
outboxStatusRetryable,
nextRetryAtMS,
trimWalletOutboxError(lastErr),
time.Now().UTC().UnixMilli(),
appcode.FromContext(ctx),
eventID,
)
return err
}
// MarkWalletOutboxDead stops retrying a poison wallet fact after the configured retry ceiling.
func (r *Repository) MarkWalletOutboxDead(ctx context.Context, eventID string, lastErr string) error {
if r == nil || r.db == nil {
return xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
_, err := r.db.ExecContext(ctx, `
UPDATE wallet_outbox
SET status = ?, worker_id = '', lock_until_ms = NULL, last_error = ?, updated_at_ms = ?
WHERE app_code = ? AND event_id = ?`,
outboxStatusFailed,
trimWalletOutboxError(lastErr),
time.Now().UTC().UnixMilli(),
appcode.FromContext(ctx),
eventID,
)
return err
}
func trimWalletOutboxError(value string) string {
value = strings.TrimSpace(value)
if len(value) > 1024 {
return value[:1024]
}
return value
}
func balanceChangedEvent(transactionID string, commandID string, userID int64, assetType string, availableDelta int64, frozenDelta int64, availableAfter int64, frozenAfter int64, balanceVersion int64, payload any, nowMs int64, bizTypes ...string) walletOutboxEvent {
bizType := ""
if len(bizTypes) > 0 {
bizType = strings.TrimSpace(bizTypes[0])
}
return walletOutboxEvent{
EventID: eventID(transactionID, "WalletBalanceChanged", userID, assetType),
EventType: "WalletBalanceChanged",
TransactionID: transactionID,
CommandID: commandID,
UserID: userID,
AssetType: assetType,
AvailableDelta: availableDelta,
FrozenDelta: frozenDelta,
Payload: map[string]any{
"transaction_id": transactionID,
"command_id": commandID,
"user_id": userID,
"asset_type": assetType,
"biz_type": bizType,
"available_delta": availableDelta,
"frozen_delta": frozenDelta,
"available_after": availableAfter,
"frozen_after": frozenAfter,
"balance_version": balanceVersion,
"metadata": payload,
"created_at_ms": nowMs,
},
CreatedAtMS: nowMs,
}
}
func coinSellerTransferredEvent(transactionID string, commandID string, sellerUserID int64, availableDelta int64, payload any, nowMs int64) walletOutboxEvent {
return walletOutboxEvent{
EventID: eventID(transactionID, "WalletCoinSellerTransferred", sellerUserID, ledger.AssetCoinSellerCoin),
EventType: "WalletCoinSellerTransferred",
TransactionID: transactionID,
CommandID: commandID,
UserID: sellerUserID,
AssetType: ledger.AssetCoinSellerCoin,
AvailableDelta: availableDelta,
FrozenDelta: 0,
Payload: payload,
CreatedAtMS: nowMs,
}
}
func rechargeRecordedEvent(transactionID string, commandID string, targetUserID int64, availableDelta int64, payload any, nowMs int64) walletOutboxEvent {
assetType := ledger.AssetCoin
if metadata, ok := payload.(coinSellerTransferMetadata); ok {
if strings.TrimSpace(metadata.TargetAssetType) != "" {
assetType = metadata.TargetAssetType
}
rechargeType := strings.TrimSpace(metadata.RechargeType)
if rechargeType == "" {
rechargeType = "coin_seller_transfer"
}
payload = map[string]any{
"app_code": metadata.AppCode,
"payment_order_id": metadata.PaymentOrderID,
"provider": metadata.ProviderCode,
"payment_method_id": metadata.PaymentMethodID,
"seller_user_id": metadata.SellerUserID,
"target_user_id": metadata.TargetUserID,
"target_country_id": metadata.TargetCountryID,
"country_id": metadata.TargetCountryID,
"google_product_id": metadata.GoogleProductID,
"product_code": metadata.ProductCode,
"product_name": metadata.ProductName,
"seller_region_id": metadata.SellerRegionID,
"target_region_id": metadata.TargetRegionID,
"region_id": metadata.TargetRegionID,
"amount": metadata.Amount,
"reason": metadata.Reason,
"seller_asset_type": metadata.SellerAssetType,
"target_asset_type": metadata.TargetAssetType,
"seller_balance_after": metadata.SellerBalanceAfter,
"target_balance_after": metadata.TargetBalanceAfter,
"recharge_sequence": metadata.RechargeSequence,
"recharge_usd_minor": metadata.RechargeUSDMinor,
"recharge_currency_code": metadata.RechargeCurrencyCode,
"recharge_policy_id": metadata.RechargePolicyID,
"recharge_policy_version": metadata.RechargePolicyVersion,
"recharge_policy_coin_amount": metadata.RechargePolicyCoinAmount,
"recharge_policy_usd_minor_amount": metadata.RechargePolicyUSDMinorAmount,
"recharge_type": rechargeType,
"created_at_ms": nowMs,
}
}
return walletOutboxEvent{
EventID: eventID(transactionID, "WalletRechargeRecorded", targetUserID, assetType),
EventType: "WalletRechargeRecorded",
TransactionID: transactionID,
CommandID: commandID,
UserID: targetUserID,
AssetType: assetType,
AvailableDelta: availableDelta,
FrozenDelta: 0,
Payload: payload,
CreatedAtMS: nowMs,
}
}
func taskRewardCreditedEvent(transactionID string, commandID string, metadata taskRewardMetadata, nowMs int64) walletOutboxEvent {
return walletOutboxEvent{
EventID: eventID(transactionID, "WalletTaskRewardCredited", metadata.TargetUserID, ledger.AssetCoin),
EventType: "WalletTaskRewardCredited",
TransactionID: transactionID,
CommandID: commandID,
UserID: metadata.TargetUserID,
AssetType: ledger.AssetCoin,
AvailableDelta: metadata.Amount,
FrozenDelta: 0,
Payload: map[string]any{
"transaction_id": transactionID,
"command_id": commandID,
"user_id": metadata.TargetUserID,
"task_type": metadata.TaskType,
"task_id": metadata.TaskID,
"cycle_key": metadata.CycleKey,
"amount": metadata.Amount,
"reason": metadata.Reason,
"created_at_ms": nowMs,
},
CreatedAtMS: nowMs,
}
}
func luckyGiftRewardCreditedEvent(transactionID string, commandID string, metadata luckyGiftRewardMetadata, nowMs int64) walletOutboxEvent {
return walletOutboxEvent{
EventID: eventID(transactionID, "WalletLuckyGiftRewardCredited", metadata.TargetUserID, ledger.AssetCoin),
EventType: "WalletLuckyGiftRewardCredited",
TransactionID: transactionID,
CommandID: commandID,
UserID: metadata.TargetUserID,
AssetType: ledger.AssetCoin,
AvailableDelta: metadata.Amount,
FrozenDelta: 0,
Payload: map[string]any{
"transaction_id": transactionID,
"command_id": commandID,
"user_id": metadata.TargetUserID,
"draw_id": metadata.DrawID,
"room_id": metadata.RoomID,
"visible_region_id": metadata.VisibleRegionID,
"region_id": metadata.VisibleRegionID,
"country_id": metadata.CountryID,
"gift_id": metadata.GiftID,
"pool_id": metadata.PoolID,
"amount": metadata.Amount,
"reason": metadata.Reason,
"created_at_ms": nowMs,
},
CreatedAtMS: nowMs,
}
}
func wheelRewardCreditedEvent(transactionID string, commandID string, metadata wheelRewardMetadata, nowMs int64) walletOutboxEvent {
return walletOutboxEvent{
EventID: eventID(transactionID, "WalletWheelRewardCredited", metadata.TargetUserID, ledger.AssetCoin),
EventType: "WalletWheelRewardCredited",
TransactionID: transactionID,
CommandID: commandID,
UserID: metadata.TargetUserID,
AssetType: ledger.AssetCoin,
AvailableDelta: metadata.Amount,
FrozenDelta: 0,
Payload: map[string]any{
"transaction_id": transactionID,
"command_id": commandID,
"user_id": metadata.TargetUserID,
"draw_id": metadata.DrawID,
"wheel_id": metadata.WheelID,
"selected_tier_id": metadata.SelectedTierID,
"visible_region_id": metadata.VisibleRegionID,
"region_id": metadata.VisibleRegionID,
"amount": metadata.Amount,
"reason": metadata.Reason,
"created_at_ms": nowMs,
},
CreatedAtMS: nowMs,
}
}
func roomTurnoverRewardCreditedEvent(transactionID string, commandID string, metadata roomTurnoverRewardMetadata, nowMs int64) walletOutboxEvent {
return walletOutboxEvent{
EventID: eventID(transactionID, "WalletRoomTurnoverRewardCredited", metadata.TargetUserID, ledger.AssetCoin),
EventType: "WalletRoomTurnoverRewardCredited",
TransactionID: transactionID,
CommandID: commandID,
UserID: metadata.TargetUserID,
AssetType: ledger.AssetCoin,
AvailableDelta: metadata.Amount,
FrozenDelta: 0,
Payload: map[string]any{
"transaction_id": transactionID,
"command_id": commandID,
"user_id": metadata.TargetUserID,
"settlement_id": metadata.SettlementID,
"room_id": metadata.RoomID,
"period_start_ms": metadata.PeriodStartMS,
"period_end_ms": metadata.PeriodEndMS,
"coin_spent": metadata.CoinSpent,
"tier_id": metadata.TierID,
"tier_code": metadata.TierCode,
"amount": metadata.Amount,
"reason": metadata.Reason,
"created_at_ms": nowMs,
},
CreatedAtMS: nowMs,
}
}
func inviteActivityRewardCreditedEvent(transactionID string, commandID string, metadata inviteActivityRewardMetadata, nowMs int64) walletOutboxEvent {
return walletOutboxEvent{
EventID: eventID(transactionID, "WalletInviteActivityRewardCredited", metadata.TargetUserID, ledger.AssetCoin),
EventType: "WalletInviteActivityRewardCredited",
TransactionID: transactionID,
CommandID: commandID,
UserID: metadata.TargetUserID,
AssetType: ledger.AssetCoin,
AvailableDelta: metadata.Amount,
FrozenDelta: 0,
Payload: map[string]any{
"transaction_id": transactionID,
"command_id": commandID,
"user_id": metadata.TargetUserID,
"claim_id": metadata.ClaimID,
"reward_type": metadata.RewardType,
"tier_id": metadata.TierID,
"tier_code": metadata.TierCode,
"cycle_key": metadata.CycleKey,
"reached_value": metadata.ReachedValue,
"amount": metadata.Amount,
"reason": metadata.Reason,
"created_at_ms": nowMs,
},
CreatedAtMS: nowMs,
}
}
func agencyOpeningRewardCreditedEvent(transactionID string, commandID string, metadata agencyOpeningRewardMetadata, nowMs int64) walletOutboxEvent {
return walletOutboxEvent{
EventID: eventID(transactionID, "WalletAgencyOpeningRewardCredited", metadata.TargetUserID, ledger.AssetCoin),
EventType: "WalletAgencyOpeningRewardCredited",
TransactionID: transactionID,
CommandID: commandID,
UserID: metadata.TargetUserID,
AssetType: ledger.AssetCoin,
AvailableDelta: metadata.Amount,
FrozenDelta: 0,
Payload: map[string]any{
"transaction_id": transactionID,
"command_id": commandID,
"user_id": metadata.TargetUserID,
"application_id": metadata.ApplicationID,
"cycle_id": metadata.CycleID,
"agency_id": metadata.AgencyID,
"rank_no": metadata.RankNo,
"score_coins": metadata.ScoreCoins,
"amount": metadata.Amount,
"reason": metadata.Reason,
"created_at_ms": nowMs,
},
CreatedAtMS: nowMs,
}
}
func coinSellerStockCreditedEvent(transactionID string, commandID string, metadata coinSellerStockMetadata, nowMs int64) walletOutboxEvent {
eventType := "WalletCoinSellerCoinCompensated"
if metadata.StockType == ledger.StockTypeUSDTPurchase || metadata.StockType == ledger.StockTypeUSDTDeduction {
eventType = "WalletCoinSellerStockPurchased"
}
return walletOutboxEvent{
EventID: eventID(transactionID, eventType, metadata.SellerUserID, ledger.AssetCoinSellerCoin),
EventType: eventType,
TransactionID: transactionID,
CommandID: commandID,
UserID: metadata.SellerUserID,
AssetType: ledger.AssetCoinSellerCoin,
AvailableDelta: metadata.CoinAmount,
FrozenDelta: 0,
Payload: map[string]any{
"transaction_id": transactionID,
"command_id": commandID,
"seller_user_id": metadata.SellerUserID,
"seller_country_id": metadata.SellerCountryID,
"seller_region_id": metadata.SellerRegionID,
"country_id": metadata.SellerCountryID,
"region_id": metadata.SellerRegionID,
"stock_type": metadata.StockType,
"coin_amount": metadata.CoinAmount,
"paid_currency_code": metadata.PaidCurrencyCode,
"paid_amount_micro": metadata.PaidAmountMicro,
"counts_as_seller_recharge": metadata.CountsAsSellerRecharge,
"operator_user_id": metadata.OperatorUserID,
"created_at_ms": nowMs,
},
CreatedAtMS: nowMs,
}
}
func giftDebitedEvent(transactionID string, commandID string, userID int64, assetType string, availableDelta int64, frozenDelta int64, payload any, nowMs int64) walletOutboxEvent {
return walletOutboxEvent{
EventID: eventID(transactionID, "WalletGiftDebited", userID, assetType),
EventType: "WalletGiftDebited",
TransactionID: transactionID,
CommandID: commandID,
UserID: userID,
AssetType: assetType,
AvailableDelta: availableDelta,
FrozenDelta: frozenDelta,
Payload: payload,
CreatedAtMS: nowMs,
}
}
func giftIncomeCreditedEvent(transactionID string, commandID string, metadata giftMetadata, nowMs int64) walletOutboxEvent {
return walletOutboxEvent{
EventID: eventID(transactionID, "WalletGiftIncomeCredited", metadata.TargetUserID, ledger.AssetCoin),
EventType: "WalletGiftIncomeCredited",
TransactionID: transactionID,
CommandID: commandID,
UserID: metadata.TargetUserID,
AssetType: ledger.AssetCoin,
AvailableDelta: metadata.GiftIncomeCoinAmount,
FrozenDelta: 0,
Payload: map[string]any{
"transaction_id": transactionID,
"command_id": commandID,
"user_id": metadata.TargetUserID,
"sender_user_id": metadata.SenderUserID,
"room_id": metadata.RoomID,
"direct_gift": metadata.DirectGift,
"gift_id": metadata.GiftID,
"gift_count": metadata.GiftCount,
"gift_type_code": metadata.GiftTypeCode,
"gift_charge_amount": metadata.ChargeAmount,
"charge_source": metadata.ChargeSource,
"amount": metadata.GiftIncomeCoinAmount,
"ratio_percent": metadata.GiftIncomeRatioPercent,
"ratio_region_id": metadata.GiftIncomeRatioRegionID,
"gift_income_balance_after": metadata.GiftIncomeBalanceAfter,
"created_at_ms": nowMs,
},
CreatedAtMS: nowMs,
}
}
func hostPeriodDiamondCreditedEvent(transactionID string, commandID string, metadata giftMetadata, nowMs int64) walletOutboxEvent {
return walletOutboxEvent{
EventID: eventID(transactionID, "HostPeriodDiamondCredited", metadata.TargetUserID, ledger.AssetHostPeriodDiamond),
EventType: "HostPeriodDiamondCredited",
TransactionID: transactionID,
CommandID: commandID,
UserID: metadata.TargetUserID,
AssetType: ledger.AssetHostPeriodDiamond,
AvailableDelta: metadata.HostPeriodDiamondAdded,
FrozenDelta: 0,
Payload: map[string]any{
"transaction_id": transactionID,
"command_id": commandID,
"host_user_id": metadata.TargetUserID,
"cycle_key": metadata.HostPeriodCycleKey,
"region_id": metadata.TargetHostRegionID,
"agency_owner_user_id": metadata.TargetAgencyOwnerUserID,
"diamond_delta": metadata.HostPeriodDiamondAdded,
"diamond_after": metadata.HostPeriodDiamondAfter,
"gift_charge_asset_type": metadata.ChargeAssetType,
"gift_charge_amount": metadata.ChargeAmount,
"gift_id": metadata.GiftID,
"gift_count": metadata.GiftCount,
"sender_user_id": metadata.SenderUserID,
"sender_region_id": metadata.SenderRegionID,
"room_id": metadata.RoomID,
"gift_diamond_ratio_percent": metadata.GiftDiamondRatioPercent,
"gift_diamond_ratio_region_id": metadata.GiftDiamondRatioRegionID,
"created_at_ms": nowMs,
},
CreatedAtMS: nowMs,
}
}