731 lines
26 KiB
Go
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,
|
|
}
|
|
}
|