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/launchPOST {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_snapshot、game_catalog_provider 和
game_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_modeis off;- the provider is enabled for the resolved brand;
- the provider has an explicit
AGGREGATOR_PROVIDER_MAPentry; - 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-KeyX-Aggregator-Relay-TimestampX-Aggregator-Relay-SignatureX-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_URLAGGREGATOR_BRAND_CREDENTIALSAGGREGATOR_PROVIDER_MAPAGGREGATOR_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=KRWAGGREGATOR_LAUNCH_MODE=realAGGREGATOR_CATALOG_SYNC_INTERVAL_SECONDS=300AGGREGATOR_CATALOG_SYNC_TIMEOUT_SECONDS=300AGGREGATOR_CATALOG_PAGE_SIZE=200AGGREGATOR_CATALOG_MAX_GAMES=100000AGGREGATOR_CATALOG_RETAINED_VERSIONS=2AGGREGATOR_CATALOG_RESPONSE_CACHE_TTL_SECONDS=10AGGREGATOR_CATALOG_SYNC_FAILURE_BUDGET=12AGGREGATOR_CATALOG_SYNC_CONCURRENCY=4AGGREGATOR_REQUEST_TIMEOUT_SECONDS=10AGGREGATOR_RESPONSE_MAX_BYTES=4194304AGGREGATOR_CALLBACK_CLOCK_SKEW_SECONDS=300AGGREGATOR_CALLBACK_MAX_BODY_BYTES=1048576AGGREGATOR_CALLBACK_MAX_RESPONSE_BYTES=1048576(nested Provider response)AGGREGATOR_CALLBACK_MAX_ENVELOPE_BYTES=4194304AGGREGATOR_CALLBACK_INFLIGHT_TIMEOUT_SECONDS=35(必须大于 Aggregator 的完整下游 HTTP 超时;默认 30 秒)AGGREGATOR_RELAY_MAINTENANCE_INTERVAL_SECONDS=60AGGREGATOR_RELAY_MAINTENANCE_BATCH_SIZE=200AGGREGATOR_RELAY_READ_RETENTION_SECONDS=86400AGGREGATOR_RELAY_WRITE_RETENTION_SECONDS=7776000AGGREGATOR_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_servicewallet_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 pytestcd servers_v2/gateway && uv run pytestmake verify-servers-v2-ui-e2efor 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.