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