feat(docs, integration): update integration documentation and redirect legacy paths

Introduced a new public documentation site for client integration at `/docs` and `/docs/integration`, removing the need for admin login. Updated the integration guide to redirect from the old admin path to the new documentation site. Added localization support for the integration documentation in English, Nepali, and Chinese. Enhanced the layout structure and improved the handling of currency display in settlement bills.
This commit is contained in:
2026-06-15 11:08:19 +08:00
parent e7b72cfdca
commit 17335cb47a
35 changed files with 2668 additions and 436 deletions

View File

@@ -39,3 +39,5 @@ This version has breaking changes — APIs, conventions, and file structure may
- 超管判定用登录态 `is_super_admin`,勿用站点角色或 `admin_user_site_roles` 绑定推断。
- 站点管理员(`profile.site != null`)代理 UI 绕过选中代理的 `can_create_*` 门控,按自身 manage 权限展示 Tab/操作。
- 站点管理员在代理下创建玩家须传 `agent_node_id`(与超管同逻辑),勿默认挂根代理。
- 客户接入文档公开站:`/docs`(首页)、`/docs/integration`(接入正文),无需 `/admin` 登录;旧 `/admin/docs/integration-guide` 重定向至此。
- `SettlementBillRow``currency_code`;账单金额展示用玩家 `default_currency`

550
package-lock.json generated
View File

@@ -24,6 +24,7 @@
"react-i18next": "^17.0.8",
"recharts": "^3.8.0",
"shadcn": "^4.7.0",
"shiki": "^4.2.0",
"sonner": "^2.0.7",
"swr": "^2.4.1",
"tailwind-merge": "^3.5.0",
@@ -2076,6 +2077,106 @@
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
"license": "MIT"
},
"node_modules/@shikijs/core": {
"version": "4.2.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/@shikijs/core/-/core-4.2.0.tgz",
"integrity": "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ==",
"license": "MIT",
"dependencies": {
"@shikijs/primitive": "4.2.0",
"@shikijs/types": "4.2.0",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4",
"hast-util-to-html": "^9.0.5"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@shikijs/engine-javascript": {
"version": "4.2.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/@shikijs/engine-javascript/-/engine-javascript-4.2.0.tgz",
"integrity": "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "4.2.0",
"@shikijs/vscode-textmate": "^10.0.2",
"oniguruma-to-es": "^4.3.6"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@shikijs/engine-oniguruma": {
"version": "4.2.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/@shikijs/engine-oniguruma/-/engine-oniguruma-4.2.0.tgz",
"integrity": "sha512-hTorK1dffPkpbMUk6Z+828PgRo7d07HbnizoP0hNPFjhxMHctj0Px/qoHeGMYafc6ju+u9iMldN4JbVzNQM++g==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "4.2.0",
"@shikijs/vscode-textmate": "^10.0.2"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@shikijs/langs": {
"version": "4.2.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/@shikijs/langs/-/langs-4.2.0.tgz",
"integrity": "sha512-bwrVRlJ0wUhZxAbVdvBbv2TTC9yLsh4C/IO5Ofz0T8MQntgDvyVnkbjw9vi50r1kx7RCIJdnJnjZAwmAsXFLZQ==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "4.2.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@shikijs/primitive": {
"version": "4.2.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/@shikijs/primitive/-/primitive-4.2.0.tgz",
"integrity": "sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "4.2.0",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@shikijs/themes": {
"version": "4.2.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/@shikijs/themes/-/themes-4.2.0.tgz",
"integrity": "sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "4.2.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@shikijs/types": {
"version": "4.2.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/@shikijs/types/-/types-4.2.0.tgz",
"integrity": "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw==",
"license": "MIT",
"dependencies": {
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@shikijs/vscode-textmate": {
"version": "10.0.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
"license": "MIT"
},
"node_modules/@sindresorhus/merge-streams": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
@@ -2547,6 +2648,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/hast": {
"version": "3.0.4",
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/hast/-/hast-3.0.4.tgz",
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
"license": "MIT",
"dependencies": {
"@types/unist": "*"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -2561,6 +2671,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/mdast": {
"version": "4.0.4",
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/mdast/-/mdast-4.0.4.tgz",
"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
"license": "MIT",
"dependencies": {
"@types/unist": "*"
}
},
"node_modules/@types/node": {
"version": "20.19.40",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.40.tgz",
@@ -2605,6 +2724,12 @@
"integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==",
"license": "MIT"
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@@ -2912,6 +3037,12 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
"integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==",
"license": "ISC"
},
"node_modules/@unrs/resolver-binding-android-arm-eabi": {
"version": "1.11.1",
"resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz",
@@ -3834,6 +3965,16 @@
],
"license": "CC-BY-4.0"
},
"node_modules/ccount": {
"version": "2.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/ccount/-/ccount-2.0.1.tgz",
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/cfb/-/cfb-1.2.2.tgz",
@@ -3864,6 +4005,26 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/character-entities-html4": {
"version": "2.1.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
"integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-entities-legacy": {
"version": "3.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@@ -4027,6 +4188,16 @@
"node": ">= 0.8"
}
},
"node_modules/comma-separated-tokens": {
"version": "2.0.3",
"resolved": "https://mirrors.cloud.tencent.com/npm/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/commander": {
"version": "14.0.3",
"resolved": "https://registry.npmmirror.com/commander/-/commander-14.0.3.tgz",
@@ -4539,6 +4710,19 @@
"node": ">=8"
}
},
"node_modules/devlop": {
"version": "1.1.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/devlop/-/devlop-1.1.0.tgz",
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
"license": "MIT",
"dependencies": {
"dequal": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/diff": {
"version": "8.0.4",
"resolved": "https://registry.npmmirror.com/diff/-/diff-8.0.4.tgz",
@@ -6111,6 +6295,42 @@
"node": ">= 0.4"
}
},
"node_modules/hast-util-to-html": {
"version": "9.0.5",
"resolved": "https://mirrors.cloud.tencent.com/npm/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
"integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"ccount": "^2.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-whitespace": "^3.0.0",
"html-void-elements": "^3.0.0",
"mdast-util-to-hast": "^13.0.0",
"property-information": "^7.0.0",
"space-separated-tokens": "^2.0.0",
"stringify-entities": "^4.0.0",
"zwitch": "^2.0.4"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-whitespace": {
"version": "3.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
"integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/headers-polyfill": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/headers-polyfill/-/headers-polyfill-5.0.1.tgz",
@@ -6156,6 +6376,16 @@
"void-elements": "3.1.0"
}
},
"node_modules/html-void-elements": {
"version": "3.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/html-void-elements/-/html-void-elements-3.0.0.tgz",
"integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz",
@@ -7512,6 +7742,27 @@
"node": ">= 0.4"
}
},
"node_modules/mdast-util-to-hast": {
"version": "13.2.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
"integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/mdast": "^4.0.0",
"@ungap/structured-clone": "^1.0.0",
"devlop": "^1.0.0",
"micromark-util-sanitize-uri": "^2.0.0",
"trim-lines": "^3.0.0",
"unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz",
@@ -7548,6 +7799,95 @@
"node": ">= 8"
}
},
"node_modules/micromark-util-character": {
"version": "2.1.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
"integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-util-encode": {
"version": "2.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
"integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromark-util-sanitize-uri": {
"version": "2.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
"integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-character": "^2.0.0",
"micromark-util-encode": "^2.0.0",
"micromark-util-symbol": "^2.0.0"
}
},
"node_modules/micromark-util-symbol": {
"version": "2.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
"integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromark-util-types": {
"version": "2.0.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
"integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
@@ -8095,6 +8435,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/oniguruma-parser": {
"version": "0.12.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz",
"integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==",
"license": "MIT"
},
"node_modules/oniguruma-to-es": {
"version": "4.3.6",
"resolved": "https://mirrors.cloud.tencent.com/npm/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz",
"integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==",
"license": "MIT",
"dependencies": {
"oniguruma-parser": "^0.12.2",
"regex": "^6.1.0",
"regex-recursion": "^6.0.2"
}
},
"node_modules/open": {
"version": "11.0.0",
"resolved": "https://registry.npmmirror.com/open/-/open-11.0.0.tgz",
@@ -8462,6 +8819,16 @@
"react-is": "^16.13.1"
}
},
"node_modules/property-information": {
"version": "7.2.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/property-information/-/property-information-7.2.0.tgz",
"integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -8733,6 +9100,30 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/regex": {
"version": "6.1.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/regex/-/regex-6.1.0.tgz",
"integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==",
"license": "MIT",
"dependencies": {
"regex-utilities": "^2.3.0"
}
},
"node_modules/regex-recursion": {
"version": "6.0.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/regex-recursion/-/regex-recursion-6.0.2.tgz",
"integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
"license": "MIT",
"dependencies": {
"regex-utilities": "^2.3.0"
}
},
"node_modules/regex-utilities": {
"version": "2.3.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/regex-utilities/-/regex-utilities-2.3.0.tgz",
"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
"license": "MIT"
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@@ -9296,6 +9687,25 @@
"node": ">=8"
}
},
"node_modules/shiki": {
"version": "4.2.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/shiki/-/shiki-4.2.0.tgz",
"integrity": "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ==",
"license": "MIT",
"dependencies": {
"@shikijs/core": "4.2.0",
"@shikijs/engine-javascript": "4.2.0",
"@shikijs/engine-oniguruma": "4.2.0",
"@shikijs/langs": "4.2.0",
"@shikijs/themes": "4.2.0",
"@shikijs/types": "4.2.0",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
},
"engines": {
"node": ">=20"
}
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
@@ -9414,6 +9824,16 @@
"node": ">=0.10.0"
}
},
"node_modules/space-separated-tokens": {
"version": "2.0.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/ssf/-/ssf-0.11.2.tgz",
@@ -9610,6 +10030,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/stringify-entities": {
"version": "4.0.4",
"resolved": "https://mirrors.cloud.tencent.com/npm/stringify-entities/-/stringify-entities-4.0.4.tgz",
"integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
"license": "MIT",
"dependencies": {
"character-entities-html4": "^2.0.0",
"character-entities-legacy": "^3.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/stringify-object": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/stringify-object/-/stringify-object-5.0.0.tgz",
@@ -9886,6 +10320,16 @@
"node": ">=16"
}
},
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/trim-lines/-/trim-lines-3.0.1.tgz",
"integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
@@ -10170,6 +10614,74 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/unist-util-is": {
"version": "6.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/unist-util-is/-/unist-util-is-6.0.1.tgz",
"integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-position": {
"version": "5.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/unist-util-position/-/unist-util-position-5.0.0.tgz",
"integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-stringify-position": {
"version": "4.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
"integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-visit": {
"version": "5.1.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
"integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0",
"unist-util-visit-parents": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-visit-parents": {
"version": "6.0.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
"integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz",
@@ -10305,6 +10817,34 @@
"node": ">= 0.8"
}
},
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://mirrors.cloud.tencent.com/npm/vfile/-/vfile-6.0.3.tgz",
"integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"vfile-message": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile-message": {
"version": "4.0.3",
"resolved": "https://mirrors.cloud.tencent.com/npm/vfile-message/-/vfile-message-4.0.3.tgz",
"integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-stringify-position": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/victory-vendor": {
"version": "37.3.6",
"resolved": "https://mirrors.cloud.tencent.com/npm/victory-vendor/-/victory-vendor-37.3.6.tgz",
@@ -10760,6 +11300,16 @@
"optional": true
}
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://mirrors.cloud.tencent.com/npm/zwitch/-/zwitch-2.0.4.tgz",
"integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
}
}
}

View File

@@ -25,6 +25,7 @@
"react-i18next": "^17.0.8",
"recharts": "^3.8.0",
"shadcn": "^4.7.0",
"shiki": "^4.2.0",
"sonner": "^2.0.7",
"swr": "^2.4.1",
"tailwind-merge": "^3.5.0",

View File

@@ -1,18 +1,6 @@
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";
import { redirect } from "next/navigation";
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>
);
}
/** 旧后台路径保留,统一跳转到公开文档站 */
export default function AdminIntegrationGuidePage(): never {
redirect("/docs/integration");
}

View File

@@ -0,0 +1,5 @@
import { ErrorsDocScreen } from "@/modules/docs/integration/integration-doc-screens";
export default function IntegrationErrorsPage(): React.ReactElement {
return <ErrorsDocScreen />;
}

View File

@@ -0,0 +1,5 @@
import { FundamentalsDocScreen } from "@/modules/docs/integration/integration-doc-screens";
export default function IntegrationFundamentalsPage(): React.ReactElement {
return <FundamentalsDocScreen />;
}

View File

@@ -0,0 +1,5 @@
import { GoLiveDocScreen } from "@/modules/docs/integration/integration-doc-screens";
export default function IntegrationGoLivePage(): React.ReactElement {
return <GoLiveDocScreen />;
}

View File

@@ -0,0 +1,5 @@
import { IframeDocScreen } from "@/modules/docs/integration/integration-doc-screens";
export default function IntegrationIframePage(): React.ReactElement {
return <IframeDocScreen />;
}

View File

