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:
@@ -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
550
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
5
src/app/docs/integration/errors/page.tsx
Normal file
5
src/app/docs/integration/errors/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ErrorsDocScreen } from "@/modules/docs/integration/integration-doc-screens";
|
||||
|
||||
export default function IntegrationErrorsPage(): React.ReactElement {
|
||||
return <ErrorsDocScreen />;
|
||||
}
|
||||
5
src/app/docs/integration/fundamentals/page.tsx
Normal file
5
src/app/docs/integration/fundamentals/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { FundamentalsDocScreen } from "@/modules/docs/integration/integration-doc-screens";
|
||||
|
||||
export default function IntegrationFundamentalsPage(): React.ReactElement {
|
||||
return <FundamentalsDocScreen />;
|
||||
}
|
||||
5
src/app/docs/integration/go-live/page.tsx
Normal file
5
src/app/docs/integration/go-live/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { GoLiveDocScreen } from "@/modules/docs/integration/integration-doc-screens";
|
||||
|
||||
export default function IntegrationGoLivePage(): React.ReactElement {
|
||||
return <GoLiveDocScreen />;
|
||||
}
|
||||
5
src/app/docs/integration/iframe/page.tsx
Normal file
5
src/app/docs/integration/iframe/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { IframeDocScreen } from "@/modules/docs/integration/integration-doc-screens";
|
||||
|
||||
export default function IntegrationIframePage(): React.ReactElement {
|
||||
return <IframeDocScreen />;
|
||||
}
|
||||
11
src/app/docs/integration/layout.tsx
Normal file
11
src/app/docs/integration/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
5
src/app/docs/integration/page.tsx
Normal file
5
src/app/docs/integration/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { OverviewDocScreen } from "@/modules/docs/integration/integration-doc-screens";
|
||||
|
||||
export default function IntegrationOverviewPage(): React.ReactElement {
|
||||
return <OverviewDocScreen />;
|
||||
}
|
||||
5
src/app/docs/integration/preparation/page.tsx
Normal file
5
src/app/docs/integration/preparation/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SetupDocScreen } from "@/modules/docs/integration/integration-doc-screens";
|
||||
|
||||
export default function IntegrationPreparationPage(): React.ReactElement {
|
||||
return <SetupDocScreen />;
|
||||
}
|
||||
5
src/app/docs/integration/quickstart/page.tsx
Normal file
5
src/app/docs/integration/quickstart/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { QuickstartDocScreen } from "@/modules/docs/integration/integration-doc-screens";
|
||||
|
||||
export default function IntegrationQuickstartPage(): React.ReactElement {
|
||||
return <QuickstartDocScreen />;
|
||||
}
|
||||
5
src/app/docs/integration/sso/page.tsx
Normal file
5
src/app/docs/integration/sso/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SsoDocScreen } from "@/modules/docs/integration/integration-doc-screens";
|
||||
|
||||
export default function IntegrationSsoPage(): React.ReactElement {
|
||||
return <SsoDocScreen />;
|
||||
}
|
||||
5
src/app/docs/integration/transfer/page.tsx
Normal file
5
src/app/docs/integration/transfer/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { TransferDocScreen } from "@/modules/docs/integration/integration-doc-screens";
|
||||
|
||||
export default function IntegrationTransferPage(): React.ReactElement {
|
||||
return <TransferDocScreen />;
|
||||
}
|
||||
5
src/app/docs/integration/wallet/page.tsx
Normal file
5
src/app/docs/integration/wallet/page.tsx
Normal 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
16
src/app/docs/layout.tsx
Normal 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
5
src/app/docs/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function DocsHomePage(): never {
|
||||
redirect("/docs/integration");
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
55
src/components/docs/doc-code.tsx
Normal file
55
src/components/docs/doc-code.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
129
src/components/docs/doc-ui.tsx
Normal file
129
src/components/docs/doc-ui.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
53
src/components/docs/docs-shell.tsx
Normal file
53
src/components/docs/docs-shell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
62
src/components/docs/docs-sidebar.tsx
Normal file
62
src/components/docs/docs-sidebar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
372
src/i18n/locales/en/integrationDocs.json
Normal file
372
src/i18n/locales/en/integrationDocs.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
372
src/i18n/locales/ne/integrationDocs.json
Normal file
372
src/i18n/locales/ne/integrationDocs.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
372
src/i18n/locales/zh/integrationDocs.json
Normal file
372
src/i18n/locales/zh/integrationDocs.json
Normal 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"],
|
||||
["时间", "JWT:Unix 秒(iat / exp)"],
|
||||
["鉴权", "玩家 API:Bearer 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": [
|
||||
"彩票 API(lotterLaravel)与彩票前端(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;彩票 H5:http://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_url(HTTPS 根地址,无 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": "方式 A:URL 跳转",
|
||||
"entryB": "方式 B:iframe 嵌入",
|
||||
"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:相同键 + 相同操作须返回首次 JSON(HTTP 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: true;data.main_balance + currency_code"],
|
||||
["参数非法", "422", "success: false;message: invalid request"],
|
||||
["鉴权失败", "401", "success: false;message: unauthorized"],
|
||||
["业务拒绝", "409", "success: false;message 说明原因"],
|
||||
["幂等重放", "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",
|
||||
"全链路:转入 → 下注 → 派奖 → 转出"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" },
|
||||
|
||||
31
src/lib/docs-nav-labels.ts
Normal file
31
src/lib/docs-nav-labels.ts
Normal 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
35
src/lib/docs-nav.ts
Normal 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
25
src/lib/highlight-code.ts
Normal 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",
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
export function IntegrationGuideScreen(): React.ReactElement {
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-[1280px] min-w-0 flex-col px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
|
||||
<div className="grid gap-10 xl:grid-cols-[220px_minmax(0,1fr)]">
|
||||
<aside className="hidden xl:block">
|
||||
<div className="sticky top-6 space-y-4 text-sm">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">技术接入文档</div>
|
||||
<div className="mt-2 font-medium text-foreground">彩票端客户接入</div>
|
||||
</div>
|
||||
<nav className="space-y-1 border-l border-border pl-4">
|
||||
{sections.map((section) => (
|
||||
<a
|
||||
key={section.id}
|
||||
href={`#${section.id}`}
|
||||
className="block py-1 text-muted-foreground transition hover:text-foreground"
|
||||
>
|
||||
{section.title}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="min-w-0">
|
||||
<header className="border-b border-border pb-8">
|
||||
<h1 className="text-3xl font-semibold tracking-tight text-foreground">彩票端客户接入文档</h1>
|
||||
<p className="mt-4 max-w-3xl text-sm leading-7 text-muted-foreground">
|
||||
本文档用于说明客户主站与彩票端之间的登录接入、钱包接口、联调方式与上线前检查项。阅读对象为客户后端、前端、测试和运维人员。
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="mt-8 space-y-10">
|
||||
<section id="overview" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">1. 文档说明</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground">
|
||||
客户完成主站登录后,通过 SSO 进入彩票端。用户在彩票端的余额查询、投注扣款、派奖加款等资金动作,由彩票端调用客户钱包接口完成。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-sm font-medium text-foreground">适用范围</div>
|
||||
<div className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
H5、Web、App WebView 等主站跳转彩票端场景。
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-sm font-medium text-foreground">职责边界</div>
|
||||
<div className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
主站负责用户身份,彩票端负责业务编排,钱包系统负责资金真理源。
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-sm font-medium text-foreground">阅读对象</div>
|
||||
<div className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
客户后端、前端、测试、运维以及联调负责人。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-sm font-medium text-foreground">接入链路</div>
|
||||
<ol className="mt-3 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>1. 用户在客户主站完成登录。</li>
|
||||
<li>2. 客户服务端生成 SSO 参数或 token。</li>
|
||||
<li>3. 浏览器跳转至彩票端入口地址。</li>
|
||||
<li>4. 彩票端校验签名并建立登录态。</li>
|
||||
<li>5. 用户在彩票端进行余额查询、投注、派奖等操作。</li>
|
||||
<li>6. 彩票端调用客户钱包接口完成账务处理。</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="preparation" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">2. 接入前准备</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground">
|
||||
联调开始前,双方需要先交换环境信息、签名密钥和测试数据,避免联调阶段反复返工。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">客户需提供</div>
|
||||
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>• 站点名称与站点编码。</li>
|
||||
<li>• 客户主站域名与钱包接口域名。</li>
|
||||
<li>• 测试环境与生产环境联系人。</li>
|
||||
<li>• 测试账号、测试余额、测试用例。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">双方需确认</div>
|
||||
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>• SSO 密钥或 JWT 验签方式。</li>
|
||||
<li>• 钱包签名算法、请求头、超时设置。</li>
|
||||
<li>• 余额、扣款、加款三个接口地址。</li>
|
||||
<li>• 白名单、重试策略、错误码定义。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="sso" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">3. SSO 登录接入</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground">
|
||||
客户用户在主站登录后,由客户服务端生成短时有效的登录凭证,浏览器携带该凭证进入彩票端。我方校验通过后建立彩票端会话。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">推荐流程</div>
|
||||
<ol className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>1. 客户主站用户完成登录。</li>
|
||||
<li>2. 客户服务端生成 SSO 负载。</li>
|
||||
<li>3. 使用共享密钥完成签名。</li>
|
||||
<li>4. 浏览器跳转至彩票端入口地址并附带凭证。</li>
|
||||
<li>5. 彩票端校验签名、时间戳、站点编码与用户标识。</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-3 text-base font-medium text-foreground">SSO 字段说明</div>
|
||||
<DocTable headers={["字段", "类型", "必填", "说明"]} rows={ssoFields} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">接入约束</div>
|
||||
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>• token 或签名参数必须为短时有效,建议 60 至 300 秒。</li>
|
||||
<li>• <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">user_id</code> 必须稳定且唯一。</li>
|
||||
<li>• 测试环境与生产环境密钥必须分离。</li>
|
||||
<li>• 密钥只允许保存在服务端,不可下发前端。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Card className="rounded-lg border-border shadow-none">
|
||||
<CardContent className="p-5">
|
||||
<div className="text-sm font-medium text-foreground">SSO 负载示例</div>
|
||||
<pre className="mt-4 overflow-x-auto whitespace-pre-wrap break-words rounded-md bg-muted px-4 py-4 text-sm leading-7 text-muted-foreground">{`{
|
||||
"site_code": "demo",
|
||||
"user_id": "100001",
|
||||
"username": "demo_user",
|
||||
"timestamp": 1718000000,
|
||||
"nonce": "N8F2X9Q1",
|
||||
"currency": "CNY",
|
||||
"device": "h5"
|
||||
}`}</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="wallet" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">4. 钱包接口</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground">
|
||||
彩票端通过客户钱包接口完成余额查询、投注扣款、派奖加款。钱包系统需要保证接口可重试、可审计、可幂等。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 xl:grid-cols-3">
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">余额查询</div>
|
||||
<div className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
用于进入彩票端、下注前或关键账务时机同步可用余额。
|
||||
</div>
|
||||
<div className="mt-3 rounded-md bg-muted px-3 py-2 text-xs text-foreground">POST /wallet/balance</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">扣款接口</div>
|
||||
<div className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
用于投注成功后的资金扣减,必须以交易号作为唯一幂等键。
|
||||
</div>
|
||||
<div className="mt-3 rounded-md bg-muted px-3 py-2 text-xs text-foreground">POST /wallet/debit</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">加款接口</div>
|
||||
<div className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
用于派奖、退款或撤单返还资金,不允许重复记账。
|
||||
</div>
|
||||
<div className="mt-3 rounded-md bg-muted px-3 py-2 text-xs text-foreground">POST /wallet/credit</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-3 text-base font-medium text-foreground">公共字段</div>
|
||||
<DocTable headers={["字段", "类型", "说明"]} rows={walletCommonFields} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<Card className="rounded-lg border-border shadow-none">
|
||||
<CardContent className="p-5">
|
||||
<div className="text-sm font-medium text-foreground">扣款请求示例</div>
|
||||
<pre className="mt-4 overflow-x-auto whitespace-pre-wrap break-words rounded-md bg-muted px-4 py-4 text-sm leading-7 text-muted-foreground">{`{
|
||||
"site_code": "demo",
|
||||
"user_id": "100001",
|
||||
"transaction_id": "BET202606100001",
|
||||
"order_id": "TICKET202606100001",
|
||||
"amount": "20.00",
|
||||
"timestamp": 1718000001,
|
||||
"sign": "xxxxxx"
|
||||
}`}</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="rounded-lg border-border shadow-none">
|
||||
<CardContent className="p-5">
|
||||
<div className="text-sm font-medium text-foreground">成功响应示例</div>
|
||||
<pre className="mt-4 overflow-x-auto whitespace-pre-wrap break-words rounded-md bg-muted px-4 py-4 text-sm leading-7 text-muted-foreground">{`{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"transaction_id": "BET202606100001",
|
||||
"balance": "980.00"
|
||||
}
|
||||
}`}</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">签名要求</div>
|
||||
<ol className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>1. 请求字段按字段名升序排序。</li>
|
||||
<li>2. 以 <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">key=value</code> 拼接原始串。</li>
|
||||
<li>3. 原始串尾部追加共享密钥。</li>
|
||||
<li>4. 使用 <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">HMAC-SHA256</code> 生成签名。</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">接口要求</div>
|
||||
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>• 所有接口必须使用 HTTPS。</li>
|
||||
<li>• 金额字段统一使用字符串,如 <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">1000.00</code>。</li>
|
||||
<li>• 请求内容类型统一为 <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">application/json</code>。</li>
|
||||
<li>• 钱包超时后我方可能重试,因此接口必须按幂等方式处理。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="errors" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">5. 错误码与幂等</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground">
|
||||
客户钱包接口需要统一错误码语义,并保证相同交易号的重复请求返回一致结果,不得重复记账。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-3 text-base font-medium text-foreground">建议错误码</div>
|
||||
<DocTable headers={["错误码", "含义", "处理方式"]} rows={errorRows} />
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">幂等要求</div>
|
||||
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>• 扣款与加款必须以 <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">transaction_id</code> 作为唯一交易键。</li>
|
||||
<li>• 同一交易号重复请求时,不允许再次扣款或再次加款。</li>
|
||||
<li>• 已成功处理的请求,再次请求时应返回首次处理结果。</li>
|
||||
<li>• 若出现网络抖动、超时或重试,钱包系统仍需保证账务一致性。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="testing" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">6. 联调与上线</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground">
|
||||
建议双方按固定顺序完成联调,先打通登录链路,再验证钱包接口,最后确认异常场景与上线条件。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">联调顺序</div>
|
||||
<ol className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>1. 域名连通、证书与白名单检查。</li>
|
||||
<li>2. SSO 参数生成与跳转验证。</li>
|
||||
<li>3. 余额查询接口联调。</li>
|
||||
<li>4. 扣款与加款接口联调。</li>
|
||||
<li>5. 超时、重复请求、余额不足场景回归。</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">上线前检查</div>
|
||||
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
<li>• 正式域名、正式密钥、正式白名单已配置。</li>
|
||||
<li>• 测试环境与生产环境参数已分离。</li>
|
||||
<li>• 核心错误码与日志字段已对齐。</li>
|
||||
<li>• 关键链路已完成验收回归。</li>
|
||||
<li>• 上线联系人与回滚预案已确认。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="appendix" className="scroll-mt-24 space-y-5 pb-2">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">7. 附录</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground">
|
||||
以下约定用于减少不同系统之间的解析差异,建议双方保持一致。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-3">
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">金额格式</div>
|
||||
<div className="mt-3 text-sm leading-6 text-muted-foreground">
|
||||
统一使用字符串传输,例如 <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">1000.00</code>。
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">时间格式</div>
|
||||
<div className="mt-3 text-sm leading-6 text-muted-foreground">
|
||||
默认使用 Unix 时间戳秒级,双方也可统一为 ISO8601。
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border px-5 py-5">
|
||||
<div className="text-base font-medium text-foreground">字符与报文</div>
|
||||
<div className="mt-3 text-sm leading-6 text-muted-foreground">
|
||||
字符编码统一 UTF-8,请求内容类型统一为 JSON。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
/** @deprecated 单页长文档已拆分为 /docs/integration/* 多页;保留导出避免旧引用报错 */
|
||||
export function IntegrationGuideScreen(): never {
|
||||
redirect("/docs/integration");
|
||||
}
|
||||
154
src/modules/docs/integration/integration-doc-data.ts
Normal file
154
src/modules/docs/integration/integration-doc-data.ts
Normal 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"
|
||||
}
|
||||
}`;
|
||||
314
src/modules/docs/integration/integration-doc-screens.tsx
Normal file
314
src/modules/docs/integration/integration-doc-screens.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
27
src/modules/docs/integration/use-integration-doc.ts
Normal file
27
src/modules/docs/integration/use-integration-doc.ts
Normal 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[],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user