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.
This commit is contained in:
18
src/app/admin/docs/integration-guide/page.tsx
Normal file
18
src/app/admin/docs/integration-guide/page.tsx
Normal file
@@ -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 (
|
||||
<ShellAuthGate>
|
||||
<AdminPermissionGate requiredAny={PRD_INTEGRATION_ACCESS_ANY}>
|
||||
<IntegrationGuideScreen />
|
||||
</AdminPermissionGate>
|
||||
</ShellAuthGate>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "मुख्य साइट एकीकरण साइटहरू",
|
||||
|
||||
@@ -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": "接入站点",
|
||||
|
||||
@@ -22,6 +22,7 @@ const EXACT_ROUTES: Record<string, PageTitleSpec> = {
|
||||
"/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" },
|
||||
|
||||
@@ -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 (
|
||||
<div className="mx-auto max-w-5xl space-y-6">
|
||||
@@ -421,8 +421,8 @@ function OverviewTab({
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
414
src/modules/docs/integration-guide-screen.tsx
Normal file
414
src/modules/docs/integration-guide-screen.tsx
Normal file
@@ -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 (
|
||||
<div className="overflow-x-auto rounded-lg border border-border">
|
||||
<table className="w-full min-w-[640px] border-collapse text-sm">
|
||||
<thead className="bg-muted/50 text-left">
|
||||
<tr>
|
||||
{headers.map((header) => (
|
||||
<th key={header} className="border-b border-border px-4 py-3 font-medium text-foreground">
|
||||
{header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row, rowIndex) => (
|
||||
<tr key={`${row[0]}-${rowIndex}`} className="align-top">
|
||||
{row.map((cell, cellIndex) => (
|
||||
<td
|
||||
key={`${row[0]}-${cellIndex}`}
|
||||
className="border-b border-border px-4 py-3 leading-6 text-muted-foreground"
|
||||
>
|
||||
{cell}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function IntegrationGuideScreen(): React.ReactElement {
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-[1280px] min-w-0 flex-col px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
|
||||
<div className="grid gap-10 xl:grid-cols-[220px_minmax(0,1fr)]">
|
||||
<aside className="hidden xl:block">
|
||||
<div className="sticky top-6 space-y-4 text-sm">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">技术接入文档</div>
|
||||
<div className="mt-2 font-medium text-foreground">彩票端客户接入</div>
|
||||
</div>
|
||||
<nav className="space-y-1 border-l border-border pl-4">
|
||||
{sections.map((section) => (
|
||||
<a
|
||||
key={section.id}
|
||||
href={`#${section.id}`}
|
||||
className="block py-1 text-muted-foreground transition hover:text-foreground"
|
||||
>
|
||||
{section.title}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="min-w-0">
|
||||
<header className="border-b border-border pb-8">
|
||||
<h1 className="text-3xl font-semibold tracking-tight text-foreground">彩票端客户接入文档</h1>
|
||||
<p className="mt-4 max-w-3xl text-sm leading-7 text-muted-foreground">
|
||||
本文档用于说明客户主站与彩票端之间的登录接入、钱包接口、联调方式与上线前检查项。阅读对象为客户后端、前端、测试和运维人员。
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="mt-8 space-y-10">
|
||||
<section id="overview" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">1. 文档说明</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground">
|
||||
客户完成主站登录后,通过 SSO 进入彩票端。用户在彩票端的余额查询、投注扣款、派奖加款等资金动作,由彩票端调用客户钱包接口完成。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-sm font-medium text-foreground">适用范围</div>
|
||||
<div className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
H5、Web、App WebView 等主站跳转彩票端场景。
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-sm font-medium text-foreground">职责边界</div>
|
||||
<div className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
主站负责用户身份,彩票端负责业务编排,钱包系统负责资金真理源。
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-sm font-medium text-foreground">阅读对象</div>
|
||||
<div className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
客户后端、前端、测试、运维以及联调负责人。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-sm font-medium text-foreground">接入链路</div>
|
||||
<ol className="mt-3 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>1. 用户在客户主站完成登录。</li>
|
||||
<li>2. 客户服务端生成 SSO 参数或 token。</li>
|
||||
<li>3. 浏览器跳转至彩票端入口地址。</li>
|
||||
<li>4. 彩票端校验签名并建立登录态。</li>
|
||||
<li>5. 用户在彩票端进行余额查询、投注、派奖等操作。</li>
|
||||
<li>6. 彩票端调用客户钱包接口完成账务处理。</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="preparation" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">2. 接入前准备</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground">
|
||||
联调开始前,双方需要先交换环境信息、签名密钥和测试数据,避免联调阶段反复返工。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">客户需提供</div>
|
||||
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>• 站点名称与站点编码。</li>
|
||||
<li>• 客户主站域名与钱包接口域名。</li>
|
||||
<li>• 测试环境与生产环境联系人。</li>
|
||||
<li>• 测试账号、测试余额、测试用例。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">双方需确认</div>
|
||||
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>• SSO 密钥或 JWT 验签方式。</li>
|
||||
<li>• 钱包签名算法、请求头、超时设置。</li>
|
||||
<li>• 余额、扣款、加款三个接口地址。</li>
|
||||
<li>• 白名单、重试策略、错误码定义。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="sso" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">3. SSO 登录接入</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground">
|
||||
客户用户在主站登录后,由客户服务端生成短时有效的登录凭证,浏览器携带该凭证进入彩票端。我方校验通过后建立彩票端会话。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">推荐流程</div>
|
||||
<ol className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>1. 客户主站用户完成登录。</li>
|
||||
<li>2. 客户服务端生成 SSO 负载。</li>
|
||||
<li>3. 使用共享密钥完成签名。</li>
|
||||
<li>4. 浏览器跳转至彩票端入口地址并附带凭证。</li>
|
||||
<li>5. 彩票端校验签名、时间戳、站点编码与用户标识。</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-3 text-base font-medium text-foreground">SSO 字段说明</div>
|
||||
<DocTable headers={["字段", "类型", "必填", "说明"]} rows={ssoFields} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">接入约束</div>
|
||||
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>• token 或签名参数必须为短时有效,建议 60 至 300 秒。</li>
|
||||
<li>• <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">user_id</code> 必须稳定且唯一。</li>
|
||||
<li>• 测试环境与生产环境密钥必须分离。</li>
|
||||
<li>• 密钥只允许保存在服务端,不可下发前端。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Card className="rounded-lg border-border shadow-none">
|
||||
<CardContent className="p-5">
|
||||
<div className="text-sm font-medium text-foreground">SSO 负载示例</div>
|
||||
<pre className="mt-4 overflow-x-auto whitespace-pre-wrap break-words rounded-md bg-muted px-4 py-4 text-sm leading-7 text-muted-foreground">{`{
|
||||
"site_code": "demo",
|
||||
"user_id": "100001",
|
||||
"username": "demo_user",
|
||||
"timestamp": 1718000000,
|
||||
"nonce": "N8F2X9Q1",
|
||||
"currency": "CNY",
|
||||
"device": "h5"
|
||||
}`}</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="wallet" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">4. 钱包接口</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground">
|
||||
彩票端通过客户钱包接口完成余额查询、投注扣款、派奖加款。钱包系统需要保证接口可重试、可审计、可幂等。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 xl:grid-cols-3">
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">余额查询</div>
|
||||
<div className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
用于进入彩票端、下注前或关键账务时机同步可用余额。
|
||||
</div>
|
||||
<div className="mt-3 rounded-md bg-muted px-3 py-2 text-xs text-foreground">POST /wallet/balance</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">扣款接口</div>
|
||||
<div className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
用于投注成功后的资金扣减,必须以交易号作为唯一幂等键。
|
||||
</div>
|
||||
<div className="mt-3 rounded-md bg-muted px-3 py-2 text-xs text-foreground">POST /wallet/debit</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">加款接口</div>
|
||||
<div className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
用于派奖、退款或撤单返还资金,不允许重复记账。
|
||||
</div>
|
||||
<div className="mt-3 rounded-md bg-muted px-3 py-2 text-xs text-foreground">POST /wallet/credit</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-3 text-base font-medium text-foreground">公共字段</div>
|
||||
<DocTable headers={["字段", "类型", "说明"]} rows={walletCommonFields} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<Card className="rounded-lg border-border shadow-none">
|
||||
<CardContent className="p-5">
|
||||
<div className="text-sm font-medium text-foreground">扣款请求示例</div>
|
||||
<pre className="mt-4 overflow-x-auto whitespace-pre-wrap break-words rounded-md bg-muted px-4 py-4 text-sm leading-7 text-muted-foreground">{`{
|
||||
"site_code": "demo",
|
||||
"user_id": "100001",
|
||||
"transaction_id": "BET202606100001",
|
||||
"order_id": "TICKET202606100001",
|
||||
"amount": "20.00",
|
||||
"timestamp": 1718000001,
|
||||
"sign": "xxxxxx"
|
||||
}`}</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="rounded-lg border-border shadow-none">
|
||||
<CardContent className="p-5">
|
||||
<div className="text-sm font-medium text-foreground">成功响应示例</div>
|
||||
<pre className="mt-4 overflow-x-auto whitespace-pre-wrap break-words rounded-md bg-muted px-4 py-4 text-sm leading-7 text-muted-foreground">{`{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"transaction_id": "BET202606100001",
|
||||
"balance": "980.00"
|
||||
}
|
||||
}`}</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">签名要求</div>
|
||||
<ol className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>1. 请求字段按字段名升序排序。</li>
|
||||
<li>2. 以 <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">key=value</code> 拼接原始串。</li>
|
||||
<li>3. 原始串尾部追加共享密钥。</li>
|
||||
<li>4. 使用 <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">HMAC-SHA256</code> 生成签名。</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">接口要求</div>
|
||||
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>• 所有接口必须使用 HTTPS。</li>
|
||||
<li>• 金额字段统一使用字符串,如 <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">1000.00</code>。</li>
|
||||
<li>• 请求内容类型统一为 <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">application/json</code>。</li>
|
||||
<li>• 钱包超时后我方可能重试,因此接口必须按幂等方式处理。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="errors" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">5. 错误码与幂等</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground">
|
||||
客户钱包接口需要统一错误码语义,并保证相同交易号的重复请求返回一致结果,不得重复记账。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-3 text-base font-medium text-foreground">建议错误码</div>
|
||||
<DocTable headers={["错误码", "含义", "处理方式"]} rows={errorRows} />
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">幂等要求</div>
|
||||
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>• 扣款与加款必须以 <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">transaction_id</code> 作为唯一交易键。</li>
|
||||
<li>• 同一交易号重复请求时,不允许再次扣款或再次加款。</li>
|
||||
<li>• 已成功处理的请求,再次请求时应返回首次处理结果。</li>
|
||||
<li>• 若出现网络抖动、超时或重试,钱包系统仍需保证账务一致性。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="testing" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">6. 联调与上线</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground">
|
||||
建议双方按固定顺序完成联调,先打通登录链路,再验证钱包接口,最后确认异常场景与上线条件。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">联调顺序</div>
|
||||
<ol className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>1. 域名连通、证书与白名单检查。</li>
|
||||
<li>2. SSO 参数生成与跳转验证。</li>
|
||||
<li>3. 余额查询接口联调。</li>
|
||||
<li>4. 扣款与加款接口联调。</li>
|
||||
<li>5. 超时、重复请求、余额不足场景回归。</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">上线前检查</div>
|
||||
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>• 正式域名、正式密钥、正式白名单已配置。</li>
|
||||
<li>• 测试环境与生产环境参数已分离。</li>
|
||||
<li>• 核心错误码与日志字段已对齐。</li>
|
||||
<li>• 关键链路已完成验收回归。</li>
|
||||
<li>• 上线联系人与回滚预案已确认。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="appendix" className="scroll-mt-24 space-y-5 pb-2">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">7. 附录</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground">
|
||||
以下约定用于减少不同系统之间的解析差异,建议双方保持一致。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-3">
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">金额格式</div>
|
||||
<div className="mt-3 text-sm leading-6 text-muted-foreground">
|
||||
统一使用字符串传输,例如 <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">1000.00</code>。
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">时间格式</div>
|
||||
<div className="mt-3 text-sm leading-6 text-muted-foreground">
|
||||
默认使用 Unix 时间戳秒级,双方也可统一为 ISO8601。
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">字符与报文</div>
|
||||
<div className="mt-3 text-sm leading-6 text-muted-foreground">
|
||||
字符编码统一 UTF-8,请求内容类型统一为 JSON。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user