From 44ad51698f541bbfb7de77e03aff2d8d47557e62 Mon Sep 17 00:00:00 2001 From: kang Date: Thu, 11 Jun 2026 09:23:41 +0800 Subject: [PATCH] feat(config, i18n): add integration guide documentation page with multi-language support Added new integration guide page accessible from config hub, providing SSO and wallet integration documentation for client engineering teams. Includes translations in English, Nepali, and Chinese. Refactored percentage conversion utilities across agent management modules to use consistent `percentValueToUi` function, removing deprecated `ratioToPercentUi` and `percentUiToRatio` helpers. --- src/app/admin/docs/integration-guide/page.tsx | 18 + src/i18n/locales/en/config.json | 7 +- src/i18n/locales/ne/config.json | 7 +- src/i18n/locales/zh/config.json | 7 +- src/lib/admin-page-title.ts | 1 + .../agents/agent-line-detail-panel.tsx | 8 +- .../agents/agent-line-provision-wizard.tsx | 6 +- src/modules/agents/agents-console.tsx | 10 +- src/modules/config/config-hub-screen.tsx | 6 + src/modules/docs/integration-guide-screen.tsx | 414 ++++++++++++++++++ 10 files changed, 468 insertions(+), 16 deletions(-) create mode 100644 src/app/admin/docs/integration-guide/page.tsx create mode 100644 src/modules/docs/integration-guide-screen.tsx diff --git a/src/app/admin/docs/integration-guide/page.tsx b/src/app/admin/docs/integration-guide/page.tsx new file mode 100644 index 0000000..08b846a --- /dev/null +++ b/src/app/admin/docs/integration-guide/page.tsx @@ -0,0 +1,18 @@ +import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; +import { ShellAuthGate } from "@/components/admin/auth-gate"; +import { IntegrationGuideScreen } from "@/modules/docs/integration-guide-screen"; +import { PRD_INTEGRATION_ACCESS_ANY } from "@/lib/admin-prd"; +import { buildPageMetadata } from "@/lib/page-metadata"; +import type { Metadata } from "next"; + +export const metadata: Metadata = buildPageMetadata("config", "integrationGuide.title"); + +export default function AdminIntegrationGuidePage(): React.ReactElement { + return ( + + + + + + ); +} \ No newline at end of file diff --git a/src/i18n/locales/en/config.json b/src/i18n/locales/en/config.json index f310d98..67257f1 100644 --- a/src/i18n/locales/en/config.json +++ b/src/i18n/locales/en/config.json @@ -32,7 +32,12 @@ "riskCapTitle": "Risk cap rules", "riskCapDesc": "Per-number payout caps and occupancy", "integrationTitle": "Integration sites", - "integrationDesc": "site_code, JWT secrets, partner wallet URL, iframe allowlist" + "integrationDesc": "site_code, JWT secrets, partner wallet URL, iframe allowlist", + "integrationGuideTitle": "Client integration guide", + "integrationGuideDesc": "SSO and wallet integration document for client engineering teams, with index, examples, and test flow" + }, + "integrationGuide": { + "title": "Lottery client integration guide" }, "integrationSites": { "title": "Integration sites", diff --git a/src/i18n/locales/ne/config.json b/src/i18n/locales/ne/config.json index d8aeaad..ab28c06 100644 --- a/src/i18n/locales/ne/config.json +++ b/src/i18n/locales/ne/config.json @@ -32,7 +32,12 @@ "riskCapTitle": "जोखिम क्याप", "riskCapDesc": "नम्बर क्याप र ओगट उपस्थिति", "integrationTitle": "मुख्य साइट एकीकरण", - "integrationDesc": "site_code, JWT गोप्य, पार्टनर वालेट URL र iframe श्वेतसूची" + "integrationDesc": "site_code, JWT गोप्य, पार्टनर वालेट URL र iframe श्वेतसूची", + "integrationGuideTitle": "ग्राहक एकीकरण दस्तावेज", + "integrationGuideDesc": "ग्राहक प्राविधिक टोलीका लागि SSO र वालेट एकीकरण दस्तावेज, अनुक्रमणिका, उदाहरण र परीक्षण प्रवाह सहित" + }, + "integrationGuide": { + "title": "लटरी ग्राहक एकीकरण दस्तावेज" }, "integrationSites": { "title": "मुख्य साइट एकीकरण साइटहरू", diff --git a/src/i18n/locales/zh/config.json b/src/i18n/locales/zh/config.json index e4cfe84..f6c400c 100644 --- a/src/i18n/locales/zh/config.json +++ b/src/i18n/locales/zh/config.json @@ -32,7 +32,12 @@ "riskCapTitle": "限额版本", "riskCapDesc": "号码赔付封顶与占用视图", "integrationTitle": "接入站点", - "integrationDesc": "site_code、JWT 密钥、主站钱包 URL 与 iframe 白名单" + "integrationDesc": "site_code、JWT 密钥、主站钱包 URL 与 iframe 白名单", + "integrationGuideTitle": "客户接入文档", + "integrationGuideDesc": "给客户技术团队的 SSO 与钱包接入文档,含目录、示例与联调流程" + }, + "integrationGuide": { + "title": "彩票端客户接入文档" }, "integrationSites": { "title": "接入站点", diff --git a/src/lib/admin-page-title.ts b/src/lib/admin-page-title.ts index 181b440..bcaa6b8 100644 --- a/src/lib/admin-page-title.ts +++ b/src/lib/admin-page-title.ts @@ -22,6 +22,7 @@ const EXACT_ROUTES: Record = { "/admin/settlement-center": { ns: "settlementCenter", key: "title" }, "/admin/agents/settlement-bills": { ns: "settlementCenter", key: "title" }, "/admin/config/integration-sites": { ns: "config", key: "integrationSites.title" }, + "/admin/docs/integration-guide": { ns: "config", key: "integrationGuide.title" }, "/admin/wallet": { ns: "wallet", key: "title" }, "/admin/wallet/transactions": { ns: "wallet", key: "walletTransactions" }, "/admin/wallet/transfer-orders": { ns: "wallet", key: "transferOrders" }, diff --git a/src/modules/agents/agent-line-detail-panel.tsx b/src/modules/agents/agent-line-detail-panel.tsx index 02e86c2..24e9a04 100644 --- a/src/modules/agents/agent-line-detail-panel.tsx +++ b/src/modules/agents/agent-line-detail-panel.tsx @@ -21,7 +21,7 @@ import { AgentsPlayersPanel } from "@/modules/agents/agents-players-panel"; import { AgentProfileFields, type AgentProfileFieldsProps } from "@/modules/agents/agent-profile-fields"; import { formatCredit } from "@/modules/agents/agent-line-sidebar"; import { Button } from "@/components/ui/button"; -import { ratioToPercentUi } from "@/lib/admin-rate-percent"; +import { percentValueToUi } from "@/lib/admin-rate-percent"; import { resolveRoleStatusTone } from "@/lib/admin-status-tone"; import { cn } from "@/lib/utils"; import type { AgentNodeProfileSummary, AgentNodeRow, AgentProfileRow } from "@/types/api/admin-agent"; @@ -366,7 +366,7 @@ function OverviewTab({ const { t } = useTranslation(["agents", "common"]); const rebateCap = - profile && !profileLoading ? ratioToPercentUi(profile.rebate_limit ?? 0) : null; + profile && !profileLoading ? percentValueToUi(profile.rebate_limit ?? 0) : null; return (
@@ -421,8 +421,8 @@ function OverviewTab({

{t("lineUi.profileFootnote", { defaultValue: "回水上限 {{rebate}}% · 默认回水 {{defaultRebate}}% · {{cycle}}", - rebate: ratioToPercentUi(profile.rebate_limit ?? 0), - defaultRebate: ratioToPercentUi(profile.default_player_rebate ?? 0), + rebate: percentValueToUi(profile.rebate_limit ?? 0), + defaultRebate: percentValueToUi(profile.default_player_rebate ?? 0), cycle: cycleLabel, })} {(profile.risk_tags?.length ?? 0) > 0 diff --git a/src/modules/agents/agent-line-provision-wizard.tsx b/src/modules/agents/agent-line-provision-wizard.tsx index 3c17243..c29ad58 100644 --- a/src/modules/agents/agent-line-provision-wizard.tsx +++ b/src/modules/agents/agent-line-provision-wizard.tsx @@ -20,7 +20,7 @@ import { } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { useAsyncEffect } from "@/hooks/use-async-effect"; -import { percentUiToRatio } from "@/lib/admin-rate-percent"; +import { percentValueToUi } from "@/lib/admin-rate-percent"; import { adminSiteCodeLabel } from "@/lib/admin-select-display"; import { LotteryApiBizError } from "@/types/api/errors"; import type { AdminIntegrationSiteRow } from "@/types/api/admin-integration-site"; @@ -73,8 +73,8 @@ export function AgentLineProvisionWizard(): React.ReactElement { password: form.password, total_share_rate: Number.parseFloat(form.total_share_rate) || 0, credit_limit: Number.parseInt(form.credit_limit, 10) || 0, - rebate_limit: percentUiToRatio(form.rebate_limit), - default_player_rebate: percentUiToRatio(form.default_player_rebate), + rebate_limit: Number.parseFloat(form.rebate_limit) || 0, + default_player_rebate: Number.parseFloat(form.default_player_rebate) || 0, settlement_cycle: form.settlement_cycle, can_grant_extra_rebate: form.can_grant_extra_rebate, }); diff --git a/src/modules/agents/agents-console.tsx b/src/modules/agents/agents-console.tsx index 01844e5..7b8f7ec 100644 --- a/src/modules/agents/agents-console.tsx +++ b/src/modules/agents/agents-console.tsx @@ -34,10 +34,8 @@ import { useAsyncEffect } from "@/hooks/use-async-effect"; import { useConfirmAction } from "@/hooks/use-confirm-action"; import { useTranslationRef } from "@/hooks/use-translation-ref"; import { - percentUiToRatio, percentValueToUi, parsePercentUi, - ratioToPercentUi, } from "@/lib/admin-rate-percent"; import { adminHasAnyPermission } from "@/lib/admin-permissions"; import { @@ -168,8 +166,8 @@ export function AgentsConsole(): React.ReactElement { setProfileShareRate(percentValueToUi(row.total_share_rate ?? 0)); } setProfileCreditLimit(String(row.credit_limit ?? 0)); - setProfileRebateLimit(ratioToPercentUi(row.rebate_limit ?? 0)); - setProfileDefaultRebate(ratioToPercentUi(row.default_player_rebate ?? 0)); + setProfileRebateLimit(percentValueToUi(row.rebate_limit ?? 0)); + setProfileDefaultRebate(percentValueToUi(row.default_player_rebate ?? 0)); setProfileSettlementCycle(normalizeAgentSettlementCycle(row.settlement_cycle)); setProfileExtraRebate(Boolean(row.can_grant_extra_rebate)); setProfileCanCreateChild(Boolean(row.can_create_child_agent)); @@ -183,8 +181,8 @@ export function AgentsConsole(): React.ReactElement { const shareRate = Number.parseFloat(profileShareRate) || 0; const base = { credit_limit: Number.parseInt(profileCreditLimit, 10) || 0, - rebate_limit: percentUiToRatio(profileRebateLimit), - default_player_rebate: percentUiToRatio(profileDefaultRebate), + rebate_limit: Number.parseFloat(profileRebateLimit) || 0, + default_player_rebate: Number.parseFloat(profileDefaultRebate) || 0, settlement_cycle: normalizeAgentSettlementCycle(profileSettlementCycle), can_grant_extra_rebate: profileExtraRebate, can_create_child_agent: profileCanCreateChild, diff --git a/src/modules/config/config-hub-screen.tsx b/src/modules/config/config-hub-screen.tsx index 23f0bbe..838e40f 100644 --- a/src/modules/config/config-hub-screen.tsx +++ b/src/modules/config/config-hub-screen.tsx @@ -48,6 +48,12 @@ const HUB_CARDS: HubCard[] = [ descKey: "hub.integrationDesc", requiredAny: PRD_INTEGRATION_ACCESS_ANY, }, + { + href: "/admin/docs/integration-guide", + titleKey: "hub.integrationGuideTitle", + descKey: "hub.integrationGuideDesc", + requiredAny: PRD_INTEGRATION_ACCESS_ANY, + }, ]; export function ConfigHubScreen() { diff --git a/src/modules/docs/integration-guide-screen.tsx b/src/modules/docs/integration-guide-screen.tsx new file mode 100644 index 0000000..91e3921 --- /dev/null +++ b/src/modules/docs/integration-guide-screen.tsx @@ -0,0 +1,414 @@ +import { Card, CardContent } from "@/components/ui/card"; + +const sections = [ + { id: "overview", title: "1. 文档说明" }, + { id: "preparation", title: "2. 接入前准备" }, + { id: "sso", title: "3. SSO 登录接入" }, + { id: "wallet", title: "4. 钱包接口" }, + { id: "errors", title: "5. 错误码与幂等" }, + { id: "testing", title: "6. 联调与上线" }, + { id: "appendix", title: "7. 附录" }, +] as const; + +const ssoFields = [ + ["site_code", "string", "是", "站点编码,由我方提供。"], + ["user_id", "string", "是", "客户侧用户唯一标识。"], + ["username", "string", "是", "客户侧用户名或展示名。"], + ["timestamp", "number", "是", "发起时间戳,单位秒。"], + ["nonce", "string", "是", "随机串,用于防重放。"], + ["currency", "string", "否", "币种,如 CNY。"], + ["device", "string", "否", "终端类型,如 h5、web。"], +] as const; + +const walletCommonFields = [ + ["site_code", "string", "站点编码"], + ["user_id", "string", "客户侧用户唯一标识"], + ["transaction_id", "string", "唯一交易号,幂等主键"], + ["timestamp", "number", "请求时间戳,单位秒"], + ["sign", "string", "签名结果"], +] as const; + +const errorRows = [ + ["0", "成功", "按成功结果落账或继续业务。"], + ["1001", "参数错误", "检查字段完整性、类型、必填项。"], + ["1002", "签名错误", "检查签名原串、密钥、时间戳。"], + ["1003", "用户不存在", "确认用户标识是否正确。"], + ["1004", "余额不足", "前端提示余额不足,不继续下注。"], + ["1006", "重复交易", "返回首次结果,不允许重复记账。"], + ["1099", "系统异常", "可按约定策略重试,并保留日志。"], +] as const; + +function DocTable({ + headers, + rows, +}: { + headers: readonly string[]; + rows: readonly (readonly string[])[]; +}): React.ReactElement { + return ( +

+ + + + {headers.map((header) => ( + + ))} + + + + {rows.map((row, rowIndex) => ( + + {row.map((cell, cellIndex) => ( + + ))} + + ))} + +
+ {header} +
+ {cell} +
+
+ ); +} + +export function IntegrationGuideScreen(): React.ReactElement { + return ( +
+
+ + +
+
+

彩票端客户接入文档

+

+ 本文档用于说明客户主站与彩票端之间的登录接入、钱包接口、联调方式与上线前检查项。阅读对象为客户后端、前端、测试和运维人员。 +

+
+ +
+
+
+

1. 文档说明

+

+ 客户完成主站登录后,通过 SSO 进入彩票端。用户在彩票端的余额查询、投注扣款、派奖加款等资金动作,由彩票端调用客户钱包接口完成。 +

+
+ +
+
+
适用范围
+
+ H5、Web、App WebView 等主站跳转彩票端场景。 +
+
+
+
职责边界
+
+ 主站负责用户身份,彩票端负责业务编排,钱包系统负责资金真理源。 +
+
+
+
阅读对象
+
+ 客户后端、前端、测试、运维以及联调负责人。 +
+
+
+ +
+
接入链路
+
    +
  1. 1. 用户在客户主站完成登录。
  2. +
  3. 2. 客户服务端生成 SSO 参数或 token。
  4. +
  5. 3. 浏览器跳转至彩票端入口地址。
  6. +
  7. 4. 彩票端校验签名并建立登录态。
  8. +
  9. 5. 用户在彩票端进行余额查询、投注、派奖等操作。
  10. +
  11. 6. 彩票端调用客户钱包接口完成账务处理。
  12. +
+
+
+ +
+
+

2. 接入前准备

+

+ 联调开始前,双方需要先交换环境信息、签名密钥和测试数据,避免联调阶段反复返工。 +

+
+ +
+
+
客户需提供
+
    +
  • • 站点名称与站点编码。
  • +
  • • 客户主站域名与钱包接口域名。
  • +
  • • 测试环境与生产环境联系人。
  • +
  • • 测试账号、测试余额、测试用例。
  • +
+
+
+
双方需确认
+
    +
  • • SSO 密钥或 JWT 验签方式。
  • +
  • • 钱包签名算法、请求头、超时设置。
  • +
  • • 余额、扣款、加款三个接口地址。
  • +
  • • 白名单、重试策略、错误码定义。
  • +
+
+
+
+ +
+
+

3. SSO 登录接入

+

+ 客户用户在主站登录后,由客户服务端生成短时有效的登录凭证,浏览器携带该凭证进入彩票端。我方校验通过后建立彩票端会话。 +

+
+ +
+
推荐流程
+
    +
  1. 1. 客户主站用户完成登录。
  2. +
  3. 2. 客户服务端生成 SSO 负载。
  4. +
  5. 3. 使用共享密钥完成签名。
  6. +
  7. 4. 浏览器跳转至彩票端入口地址并附带凭证。
  8. +
  9. 5. 彩票端校验签名、时间戳、站点编码与用户标识。
  10. +
+
+ +
+
SSO 字段说明
+ +
+ +
+
+
接入约束
+
    +
  • • token 或签名参数必须为短时有效,建议 60 至 300 秒。
  • +
  • user_id 必须稳定且唯一。
  • +
  • • 测试环境与生产环境密钥必须分离。
  • +
  • • 密钥只允许保存在服务端,不可下发前端。
  • +
+
+ + +
SSO 负载示例
+
{`{
+  "site_code": "demo",
+  "user_id": "100001",
+  "username": "demo_user",
+  "timestamp": 1718000000,
+  "nonce": "N8F2X9Q1",
+  "currency": "CNY",
+  "device": "h5"
+}`}
+
+
+
+
+ +
+
+

4. 钱包接口

+

+ 彩票端通过客户钱包接口完成余额查询、投注扣款、派奖加款。钱包系统需要保证接口可重试、可审计、可幂等。 +

+
+ +
+
+
余额查询
+
+ 用于进入彩票端、下注前或关键账务时机同步可用余额。 +
+
POST /wallet/balance
+
+
+
扣款接口
+
+ 用于投注成功后的资金扣减,必须以交易号作为唯一幂等键。 +
+
POST /wallet/debit
+
+
+
加款接口
+
+ 用于派奖、退款或撤单返还资金,不允许重复记账。 +
+
POST /wallet/credit
+
+
+ +
+
公共字段
+ +
+ +
+ + +
扣款请求示例
+
{`{
+  "site_code": "demo",
+  "user_id": "100001",
+  "transaction_id": "BET202606100001",
+  "order_id": "TICKET202606100001",
+  "amount": "20.00",
+  "timestamp": 1718000001,
+  "sign": "xxxxxx"
+}`}
+
+
+ + +
成功响应示例
+
{`{
+  "code": 0,
+  "message": "success",
+  "data": {
+    "transaction_id": "BET202606100001",
+    "balance": "980.00"
+  }
+}`}
+
+
+
+ +
+
+
签名要求
+
    +
  1. 1. 请求字段按字段名升序排序。
  2. +
  3. 2. 以 key=value 拼接原始串。
  4. +
  5. 3. 原始串尾部追加共享密钥。
  6. +
  7. 4. 使用 HMAC-SHA256 生成签名。
  8. +
+
+
+
接口要求
+
    +
  • • 所有接口必须使用 HTTPS。
  • +
  • • 金额字段统一使用字符串,如 1000.00
  • +
  • • 请求内容类型统一为 application/json
  • +
  • • 钱包超时后我方可能重试,因此接口必须按幂等方式处理。
  • +
+
+
+
+ +
+
+

5. 错误码与幂等

+

+ 客户钱包接口需要统一错误码语义,并保证相同交易号的重复请求返回一致结果,不得重复记账。 +

+
+ +
+
建议错误码
+ +
+ +
+
幂等要求
+
    +
  • • 扣款与加款必须以 transaction_id 作为唯一交易键。
  • +
  • • 同一交易号重复请求时,不允许再次扣款或再次加款。
  • +
  • • 已成功处理的请求,再次请求时应返回首次处理结果。
  • +
  • • 若出现网络抖动、超时或重试,钱包系统仍需保证账务一致性。
  • +
+
+
+ +
+
+

6. 联调与上线

+

+ 建议双方按固定顺序完成联调,先打通登录链路,再验证钱包接口,最后确认异常场景与上线条件。 +

+
+ +
+
+
联调顺序
+
    +
  1. 1. 域名连通、证书与白名单检查。
  2. +
  3. 2. SSO 参数生成与跳转验证。
  4. +
  5. 3. 余额查询接口联调。
  6. +
  7. 4. 扣款与加款接口联调。
  8. +
  9. 5. 超时、重复请求、余额不足场景回归。
  10. +
+
+
+
上线前检查
+
    +
  • • 正式域名、正式密钥、正式白名单已配置。
  • +
  • • 测试环境与生产环境参数已分离。
  • +
  • • 核心错误码与日志字段已对齐。
  • +
  • • 关键链路已完成验收回归。
  • +
  • • 上线联系人与回滚预案已确认。
  • +
+
+
+
+ +
+
+

7. 附录

+

+ 以下约定用于减少不同系统之间的解析差异,建议双方保持一致。 +

+
+ +
+
+
金额格式
+
+ 统一使用字符串传输,例如 1000.00。 +
+
+
+
时间格式
+
+ 默认使用 Unix 时间戳秒级,双方也可统一为 ISO8601。 +
+
+
+
字符与报文
+
+ 字符编码统一 UTF-8,请求内容类型统一为 JSON。 +
+
+
+
+
+
+
+
+ ); +} \ No newline at end of file