874 lines
32 KiB
Go
874 lines
32 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
|
||
}
|
||
|
||
// 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,
|
||
}
|
||
}
|