본문으로 건너뛰기

Game Service

Status

Active — Aggregator-only

Purpose

game_service 是 RGB 与 Aggregator 之间唯一的游戏边界。它向旧客户端维持 /api/v1/game/* 所需的数据结构和行为,但不与 HO、WC、MG、BTI、Digitain 或其他游戏公司直接通信。

Runtime contract

RGB client
-> gateway /api/v1/game/*
-> game_service /integration/*
-> active PostgreSQL catalog snapshot

game_catalog_worker (periodic, per configured brand)
-> Aggregator GET /api/v1/providers
-> Aggregator paginated GET /api/v1/games
-> validate one closed catalog version
-> atomically publish immutable PostgreSQL snapshot

RGB launch / BTI event widget
-> game_service live policy and active-snapshot membership checks
-> Aggregator Brand API
-> game provider

game provider
-> Aggregator provider authentication + player resolution
-> gateway /aggregator/provider/{team_code}/{provider_code}/{operation}
-> game_service provider-protocol handler
-> wallet_service

Allowed outbound game requests:

  • GET {AGGREGATOR_BASE_URL}/api/v1/providers(目录 worker)
  • GET {AGGREGATOR_BASE_URL}/api/v1/games(目录 worker,分页)
  • POST {AGGREGATOR_BASE_URL}/api/v1/launch
  • POST {AGGREGATOR_BASE_URL}/api/v1/events(旧客户端 /game/bti/events 的兼容来源)

Allowed public callbacks are the exact provider method/path combinations under /aggregator/provider/{team_code}/{provider_code}/{operation}. Aggregator preserves the provider's HTTP method, raw query, raw body and response envelope, then adds a length-framed HMAC signature and signed player context. RGB still executes the provider-specific protocol and every wallet/ledger mutation. The old normalized /aggregator/{balance|debit|credit|rollback} routes and the old public native provider prefixes do not exist. There is no provider-direct launch fallback.

Catalog read model

玩家列表请求不会同步等待 Aggregator。独立 game_catalog_worker 为每个明确配置的 RGB brand_id 定期拉取 provider 和全部 game 分页:先读 provider 版本、读取每个 game 分页,再读一次 provider 版本关闭扫描。版本、总数、分页元数据、provider/game 唯一性、provider 归属、产品子标识、默认启动游戏和所有字段边界必须全部通过验证, 否则整轮不发布。

成功扫描写入 game_catalog_snapshotgame_catalog_providergame_catalog_game,并在同一 PostgreSQL 事务、同一 brand advisory lock 内切换 game_catalog_sync_state.active_version。API 副本始终读取一个不可变版本;同步失败 只更新失败状态和连续失败次数,保留最后有效版本。首次成功前列表和启动明确返回 Game catalog is not ready,绝不回退到实时 Aggregator 列表或旧直连表。

每个 Aggregator provider code 可以通过 provider_product_ids 无歧义拆分为多个 RGB provider(例如同一上游家族的真人/老虎机产品)。重复 provider code 若没有 互斥产品集合,或上游 game 缺少必要产品标识,整轮同步失败。

读取时再叠加实时 RGB 全局展示、维护开关、品牌 provider allow-list 和功能开关。 provider 列表与全 provider 游戏页直接查 PostgreSQL,保证维护/隐藏变更立即可见; 只有已经重新校验实时 provider 策略的 provider-scoped 游戏页使用 brand + snapshot version + policy digest Redis 短缓存。Redis 故障自动回退数据库, 不能改变可见性或阻止列表服务。

Policy enforcement

Before exposing or launching an Aggregator game, RGB still enforces its own policy:

  • the RGB provider row exists and provider.is_show=1;
  • maintenance_mode is off;
  • the provider is enabled for the resolved brand;
  • the provider has an explicit AGGREGATOR_PROVIDER_MAP entry;
  • the player is active and belongs to the request brand;
  • game feature flags and coupon provider restrictions allow the operation.

All-provider catalog calls intersect the active local snapshot with the resolved brand's RGB allow-list. An Aggregator item that is unmapped, disabled, hidden or under maintenance is discarded. Launch additionally proves that the exact game belongs to the current active snapshot before making a live Aggregator call.

Callback handlers apply the same provider policy to new debits: the provider must exist and be enabled/non-maintained in both live RGB policy and the active Aggregator snapshot. Settlement, rollback and reserve completion remain available during maintenance so already accepted money reaches a terminal idempotent state.

Native HO and MG lifecycle

HO reaches RGB as the original attribute-only XML protocol. GetBalance is read-only; FundTransfer supports 500 Bet, 510 Win, 520 Loss, 502 place-bet cancellation and 501 game cancellation. One gameid may contain several 500 transactions. Win/loss closes all wagers in the round with stable child wallet commands, while game cancellation refunds every wager and verifies the Provider amount equals the round's total stake. ho_transaction retains the complete normalized payload and is unique by (brand_id, txn_id).

MG uses the original /login, /getbalance, /updatebalance, /rollback and /monitor contract with X-MGP-TOKEN, request correlation headers and signed relay identity. GAME credits can be incremental and close the entire betId round only when completed=true; offline credit event types use the configured wallet bucket. RGB retains complete 1,000-character identifiers, metadata, response identity and rollback state in mg_transaction. The current official callback channel can distinguish the RGB Live (30020) and Slots (30022) products; MG Plus (30021) launch is deliberately blocked until a distinct callback product identity can be proven. Those channel values select exact RGB Provider rows; they are not per-game provider_product_id values.

Digitain CreditBetByBatch crosses from game_service to wallet_service as one /internal/wallet/v2/bets/batch/authorize command. Wallet authorization, child authorization creation and unused-reserve release share one database transaction. An error therefore leaves no externally visible reserve for game_service to compensate, while exact retries reuse the historical reserve/commit sub-keys and a changed batch payload fails with an idempotency conflict instead of releasing money owned by the earlier request.

Authentication and isolation

Brand API calls use the credential explicitly mapped to the request brand_id. A credential mapped to another brand is never used as a fallback. RGB always sends X-Signature-Version: 2; its length-framed HMAC binds timestamp, HTTP method, exact path and canonical parameter JSON. Aggregator's versionless v1 verifier exists only for older non-RGB consumers.

Aggregator relay callbacks use length-framed HMAC authentication over timestamp, method, target path, raw query, signed context and raw body:

  • X-Aggregator-Relay-Key
  • X-Aggregator-Relay-Timestamp
  • X-Aggregator-Relay-Signature
  • X-Aggregator-Relay-Context

The team path and relay key select one explicit relay credential. The signed context must match the path and prove that Aggregator authenticated the provider. RGB resolves the signed external player ID through player_service and verifies brand ownership before any handler or wallet call. Timestamps outside the configured skew window, oversized bodies and invalid context/signatures are rejected first.

Relay context version 2 additionally binds a stable event_id, a stable length-framed request_hash, exact forwarded-header integrity hash, and the contract id. Volatile Provider timestamps/signatures are excluded only from event identity, so a re-signed retry keeps the same write event while any forwarded-header mutation still fails authentication. After authentication and identity resolution, game_service claims provider_relay_inbox before entering a Provider handler. An exact completed replay returns the cached result; a concurrent live lease returns 409 without running the handler; reuse of an event id with different scope fails closed.

For stale write recovery, Aggregator first reads the signed Inbox status. If no completed envelope exists it can replay the immutable callback with reconciliation=true covered by the same relay HMAC. This recovery path keeps the original header hashes and bounded Content-Type in signed context instead of storing Provider API keys/signatures. MG also retains only its non-secret request UUID/millisecond timestamp, restores both headers and recomputes the request identity hash before handler execution. The path reclaims the same Inbox event and remains subject to the native Provider and wallet idempotency keys.

Provider handlers still execute RGB's native protocol, wallet commands and complete bet/business logic. ORM mutations for the allow-listed game-domain tables are copied to provider_relay_mutation_outbox through the same database transaction as the business row. This makes a committed bet reconstructable even if the process exits after commit but before the HTTP response is stored. Every signed mutation includes the Outbox mutation_id and database-clock committed_at, so a delayed recovery cannot supersede a newer entity snapshot merely because its envelope completed later. After the handler succeeds, game_service builds one canonical result envelope containing the native handler's Provider business status/headers/body, committed entity snapshots and credential-free wallet-call audit, caches it in the Inbox, and signs the exact envelope bytes in X-RGB-Relay-Signature. Gateway passes that internal envelope back unchanged.

Each credential-free wallet request/result is upserted immediately into provider_relay_wallet_outbox. A successful audit cannot be replaced by a late stale transport failure. An exact retry can therefore rebuild wallet audit from PostgreSQL even when the prior worker exited after wallet_service committed but before the Inbox envelope was stored. Stable wallet commands and their balance snapshots use distinct, bounded SHA-256 call identities; repeated batch GETs are not hash-collapsed. Funding rows are accepted only when source kind, source code, role and physical Bucket/Coupon identity agree. Digitain batch children additionally preserve one source's exact before/after chain across child boundaries. Balance snapshots reject empty or duplicate Bucket codes, unknown roles, negative/non-finite values and sub-cent precision before any Provider balance is derived. Retryable Inbox reasons retain the wallet boundary's exact failure class: wallet_protocol_error identifies an invalid response contract, while wallet_unavailable is reserved for transport and availability failures.

Aggregator verifies the signature and scope, applies its own append-only projection, then releases the nested Provider response. For Digitain, Aggregator validates the unsigned business body before APPLIED and adds the Provider-facing TimeStamp and ordered Signature with the Aggregator-owned credential at the public edge. RGB remains the only balance executor; Aggregator never issues a second wallet command for a relayed Team.

/integration/launch is an internal gateway backing route. Gateway replaces any client-supplied player identity with the JWT-verified player_id and adds per-caller internal authentication.

/integration/bti/league-candidates is an internal Admin backing route even though it remains under the compatibility /integration/* namespace. admin_service must attach the selected X-Brand-Id plus its own per-caller internal token; game_service rejects unauthenticated calls and never accepts a brand from the request payload. This keeps operator candidate discovery on the same brand-scoped Aggregator credential and catalog policy as the public sports-home feed.

Configuration

Required:

  • AGGREGATOR_BASE_URL
  • AGGREGATOR_BRAND_CREDENTIALS
  • AGGREGATOR_PROVIDER_MAP
  • AGGREGATOR_RELAY_CREDENTIALS

AGGREGATOR_PROVIDER_MAP is an exact allow-list, not a default. It must contain every RGB Provider enabled for a Brand. Repeated Aggregator codes require mutually disjoint provider_product_ids; a callback or catalog product that matches zero or multiple entries fails closed. Relay credentials are keyed by the Aggregator Team code, while Brand API credentials are keyed by RGB brand_id; these namespaces are intentionally different. For MG, callback channel selects the exact RGB row 30020/30022; the mapping's provider_product_ids remain Aggregator catalog game identities used to split repeated mg catalog rows and must not be replaced by those RGB row IDs.

Optional with validated defaults:

  • AGGREGATOR_CURRENCY=KRW
  • AGGREGATOR_LAUNCH_MODE=real
  • AGGREGATOR_CATALOG_SYNC_INTERVAL_SECONDS=300
  • AGGREGATOR_CATALOG_SYNC_TIMEOUT_SECONDS=300
  • AGGREGATOR_CATALOG_PAGE_SIZE=200
  • AGGREGATOR_CATALOG_MAX_GAMES=100000
  • AGGREGATOR_CATALOG_RETAINED_VERSIONS=2
  • AGGREGATOR_CATALOG_RESPONSE_CACHE_TTL_SECONDS=10
  • AGGREGATOR_CATALOG_SYNC_FAILURE_BUDGET=12
  • AGGREGATOR_CATALOG_SYNC_CONCURRENCY=4
  • AGGREGATOR_REQUEST_TIMEOUT_SECONDS=10
  • AGGREGATOR_RESPONSE_MAX_BYTES=4194304
  • AGGREGATOR_CALLBACK_CLOCK_SKEW_SECONDS=300
  • AGGREGATOR_CALLBACK_MAX_BODY_BYTES=1048576
  • AGGREGATOR_CALLBACK_MAX_RESPONSE_BYTES=1048576 (nested Provider response)
  • AGGREGATOR_CALLBACK_MAX_ENVELOPE_BYTES=4194304
  • AGGREGATOR_CALLBACK_INFLIGHT_TIMEOUT_SECONDS=35(必须大于 Aggregator 的完整下游 HTTP 超时;默认 30 秒)
  • AGGREGATOR_RELAY_MAINTENANCE_INTERVAL_SECONDS=60
  • AGGREGATOR_RELAY_MAINTENANCE_BATCH_SIZE=200
  • AGGREGATOR_RELAY_READ_RETENTION_SECONDS=86400
  • AGGREGATOR_RELAY_WRITE_RETENTION_SECONDS=7776000
  • AGGREGATOR_ADJUST_BUCKET_TYPE_CODE

Native provider URLs, credentials and callback secrets belong exclusively to the Aggregator deployment and must not be present in RGB configuration.

Before upgrading a database from a revision older than 0064, run the following read-only preflight. Every query must return zero rows. Complete/cancel active legacy BTI reserves, reconcile invalid coupon balances, and explicitly reconcile/archive any ambiguous HO/MG history before Alembic; the migration never guesses a funding order or money direction:

SELECT brand_id, sportsbook, reserve_id, status
FROM rgb.bti_reserve
WHERE status IN (0, 2);

SELECT id, original_amount, remaining_amount
FROM wallet.wallet_coupon_grant
WHERE original_amount < 0
OR remaining_amount < 0
OR remaining_amount > original_amount;

SELECT brand_id, txn_id, count(*)
FROM rgb.ho_transaction
GROUP BY brand_id, txn_id HAVING count(*) > 1;

SELECT brand_id, transaction_id, count(*)
FROM rgb.mg_transaction
GROUP BY brand_id, transaction_id HAVING count(*) > 1;

SELECT m.guid, m.brand_id, m.transaction_id, m.bet_id, m.status
FROM rgb.mg_transaction AS m
WHERE m.status <> 1
AND NOT EXISTS (
SELECT 1 FROM rgb.bet AS b
WHERE b.brand_id = m.brand_id
AND b.provider_id IN (30020, 30022)
AND b.transaction_id = m.transaction_id
);

SELECT m.guid, m.brand_id, m.transaction_id, m.bet_id
FROM rgb.mg_transaction AS m
WHERE NOT EXISTS (
SELECT 1 FROM rgb.bet AS b
WHERE b.brand_id = m.brand_id
AND b.provider_id IN (30020, 30022)
AND (
b.transaction_id = m.transaction_id
OR b.game_round_id = m.bet_id
)
);

SELECT h.guid, h.brand_id, h.txn_id, h.game_id
FROM rgb.ho_transaction AS h
WHERE NOT EXISTS (
SELECT 1 FROM rgb.bet AS b
WHERE b.brand_id = h.brand_id
AND b.provider_id = 30000
AND (b.transaction_id = h.txn_id OR b.game_round_id = h.game_id)
);

Dependencies

  • PostgreSQL
  • Redis
  • player_service
  • wallet_service
  • Aggregator Brand API

game_service /health checks DB and Redis. game_catalog_worker has its own heartbeat and failure budget; its health turns red after consecutive complete sync failures. Operators must also alert on game_catalog_sync_state.last_success_at, status and consecutive_failures so a worker that is alive but serving an aging last-known-good snapshot is visible.

An Aggregator outage does not block site startup or cached catalog reads. It prevents new catalog publication and fails live launch/events explicitly; the last valid list remains readable until recovery. A fresh environment with no valid snapshot fails list/launch requests closed.

Verification

  • cd servers_v2/game_service && uv run pytest
  • cd servers_v2/gateway && uv run pytest
  • make verify-servers-v2-ui-e2e for the Docker cross-service flow

The E2E stack runs a local Aggregator Brand API stub for provider/game catalog, waits for the real catalog worker publication, launches through the Brand API, then exercises relay-backed Seamless through gateway with the original BTI protocol. It asserts Inbox/outbox recovery, exact Provider response release, both ledgers, wallet balances, replay idempotency, resettlement, tamper rejection and removal of both the old normalized callbacks and native public prefixes.

make verify-servers-v2-cross-repo replaces that stub with the real disposable Aggregator stack. It additionally covers mandatory relay startup validation, two-brand Provider-id isolation, authoritative mixed-source funding for Digitain batch commands, an ordinary MG command, and post-commit response loss followed by Aggregator UNKNOWN reconciliation without a second wallet debit.