@@ -0,0 +1,11 @@
import type { ReactNode } from "react";
import { DocsSidebar } from "@/components/docs/docs-sidebar";
import { DocsBody } from "@/components/docs/docs-shell";
import { DOCS_NAV_GROUPS } from "@/lib/docs-nav";
export default function IntegrationDocsLayout({ children }: { children: ReactNode }): React.ReactElement {
return (
<DocsBody sidebar={<DocsSidebar groups={DOCS_NAV_GROUPS} />}>{children}</DocsBody>
);
}

View File

@@ -0,0 +1,5 @@
import { OverviewDocScreen } from "@/modules/docs/integration/integration-doc-screens";
export default function IntegrationOverviewPage(): React.ReactElement {
return <OverviewDocScreen />;
}

View File

@@ -0,0 +1,5 @@
import { SetupDocScreen } from "@/modules/docs/integration/integration-doc-screens";
export default function IntegrationPreparationPage(): React.ReactElement {
return <SetupDocScreen />;
}

View File

@@ -0,0 +1,5 @@
import { QuickstartDocScreen } from "@/modules/docs/integration/integration-doc-screens";
export default function IntegrationQuickstartPage(): React.ReactElement {
return <QuickstartDocScreen />;
}

View File

@@ -0,0 +1,5 @@
import { SsoDocScreen } from "@/modules/docs/integration/integration-doc-screens";
export default function IntegrationSsoPage(): React.ReactElement {
return <SsoDocScreen />;
}

View File

@@ -0,0 +1,5 @@
import { TransferDocScreen } from "@/modules/docs/integration/integration-doc-screens";
export default function IntegrationTransferPage(): React.ReactElement {
return <TransferDocScreen />;
}

View File

@@ -0,0 +1,5 @@
import { WalletDocScreen } from "@/modules/docs/integration/integration-doc-screens";
export default function IntegrationWalletPage(): React.ReactElement {
return <WalletDocScreen />;
}

16
src/app/docs/layout.tsx Normal file
View File

@@ -0,0 +1,16 @@
import type { Metadata } from "next";
import type { ReactNode } from "react";
import { DocsShell } from "@/components/docs/docs-shell";
export const metadata: Metadata = {
title: {
template: "%s · Integration API",
default: "Integration API",
},
description: "Lottery integration docs: SSO, wallet gateway, transfers.",
};
export default function DocsLayout({ children }: { children: ReactNode }): React.ReactElement {
return <DocsShell>{children}</DocsShell>;
}

5
src/app/docs/page.tsx Normal file
View File

@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function DocsHomePage(): never {
redirect("/docs/integration");
}

View File

