874 lines
32 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
// ListWalletOutboxAppCodes enumerates the tenant key stored in this owner table.
// PRIMARY starts with app_code, so MySQL can use a loose ordered index scan;
// callers cache the result and never run this discovery query on every poll.
func (r *Repository) ListWalletOutboxAppCodes(ctx context.Context) ([]string, error) {
if r == nil || r.db == nil {
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
rows, err := r.db.QueryContext(ctx, `SELECT DISTINCT app_code FROM wallet_outbox FORCE INDEX (PRIMARY) ORDER BY app_code`)
if err != nil {
return nil, err
}
defer rows.Close()
appCodes := make([]string, 0, 4)
for rows.Next() {
var appCode string
if err := rows.Scan(&appCode); err != nil {
return nil, err
}
appCodes = append(appCodes, appCode)
}
return appCodes, rows.Err()
}
// 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])
}
eventPayload := 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,
}
if suppressWalletPrivateNotice(assetType, payload) {
// 抑制决策必须和账务事实一起固化notice-service 只能消费这份不可变快照,
// 不能跨库读取用户 source 或根据当前资料反推交易发生时的账号语义。
eventPayload["suppress_private_notice"] = true
}
return walletOutboxEvent{
EventID: eventID(transactionID, "WalletBalanceChanged", userID, assetType),
EventType: "WalletBalanceChanged",
TransactionID: transactionID,
CommandID: commandID,
UserID: userID,
AssetType: assetType,
AvailableDelta: availableDelta,
FrozenDelta: frozenDelta,
Payload: eventPayload,
CreatedAtMS: nowMs,
}
}
func suppressWalletPrivateNotice(assetType string, payload any) bool {
if strings.EqualFold(strings.TrimSpace(assetType), ledger.AssetRobotCoin) {
return true
}
// giftMetadata.RobotGift 是 wallet 扣费时的 owner 快照;只认可强类型业务元数据,
// 不在通用 JSON/map 里猎猜 source、command_id 或 biz_type。
switch metadata := payload.(type) {
case giftMetadata:
return metadata.RobotGift
case *giftMetadata:
return metadata != nil && metadata.RobotGift
default:
return false
}
}
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 coinSellerSubTransferredEvent(transactionID string, commandID string, metadata coinSellerSubTransferMetadata, nowMs int64) walletOutboxEvent {
return walletOutboxEvent{
EventID: eventID(transactionID, "WalletCoinSellerSubTransferred", metadata.ParentUserID, ledger.AssetCoinSellerCoin),
EventType: "WalletCoinSellerSubTransferred",
TransactionID: transactionID,
CommandID: commandID,
UserID: metadata.ParentUserID,
AssetType: ledger.AssetCoinSellerCoin,
AvailableDelta: -metadata.Amount,
FrozenDelta: 0,
Payload: map[string]any{
"transaction_id": transactionID,
"command_id": commandID,
"parent_user_id": metadata.ParentUserID,
"child_user_id": metadata.ChildUserID,
"region_id": metadata.RegionID,
"amount": metadata.Amount,
"asset_type": metadata.AssetType,
"parent_balance_after": metadata.ParentBalanceAfter,
"child_balance_after": metadata.ChildBalanceAfter,
"created_at_ms": nowMs,
},
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, metadata.AssetType),
EventType: "WalletTaskRewardCredited",
TransactionID: transactionID,
CommandID: commandID,
UserID: metadata.TargetUserID,
AssetType: metadata.AssetType,
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,
"asset_type": metadata.AssetType,
"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,
}
}
func hostPointCreditedEvent(transactionID string, commandID string, metadata giftMetadata, nowMs int64) walletOutboxEvent {
return walletOutboxEvent{
EventID: eventID(transactionID, "HostPointCredited", metadata.TargetUserID, ledger.AssetPoint),
EventType: "HostPointCredited",
TransactionID: transactionID,
CommandID: commandID,
UserID: metadata.TargetUserID,
AssetType: ledger.AssetPoint,
AvailableDelta: metadata.HostPointAdded,
FrozenDelta: 0,
Payload: map[string]any{
"transaction_id": transactionID,
"command_id": commandID,
"host_user_id": metadata.TargetUserID,
"sender_user_id": metadata.SenderUserID,
"room_id": metadata.RoomID,
"region_id": metadata.TargetHostRegionID,
"asset_type": ledger.AssetPoint,
"point_delta": metadata.HostPointAdded,
"point_balance_after": metadata.HostPointBalanceAfter,
"gift_charge_asset_type": metadata.ChargeAssetType,
"gift_charge_amount": metadata.ChargeAmount,
"charge_source": metadata.ChargeSource,
"gift_id": metadata.GiftID,
"gift_count": metadata.GiftCount,
"policy_instance_code": metadata.HostPointPolicyInstanceCode,
"policy_template_code": metadata.HostPointTemplateCode,
"policy_template_version": metadata.HostPointTemplateVersion,
"host_point_ratio_ppm": metadata.HostPointRatioPPM,
"points_per_usd": metadata.PointsPerUSD,
"allow_self_brushing": true,
"created_at_ms": nowMs,
},
CreatedAtMS: nowMs,
}
}
func agencyPointCreditedEvent(transactionID string, commandID string, metadata giftMetadata, nowMs int64) walletOutboxEvent {
return walletOutboxEvent{
EventID: eventID(transactionID, "AgencyPointCredited", metadata.TargetAgencyOwnerUserID, ledger.AssetPoint),
EventType: "AgencyPointCredited",
TransactionID: transactionID,
CommandID: commandID,
UserID: metadata.TargetAgencyOwnerUserID,
AssetType: ledger.AssetPoint,
AvailableDelta: metadata.AgencyPointAdded,
FrozenDelta: 0,
Payload: map[string]any{
"transaction_id": transactionID,
"command_id": commandID,
"host_user_id": metadata.TargetUserID,
"agency_owner_user_id": metadata.TargetAgencyOwnerUserID,
"sender_user_id": metadata.SenderUserID,
"room_id": metadata.RoomID,
"asset_type": ledger.AssetPoint,
"point_delta": metadata.AgencyPointAdded,
"point_balance_after": metadata.AgencyPointBalanceAfter,
"gift_charge_asset_type": metadata.ChargeAssetType,
"gift_charge_amount": metadata.ChargeAmount,
"gift_id": metadata.GiftID,
"gift_count": metadata.GiftCount,
"policy_instance_code": metadata.HostPointPolicyInstanceCode,
"policy_template_code": metadata.HostPointTemplateCode,
"policy_template_version": metadata.HostPointTemplateVersion,
"agency_point_ratio_ppm": metadata.AgencyPointRatioPPM,
"created_at_ms": nowMs,
},
CreatedAtMS: nowMs,
}
}