@@ -1,6 +1,5 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Script from "next/script";
import { Providers } from "@/components/providers";
import "./globals.css";
@@ -37,12 +36,12 @@ export default function RootLayout({
suppressHydrationWarning
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="flex min-h-full flex-col">
<Script
id="lottery-admin-locale-bootstrap"
strategy="beforeInteractive"
<head>
<script
dangerouslySetInnerHTML={{ __html: ADMIN_LOCALE_BOOTSTRAP }}
/>
</head>
<body className="flex min-h-full flex-col">
<Providers>{children}</Providers>
</body>
</html>

View File

@@ -0,0 +1,55 @@
"use client";
import { useEffect, useState } from "react";
import { highlightCode, type HighlightLang } from "@/lib/highlight-code";
import { cn } from "@/lib/utils";
type DocCodeProps = {
children: string;
language?: HighlightLang;
className?: string;
};
export function DocCode({
children,
language = "json",
className,
}: DocCodeProps): React.ReactElement {
const [html, setHtml] = useState<string | null>(null);
const code = children.trimEnd();
useEffect(() => {
let cancelled = false;
void highlightCode(code, language).then((result) => {
if (!cancelled) {
setHtml(result);
}
});
return () => {
cancelled = true;
};
}, [code, language]);
return (
<div
className={cn(
"overflow-x-auto rounded-xl border border-border bg-[#f6f8fa]",
className,
)}
>
{html ? (
<div
className="[&_code]:font-mono [&_pre]:m-0 [&_pre]:overflow-x-auto [&_pre]:bg-transparent [&_pre]:p-4 [&_pre]:text-[13px] [&_pre]:leading-7"
dangerouslySetInnerHTML={{ __html: html }}
/>
) : (
<pre className="m-0 overflow-x-auto p-4 font-mono text-[13px] leading-7 text-foreground">
{code}
</pre>
)}
</div>
);
}

View File

@@ -0,0 +1,129 @@
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
export { DocCode } from "@/components/docs/doc-code";
export function DocPageHeader({
title,
description,
}: {
title: string;
description?: string;
}): React.ReactElement {
return (
<header className="border-b border-border pb-6">
<h1 className="text-2xl font-semibold tracking-tight text-foreground">{title}</h1>
{description ? (
<p className="mt-2 max-w-3xl text-sm leading-6 text-muted-foreground">{description}</p>
) : null}
</header>
);
}
export function DocSection({
title,
children,
className,
}: {
title?: string;
children: ReactNode;
className?: string;
}): React.ReactElement {
return (
<section className={cn("space-y-3", className)}>
{title ? <h2 className="text-base font-semibold text-foreground">{title}</h2> : null}
{children}
</section>
);
}
export function DocNote({ children }: { children: ReactNode }): React.ReactElement {
return (
<p className="border-l-2 border-border pl-3 text-sm leading-6 text-muted-foreground">{children}</p>
);
}
export function DocList({ items }: { items: readonly string[] }): React.ReactElement {
return (
<ul className="space-y-1 text-sm leading-6 text-muted-foreground">
{items.map((item) => (
<li key={item} className="flex gap-2">
<span className="text-muted-foreground/50">·</span>
<span>{item}</span>
</li>
))}
</ul>
);
}
export function DocOrderedList({ items }: { items: readonly string[] }): React.ReactElement {
return (
<ol className="list-decimal space-y-1 pl-5 text-sm leading-6 text-muted-foreground">
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ol>
);
}
export function DocTable({
headers,
rows,
compact,
}: {
headers: readonly string[];
rows: readonly (readonly string[])[];
compact?: boolean;
}): React.ReactElement {
return (
<div className="overflow-x-auto rounded-lg border border-border">
<table className="w-full min-w-[480px] border-collapse text-sm">
<thead>
<tr className="border-b border-border bg-muted/30">
{headers.map((header) => (
<th
key={header}
className="px-3 py-2 text-left text-[11px] font-medium uppercase tracking-wide text-muted-foreground"
>
{header}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, rowIndex) => (
<tr key={`${row[0]}-${rowIndex}`} className="border-b border-border/60 last:border-0">
{row.map((cell, cellIndex) => (
<td
key={`${row[0]}-${cellIndex}`}
className={cn(
"px-3 align-top text-muted-foreground",
compact ? "py-1.5 text-[13px] leading-5" : "py-2 text-[13px] leading-6",
)}
>
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
export function DocEndpoint({ method, path }: { method: string; path: string }): React.ReactElement {
return (
<div className="inline-flex items-center gap-2 font-mono text-xs">
<span className="rounded bg-primary/10 px-1.5 py-0.5 font-semibold text-primary">{method}</span>
<span className="text-foreground">{path}</span>
</div>
);
}
export function DocInlineCode({ children }: { children: ReactNode }): React.ReactElement {
return (
<code className="rounded bg-muted px-1 py-0.5 font-mono text-[12px] text-foreground">{children}</code>
);
}

View File

@@ -0,0 +1,53 @@
"use client";
import Link from "next/link";
import { useTranslation } from "react-i18next";
import { AdminLanguageSwitcher } from "@/components/admin/admin-language-switcher";
import { cn } from "@/lib/utils";
type DocsShellProps = {
children: React.ReactNode;
className?: string;
};
export function DocsShell({ children, className }: DocsShellProps): React.ReactElement {
const { t } = useTranslation("integrationDocs");
return (
<div className={cn("min-h-dvh bg-background text-foreground", className)}>
<header className="sticky top-0 z-40 border-b border-border/80 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80">
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8">
<Link href="/docs/integration" className="truncate text-sm font-semibold tracking-tight">
{t("shell.title")}
</Link>
<div className="flex items-center gap-2">
<AdminLanguageSwitcher />
<Link
href="/admin/login"
className="text-xs text-muted-foreground transition-colors hover:text-foreground"
>
{t("shell.admin")}
</Link>
</div>
</div>
</header>
{children}
</div>
);
}
export function DocsBody({
sidebar,
children,
}: {
sidebar?: React.ReactNode;
children: React.ReactNode;
}): React.ReactElement {
return (
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6 px-4 py-6 sm:px-6 lg:flex-row lg:gap-8 lg:px-8 lg:py-8">
{sidebar}
<main className="min-w-0 flex-1 pb-12">{children}</main>
</div>
);
}

View File

@@ -0,0 +1,62 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useTranslation } from "react-i18next";
import type { DocsNavGroup } from "@/lib/docs-nav";
import { resolveDocsNavLabel } from "@/lib/docs-nav-labels";
import { cn } from "@/lib/utils";
export function DocsSidebar({ groups }: { groups: readonly DocsNavGroup[] }): React.ReactElement {
const pathname = usePathname();
const { t, i18n } = useTranslation("integrationDocs");
const label = (key: string): string => {
const translated = t(key);
if (translated !== key) {
return translated;
}
return resolveDocsNavLabel(key, i18n.resolvedLanguage ?? i18n.language);
};
return (
<aside className="w-full shrink-0 lg:w-44">
<div className="lg:sticky lg:top-14 lg:max-h-[calc(100dvh-4rem)] lg:overflow-y-auto lg:pr-2">
<nav className="space-y-4">
{groups.map((group) => (
<div key={group.titleKey}>
<div className="mb-1 px-1.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground/80">
{label(group.titleKey)}
</div>
<ul className="space-y-px">
{group.items.map((item) => {
const active =
pathname === item.href ||
(item.href !== "/docs/integration" && pathname.startsWith(item.href)) ||
(item.href === "/docs/integration" && pathname === "/docs/integration");
return (
<li key={item.href}>
<Link
href={item.href}
className={cn(
"block rounded-md px-2 py-1.5 text-[13px] leading-5 transition-colors",
active
? "bg-primary/10 font-medium text-foreground"
: "text-muted-foreground hover:bg-muted/40 hover:text-foreground",
)}
>
{label(item.titleKey)}
</Link>
</li>
);
})}
</ul>
</div>
))}
</nav>
</div>
</aside>
);
}

View File

@@ -26,6 +26,7 @@ import enReports from "@/i18n/locales/en/reports.json";
import enWallet from "@/i18n/locales/en/wallet.json";
import enAgents from "@/i18n/locales/en/agents.json";
import enSettlementCenter from "@/i18n/locales/en/settlementCenter.json";
import enIntegrationDocs from "@/i18n/locales/en/integrationDocs.json";
import neAudit from "@/i18n/locales/ne/audit.json";
import neAdminUsers from "@/i18n/locales/ne/adminUsers.json";
import neAuth from "@/i18n/locales/ne/auth.json";
@@ -43,6 +44,7 @@ import neReports from "@/i18n/locales/ne/reports.json";
import neWallet from "@/i18n/locales/ne/wallet.json";
import neAgents from "@/i18n/locales/ne/agents.json";
import neSettlementCenter from "@/i18n/locales/ne/settlementCenter.json";
import neIntegrationDocs from "@/i18n/locales/ne/integrationDocs.json";
import zhAudit from "@/i18n/locales/zh/audit.json";
import zhAdminUsers from "@/i18n/locales/zh/adminUsers.json";
import zhAuth from "@/i18n/locales/zh/auth.json";
@@ -60,12 +62,13 @@ import zhReports from "@/i18n/locales/zh/reports.json";
import zhWallet from "@/i18n/locales/zh/wallet.json";
import zhAgents from "@/i18n/locales/zh/agents.json";
import zhSettlementCenter from "@/i18n/locales/zh/settlementCenter.json";
import zhIntegrationDocs from "@/i18n/locales/zh/integrationDocs.json";
export const ADMIN_SUPPORTED_LANGUAGES = ["en", "ne", "zh"] as const;
export type AdminLanguage = (typeof ADMIN_SUPPORTED_LANGUAGES)[number];
export const ADMIN_DEFAULT_LANGUAGE: AdminLanguage = "zh";
const namespaces = ["common", "auth", "dashboard", "audit", "draws", "settlement", "settlementCenter", "risk", "jackpot", "players", "tickets", "reconcile", "reports", "wallet", "adminUsers", "agents", "config"] as const;
const namespaces = ["common", "auth", "dashboard", "audit", "draws", "settlement", "settlementCenter", "risk", "jackpot", "players", "tickets", "reconcile", "reports", "wallet", "adminUsers", "agents", "config", "integrationDocs"] as const;
const resources = {
en: {
@@ -86,6 +89,7 @@ const resources = {
wallet: enWallet,
agents: enAgents,
settlementCenter: enSettlementCenter,
integrationDocs: enIntegrationDocs,
},
ne: {
common: neCommon,
@@ -105,6 +109,7 @@ const resources = {
wallet: neWallet,
agents: neAgents,
settlementCenter: neSettlementCenter,
integrationDocs: neIntegrationDocs,
},
zh: {
common: zhCommon,
@@ -124,6 +129,7 @@ const resources = {
wallet: zhWallet,
agents: zhAgents,
settlementCenter: zhSettlementCenter,
integrationDocs: zhIntegrationDocs,
},
} satisfies Record<AdminLanguage, Record<(typeof namespaces)[number], Record<string, unknown>>>;
@@ -164,6 +170,7 @@ if (!i18n.isInitialized) {
defaultNS: "common",
ns: [...namespaces],
load: "languageOnly",
initAsync: false,
interpolation: {
escapeValue: false,
},

View File

@@ -0,0 +1,372 @@
{
"shell": {
"title": "Integration API",
"admin": "Admin"
},
"nav": {
"overview": "Overview",
"api": "API",
"ship": "Ship",
"home": "Overview",
"quickstart": "Quickstart",
"fundamentals": "Money model",
"setup": "Setup",
"sso": "SSO",
"iframe": "iframe protocol",
"wallet": "Wallet gateway",
"transfer": "Transfers (ref)",
"errors": "Errors",
"golive": "Go-live"
},
"headers": {
"component": ["Component", "Role", "Owner"],
"convention": ["Item", "Rule"],
"claim": ["Claim", "Type", "Req", "Note"],
"param": ["Parameter", "Purpose"],
"methodPath": ["Method", "Path", ""],
"query": ["Query", "Type", ""],
"field": ["Field", "Type", "Note"],
"code": ["Code", "Message"],
"http": ["Status", "message", "Cause"],
"message": ["Dir", "type", "Payload"],
"balance": ["Field", "Account", "Note"],
"call": ["Direction", "API", "Auth"],
"sequence": ["Step", "Actor", "Action"],
"envMap": ["Item", "Admin site", "Main-site .env", "Note"],
"account": ["User", "Password", "site_player_id"],
"contract": ["Scenario", "HTTP", "Body"],
"adminField": ["Field", "Note", "Example"]
},
"pages": {
"overview": {
"title": "Integration",
"description": "Main-site SSO + wallet gateway. Identity via JWT; funds split between main wallet and in-lottery balance.",
"roles": "Roles",
"flow": "Flow",
"e2eSequence": "End-to-end sequence",
"conventions": "Conventions",
"readingOrder": "Reading order",
"matrix": [
["Main site", "Issue JWT; implement wallet gateway", "Partner"],
["Lottery API", "Verify JWT, play, transfers, bets", "Us"],
["Lottery H5", "H5 / iframe shell", "Us"]
],
"flowItems": [
"Main-site login → issue JWT",
"Enter lottery (URL or iframe)",
"transfer-in → debit main + credit lottery",
"Bet / settle (lottery balance)",
"transfer-out → debit lottery + credit main"
],
"e2eRows": [
["1", "Main site", "User logs in; server signs JWT"],
["2", "Main site", "Embed lottery H5 in iframe, or redirect ?token="],
["3", "Lottery H5", "Receives token; calls GET /api/v1/player/me"],
["4", "Player", "Taps transfer-in inside lottery H5"],
["5", "Lottery API", "Server calls POST /wallet/debit-for-lottery"],
["6", "Partner wallet", "Debits main_balance; returns success"],
["7", "Lottery API", "Credits in-lottery balance"],
["8", "Player", "Bets / settles in H5"],
["9", "Player", "(optional) transfer-out in H5"],
["10", "Lottery API", "Calls POST /wallet/credit-from-lottery"]
],
"conventionRows": [
["Amount", "Minor units (integer), e.g. 2000 = 20.00"],
["Encoding", "UTF-8 JSON"],
["Time", "JWT: Unix seconds (iat / exp)"],
["Auth", "Player API: Bearer JWT; gateway: Bearer wallet_api_key"]
],
"readingItems": ["Quickstart → Setup → SSO → iframe protocol → Wallet → Errors → Go-live"]
},
"quickstart": {
"title": "Quickstart",
"description": "Local integration guide. The main-site/ package in the repo is a runnable reference; secrets must match the admin integration site or lottery .env.",
"prereq": "Prerequisites",
"steps": "Integration steps",
"testAccounts": "Test accounts (main-site)",
"reference": "Reference implementation",
"note": "Production requires HTTPS and isolated site_code/secrets. Without wallet_api_url locally, lottery API may stub main-site debits (non-production only).",
"prereqItems": [
"Lottery API (lotterLaravel) and lottery H5 (lotteryfront) running",
"main-site running (default http://localhost:5173)",
"Integration site created in admin, or lottery .env MAIN_SITE_* aligned with main-site .env"
],
"stepItems": [
"Super admin creates integration site in admin (see Setup) and saves secrets",
"Copy secrets to main-site .env; set wallet_api_url and iframe_allowed_origins in admin",
"Log in on main-site → open lottery H5 in iframe (/player)",
"On LOTTERY_READY, send MAIN_INIT_TOKEN",
"Transfer-in inside lottery H5 → observe /wallet/debit-for-lottery callback",
"Place a bet in H5 after balance increases",
"(optional) transfer-out in H5 → observe /wallet/credit-from-lottery",
"Run acceptance curl checks for JWT and wallet gateway"
],
"accountRows": [
["alice", "alice123", "10001"],
["bob", "bob123", "10002"],
["demo", "demo123", "10003"]
],
"referenceItems": [
"Code: main-site/ in the monorepo (Next.js test shell)",
"Main site: http://localhost:5173; lottery H5: http://localhost:3800",
"See main-site README for env vars and postMessage protocol",
"Config mapping table on the Setup page"
],
"acceptance": "Acceptance checklist",
"acceptanceItems": [
"Sign JWT → curl GET /api/v1/player/me returns code=0",
"Self-test wallet debit: success:true and correct main_balance",
"Replay same idempotent_key: identical response, no double debit",
"iframe: after LOTTERY_READY receive MAIN_INIT_TOKEN and enter hall",
"H5 transfer-in: partner gateway logs show debit-for-lottery"
]
},
"fundamentals": {
"title": "Money model",
"balances": "Two balances",
"calls": "Call directions",
"note": "All amounts use minor integers. Credit-line players are out of scope.",
"balanceRows": [
["main_balance", "Main wallet", "Partner gateway; lottery calls back"],
["lottery balance", "In-lottery balance", "Used for betting after transfer-in"]
],
"callRows": [
["Lottery → main", "balance / debit / credit", "wallet_api_key"],
["Lottery H5 → lottery API", "me / transfers / bets", "Player JWT (not main site)"]
]
},
"setup": {
"title": "Setup",
"description": "Secrets are shown once when the integration site is created. Store them immediately.",
"weProvide": "We provide",
"youProvide": "Partner provides",
"defaultPaths": "Default wallet paths",
"envMapping": "Config mapping",
"note": "Isolate test/prod site_code, secrets, and domains. Copy secrets into main-site .env manually. Local dev may use lottery .env MAIN_SITE_* as fallback.",
"receiveRows": [
["site_code", "Site code"],
["sso_jwt_secret", "JWT signing secret (held by main site)"],
["wallet_api_key", "Wallet callback auth (validated by main site)"],
["lottery_h5_base_url", "Lottery entry URL"]
],
"provideRows": [
["wallet_api_url", "HTTPS wallet base URL"],
["Test accounts", "site_player_id + initial balance"],
["iframe origin", "Parent origin when embedding"]
],
"pathRows": [
["GET", "/wallet/balance", "Balance"],
["POST", "/wallet/debit-for-lottery", "Debit"],
["POST", "/wallet/credit-from-lottery", "Credit"]
],
"envMappingRows": [
["site_code", "site_code", "MAIN_SITE_CODE", "JWT + player identity; must match"],
["SSO secret", "sso_jwt_secret", "MAIN_SITE_SSO_JWT_SECRET", "Main site signs; lottery verifies"],
["Wallet auth", "wallet_api_key", "MAIN_SITE_WALLET_API_KEY", "Lottery sends on callbacks; main site validates"],
["Wallet base URL", "wallet_api_url", "— (routes on main site)", "Partner HTTPS base; lottery appends /wallet/*"],
["Lottery entry", "lottery_h5_base_url", "NEXT_PUBLIC_LOTTERY_IFRAME_URL", "Redirect or iframe target"],
["iframe allowlist", "iframe_allowed_origins", "NEXT_PUBLIC_LOTTERY_ORIGIN", "Parent origin allowed to embed"],
["Lottery API", "—", "LOTTERY_API_BASE_URL", "Reference impl only"]
],
"adminSop": "Admin provisioning",
"adminSopSteps": [
"Super admin → Config → Integration sites",
"Create site: code, name, currency",
"Set wallet_api_url (HTTPS root, no path), lottery_h5_base_url, iframe_allowed_origins (one origin per line)",
"Save sso_jwt_secret and wallet_api_key shown once at creation",
"Copy secrets to main-site .env; run connectivity test (probes GET /wallet/balance)",
"Local dev: use main-site/ reference; production wallet_api_url must be public HTTPS"
],
"adminFieldRows": [
["code", "Site code in JWT site_code", "demo"],
["wallet_api_url", "Partner wallet HTTPS base", "https://wallet.partner.com"],
["lottery_h5_base_url", "Lottery H5 entry URL", "https://lottery.partner.com"],
["iframe_allowed_origins", "Parent origins allowed to embed", "https://www.partner.com"],
["sso_jwt_secret", "Shown once at create", "—"],
["wallet_api_key", "Shown once at create", "—"]
],
"network": "Network",
"networkItems": [
"Wallet callbacks are server-to-server from lottery to partner — not from the browser",
"Production wallet_api_url: HTTPS public only (no localhost / private IP)",
"Default paths: /wallet/balance, /wallet/debit-for-lottery, /wallet/credit-from-lottery",
"Timeout ≤ 10s recommended; timeouts may enter pending reconcile"
]
},
"sso": {
"title": "SSO",
"description": "HS256 JWT. Main site signs; lottery verifies. Entry: URL redirect or iframe postMessage.",
"claims": "Claims",
"sign": "Sign",
"entryA": "Entry A — redirect",
"entryB": "Entry B — iframe",
"noExchangeNote": "Lottery has no token-exchange login API. After main-site login, sign a JWT and send Authorization: Bearer on player APIs. First valid call to GET /api/v1/player/me auto-provisions the player.",
"entryApi": "Entry API (lottery)",
"entryApiNote": "Optional: main site may call once server-side after login to verify JWT and provision (see main-site). Day-to-day play APIs are called by lottery H5.",
"publicApis": "Public APIs (no token)",
"h5ScopeNote": "Transfers, betting, and in-lottery balance are called by our H5 with the player JWT — out of scope for main-site integration. You only issue JWT and implement the wallet gateway.",
"partnerApis": "Main-site APIs (partner implements)",
"refreshNote": "iframe refresh: on LOTTERY_TOKEN_NEEDED, re-issue JWT and send MAIN_REFRESH_TOKEN. See main-site POST /api/auth/refresh.",
"authResponse": "Auth failure response",
"errors": "Errors",
"iframeNote": "Set iframe_allowed_origins. Do not resend LOTTERY_READY after token is delivered.",
"claimRows": [
["site_code", "string", "Y", "Integration site code"],
["site_player_id", "string", "Y", "Stable main-site user ID"],
["iat", "number", "Y", "Issued at (seconds)"],
["exp", "number", "Y", "Expires (seconds); ≤ 300s"]
],
"messageRows": [
["→ main", "LOTTERY_READY", "Child ready"],
["→ main", "LOTTERY_TOKEN_NEEDED", "Refresh requested"],
["→ lottery", "MAIN_INIT_TOKEN", "{ token }"],
["→ lottery", "MAIN_REFRESH_TOKEN", "{ token }"]
],
"publicApiRows": [
["GET", "/api/v1/player/ping", "Player API connectivity probe"],
["GET", "/api/v1/integration/runtime-origins", "Allowed iframe parent origins"]
],
"partnerApiRows": [
["POST", "/api/auth/refresh", "(reference) Re-issue JWT for MAIN_REFRESH_TOKEN"]
],
"errorRows": [
["8001", "Missing Authorization"],
["8002", "JWT invalid or expired"],
["8003", "Player not provisioned"],
["8004", "SSO secret not configured"],
["8005", "Account suspended"]
]
},
"iframe": {
"title": "iframe protocol",
"description": "postMessage contract when embedding lottery H5. Skip if using URL redirect only.",
"sequence": "Recommended sequence",
"envelope": "Message shape",
"childMessages": "Lottery → main",
"parentMessages": "Main → lottery",
"targetOrigin": "targetOrigin",
"envelopeNote": "JSON objects. Lottery sends LOTTERY_* types; main site sends MAIN_*. Include timestamp and source when possible.",
"targetOriginNote": "postMessage targetOrigin must be a specific origin (e.g. https://www.partner.com), never *. Main site validates event.origin against iframe_allowed_origins; lottery child validates parent origin against the allowlist.",
"timingNote": "After MAIN_INIT_TOKEN, do not send LOTTERY_READY again. Refresh: child sends LOTTERY_TOKEN_NEEDED or LOTTERY_TOKEN_REFRESH_REQUEST → parent replies MAIN_REFRESH_TOKEN.",
"sequenceSteps": [
"Embed <iframe src=\"{lottery_h5_base_url}\">",
"Lottery H5 loads allowlist then sends LOTTERY_READY",
"Parent validates origin and sends MAIN_INIT_TOKEN",
"H5 stores token and calls /api/v1/player/me",
"Before expiry: LOTTERY_TOKEN_NEEDED → parent sends MAIN_REFRESH_TOKEN"
],
"childMessageRows": [
["→ main", "LOTTERY_READY", "Child ready; request token"],
["→ main", "LOTTERY_TOKEN_NEEDED", "Token expired; request refresh"],
["→ main", "LOTTERY_TOKEN_REFRESH_REQUEST", "Active refresh request"],
["→ main", "LOTTERY_HEARTBEAT", "Heartbeat (optional)"],
["→ main", "LOTTERY_TOKEN_REFRESHED", "Refresh succeeded notice"]
],
"parentMessageRows": [
["→ lottery", "MAIN_INIT_TOKEN", "{ token } initial"],
["→ lottery", "MAIN_REFRESH_TOKEN", "{ token } refresh"],
["→ lottery", "MAIN_REQUEST_STATUS", "Request child status"],
["→ lottery", "MAIN_NAVIGATE", "{ path } navigate"]
]
},
"wallet": {
"title": "Wallet gateway",
"description": "Partner implements. Lottery calls server-to-server. Auth: Bearer wallet_api_key.",
"balance": "GET balance",
"debit": "POST debit",
"credit": "POST credit",
"response": "Response",
"httpContract": "HTTP contract",
"httpErrors": "HTTP errors",
"creditNote": "Same body as debit; used for transfer-out or refund after failed transfer-in.",
"idempotentNote": "idempotent_key: same key + same operation must return the first JSON (HTTP 200); no double posting. Different operation/amount → success: false.",
"queryRows": [
["site_code", "string", ""],
["site_player_id", "string", ""],
["currency_code", "string", ""]
],
"fieldRows": [
["site_code", "string", ""],
["site_player_id", "string", ""],
["player_id", "number", "Lottery player ID"],
["currency_code", "string", ""],
["amount_minor", "integer", "Positive minor units"],
["idempotent_key", "string", "Idempotency key"]
],
"httpErrorRows": [
["401", "unauthorized", "Invalid API key"],
["422", "invalid request", "Invalid fields/amount"],
["409", "main balance insufficient", "Business rejection; may include data.main_balance"]
],
"httpContractRows": [
["Debit/credit success", "200", "success: true; external_ref_no (recommended) + data.main_balance"],
["Balance success", "200", "success: true; data.main_balance + currency_code"],
["Invalid params", "422", "success: false; message: invalid request"],
["Unauthorized", "401", "success: false; message: unauthorized"],
["Business reject", "409", "success: false; message explains reason"],
["Idempotent replay", "200", "Identical JSON to first success/reject response"]
]
},
"transfer": {
"title": "Transfers (reference)",
"description": "Internal: called by lottery H5, not a partner integration surface.",
"outOfScopeNote": "Partners do not implement these APIs. Transfer-in/out is invoked by our H5 with the player JWT; you only implement wallet gateway debit/credit. This page explains how funds move internally.",
"requestFields": "Request fields",
"transferIn": "transfer-in",
"transferOut": "transfer-out",
"transferResponse": "Success response",
"errors": "Common errors",
"inNote": "Flow: lottery calls debit-for-lottery → credits in-lottery balance.",
"outNote": "Flow: debits lottery balance → calls credit-from-lottery.",
"responseNote": "transfer-in and transfer-out share the same shape; direction is in / out. Idempotent replay returns the same data.",
"requestFieldRows": [
["amount", "integer", "Positive minor units"],
["currency", "string", "Optional; defaults to player default_currency"],
["idempotent_key", "string", "Globally unique; retries return same result"]
],
"errorRows": [
["1001", "Insufficient lottery balance (transfer-out)"],
["1009", "Main wallet operation failed"],
["1010", "Idempotency conflict (same key, different amount)"],
["2003", "Transfer in before betting"]
]
},
"errors": {
"title": "Errors",
"sso": "SSO",
"lotteryWallet": "Lottery wallet",
"gateway": "Wallet gateway (HTTP)",
"idempotentNote": "Idempotency: same idempotent_key must return the same result; different amount → 1010.",
"ssoRows": [
["8001", "Missing Authorization"],
["8002", "JWT invalid or expired"],
["8003", "Player not provisioned"],
["8004", "SSO secret not configured"],
["8005", "Account suspended"]
],
"lotteryRows": [
["1001", "Insufficient lottery balance"],
["1009", "Main wallet operation failed"],
["1010", "Idempotency conflict"],
["2003", "Transfer in first"]
],
"gatewayRows": [
["401", "unauthorized", "Invalid API key"],
["422", "invalid request", "Invalid fields/amount"],
["409", "—", "Business rejection"]
]
},
"golive": {
"title": "Go-live",
"checklist": "Checklist",
"items": [
"Isolate test/prod site_code, secrets, domains",
"JWT server-side only, TTL ≤ 5min",
"Wallet HTTPS, timeout ≤ 10s",
"idempotent_key idempotency",
"iframe: configure iframe_allowed_origins",
"Full path: transfer-in → bet → settle → transfer-out"
]
}
}
}

View File

@@ -0,0 +1,372 @@
{
"shell": {
"title": "Integration API",
"admin": "Admin"
},
"nav": {
"overview": "अवलोकन",
"api": "API",
"ship": "लाइभ",
"home": "सारांश",
"quickstart": "छिटो सुरु",
"fundamentals": "रकम मोडेल",
"setup": "सेटअप",
"sso": "SSO",
"iframe": "iframe प्रोटोकल",
"wallet": "वालेट गेटवे",
"transfer": "स्थानान्तरण (सन्दर्भ)",
"errors": "त्रुटि कोड",
"golive": "लाइभ सूची"
},
"headers": {
"component": ["कम्पोनेन्ट", "भूमिका", "मालिक"],
"convention": ["वस्तु", "नियम"],
"claim": ["Claim", "Type", "Req", "Note"],
"param": ["प्यारामिटर", "उद्देश्य"],
"methodPath": ["Method", "Path", ""],
"query": ["Query", "Type", ""],
"field": ["Field", "Type", "Note"],
"code": ["Code", "Message"],
"http": ["Status", "message", "Cause"],
"message": ["Dir", "type", "Payload"],
"balance": ["फिल्ड", "खाता", "नोट"],
"call": ["दिशा", "API", "Auth"],
"sequence": ["चरण", "अभिनेता", "कार्य"],
"envMap": ["वस्तु", "Admin साइट", "मुख्य .env", "नोट"],
"account": ["प्रयोगकर्ता", "पासवर्ड", "site_player_id"],
"contract": ["परिदृश्य", "HTTP", "Body"],
"adminField": ["फिल्ड", "नोट", "उदाहरण"]
},
"pages": {
"overview": {
"title": "Integration",
"description": "मुख्य साइट SSO + वालेट गेटवे। पहिचान JWT; रकम मुख्य वालेट र लटरी भित्रको ब्यालेन्समा विभाजित।",
"roles": "भूमिका",
"flow": "प्रवाह",
"e2eSequence": "End-to-end क्रम",
"conventions": "सम्झौता",
"readingOrder": "पढ्ने क्रम",
"matrix": [
["मुख्य साइट", "JWT जारी; वालेट गेटवे कार्यान्वयन", "साझेदार"],
["लटरी API", "JWT प्रमाणीकरण, खेल, स्थानान्तरण, बेट", "हामी"],
["लटरी H5", "H5 / iframe", "हामी"]
],
"flowItems": [
"मुख्य साइट लगइन → JWT जारी",
"लटरी प्रवेश (URL वा iframe)",
"transfer-in → मुख्य डेबिट + लटरी क्रेडिट",
"बेट / सेटल (लटरी ब्यालेन्स)",
"transfer-out → लटरी डेबिट + मुख्य क्रेडिट"
],
"e2eRows": [
["1", "मुख्य साइट", "लगइन; JWT जारी"],
["2", "मुख्य साइट", "iframe वा ?token= प्रवेश"],
["3", "लटरी H5", "token + GET /api/v1/player/me"],
["4", "प्लेयर", "H5 मा transfer-in"],
["5", "लटरी API", "POST /wallet/debit-for-lottery"],
["6", "साझेदार वालेट", "main_balance घटाउने"],
["7", "लटरी API", "लटरी भित्र क्रेडिट"],
["8", "प्लेयर", "H5 मा बेट"],
["9", "प्लेयर", "(वैकल्पिक) H5 transfer-out"],
["10", "लटरी API", "POST /wallet/credit-from-lottery"]
],
"conventionRows": [
["रकम", "Minor इकाई (पूर्णांक), जस्तै 2000 = 20.00"],
["एन्कोडिङ", "UTF-8 JSON"],
["समय", "JWT: Unix सेकेन्ड (iat / exp)"],
["Auth", "प्लेयर API: Bearer JWT; गेटवे: Bearer wallet_api_key"]
],
"readingItems": ["छिटो सुरु → सेटअप → SSO → iframe → वालेट → त्रुटि → लाइभ"]
},
"quickstart": {
"title": "छिटो सुरु",
"description": "स्थानीय इन्टिग्रेसन। repo मा main-site/ सन्दर्भ कार्यान्वयन; गोप्य कुञ्जी admin वा lottery .env सँग मिल्नुपर्छ।",
"prereq": "पूर्वशर्त",
"steps": "इन्टिग्रेसन चरण",
"testAccounts": "परीक्षण खाता (main-site)",
"reference": "सन्दर्भ कार्यान्वयन",
"note": "प्रोडक्सनमा HTTPS र अलग site_code/गोप्य। स्थानीयमा wallet_api_url बिना lottery API stub हुन सक्छ (non-production)।",
"prereqItems": [
"लटरी API (lotterLaravel) र lotteryfront चलिरहेको",
"main-site चलिरहेको (http://localhost:5173)",
"admin मा इन्टिग्रेसन साइट, वा lottery .env MAIN_SITE_* मिलेको"
],
"stepItems": [
"सुपर एडमिनले admin मा इन्टिग्रेसन साइट सिर्जना",
"गोप्य .env मा; admin मा wallet_api_url र iframe_allowed_origins",
"लगइन → iframe मा लटरी H5",
"LOTTERY_READY पछि MAIN_INIT_TOKEN",
"H5 मा transfer-in → debit-for-lottery कलब्याक",
"H5 मा बेट",
"(वैकल्पिक) H5 transfer-out",
"acceptance curl जाँच"
],
"accountRows": [
["alice", "alice123", "10001"],
["bob", "bob123", "10002"],
["demo", "demo123", "10003"]
],
"referenceItems": [
"कोड: monorepo मा main-site/",
"मुख्य: http://localhost:5173; लटरी H5: http://localhost:3800",
"main-site README: env र postMessage",
"सेटअप पृष्ठमा config mapping तालिका"
],
"acceptance": "स्वीकृति सूची",
"acceptanceItems": [
"JWT → curl /player/me code=0",
"debit self-test success:true",
"idempotent_key replay एउटै नतिजा",
"iframe: LOTTERY_READY → MAIN_INIT_TOKEN",
"H5 transfer-in: debit लग"
]
},
"fundamentals": {
"title": "Money model",
"balances": "दुई तह ब्यालेन्स",
"calls": "कल दिशा",
"note": "सबै रकम minor पूर्णांक। क्रेडिट-लाइन प्लेयर यो दस्तावेज बाहिर।",
"balanceRows": [
["main_balance", "मुख्य वालेट", "साझेदार गेटवे; लटरी कलब्याक"],
["lottery balance", "लटरी भित्रको ब्यालेन्स", "transfer-in पछि बेटिङ"]
],
"callRows": [
["लटरी → मुख्य", "balance / debit / credit", "wallet_api_key"],
["लटरी H5 → लटरी API", "me / transfer / bet", "प्लेयर JWT (मुख्य होइन)"]
]
},
"setup": {
"title": "Setup",
"description": "इन्टिग्रेसन साइट सिर्जना पछि गोप्य कुञ्जी एक पटक मात्र देखाइन्छ। तुरुन्त सुरक्षित राख्नुहोस्।",
"weProvide": "हामी दिन्छौं",
"youProvide": "साझेदारले दिन्छ",
"defaultPaths": "पूर्वनिर्धारित वालेट पथ",
"envMapping": "Config mapping",
"note": "परीक्षण/प्रोडक्सन अलग। गोप्य मुख्य साइट .env मा म्यानुअल। स्थानीयमा lottery .env MAIN_SITE_* fallback।",
"receiveRows": [
["site_code", "साइट कोड"],
["sso_jwt_secret", "JWT हस्ताक्षर गोप्य (मुख्य साइट)"],
["wallet_api_key", "वालेट कलब्याक auth (मुख्य साइट जाँच)"],
["lottery_h5_base_url", "लटरी प्रवेश URL"]
],
"provideRows": [
["wallet_api_url", "HTTPS वालेट आधार URL"],
["परीक्षण खाता", "site_player_id + सुरु ब्यालेन्स"],
["iframe origin", "एम्बेड गर्दा मुख्य origin"]
],
"pathRows": [
["GET", "/wallet/balance", "ब्यालेन्स"],
["POST", "/wallet/debit-for-lottery", "डेबिट"],
["POST", "/wallet/credit-from-lottery", "क्रेडिट"]
],
"envMappingRows": [
["site_code", "site_code", "MAIN_SITE_CODE", "JWT + प्लेयर; मिल्नुपर्छ"],
["SSO गोप्य", "sso_jwt_secret", "MAIN_SITE_SSO_JWT_SECRET", "मुख्य हस्ताक्षर; लटरी जाँच"],
["वालेट auth", "wallet_api_key", "MAIN_SITE_WALLET_API_KEY", "लटरी कलब्याक; मुख्य जाँच"],
["वालेट URL", "wallet_api_url", "—", "साझेदार HTTPS आधार"],
["लटरी प्रवेश", "lottery_h5_base_url", "NEXT_PUBLIC_LOTTERY_IFRAME_URL", "redirect/iframe"],
["iframe allowlist", "iframe_allowed_origins", "NEXT_PUBLIC_LOTTERY_ORIGIN", "एम्बेड origin"],
["लटरी API", "—", "LOTTERY_API_BASE_URL", "सन्दर्भ कार्यान्वयन मात्र"]
],
"adminSop": "Admin provisioning",
"adminSopSteps": [
"सुपर एडमिन → Config → Integration sites",
"साइट सिर्जना: code, name, currency",
"wallet_api_url, lottery_h5_base_url, iframe_allowed_origins",
"sso_jwt_secret, wallet_api_key एक पटक सुरक्षित",
"connectivity test (GET /wallet/balance)",
"प्रोडक्सन: सार्वजनिक HTTPS wallet_api_url"
],
"adminFieldRows": [
["code", "JWT site_code", "demo"],
["wallet_api_url", "HTTPS wallet base", "https://wallet.partner.com"],
["lottery_h5_base_url", "H5 entry", "https://lottery.partner.com"],
["iframe_allowed_origins", "Parent origins", "https://www.partner.com"],
["sso_jwt_secret", "एक पटक", "—"],
["wallet_api_key", "एक पटक", "—"]
],
"network": "Network",
"networkItems": [
"वालेट कलब्याक server-to-server",
"प्रोडक्सन: HTTPS सार्वजनिक मात्र",
"पथ: /wallet/balance, debit, credit",
"timeout ≤ 10s"
]
},
"sso": {
"title": "SSO",
"description": "HS256 JWT। मुख्य साइट हस्ताक्षर; लटरी प्रमाणीकरण। प्रवेश: URL वा iframe postMessage।",
"claims": "Claims",
"sign": "Sign",
"entryA": "Entry A — redirect",
"entryB": "Entry B — iframe",
"noExchangeNote": "लटरीमा token-exchange login API छैन। मुख्य साइट लगइन पछि JWT जारी गर्नुहोस्; player API मा Authorization: Bearer। पहिलो वैध GET /api/v1/player/me ले प्लेयर auto-provision गर्छ।",
"entryApi": "Entry API (लटरी)",
"entryApiNote": "वैकल्पिक: लगइन पछि मुख्य साइटले एक पटक server-side कल गर्न सक्छ (main-site हेर्नुहोस्)। दैनिक play API लटरी H5 ले कल गर्छ।",
"publicApis": "सार्वजनिक API (token बिना)",
"h5ScopeNote": "स्थानान्तरण, बेट, लटरी ब्यालेन्स हाम्रो H5 ले JWT सँग कल गर्छ — मुख्य साइट इन्टिग्रेसन दायरा बाहिर। तपाईंले JWT जारी र वालेट गेटवे मात्र।",
"partnerApis": "मुख्य साइट API (साझेदार कार्यान्वयन)",
"refreshNote": "iframe refresh: LOTTERY_TOKEN_NEEDED मा नयाँ JWT जारी गरी MAIN_REFRESH_TOKEN पठाउनुहोस्। main-site POST /api/auth/refresh हेर्नुहोस्।",
"authResponse": "Auth असफल response",
"errors": "Errors",
"iframeNote": "iframe_allowed_origins सेट गर्नुहोस्। token पछि LOTTERY_READY दोहोर्याउनुहोस्।",
"claimRows": [
["site_code", "string", "Y", "इन्टिग्रेसन साइट कोड"],
["site_player_id", "string", "Y", "स्थिर मुख्य साइट प्रयोगकर्ता ID"],
["iat", "number", "Y", "जारी समय (सेकेन्ड)"],
["exp", "number", "Y", "म्याद (सेकेन्ड); ≤ 300s"]
],
"messageRows": [
["→ मुख्य", "LOTTERY_READY", "चाइल्ड तयार"],
["→ मुख्य", "LOTTERY_TOKEN_NEEDED", "रिफ्रेस अनुरोध"],
["→ लटरी", "MAIN_INIT_TOKEN", "{ token }"],
["→ लटरी", "MAIN_REFRESH_TOKEN", "{ token }"]
],
"publicApiRows": [
["GET", "/api/v1/player/ping", "Player API connectivity"],
["GET", "/api/v1/integration/runtime-origins", "iframe allowlist origins"]
],
"partnerApiRows": [
["POST", "/api/auth/refresh", "(सन्दर्भ) JWT re-issue → MAIN_REFRESH_TOKEN"]
],
"errorRows": [
["8001", "Authorization छैन"],
["8002", "JWT अमान्य वा म्याद सकियो"],
["8003", "प्लेयर छैन"],
["8004", "SSO गोप्य सेट छैन"],
["8005", "खाता निलम्बित"]
]
},
"iframe": {
"title": "iframe protocol",
"description": "H5 embed गर्दा postMessage। URL redirect मात्र भए यो अध्याय छोड्न सकिन्छ।",
"sequence": "क्रम",
"envelope": "सन्देश संरचना",
"childMessages": "लटरी → मुख्य",
"parentMessages": "मुख्य → लटरी",
"targetOrigin": "targetOrigin",
"envelopeNote": "JSON। लटरी LOTTERY_*; मुख्य MAIN_*। timestamp र source सिफारिस।",
"targetOriginNote": "targetOrigin ठोस origin हुनुपर्छ, * होइन। iframe_allowed_origins मा मात्र।",
"timingNote": "MAIN_INIT_TOKEN पछि LOTTERY_READY दोहोर्याउनुहोस्। LOTTERY_TOKEN_NEEDED / LOTTERY_TOKEN_REFRESH_REQUEST → MAIN_REFRESH_TOKEN।",
"sequenceSteps": [
"iframe embed",
"LOTTERY_READY",
"MAIN_INIT_TOKEN",
"/player/me",
"LOTTERY_TOKEN_NEEDED → MAIN_REFRESH_TOKEN"
],
"childMessageRows": [
["→ मुख्य", "LOTTERY_READY", "तयार"],
["→ मुख्य", "LOTTERY_TOKEN_NEEDED", "रिफ्रेस"],
["→ मुख्य", "LOTTERY_TOKEN_REFRESH_REQUEST", "सक्रिय रिफ्रेस"],
["→ मुख्य", "LOTTERY_HEARTBEAT", "हार्टबिट"],
["→ मुख्य", "LOTTERY_TOKEN_REFRESHED", "रिफ्रेस सफल"]
],
"parentMessageRows": [
["→ लटरी", "MAIN_INIT_TOKEN", "{ token }"],
["→ लटरी", "MAIN_REFRESH_TOKEN", "{ token }"],
["→ लटरी", "MAIN_REQUEST_STATUS", "स्थिति"],
["→ लटरी", "MAIN_NAVIGATE", "{ path }"]
]
},
"wallet": {
"title": "Wallet gateway",
"description": "साझेदारले कार्यान्वयन। लटरी server-to-server। Auth: Bearer wallet_api_key।",
"balance": "GET balance",
"debit": "POST debit",
"credit": "POST credit",
"response": "Response",
"httpContract": "HTTP contract",
"httpErrors": "HTTP errors",
"creditNote": "Body debit जस्तै; transfer-out वा refund।",
"idempotentNote": "idempotent_key: एउटै key + operation ले पहिलो JSON (HTTP 200); दोहोरो लेखा निषेध।",
"queryRows": [
["site_code", "string", ""],
["site_player_id", "string", ""],
["currency_code", "string", ""]
],
"fieldRows": [
["site_code", "string", ""],
["site_player_id", "string", ""],
["player_id", "number", "लटरी प्लेयर ID"],
["currency_code", "string", ""],
["amount_minor", "integer", "धनात्मक minor"],
["idempotent_key", "string", "इडेम्पोटेन्सी"]
],
"httpErrorRows": [
["401", "unauthorized", "API Key गलत"],
["422", "invalid request", "फिल्ड/रकम गलत"],
["409", "main balance insufficient", "व्यापार अस्वीकार; data.main_balance हुन सक्छ"]
],
"httpContractRows": [
["डेबिट/क्रेडिट सफल", "200", "success: true; external_ref_no + data.main_balance"],
["ब्यालेन्स सफल", "200", "success: true; data.main_balance + currency_code"],
["अमान्य params", "422", "success: false; message: invalid request"],
["Unauthorized", "401", "success: false; message: unauthorized"],
["व्यापार अस्वीकार", "409", "success: false; message"],
["इडेम्पोटेन्ट replay", "200", "पहिलो response जस्तै JSON"]
]
},
"transfer": {
"title": "स्थानान्तरण (सन्दर्भ)",
"description": "आन्तरिक: लटरी H5 ले कल गर्छ, साझेदार इन्टिग्रेसन होइन।",
"outOfScopeNote": "साझेदारले यी API कार्यान्वयन गर्नुपर्दैन। transfer हाम्रो H5 ले JWT सँग कल गर्छ; तपाईंले वालेट गेटवे debit/credit मात्र।",
"requestFields": "अनुरोध फिल्ड",
"transferIn": "transfer-in",
"transferOut": "transfer-out",
"transferResponse": "सफल response",
"errors": "सामान्य त्रुटि",
"inNote": "लटरी debit-for-lottery → लटरी भित्र क्रेडिट।",
"outNote": "लटरी डेबिट → credit-from-lottery।",
"responseNote": "transfer-in/out एउटै संरचना; direction in/out। इडेम्पोटेन्ट replay एउटै data।",
"requestFieldRows": [
["amount", "integer", "धनात्मक minor"],
["currency", "string", "वैकल्पिक; default_currency"],
["idempotent_key", "string", "अद्वितीय; retry एउटै नतिजा"]
],
"errorRows": [
["1001", "लटरी ब्यालेन्स अपर्याप्त (transfer-out)"],
["1009", "मुख्य वालेट असफल"],
["1010", "इडेम्पोटेन्सी द्वन्द्व"],
["2003", "पहिले transfer-in"]
]
},
"errors": {
"title": "Errors",
"sso": "SSO",
"lotteryWallet": "Lottery wallet",
"gateway": "Wallet gateway (HTTP)",
"idempotentNote": "एउटै idempotent_key ले एउटै नतिजा; फरक रकम → 1010।",
"ssoRows": [
["8001", "Authorization छैन"],
["8002", "JWT अमान्य वा म्याद सकियो"],
["8003", "प्लेयर छैन"],
["8004", "SSO गोप्य सेट छैन"],
["8005", "खाता निलम्बित"]
],
"lotteryRows": [
["1001", "लटरी ब्यालेन्स अपर्याप्त"],
["1009", "मुख्य वालेट असफल"],
["1010", "इडेम्पोटेन्सी द्वन्द्व"],
["2003", "पहिले transfer-in"]
],
"gatewayRows": [
["401", "unauthorized", "API Key गलत"],
["422", "invalid request", "फिल्ड/रकम गलत"],
["409", "—", "व्यापार अस्वीकार"]
]
},
"golive": {
"title": "Go-live",
"checklist": "Checklist",
"items": [
"परीक्षण/प्रोडक्सन site_code, गोप्य, डोमेन अलग",
"JWT सर्वर-साइड मात्र, TTL ≤ 5min",
"वालेट HTTPS, timeout ≤ 10s",
"idempotent_key इडेम्पोटेन्सी",
"iframe: iframe_allowed_origins",
"पूर्ण: transfer-in → bet → settle → transfer-out"
]
}
}
}

View File

@@ -0,0 +1,372 @@
{
"shell": {
"title": "彩票接入文档",
"admin": "管理后台"
},
"nav": {
"overview": "概览",
"api": "接口",
"ship": "发布",
"home": "总览",
"quickstart": "快速开始",
"fundamentals": "资金模型",
"setup": "接入配置",
"sso": "单点登录",
"iframe": "iframe 协议",
"wallet": "钱包网关",
"transfer": "划转(参考)",
"errors": "错误码",
"golive": "上线清单"
},
"headers": {
"component": ["组件", "职责", "实现方"],
"convention": ["项", "规则"],
"claim": ["字段", "类型", "必填", "说明"],
"param": ["参数", "用途"],
"methodPath": ["方法", "路径", ""],
"query": ["参数", "类型", ""],
"field": ["字段", "类型", "说明"],
"code": ["错误码", "说明"],
"http": ["状态码", "message", "原因"],
"message": ["方向", "消息类型", "载荷"],
"balance": ["字段", "账户", "说明"],
"call": ["方向", "接口", "鉴权"],
"sequence": ["步骤", "发起方", "说明"],
"envMap": ["项", "后台接入站点", "主站 .env", "说明"],
"account": ["账号", "密码", "site_player_id"],
"contract": ["场景", "HTTP", "响应体"],
"adminField": ["字段", "说明", "示例"]
},
"pages": {
"overview": {
"title": "接入总览",
"description": "主站 SSO + 钱包网关。身份用 JWT资金分主站钱包与彩票内余额。",
"roles": "职责",
"flow": "链路",
"e2eSequence": "端到端时序",
"conventions": "约定",
"readingOrder": "阅读顺序",
"matrix": [
["主站", "签发 JWT实现钱包网关", "客户"],
["彩票 API", "验签、玩法、划转、下注", "我方"],
["彩票前端", "H5 / iframe 承载", "我方"]
],
"flowItems": [
"主站登录 → 签发 JWT",
"进入彩票URL 跳转或 iframe",
"转入:主站扣款 + 彩票加款",
"下注 / 派奖(彩票内余额)",
"转出:彩票扣款 + 主站加款"
],
"e2eRows": [
["1", "主站", "用户登录;服务端签发 JWT"],
["2", "主站", "iframe 嵌入彩票 H5或跳转 ?token="],
["3", "彩票 H5", "收到 token调用 GET /api/v1/player/me 验签建档"],
["4", "玩家", "在彩票 H5 内点「转入」"],
["5", "彩票 API", "服务端回调主站 POST /wallet/debit-for-lottery"],
["6", "主站钱包", "扣减 main_balance返回 success"],
["7", "彩票 API", "彩票内加款"],
["8", "玩家", "在 H5 内下注 / 派奖"],
["9", "玩家", "(可选)在 H5 内转出"],
["10", "彩票 API", "回调主站 POST /wallet/credit-from-lottery"]
],
"conventionRows": [
["金额", "最小货币单位整数minor如 2000 = 20.00"],
["编码", "UTF-8 JSON"],
["时间", "JWTUnix 秒iat / exp"],
["鉴权", "玩家 APIBearer JWT钱包网关Bearer wallet_api_key"]
],
"readingItems": ["快速开始 → 接入配置 → 单点登录 → iframe 协议 → 钱包网关 → 错误码 → 上线清单"]
},
"quickstart": {
"title": "快速开始",
"description": "本地联调参考。仓库内 main-site/ 为可运行示例,密钥须与后台接入站点或彩票 .env 一致。",
"prereq": "前置条件",
"steps": "联调步骤",
"testAccounts": "测试账号main-site",
"reference": "参考实现",
"note": "生产环境须使用 HTTPS、独立 site_code 与密钥。本地未配置 wallet_api_url 时,彩票 API 可能以 stub 模式跳过主站扣款(仅非 production。",
"prereqItems": [
"彩票 APIlotterLaravel与彩票前端lotteryfront已启动",
"main-site 已启动(默认 http://localhost:5173",
"后台已创建接入站点,或彩票 .env 配置 MAIN_SITE_* 与主站 .env 对齐"
],
"stepItems": [
"超管在后台创建接入站点(见「接入配置」)并保存密钥",
"密钥写入主站 .env后台填入 wallet_api_url 与 iframe_allowed_origins",
"主站登录测试账号 → iframe 打开彩票 H5/player",
"确认收到 LOTTERY_READY 后下发 MAIN_INIT_TOKEN",
"在彩票 H5 内发起转入 → 观察主站 /wallet/debit-for-lottery 被回调",
"彩票内余额增加后在 H5 内下注",
"可选H5 内转出 → 观察 /wallet/credit-from-lottery",
"按「验收清单」用 curl 自测 JWT 与钱包网关"
],
"accountRows": [
["alice", "alice123", "10001"],
["bob", "bob123", "10002"],
["demo", "demo123", "10003"]
],
"referenceItems": [
"代码monorepo 内 main-site/Next.js 测试主站壳)",
"主站http://localhost:5173彩票 H5http://localhost:3800",
"主站 README 含环境变量与消息协议说明",
"配置字段对照见「接入配置」页的映射表"
],
"acceptance": "验收清单",
"acceptanceItems": [
"签发 JWT → curl GET /api/v1/player/me 返回 code=0",
"自测钱包网关 debit返回 success:true 且 main_balance 正确",
"相同 idempotent_key 重放:响应与首次一致,余额不重复扣",
"iframe子页 LOTTERY_READY 后收到 MAIN_INIT_TOKEN可进入大厅",
"H5 内转入成功:主站网关日志有 debit-for-lottery 记录"
]
},
"fundamentals": {
"title": "资金模型",
"balances": "两层余额",
"calls": "调用方向",
"note": "金额一律使用 minor 整数。信用盘(代理授信)不在本文档范围。",
"balanceRows": [
["main_balance", "主站钱包", "客户实现网关;彩票回调"],
["lottery balance", "彩票内余额", "转入后用于下注"]
],
"callRows": [
["彩票 → 主站", "balance / debit / credit", "wallet_api_key"],
["彩票 H5 → 彩票 API", "me / 划转 / 下注", "玩家 JWT主站不接"]
]
},
"setup": {
"title": "接入配置",
"description": "接入站点创建后一次性下发密钥,请立即保存。",
"weProvide": "我方提供",
"youProvide": "客户需提供",
"defaultPaths": "默认钱包路径",
"envMapping": "配置映射",
"note": "测试与生产环境的 site_code、密钥、域名须隔离。密钥写入主站 .env不会从后台自动同步。本地开发可在彩票 .env 用 MAIN_SITE_* 作兜底。",
"receiveRows": [
["site_code", "站点编码"],
["sso_jwt_secret", "JWT 签名密钥(主站持有)"],
["wallet_api_key", "钱包回调鉴权(主站校验)"],
["lottery_h5_base_url", "彩票入口地址"]
],
"provideRows": [
["wallet_api_url", "HTTPS 钱包根地址"],
["测试账号", "site_player_id + 初始余额"],
["iframe origin", "嵌入时提供主站 origin"]
],
"pathRows": [
["GET", "/wallet/balance", "余额查询"],
["POST", "/wallet/debit-for-lottery", "扣款"],
["POST", "/wallet/credit-from-lottery", "加款"]
],
"envMappingRows": [
["site_code", "site_code", "MAIN_SITE_CODE", "JWT 与玩家建档标识;双方须一致"],
["SSO 密钥", "sso_jwt_secret", "MAIN_SITE_SSO_JWT_SECRET", "主站签发;彩票验签"],
["钱包鉴权", "wallet_api_key", "MAIN_SITE_WALLET_API_KEY", "彩票回调主站时 Bearer 携带;主站校验"],
["钱包根地址", "wallet_api_url", "—(主站部署路由)", "客户 HTTPS 根地址;彩票拼接 /wallet/* 路径"],
["彩票入口", "lottery_h5_base_url", "NEXT_PUBLIC_LOTTERY_IFRAME_URL", "跳转或 iframe 目标"],
["iframe 白名单", "iframe_allowed_origins", "NEXT_PUBLIC_LOTTERY_ORIGIN", "主站 origin彩票允许嵌入"],
["彩票 API", "—", "LOTTERY_API_BASE_URL", "仅参考实现需要"]
],
"adminSop": "后台建站步骤",
"adminSopSteps": [
"超管登录管理后台 → 配置 → 接入站点",
"新建站点填写站点编码site_code、名称、币种",
"填写 wallet_api_urlHTTPS 根地址,无 path、lottery_h5_base_url、iframe_allowed_origins主站 origin每行一个",
"创建成功后立即保存一次性展示的 sso_jwt_secret、wallet_api_key",
"将密钥写入主站 .env在列表中对站点执行「连通性测试」探测 GET /wallet/balance",
"本地联调可用 main-site/ 参考实现;生产 wallet_api_url 须公网 HTTPS"
],
"adminFieldRows": [
["code", "站点编码,写入 JWT site_code", "demo"],
["wallet_api_url", "客户钱包网关 HTTPS 根地址", "https://wallet.partner.com"],
["lottery_h5_base_url", "彩票 H5 入口", "https://lottery.partner.com"],
["iframe_allowed_origins", "允许嵌入的主站 origin", "https://www.partner.com"],
["sso_jwt_secret", "创建后一次性下发", "—"],
["wallet_api_key", "创建后一次性下发", "—"]
],
"network": "网络要求",
"networkItems": [
"钱包回调由彩票服务端发起,非玩家浏览器;客户网关须对彩票服务器可达",
"生产环境 wallet_api_url 仅允许 HTTPS 公网地址(拒绝 localhost / 私网 IP",
"路径固定为 /wallet/balance、/wallet/debit-for-lottery、/wallet/credit-from-lottery可后台改 path 前缀)",
"建议超时 ≤ 10 秒;超时可能进入待对账状态"
]
},
"sso": {
"title": "单点登录SSO",
"description": "HS256 JWT。主站签发彩票验签。进入方式URL 跳转或 iframe postMessage。",
"claims": "JWT 字段",
"sign": "签名示例",
"entryA": "方式 AURL 跳转",
"entryB": "方式 Biframe 嵌入",
"noExchangeNote": "彩票不提供「登录换票」接口。主站登录后自行签发 JWT玩家 API 统一用 Authorization: Bearer 携带。首次有效 JWT 调用 GET /api/v1/player/me 时自动建档。",
"entryApi": "入场接口(彩票)",
"entryApiNote": "可选:主站登录后服务端代调一次,用于验签与建档(参考 main-site。日常业务由彩票 H5 自行调用玩家 API。",
"publicApis": "公开接口(无需 token",
"h5ScopeNote": "划转、下注、彩票内余额查询等玩家业务接口由我方 H5 携带 JWT 调用,不在主站接入范围内。主站只需签发 JWT 并实现钱包网关。",
"partnerApis": "主站侧接口(客户实现)",
"refreshNote": "iframe 续签详见「iframe 协议」页。主站收到 LOTTERY_TOKEN_NEEDED 或 LOTTERY_TOKEN_REFRESH_REQUEST 后重新签发 JWT发送 MAIN_REFRESH_TOKEN。",
"authResponse": "鉴权失败响应",
"errors": "错误码",
"iframeNote": "须配置 iframe_allowed_origins。收到 token 后勿重复发送 LOTTERY_READY。",
"claimRows": [
["site_code", "string", "是", "接入站点编码"],
["site_player_id", "string", "是", "主站用户 ID稳定唯一"],
["iat", "number", "是", "签发时间(秒)"],
["exp", "number", "是", "过期时间(秒);≤ 300 秒"]
],
"messageRows": [
["→ 主站", "LOTTERY_READY", "子页就绪"],
["→ 主站", "LOTTERY_TOKEN_NEEDED", "请求续签"],
["→ 彩票", "MAIN_INIT_TOKEN", "{ token }"],
["→ 彩票", "MAIN_REFRESH_TOKEN", "{ token }"]
],
"publicApiRows": [
["GET", "/api/v1/player/ping", "玩家 API 连通性探测"],
["GET", "/api/v1/integration/runtime-origins", "iframe 允许嵌入的 origin 列表"]
],
"partnerApiRows": [
["POST", "/api/auth/refresh", "(参考)续签 JWT返回新 token 供 MAIN_REFRESH_TOKEN"]
],
"errorRows": [
["8001", "缺少 Authorization 头"],
["8002", "JWT 无效或已过期"],
["8003", "玩家未建档"],
["8004", "SSO 密钥未配置"],
["8005", "账号已冻结"]
]
},
"iframe": {
"title": "iframe 协议",
"description": "主站嵌入彩票 H5 时的 postMessage 约定。URL 跳转模式可跳过本章。",
"sequence": "推荐时序",
"envelope": "消息结构",
"childMessages": "彩票 → 主站",
"parentMessages": "主站 → 彩票",
"targetOrigin": "targetOrigin",
"envelopeNote": "消息为 JSON 对象。彩票发出 type 前缀 LOTTERY_主站发出 MAIN_。建议带 timestamp 与 source。",
"targetOriginNote": "postMessage 第二参数须为具体 origin如 https://www.partner.com禁止使用 *。主站只接受后台 iframe_allowed_origins 中的 origin彩票子页校验 event.origin 在白名单内。",
"timingNote": "收到 MAIN_INIT_TOKEN 后彩票子页勿再发送 LOTTERY_READY避免主站重复下发 token。续签子页发 LOTTERY_TOKEN_NEEDED 或 LOTTERY_TOKEN_REFRESH_REQUEST → 主站回 MAIN_REFRESH_TOKEN。",
"sequenceSteps": [
"主站页面嵌入 <iframe src=\"{lottery_h5_base_url}\">",
"彩票 H5 加载白名单后发送 LOTTERY_READY",
"主站监听 message校验 origin 后发送 MAIN_INIT_TOKEN",
"彩票 H5 保存 token调用 /api/v1/player/me 入场",
"Token 将过期时:彩票发 LOTTERY_TOKEN_NEEDED → 主站续签后发 MAIN_REFRESH_TOKEN"
],
"childMessageRows": [
["→ 主站", "LOTTERY_READY", "子页就绪,请求下发 token"],
["→ 主站", "LOTTERY_TOKEN_NEEDED", "token 失效,请求续签"],
["→ 主站", "LOTTERY_TOKEN_REFRESH_REQUEST", "主动请求刷新 token"],
["→ 主站", "LOTTERY_HEARTBEAT", "心跳(可忽略)"],
["→ 主站", "LOTTERY_TOKEN_REFRESHED", "续签成功通知"]
],
"parentMessageRows": [
["→ 彩票", "MAIN_INIT_TOKEN", "{ token } 首次下发"],
["→ 彩票", "MAIN_REFRESH_TOKEN", "{ token } 续签"],
["→ 彩票", "MAIN_REQUEST_STATUS", "请求子页状态"],
["→ 彩票", "MAIN_NAVIGATE", "{ path } 导航"]
]
},
"wallet": {
"title": "钱包网关",
"description": "由客户实现。彩票服务端调用。鉴权Bearer wallet_api_key。",
"balance": "查询余额",
"debit": "扣款",
"credit": "加款",
"response": "响应示例",
"httpContract": "HTTP 契约",
"httpErrors": "HTTP 错误",
"creditNote": "请求体与扣款相同;用于转出或失败回滚加款。",
"idempotentNote": "idempotent_key相同键 + 相同操作须返回首次 JSONHTTP 200禁止重复记账同键不同操作/金额 → success: false。",
"queryRows": [
["site_code", "string", ""],
["site_player_id", "string", ""],
["currency_code", "string", ""]
],
"fieldRows": [
["site_code", "string", ""],
["site_player_id", "string", ""],
["player_id", "number", "彩票玩家 ID"],
["currency_code", "string", ""],
["amount_minor", "integer", "minor 正整数"],
["idempotent_key", "string", "幂等键"]
],
"httpErrorRows": [
["401", "unauthorized", "API Key 错误"],
["422", "invalid request", "字段或金额非法"],
["409", "main balance insufficient", "业务拒绝(如余额不足);可含 data.main_balance"]
],
"httpContractRows": [
["扣款/加款成功", "200", "success: true含 external_ref_no建议与 data.main_balance"],
["余额查询成功", "200", "success: truedata.main_balance + currency_code"],
["参数非法", "422", "success: falsemessage: invalid request"],
["鉴权失败", "401", "success: falsemessage: unauthorized"],
["业务拒绝", "409", "success: falsemessage 说明原因"],
["幂等重放", "200", "与首次成功/拒绝响应完全一致"]
]
},
"transfer": {
"title": "资金划转(参考)",
"description": "内部说明:由彩票 H5 调用,非主站接入面。",
"outOfScopeNote": "客户无需实现本节 API。转入/转出由我方 H5 携带玩家 JWT 调用;主站只需实现钱包网关 debit/credit。本节仅供理解资金如何在两端移动。",
"requestFields": "请求字段",
"transferIn": "转入(主站 → 彩票)",
"transferOut": "转出(彩票 → 主站)",
"transferResponse": "成功响应",
"errors": "常见错误码",
"inNote": "流程:彩票调主站 debit-for-lottery → 彩票内加款。",
"outNote": "流程:彩票内扣款 → 彩票调主站 credit-from-lottery。",
"responseNote": "转入与转出结构相同direction 为 in / out。幂等重放返回相同 data。",
"requestFieldRows": [
["amount", "integer", "minor 正整数"],
["currency", "string", "可选;默认玩家 default_currency"],
["idempotent_key", "string", "全局唯一;重复须返回相同结果"]
],
"errorRows": [
["1001", "彩票余额不足(转出)"],
["1009", "主站钱包处理失败"],
["1010", "幂等键冲突(同键不同金额)"],
["2003", "请先转入后再下注"]
]
},
"errors": {
"title": "错误码",
"sso": "SSO 鉴权",
"lotteryWallet": "彩票钱包 / 划转",
"gateway": "客户钱包网关HTTP",
"idempotentNote": "幂等:同一 idempotent_key 须返回相同结果;同键不同金额 → 1010。",
"ssoRows": [
["8001", "缺少 Authorization 头"],
["8002", "JWT 无效或已过期"],
["8003", "玩家未建档"],
["8004", "SSO 密钥未配置"],
["8005", "账号已冻结"]
],
"lotteryRows": [
["1001", "彩票余额不足"],
["1009", "主站钱包处理失败"],
["1010", "幂等键冲突"],
["2003", "请先转入后再下注"]
],
"gatewayRows": [
["401", "unauthorized", "API Key 错误"],
["422", "invalid request", "字段或金额非法"],
["409", "—", "业务拒绝(如余额不足)"]
]
},
"golive": {
"title": "上线清单",
"checklist": "检查项",
"items": [
"测试与生产site_code、密钥、域名完全分离",
"JWT 仅服务端签发,有效期 ≤ 5 分钟",
"钱包接口走 HTTPS超时建议 ≤ 10 秒",
"idempotent_key 幂等处理",
"iframe 模式:配置 iframe_allowed_origins",
"全链路:转入 → 下注 → 派奖 → 转出"
]
}
}
}

View File

@@ -22,6 +22,16 @@ 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" },
"/docs": { ns: "config", key: "integrationGuide.title" },
"/docs/integration": { ns: "config", key: "integrationGuide.title" },
"/docs/integration/fundamentals": { ns: "config", key: "integrationGuide.title" },
"/docs/integration/preparation": { ns: "config", key: "integrationGuide.title" },
"/docs/integration/sso": { ns: "config", key: "integrationGuide.title" },
"/docs/integration/iframe": { ns: "config", key: "integrationGuide.title" },
"/docs/integration/wallet": { ns: "config", key: "integrationGuide.title" },
"/docs/integration/transfer": { ns: "config", key: "integrationGuide.title" },
"/docs/integration/errors": { ns: "config", key: "integrationGuide.title" },
"/docs/integration/go-live": { ns: "config", key: "integrationGuide.title" },
"/admin/docs/integration-guide": { ns: "config", key: "integrationGuide.title" },
"/admin/wallet": { ns: "wallet", key: "title" },
"/admin/wallet/transactions": { ns: "wallet", key: "walletTransactions" },

View File

@@ -0,0 +1,31 @@
import enIntegrationDocs from "@/i18n/locales/en/integrationDocs.json";
import neIntegrationDocs from "@/i18n/locales/ne/integrationDocs.json";
import zhIntegrationDocs from "@/i18n/locales/zh/integrationDocs.json";
const ADMIN_DEFAULT_LANGUAGE = "zh";
const NAV_RESOURCES = {
zh: zhIntegrationDocs,
en: enIntegrationDocs,
ne: neIntegrationDocs,
} as const;
/** SSR / i18n 未就绪时同步解析 nav 文案,避免侧栏 hydration 显示 nav.xxx */
export function resolveDocsNavLabel(
key: string,
language: string = ADMIN_DEFAULT_LANGUAGE,
): string {
const base = language.split("-")[0]?.toLowerCase();
const resource =
base === "ne" ? NAV_RESOURCES.ne : base === "en" ? NAV_RESOURCES.en : NAV_RESOURCES.zh;
let node: unknown = resource;
for (const part of key.split(".")) {
if (node === null || typeof node !== "object") {
return key;
}
node = (node as Record<string, unknown>)[part];
}
return typeof node === "string" ? node : key;
}

35
src/lib/docs-nav.ts Normal file
View File

@@ -0,0 +1,35 @@
export type DocsNavItem = {
href: string;
titleKey: string;
};
export type DocsNavGroup = {
titleKey: string;
items: DocsNavItem[];
};
export const DOCS_NAV_GROUPS: DocsNavGroup[] = [
{
titleKey: "nav.overview",
items: [
{ href: "/docs/integration", titleKey: "nav.home" },
{ href: "/docs/integration/quickstart", titleKey: "nav.quickstart" },
{ href: "/docs/integration/fundamentals", titleKey: "nav.fundamentals" },
{ href: "/docs/integration/preparation", titleKey: "nav.setup" },
],
},
{
titleKey: "nav.api",
items: [
{ href: "/docs/integration/sso", titleKey: "nav.sso" },
{ href: "/docs/integration/iframe", titleKey: "nav.iframe" },
{ href: "/docs/integration/wallet", titleKey: "nav.wallet" },
{ href: "/docs/integration/transfer", titleKey: "nav.transfer" },
{ href: "/docs/integration/errors", titleKey: "nav.errors" },
],
},
{
titleKey: "nav.ship",
items: [{ href: "/docs/integration/go-live", titleKey: "nav.golive" }],
},
];

25
src/lib/highlight-code.ts Normal file
View File

@@ -0,0 +1,25 @@
import { createHighlighter, type Highlighter } from "shiki";
export type HighlightLang = "json" | "bash" | "typescript" | "http";
let highlighterPromise: Promise<Highlighter> | null = null;
function getHighlighter(): Promise<Highlighter> {
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
themes: ["github-light"],
langs: ["json", "bash", "typescript", "http"],
});
}
return highlighterPromise;
}
export async function highlightCode(code: string, lang: HighlightLang = "json"): Promise<string> {
const highlighter = await getHighlighter();
return highlighter.codeToHtml(code.trimEnd(), {
lang,
theme: "github-light",
});
}

View File

@@ -49,7 +49,7 @@ const HUB_CARDS: HubCard[] = [
requiredAny: PRD_INTEGRATION_ACCESS_ANY,
},
{
href: "/admin/docs/integration-guide",
href: "/docs/integration",
titleKey: "hub.integrationGuideTitle",
descKey: "hub.integrationGuideDesc",
requiredAny: PRD_INTEGRATION_ACCESS_ANY,

View File

@@ -1,414 +1,6 @@
import { Card, CardContent } from "@/components/ui/card";
import { redirect } from "next/navigation";
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>
);
/** @deprecated 单页长文档已拆分为 /docs/integration/* 多页;保留导出避免旧引用报错 */
export function IntegrationGuideScreen(): never {
redirect("/docs/integration");
}
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">
H5WebApp 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>
);
}

View File

@@ -0,0 +1,154 @@
/** 代码示例(语言无关,三语共用) */
export const SSO_JWT_PAYLOAD_EXAMPLE = `{
"site_code": "demo",
"site_player_id": "100001",
"iat": 1718000000,
"exp": 1718000300
}`;
export const SSO_JWT_SIGN_EXAMPLE = `const header = base64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
const payload = base64url(JSON.stringify({
site_code: "demo",
site_player_id: "100001",
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 300,
}));
const sig = hmacSha256Base64url(\`\${header}.\${payload}\`, SSO_JWT_SECRET);
const token = \`\${header}.\${payload}.\${sig}\`;`;
export const SSO_ENTRY_URL = `https://{lottery_host}/?token={JWT}`;
export const SSO_POSTMESSAGE = `iframe.contentWindow.postMessage(
{ type: "MAIN_INIT_TOKEN", token: jwt, source: "main-site" },
"https://lottery.example.com"
);`;
export const PLAYER_ME_REQUEST = `GET /api/v1/player/me
Authorization: Bearer {JWT}
Accept-Language: zh`;
export const PLAYER_ME_SUCCESS = `{
"code": 0,
"msg": "success",
"data": {
"id": 42,
"site_code": "demo",
"site_player_id": "100001",
"auth_source": "main_site_sso",
"funding_mode": "wallet",
"username": null,
"nickname": null,
"default_currency": "NPR",
"status": 0,
"locale": "zh",
"last_login_at": "2026-06-14T10:00:00+00:00",
"created_at": "2026-06-14T10:00:00+00:00"
}
}`;
export const IFRAME_CHILD_READY = `{
"type": "LOTTERY_READY",
"payload": { "url": "https://lottery.example.com/", "userAgent": "..." },
"timestamp": 1718000000000,
"source": "lottery-iframe"
}`;
export const IFRAME_PARENT_INIT = `{
"type": "MAIN_INIT_TOKEN",
"token": "eyJhbGciOiJIUzI1NiIs...",
"timestamp": 1718000000000,
"source": "main-site"
}`;
export const ACCEPTANCE_PLAYER_ME = `curl -sS "https://{lottery_api}/api/v1/player/me" \\
-H "Authorization: Bearer {JWT}" \\
-H "Accept: application/json"`;
export const ACCEPTANCE_WALLET_DEBIT = `curl -sS -X POST "https://{wallet_host}/wallet/debit-for-lottery" \\
-H "Authorization: Bearer {wallet_api_key}" \\
-H "Content-Type: application/json" \\
-d '{
"site_code": "demo",
"site_player_id": "100001",
"player_id": 42,
"currency_code": "NPR",
"amount_minor": 100,
"idempotent_key": "accept-debit-001"
}'`;
export const PLAYER_AUTH_ERROR = `HTTP/1.1 401 Unauthorized
{
"code": 8002,
"msg": "Token 无效或已过期",
"data": null
}`;
export const WALLET_BALANCE_RESPONSE = `{
"success": true,
"data": { "main_balance": 500000, "currency_code": "NPR" }
}`;
export const WALLET_DEBIT_REQUEST = `POST /wallet/debit-for-lottery
Authorization: Bearer {wallet_api_key}
{
"site_code": "demo",
"site_player_id": "100001",
"player_id": 42,
"currency_code": "NPR",
"amount_minor": 2000,
"idempotent_key": "TI-20260610-abc"
}`;
export const WALLET_SUCCESS = `{
"success": true,
"external_ref_no": "MW-001",
"data": { "main_balance": 498000, "currency_code": "NPR" }
}`;
export const WALLET_FAIL = `{
"success": false,
"message": "main balance insufficient",
"data": { "main_balance": 500, "currency_code": "NPR" }
}`;
export const WALLET_CREDIT_REQUEST = `POST /wallet/credit-from-lottery
Authorization: Bearer {wallet_api_key}
{
"site_code": "demo",
"site_player_id": "100001",
"player_id": 42,
"currency_code": "NPR",
"amount_minor": 2000,
"idempotent_key": "TO-20260610-abc"
}`;
export const TRANSFER_IN = `POST /api/v1/wallet/transfer-in
Authorization: Bearer {player_jwt}
{ "amount": 2000, "currency": "NPR", "idempotent_key": "ti-001" }`;
export const TRANSFER_OUT = `POST /api/v1/wallet/transfer-out
Authorization: Bearer {player_jwt}
{ "amount": 1000, "currency": "NPR", "idempotent_key": "to-001" }`;
export const TRANSFER_SUCCESS = `{
"code": 0,
"msg": "success",
"data": {
"transfer_no": "TI20260614001",
"direction": "in",
"currency_code": "NPR",
"amount": 2000,
"status": "success",
"external_ref_no": "debit-for-lottery-1718360000-a1b2c3",
"balance": 2000,
"log_id": "WX_20260614_0001",
"lottery_balance_after": 2000,
"lottery_available_after": 2000,
"finished_at": "2026-06-14T10:00:00+00:00"
}
}`;

View File

@@ -0,0 +1,314 @@
"use client";
import {
DocCode,
DocEndpoint,
DocList,
DocNote,
DocOrderedList,
DocPageHeader,
DocSection,
DocTable,
} from "@/components/docs/doc-ui";
import {
SSO_ENTRY_URL,
SSO_JWT_PAYLOAD_EXAMPLE,
SSO_JWT_SIGN_EXAMPLE,
SSO_POSTMESSAGE,
ACCEPTANCE_PLAYER_ME,
ACCEPTANCE_WALLET_DEBIT,
IFRAME_CHILD_READY,
IFRAME_PARENT_INIT,
PLAYER_AUTH_ERROR,
PLAYER_ME_REQUEST,
PLAYER_ME_SUCCESS,
TRANSFER_IN,
TRANSFER_OUT,
TRANSFER_SUCCESS,
WALLET_BALANCE_RESPONSE,
WALLET_CREDIT_REQUEST,
WALLET_DEBIT_REQUEST,
WALLET_FAIL,
WALLET_SUCCESS,
} from "@/modules/docs/integration/integration-doc-data";
import { useIntegrationDoc } from "@/modules/docs/integration/use-integration-doc";
function DocPage({ children }: { children: React.ReactNode }): React.ReactElement {
return <div className="space-y-8">{children}</div>;
}
export function OverviewDocScreen(): React.ReactElement {
const { p, rows, list, header } = useIntegrationDoc("overview");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("roles")}>
<DocTable compact headers={header("component")} rows={rows("matrix")} />
</DocSection>
<DocSection title={p("flow")}>
<DocOrderedList items={list("flowItems")} />
</DocSection>
<DocSection title={p("e2eSequence")}>
<DocTable compact headers={header("sequence")} rows={rows("e2eRows")} />
</DocSection>
<DocSection title={p("conventions")}>
<DocTable compact headers={header("convention")} rows={rows("conventionRows")} />
</DocSection>
<DocSection title={p("readingOrder")}>
<DocList items={list("readingItems")} />
</DocSection>
</DocPage>
);
}
export function QuickstartDocScreen(): React.ReactElement {
const { p, rows, list, header } = useIntegrationDoc("quickstart");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("prereq")}>
<DocList items={list("prereqItems")} />
</DocSection>
<DocSection title={p("steps")}>
<DocOrderedList items={list("stepItems")} />
</DocSection>
<DocSection title={p("testAccounts")}>
<DocTable compact headers={header("account")} rows={rows("accountRows")} />
</DocSection>
<DocSection title={p("reference")}>
<DocList items={list("referenceItems")} />
</DocSection>
<DocSection title={p("acceptance")}>
<DocOrderedList items={list("acceptanceItems")} />
<DocCode language="bash">{ACCEPTANCE_PLAYER_ME}</DocCode>
<DocCode language="bash">{ACCEPTANCE_WALLET_DEBIT}</DocCode>
</DocSection>
<DocNote>{p("note")}</DocNote>
</DocPage>
);
}
export function FundamentalsDocScreen(): React.ReactElement {
const { p, rows, header } = useIntegrationDoc("fundamentals");
return (
<DocPage>
<DocPageHeader title={p("title")} />
<DocSection title={p("balances")}>
<DocTable compact headers={header("balance")} rows={rows("balanceRows")} />
</DocSection>
<DocSection title={p("calls")}>
<DocTable compact headers={header("call")} rows={rows("callRows")} />
</DocSection>
<DocNote>{p("note")}</DocNote>
</DocPage>
);
}
export function SetupDocScreen(): React.ReactElement {
const { p, rows, list, header } = useIntegrationDoc("setup");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("weProvide")}>
<DocTable compact headers={header("param")} rows={rows("receiveRows")} />
</DocSection>
<DocSection title={p("youProvide")}>
<DocTable compact headers={header("param")} rows={rows("provideRows")} />
</DocSection>
<DocSection title={p("defaultPaths")}>
<DocTable compact headers={header("methodPath")} rows={rows("pathRows")} />
</DocSection>
<DocSection title={p("envMapping")}>
<DocTable compact headers={header("envMap")} rows={rows("envMappingRows")} />
</DocSection>
<DocSection title={p("adminSop")}>
<DocOrderedList items={list("adminSopSteps")} />
<DocTable compact headers={header("adminField")} rows={rows("adminFieldRows")} />
</DocSection>
<DocSection title={p("network")}>
<DocList items={list("networkItems")} />
</DocSection>
<DocNote>{p("note")}</DocNote>
</DocPage>
);
}
export function SsoDocScreen(): React.ReactElement {
const { p, rows, header } = useIntegrationDoc("sso");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("claims")}>
<DocTable compact headers={header("claim")} rows={rows("claimRows")} />
<DocCode>{SSO_JWT_PAYLOAD_EXAMPLE}</DocCode>
</DocSection>
<DocSection title={p("sign")}>
<DocCode language="typescript">{SSO_JWT_SIGN_EXAMPLE}</DocCode>
</DocSection>
<DocSection title={p("entryA")}>
<DocCode>{SSO_ENTRY_URL}</DocCode>
</DocSection>
<DocSection title={p("entryB")}>
<DocTable compact headers={header("message")} rows={rows("messageRows")} />
<DocCode language="typescript">{SSO_POSTMESSAGE}</DocCode>
<DocNote>{p("iframeNote")}</DocNote>
</DocSection>
<DocNote>{p("noExchangeNote")}</DocNote>
<DocSection title={p("entryApi")}>
<DocEndpoint method="GET" path="/api/v1/player/me" />
<DocNote>{p("entryApiNote")}</DocNote>
<DocCode language="http">{PLAYER_ME_REQUEST}</DocCode>
<DocCode>{PLAYER_ME_SUCCESS}</DocCode>
</DocSection>
<DocSection title={p("publicApis")}>
<DocTable compact headers={header("methodPath")} rows={rows("publicApiRows")} />
</DocSection>
<DocNote>{p("h5ScopeNote")}</DocNote>
<DocSection title={p("partnerApis")}>
<DocTable compact headers={header("methodPath")} rows={rows("partnerApiRows")} />
<DocNote>{p("refreshNote")}</DocNote>
</DocSection>
<DocSection title={p("authResponse")}>
<DocCode language="http">{PLAYER_AUTH_ERROR}</DocCode>
</DocSection>
<DocSection title={p("errors")}>
<DocTable compact headers={header("code")} rows={rows("errorRows")} />
</DocSection>
</DocPage>
);
}
export function IframeDocScreen(): React.ReactElement {
const { p, rows, list, header } = useIntegrationDoc("iframe");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("sequence")}>
<DocOrderedList items={list("sequenceSteps")} />
</DocSection>
<DocSection title={p("envelope")}>
<DocNote>{p("envelopeNote")}</DocNote>
</DocSection>
<DocSection title={p("childMessages")}>
<DocTable compact headers={header("message")} rows={rows("childMessageRows")} />
<DocCode>{IFRAME_CHILD_READY}</DocCode>
</DocSection>
<DocSection title={p("parentMessages")}>
<DocTable compact headers={header("message")} rows={rows("parentMessageRows")} />
<DocCode>{IFRAME_PARENT_INIT}</DocCode>
</DocSection>
<DocSection title={p("targetOrigin")}>
<DocNote>{p("targetOriginNote")}</DocNote>
</DocSection>
<DocNote>{p("timingNote")}</DocNote>
</DocPage>
);
}
export function WalletDocScreen(): React.ReactElement {
const { p, rows, header } = useIntegrationDoc("wallet");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("balance")}>
<DocEndpoint method="GET" path="/wallet/balance" />
<DocTable compact headers={header("query")} rows={rows("queryRows")} />
<DocCode>{WALLET_BALANCE_RESPONSE}</DocCode>
</DocSection>
<DocSection title={p("debit")}>
<DocEndpoint method="POST" path="/wallet/debit-for-lottery" />
<DocTable compact headers={header("field")} rows={rows("fieldRows")} />
<DocCode language="http">{WALLET_DEBIT_REQUEST}</DocCode>
</DocSection>
<DocSection title={p("credit")}>
<DocEndpoint method="POST" path="/wallet/credit-from-lottery" />
<DocCode language="http">{WALLET_CREDIT_REQUEST}</DocCode>
<DocNote>{p("creditNote")}</DocNote>
</DocSection>
<DocSection title={p("httpContract")}>
<DocTable compact headers={header("contract")} rows={rows("httpContractRows")} />
</DocSection>
<DocSection title={p("response")}>
<div className="grid gap-3 lg:grid-cols-2">
<DocCode>{WALLET_SUCCESS}</DocCode>
<DocCode>{WALLET_FAIL}</DocCode>
</div>
</DocSection>
<DocSection title={p("httpErrors")}>
<DocTable compact headers={header("http")} rows={rows("httpErrorRows")} />
</DocSection>
<DocNote>{p("idempotentNote")}</DocNote>
</DocPage>
);
}
export function TransferDocScreen(): React.ReactElement {
const { p, rows, header } = useIntegrationDoc("transfer");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocNote>{p("outOfScopeNote")}</DocNote>
<DocSection title={p("requestFields")}>
<DocTable compact headers={header("field")} rows={rows("requestFieldRows")} />
</DocSection>
<DocSection title={p("transferIn")}>
<DocEndpoint method="POST" path="/api/v1/wallet/transfer-in" />
<DocNote>{p("inNote")}</DocNote>
<DocCode language="http">{TRANSFER_IN}</DocCode>
</DocSection>
<DocSection title={p("transferOut")}>
<DocEndpoint method="POST" path="/api/v1/wallet/transfer-out" />
<DocNote>{p("outNote")}</DocNote>
<DocCode language="http">{TRANSFER_OUT}</DocCode>
</DocSection>
<DocSection title={p("transferResponse")}>
<DocNote>{p("responseNote")}</DocNote>
<DocCode>{TRANSFER_SUCCESS}</DocCode>
</DocSection>
<DocSection title={p("errors")}>
<DocTable compact headers={header("code")} rows={rows("errorRows")} />
</DocSection>
</DocPage>
);
}
export function ErrorsDocScreen(): React.ReactElement {
const { p, rows, header } = useIntegrationDoc("errors");
return (
<DocPage>
<DocPageHeader title={p("title")} />
<DocSection title={p("sso")}>
<DocTable compact headers={header("code")} rows={rows("ssoRows")} />
</DocSection>
<DocSection title={p("lotteryWallet")}>
<DocTable compact headers={header("code")} rows={rows("lotteryRows")} />
</DocSection>
<DocSection title={p("gateway")}>
<DocTable compact headers={header("http")} rows={rows("gatewayRows")} />
</DocSection>
<DocNote>{p("idempotentNote")}</DocNote>
</DocPage>
);
}
export function GoLiveDocScreen(): React.ReactElement {
const { p, list } = useIntegrationDoc("golive");
return (
<DocPage>
<DocPageHeader title={p("title")} />
<DocSection title={p("checklist")}>
<DocList items={list("items")} />
</DocSection>
</DocPage>
);
}

View File

@@ -0,0 +1,27 @@
"use client";
import { useTranslation } from "react-i18next";
type DocPageKey =
| "overview"
| "quickstart"
| "fundamentals"
| "setup"
| "sso"
| "iframe"
| "wallet"
| "transfer"
| "errors"
| "golive";
export function useIntegrationDoc(page: DocPageKey) {
const { t } = useTranslation("integrationDocs");
return {
t,
p: (key: string) => t(`pages.${page}.${key}`),
rows: (key: string) => t(`pages.${page}.${key}`, { returnObjects: true }) as string[][],
list: (key: string) => t(`pages.${page}.${key}`, { returnObjects: true }) as string[],
header: (key: string) => t(`headers.${key}`, { returnObjects: true }) as string[],
};
}