feat(docs, agents, risk): enhance documentation, API queries, and UI components

Updated the public documentation site with improved layout and accessibility, including new sections for client integration and admin guides. Enhanced API queries by adding 'active_only' and 'group_by' parameters for better data filtering in risk management. Refined UI components for agent management, ensuring consistent styling and improved user experience across the application. Added localization support for new documentation content in English and Nepali.
This commit is contained in:
2026-06-15 17:21:50 +08:00
parent 17335cb47a
commit 641c87ff50
107 changed files with 5114 additions and 943 deletions

View File

@@ -32,6 +32,8 @@ This version has breaking changes — APIs, conventions, and file structure may
## Learned User Preferences
- 占成/授信/回水/上限等数值字段用 `AdminNumericStepper`(± 步进 + 可手输),勿单独裸 `input[type=number]`
- 对外文档(接入 + 后台运营手册)禁用 RBAC slug`prd.settlement.agent.manage`);对客户称「贵司」;排版忌 AI 感,正文对比度与字号可读优先。
- 文档 i18n`useTranslation` 须显式 `ns``returnObjects` 列表用 `Array.isArray` 守卫;避免节名与表头 key 冲突(如 `billStatus`)。
## Learned Workspace Facts
@@ -39,5 +41,11 @@ 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` 重定向至此
- 客户对外文档`/docs`(首页)、`/docs/integration`API 接入)、`/docs/admin`(后台运营手册),均公开免 `/admin` 登录;读者为接入方技术与站点运营/代理,非内部运维;旧 `/admin/docs/integration-guide` 重定向 `/docs/integration`;顶栏「管理后台」链 `/admin`(勿 `/admin/login`
- `SettlementBillRow``currency_code`;账单金额展示用玩家 `default_currency`
- 浏览器 `/api/v1/*` 由 Next 转发到 `LOTTERY_API_UPSTREAM`;与本地 Postgres 对账前须确认 upstream 与所查库一致。
- 接入文档页静态内容校验用 `document.body.innerText``DocCode` 非裸 `<pre>/<code>`,勿只查 code 选择器。
- Docs 侧栏粘性定位用 CSS 变量 `--docs-sticky-top`(含顶栏/header 偏移)。
- Tanumo 联调/生产默认H5 `https://front.tanumo.com`、API `https://lotterylaravel.tanumo.com`、管理端/文档 `https://lotteryadmin.tanumo.com`
- 接入文档 SSO无「登录换票」主站 JWT 直传 H5/iframe`GET /api/v1/player/me` 自动建档iframe 约定 token 在顶层 `data.token`;勿引用 main-site/monorepo 等内部仓库路径。
- 风控页默认:占用流水按注单聚合;风险池仅显示有占用/高风险;组合明细放注单详情二级页。

View File

@@ -15,6 +15,7 @@ export type AdminRiskPoolListQuery = {
per_page?: number;
sold_out_only?: boolean;
high_risk_only?: boolean;
active_only?: boolean;
normalized_number?: string;
sort?: "usage_desc" | "locked_desc" | "remaining_asc" | "number_asc";
};
@@ -29,6 +30,7 @@ export async function getAdminRiskPools(
per_page: q.per_page,
sold_out_only: q.sold_out_only === true ? 1 : undefined,
high_risk_only: q.high_risk_only === true ? 1 : undefined,
active_only: q.active_only === true ? 1 : undefined,
normalized_number: q.normalized_number || undefined,
sort: q.sort,
},
@@ -58,8 +60,10 @@ export async function postAdminRiskPoolRecover(
export type AdminRiskLockLogQuery = {
page?: number;
per_page?: number;
group_by?: "ticket" | "entry";
action_type?: "lock" | "release";
normalized_number?: string;
ticket_item_id?: number;
};
export async function getAdminRiskPoolLockLogs(
@@ -72,8 +76,10 @@ export async function getAdminRiskPoolLockLogs(
params: {
page: q.page,
per_page: q.per_page,
group_by: q.group_by ?? "ticket",
action_type: q.action_type,
normalized_number: q.normalized_number,
ticket_item_id: q.ticket_item_id,
},
},
);

View File

@@ -0,0 +1,10 @@
import { adminRequest } from "@/lib/admin-http";
import type { AdminTicketItemDetail } from "@/types/api/admin-tickets";
const A = `/admin`;
export async function getAdminTicketItem(ticketNo: string): Promise<AdminTicketItemDetail> {
const encoded = encodeURIComponent(ticketNo);
return adminRequest.get<AdminTicketItemDetail>(`${A}/tickets/${encoded}`);
}

View File

@@ -4,7 +4,7 @@ import { AgentsSubnav } from "@/modules/agents/agents-subnav";
export default function AdminAgentsLayout({ children }: { children: ReactNode }) {
return (
<div className="mx-auto flex w-full max-w-[1680px] min-w-0 flex-col gap-4 px-4 py-4 sm:px-6 lg:px-8 lg:py-5">
<div className="mx-auto flex w-full max-w-[1680px] min-h-0 min-w-0 flex-1 flex-col gap-4 px-4 py-4 sm:px-6 lg:px-8 lg:py-5">
<AgentsSubnav />
{children}
</div>

View File

@@ -3,7 +3,7 @@ import type { Metadata } from "next";
import { AgentsDirectoryConsole } from "@/modules/agents/agents-directory-console";
import { buildPageMetadata } from "@/lib/page-metadata";
export const metadata: Metadata = buildPageMetadata("agents", "directoryTitle");
export const metadata: Metadata = buildPageMetadata("agents", "listTitle");
export default function AgentsListPage() {
return <AgentsDirectoryConsole />;

View File

@@ -4,10 +4,10 @@ import {
} from "@/modules/risk/risk-pools-console";
function parsePoolFilter(raw: string | undefined): RiskPoolListFilter {
if (raw === "sold_out" || raw === "high_risk") {
if (raw === "sold_out" || raw === "high_risk" || raw === "all" || raw === "active") {
return raw;
}
return "all";
return "active";
}
export default async function AdminDrawRiskPoolsPage(props: {
@@ -24,7 +24,7 @@ export default async function AdminDrawRiskPoolsPage(props: {
drawId={id}
titleKey="allPoolsPageTitle"
initialFilter={filter}
defaultSort={filter === "high_risk" ? "usage_desc" : "number_asc"}
defaultSort={filter === "high_risk" || filter === "active" ? "usage_desc" : "number_asc"}
allowSortChange
/>
);

View File

@@ -0,0 +1,22 @@
import { AdminPermissionGate } from "@/components/admin/admin-permission-gate";
import { ModuleScaffold } from "@/components/admin/module-scaffold";
import { PRD_TICKETS_ACCESS_ANY } from "@/lib/admin-prd";
import { TicketDetailConsole } from "@/modules/tickets/ticket-detail-console";
import { buildPageMetadata } from "@/lib/page-metadata";
import type { Metadata } from "next";
export const metadata: Metadata = buildPageMetadata("tickets", "detailTitle");
export default async function AdminTicketDetailPage(props: {
params: Promise<{ ticketNo: string }>;
}) {
const { ticketNo } = await props.params;
return (
<ModuleScaffold>
<AdminPermissionGate requiredAny={PRD_TICKETS_ACCESS_ANY}>
<TicketDetailConsole ticketNo={decodeURIComponent(ticketNo)} />
</AdminPermissionGate>
</ModuleScaffold>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
import { redirect } from "next/navigation";
/** 旧链接兼容:完整内容已拆至左侧分章导航 */
export default function ApiReferencePage(): never {
redirect("/docs/integration");
}

View File

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

View File

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

View File

@@ -5,10 +5,10 @@ import { DocsShell } from "@/components/docs/docs-shell";
export const metadata: Metadata = {
title: {
template: "%s · Integration API",
default: "Integration API",
template: "%s · Docs",
default: "Documentation",
},
description: "Lottery integration docs: SSO, wallet gateway, transfers.",
description: "Lottery admin user guide and API integration documentation.",
};
export default function DocsLayout({ children }: { children: ReactNode }): React.ReactElement {

View File

@@ -238,3 +238,21 @@
}
}
/* 公开文档站:正文链接与强调,避免继承后台 muted 色 */
.docs-site .doc-content a:not([class]) {
color: #0f172a;
font-weight: 500;
text-decoration: underline;
text-decoration-color: #cbd5e1;
text-underline-offset: 2px;
}
.docs-site .doc-content a:not([class]):hover {
text-decoration-color: #64748b;
}
.docs-site .doc-content strong {
color: #0f172a;
font-weight: 600;
}

View File

@@ -41,7 +41,7 @@ const TOP_ROUTE_LABELS: Record<string, string> = {
};
const AGENT_ROUTE_LABELS: Record<string, string> = {
list: "agents.directoryTitle",
list: "agents.listTitle",
provision: "agents.subnav.provision",
"settlement-bills": "settlementCenter.title",
};

View File

@@ -0,0 +1,37 @@
"use client";
import Link from "next/link";
import { BookOpenIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
type AdminPageGuideProps = {
guide: string;
docHref?: string;
className?: string;
};
export function AdminPageGuide({ guide, docHref, className }: AdminPageGuideProps): React.ReactElement {
const { t } = useTranslation("common");
return (
<div
className={cn(
"rounded-lg border border-border/80 bg-muted/25 px-4 py-3 text-sm leading-6 text-muted-foreground",
className,
)}
>
<p>{guide}</p>
{docHref ? (
<Link
href={docHref}
className="mt-2 inline-flex items-center gap-1.5 text-xs font-medium text-primary underline-offset-2 hover:underline"
>
<BookOpenIcon className="size-3.5 shrink-0" aria-hidden />
{t("docs.learnMore")}
</Link>
) : null}
</div>
);
}

View File

@@ -48,7 +48,7 @@ export function AdminShell({ children }: { children: ReactNode }) {
)}
</div>
</header>
<div className="flex min-w-0 flex-1 flex-col overflow-x-clip px-4 pt-4 pb-6 md:px-5 md:pt-4 md:pb-6">
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-clip px-4 pt-4 pb-6 md:px-5 md:pt-4 md:pb-6">
{children}
</div>
</SidebarInset>

View File

@@ -15,7 +15,7 @@ export function ModuleScaffold({ children, className, embedded = false }: Module
<div
className={cn(
embedded
? "flex w-full min-w-0 flex-col gap-6"
? "flex min-h-0 w-full min-w-0 flex-1 flex-col gap-6"
: "mx-auto flex w-full max-w-[1680px] min-w-0 flex-col gap-6 px-4 py-5 sm:px-6 lg:px-8 lg:py-6",
className,
)}

View File

@@ -1,5 +1,6 @@
"use client";
import {
BookOpenIcon,
ChevronDownIcon,
LogOutIcon,
UserRoundIcon,
@@ -72,6 +73,13 @@ export function ShellToolbar() {
<UserRoundIcon className="size-4" />
{t("toolbar.accountSettings")}
</DropdownMenuItem>
<DropdownMenuItem
className="flex cursor-pointer items-center gap-2"
onClick={() => router.push("/docs/admin")}
>
<BookOpenIcon className="size-4" />
{t("toolbar.relatedDocs")}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>

View File

@@ -9,12 +9,14 @@ type DocCodeProps = {
children: string;
language?: HighlightLang;
className?: string;
title?: string;
};
export function DocCode({
children,
language = "json",
className,
title,
}: DocCodeProps): React.ReactElement {
const [html, setHtml] = useState<string | null>(null);
const code = children.trimEnd();
@@ -36,17 +38,22 @@ export function DocCode({
return (
<div
className={cn(
"overflow-x-auto rounded-xl border border-border bg-[#f6f8fa]",
"overflow-hidden rounded-lg border border-slate-300 bg-white shadow-sm",
className,
)}
>
{title ? (
<div className="border-b border-slate-200 bg-slate-50 px-4 py-2 text-xs font-medium text-slate-600">
{title}
</div>
) : null}
{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"
className="[&_code]:font-mono [&_pre]:m-0 [&_pre]:overflow-x-auto [&_pre]:bg-white [&_pre]:p-4 [&_pre]:text-[13.5px] [&_pre]:leading-7 [&_pre]:text-slate-900"
dangerouslySetInnerHTML={{ __html: html }}
/>
) : (
<pre className="m-0 overflow-x-auto p-4 font-mono text-[13px] leading-7 text-foreground">
<pre className="m-0 overflow-x-auto p-4 font-mono text-[13.5px] leading-7 text-slate-900">
{code}
</pre>
)}

View File

@@ -4,6 +4,11 @@ import { cn } from "@/lib/utils";
export { DocCode } from "@/components/docs/doc-code";
/** 文档正文容器:统一字号与段落间距 */
export function DocPage({ children }: { children: ReactNode }): React.ReactElement {
return <article className="doc-content space-y-10">{children}</article>;
}
export function DocPageHeader({
title,
description,
@@ -12,10 +17,14 @@ export function DocPageHeader({
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>
<header className="border-b border-slate-200/90 pb-8">
<h1 className="text-[1.75rem] font-bold leading-tight tracking-tight text-slate-900 sm:text-3xl">
{title}
</h1>
{description ? (
<p className="mt-2 max-w-3xl text-sm leading-6 text-muted-foreground">{description}</p>
<p className="mt-3 max-w-3xl text-[15px] leading-7 text-slate-600 sm:text-base sm:leading-8">
{description}
</p>
) : null}
</header>
);
@@ -31,26 +40,39 @@ export function DocSection({
className?: string;
}): React.ReactElement {
return (
<section className={cn("space-y-3", className)}>
{title ? <h2 className="text-base font-semibold text-foreground">{title}</h2> : null}
<section className={cn("space-y-4", className)}>
{title ? (
<h2 className="border-b border-slate-100 pb-2 text-[17px] font-semibold leading-snug text-slate-900">
{title}
</h2>
) : null}
{children}
</section>
);
}
export function DocParagraph({ children }: { children: ReactNode }): React.ReactElement {
return <p className="text-[15px] leading-7 text-slate-700 sm:leading-8">{children}</p>;
}
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>
<aside className="rounded-lg border border-amber-200/90 bg-amber-50/90 px-4 py-3.5 text-[15px] leading-7 text-amber-950 shadow-sm">
{children}
</aside>
);
}
export function DocList({ items }: { items: readonly string[] }): React.ReactElement {
return (
<ul className="space-y-1 text-sm leading-6 text-muted-foreground">
<ul className="space-y-2 pl-1 text-[15px] leading-7 text-slate-700">
{items.map((item) => (
<li key={item} className="flex gap-2">
<span className="text-muted-foreground/50">·</span>
<span>{item}</span>
<li key={item} className="flex gap-3">
<span
aria-hidden
className="mt-[0.55rem] size-1.5 shrink-0 rounded-full bg-slate-400"
/>
<span className="min-w-0 flex-1">{item}</span>
</li>
))}
</ul>
@@ -59,9 +81,11 @@ export function DocList({ items }: { items: readonly string[] }): React.ReactEle
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">
<ol className="list-decimal space-y-2.5 pl-6 text-[15px] leading-7 text-slate-700 marker:font-semibold marker:text-slate-500">
{items.map((item) => (
<li key={item}>{item}</li>
<li key={item} className="pl-1">
{item}
</li>
))}
</ol>
);
@@ -77,14 +101,14 @@ export function DocTable({
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">
<div className="overflow-x-auto rounded-lg border border-slate-200 bg-white shadow-sm">
<table className="w-full min-w-[520px] border-collapse text-left">
<thead>
<tr className="border-b border-border bg-muted/30">
<tr className="border-b border-slate-200 bg-slate-50">
{headers.map((header) => (
<th
key={header}
className="px-3 py-2 text-left text-[11px] font-medium uppercase tracking-wide text-muted-foreground"
className="px-4 py-2.5 text-[13px] font-semibold text-slate-600"
>
{header}
</th>
@@ -93,13 +117,17 @@ export function DocTable({
</thead>
<tbody>
{rows.map((row, rowIndex) => (
<tr key={`${row[0]}-${rowIndex}`} className="border-b border-border/60 last:border-0">
<tr
key={`${row[0]}-${rowIndex}`}
className="border-b border-slate-100 last:border-0 even:bg-slate-50/40"
>
{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",
"px-4 align-top text-slate-800",
cellIndex === 0 && "font-medium text-slate-900",
compact ? "py-2 text-[14px] leading-6" : "py-3 text-[14px] leading-7",
)}
>
{cell}
@@ -115,15 +143,19 @@ export function DocTable({
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 className="inline-flex flex-wrap items-center gap-2 rounded-md border border-slate-200 bg-white px-3 py-2 font-mono text-[13px] shadow-sm">
<span className="rounded bg-slate-900 px-2 py-0.5 text-[11px] font-bold tracking-wide text-white">
{method}
</span>
<span className="break-all text-slate-800">{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>
<code className="rounded border border-slate-200 bg-slate-100 px-1.5 py-0.5 font-mono text-[13px] font-medium text-slate-900">
{children}
</code>
);
}

View File

@@ -0,0 +1,31 @@
"use client";
import Link from "next/link";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
type DocsAdminConsoleLinkProps = {
namespace: "adminDocs" | "integrationDocs";
className?: string;
};
export function DocsAdminConsoleLink({
namespace,
className,
}: DocsAdminConsoleLinkProps): React.ReactElement {
const { t } = useTranslation(namespace);
const label = t("shell.adminLogin");
return (
<Link
href="/admin"
className={cn(
"inline-flex h-9 shrink-0 items-center rounded-lg border border-slate-200 bg-white px-3 text-sm font-medium text-slate-800 shadow-sm transition-colors hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400",
className,
)}
>
{label}
</Link>
);
}

View File

@@ -1,9 +1,12 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useTranslation } from "react-i18next";
import { AdminLanguageSwitcher } from "@/components/admin/admin-language-switcher";
import { DocsAdminConsoleLink } from "@/components/docs/docs-admin-console-link";
import { DocsTopNav } from "@/components/docs/docs-top-nav";
import { cn } from "@/lib/utils";
type DocsShellProps = {
@@ -12,23 +15,28 @@ type DocsShellProps = {
};
export function DocsShell({ children, className }: DocsShellProps): React.ReactElement {
const { t } = useTranslation("integrationDocs");
const pathname = usePathname();
const isAdminDocs = pathname === "/docs/admin" || pathname.startsWith("/docs/admin/");
const namespace = isAdminDocs ? "adminDocs" : "integrationDocs";
const homeHref = isAdminDocs ? "/docs/admin" : "/docs/integration";
const { t } = useTranslation(namespace);
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 />
<div className={cn("docs-site min-h-dvh bg-[#f4f6f9] text-slate-900", className)}>
<header className="sticky top-0 z-40 border-b border-slate-200 bg-white/95 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-white/90">
<div className="mx-auto flex h-[3.25rem] max-w-6xl items-center justify-between gap-3 px-4 sm:px-6 lg:gap-4 lg:px-8">
<div className="flex min-w-0 items-center gap-4">
<Link
href="/admin/login"
className="text-xs text-muted-foreground transition-colors hover:text-foreground"
href={homeHref}
className="truncate text-[15px] font-semibold tracking-tight text-slate-900"
>
{t("shell.admin")}
{t("shell.title")}
</Link>
<DocsTopNav />
</div>
<div className="flex shrink-0 items-center gap-2">
<AdminLanguageSwitcher />
<DocsAdminConsoleLink namespace={namespace} />
</div>
</div>
</header>
@@ -45,9 +53,11 @@ export function DocsBody({
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">
<div className="mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 py-8 sm:px-6 lg:flex-row lg:items-start lg:gap-10 lg:px-8 lg:py-10 lg:[--docs-sticky-top:calc(3.25rem+2.5rem)]">
{sidebar}
<main className="min-w-0 flex-1 pb-12">{children}</main>
<main className="min-w-0 flex-1 rounded-xl border border-slate-200/80 bg-white p-6 shadow-sm sm:p-8 lg:pb-12">
{children}
</main>
</div>
);
}

View File

@@ -8,43 +8,49 @@ 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 {
export function DocsSidebar({
groups,
namespace = "integrationDocs",
}: {
groups: readonly DocsNavGroup[];
namespace?: "integrationDocs" | "adminDocs";
}): React.ReactElement {
const pathname = usePathname();
const { t, i18n } = useTranslation("integrationDocs");
const { t, i18n } = useTranslation(namespace);
const homeHref = namespace === "adminDocs" ? "/docs/admin" : "/docs/integration";
const label = (key: string): string => {
const translated = t(key);
if (translated !== key) {
return translated;
}
return resolveDocsNavLabel(key, i18n.resolvedLanguage ?? i18n.language);
return resolveDocsNavLabel(key, i18n.resolvedLanguage ?? i18n.language, namespace);
};
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">
<aside className="w-full shrink-0 lg:sticky lg:top-[var(--docs-sticky-top)] lg:z-30 lg:w-52 lg:self-start">
<div className="rounded-xl border border-slate-200/80 bg-white p-3 shadow-sm lg:max-h-[calc(100dvh-var(--docs-sticky-top))] lg:overflow-y-auto lg:overscroll-y-contain">
<nav className="space-y-5">
{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">
<div className="mb-2 px-2 text-xs font-semibold text-slate-500">{label(group.titleKey)}</div>
<ul className="space-y-0.5">
{group.items.map((item) => {
const active =
pathname === item.href ||
(item.href !== "/docs/integration" && pathname.startsWith(item.href)) ||
(item.href === "/docs/integration" && pathname === "/docs/integration");
(item.href !== homeHref && pathname.startsWith(item.href)) ||
(item.href === homeHref && pathname === homeHref);
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",
"block rounded-md px-2.5 py-2 text-[14px] leading-5 transition-colors",
active
? "bg-primary/10 font-medium text-foreground"
: "text-muted-foreground hover:bg-muted/40 hover:text-foreground",
? "bg-slate-900 font-medium text-white shadow-sm"
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900",
)}
>
{label(item.titleKey)}

View File

@@ -0,0 +1,54 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
type DocSite = {
href: string;
prefix: string;
titleKey: string;
namespace: "adminDocs" | "integrationDocs";
};
const DOC_SITES: readonly DocSite[] = [
{ href: "/docs/admin", prefix: "/docs/admin", titleKey: "shell.title", namespace: "adminDocs" },
{
href: "/docs/integration",
prefix: "/docs/integration",
titleKey: "shell.title",
namespace: "integrationDocs",
},
];
function DocsTopNavLink({ site }: { site: DocSite }): React.ReactElement {
const pathname = usePathname();
const { t } = useTranslation(site.namespace);
const active = pathname === site.href || pathname.startsWith(`${site.prefix}/`);
return (
<Link
href={site.href}
className={cn(
"rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors",
active
? "bg-white text-slate-900 shadow-sm ring-1 ring-slate-200"
: "text-slate-600 hover:text-slate-900",
)}
>
{t(site.titleKey)}
</Link>
);
}
export function DocsTopNav(): React.ReactElement {
return (
<nav className="hidden items-center gap-1 rounded-lg border border-slate-200 bg-slate-100/80 p-1 sm:flex">
{DOC_SITES.map((site) => (
<DocsTopNavLink key={site.href} site={site} />
))}
</nav>
);
}

View File

@@ -21,6 +21,7 @@ export type ExportLabelKey =
| "riskPools"
| "riskIndex"
| "riskPoolDetail"
| "ticketCombinations"
| "auditLogs"
| "currencies";

View File

@@ -26,6 +26,7 @@ import enReports from "@/i18n/locales/en/reports.json";
import enWallet from "@/i18n/locales/en/wallet.json";
import enAgents from "@/i18n/locales/en/agents.json";
import enSettlementCenter from "@/i18n/locales/en/settlementCenter.json";
import enAdminDocs from "@/i18n/locales/en/adminDocs.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";
@@ -44,6 +45,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 neAdminDocs from "@/i18n/locales/ne/adminDocs.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";
@@ -62,13 +64,14 @@ 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 zhAdminDocs from "@/i18n/locales/zh/adminDocs.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", "integrationDocs"] as const;
const namespaces = ["common", "auth", "dashboard", "audit", "draws", "settlement", "settlementCenter", "risk", "jackpot", "players", "tickets", "reconcile", "reports", "wallet", "adminUsers", "agents", "config", "integrationDocs", "adminDocs"] as const;
const resources = {
en: {
@@ -90,6 +93,7 @@ const resources = {
agents: enAgents,
settlementCenter: enSettlementCenter,
integrationDocs: enIntegrationDocs,
adminDocs: enAdminDocs,
},
ne: {
common: neCommon,
@@ -110,6 +114,7 @@ const resources = {
agents: neAgents,
settlementCenter: neSettlementCenter,
integrationDocs: neIntegrationDocs,
adminDocs: neAdminDocs,
},
zh: {
common: zhCommon,
@@ -130,6 +135,7 @@ const resources = {
agents: zhAgents,
settlementCenter: zhSettlementCenter,
integrationDocs: zhIntegrationDocs,
adminDocs: zhAdminDocs,
},
} satisfies Record<AdminLanguage, Record<(typeof namespaces)[number], Record<string, unknown>>>;

View File

@@ -0,0 +1,636 @@
{
"shell": {
"title": "Admin Operations Guide",
"integrationDocs": "API Integration Docs",
"adminLogin": "Admin Console"
},
"nav": {
"gettingStarted": "Getting started",
"operations": "Core operations",
"management": "Management",
"finance": "Funds & tickets",
"platform": "Platform configuration",
"reference": "Reference",
"overview": "Overview",
"roles": "Roles & permissions",
"siteSetup": "Integration sites",
"draws": "Draws & results",
"settlementCenter": "Settlement center",
"agents": "Agent hierarchy",
"players": "Player management",
"tickets": "Ticket search",
"wallet": "Wallet & reconciliation",
"config": "Rules & risk control",
"reports": "Reports",
"fundOperations": "Fund operations guide",
"manualReview": "Manual review & payouts",
"faq": "FAQ"
},
"headers": {
"role": ["Role", "Primary responsibilities", "Typical access"],
"status": ["Status", "Meaning", "Available actions"],
"module": ["Module", "Description"],
"field": ["Field", "Description", "Example"],
"billStatusTable": ["Bill status", "Meaning", "Next step"],
"faq": ["Issue", "Resolution"],
"report": ["Report", "Purpose"],
"menu": ["Sidebar group", "Menu", "Typical users", "Description"],
"ledger": ["Ledger type", "When it occurs", "Effect on player funds", "Where to view"],
"walletTxn": ["Wallet ledger type", "When it occurs", "Description"],
"compare": ["Stage", "Wallet mode", "Credit mode"],
"reconcile": ["Situation", "Action", "Notes"],
"setting": ["System setting", "Location", "When enabled", "When disabled"],
"batchStatus": ["Batch status", "Meaning", "Available actions"]
},
"pages": {
"overview": {
"title": "Admin operations overview",
"description": "For your organization's super admins, site operators, and bound agent staff. This guide follows the actual admin menu and includes step-by-step instructions. For wallet-mode technical integration, see the API Integration Docs.",
"loginNote": "Admin console: https://lotteryadmin.tanumo.com/admin (use the operations account assigned by your organization or our team)",
"scope": "System capabilities",
"scopeItems": [
"Draw management: schedule generation, close, result review, and payout settlement",
"Players & tickets: search, freeze, view ledgers and ticket history",
"Rule configuration: play switches, odds, caps, jackpot (platform super admin)",
"Settlement center: credit-line period open/close, bill confirmation, payment recording",
"Agent hierarchy: agent tree, share/credit/rebate, downstream operator accounts",
"Wallet & reconciliation: main-site transfer ledger, missing-order handling (wallet mode)",
"Reports: P&L summary, risk occupancy, async export"
],
"menuMap": "Admin menu map",
"menuMapNote": "Visible menus depend on your account role. Bound agent accounts do not see platform finance menus such as Wallet ledger or Reconciliation.",
"menuMapRows": [
["Overview", "Dashboard", "All users", "Home after sign-in; current-period operations summary"],
["Agent organization", "Agent lines", "Super admin", "Provision independent sites and root agents for credit-line customers (wallet mode usually needs Integration sites only)"],
["Agent organization", "Agent list", "Site ops / agents", "Maintain agent tree, share/credit/rebate, downstream accounts"],
["Agent organization", "Settlement center", "Site finance / bound agents", "Credit-line periods, bill confirmation, record payments"],
["Daily operations", "Draw list", "Platform super admin", "Draw schedule, result review, redraw (shared by wallet and credit modes)"],
["Daily operations", "Ticket list", "Ops / support / agents", "Search tickets by player, draw, play type"],
["Daily operations", "Player list", "Site ops / agents", "Create profiles, freeze, view ledgers and tickets"],
["Daily operations", "Settlement", "Platform ops", "Post-draw payout settlement batches (not credit-line periods)"],
["Funds & reports", "Wallet ledger", "Platform / site finance", "Wallet mode: main-site transfers and in-lottery balance changes"],
["Funds & reports", "Reconciliation", "Platform / site finance", "Wallet mode: handle long-pending transfer orders"],
["Funds & reports", "Reports", "Ops / finance / agents", "P&L, player win/loss, risk, export"],
["Platform management", "Play rules / odds / limits / risk / jackpot", "Super admin", "Site-wide play and risk parameters (site operators usually do not see these menus)"],
["Platform management", "Integration sites / Admin users / Role management", "Super admin", "Site secrets, admin accounts, and permission assignment"]
],
"readingOrder": "Suggested reading order",
"readingItems": [
"Roles & permissions → confirm what your account can do",
"Wallet super admin: Integration sites → API Integration Docs → Draws & results → Wallet & reconciliation",
"Credit-line site ops: Agent hierarchy → Player management → Settlement center (open/close/payments)",
"Bound agent operator: Agent list → Player management → Settlement center (payments for own line only)",
"Fund semantics and ledger types → Fund operations guide",
"Draw review and payout batches → Manual review & payouts"
],
"modes": "Two player funding modes",
"modeRows": [
["Wallet mode", "Players enter via your main-site SSO; funds move through your wallet gateway; ops use Wallet ledger and Reconciliation. Technical details are in the API Integration Docs"],
["Credit mode", "Lottery-native accounts or agent-created profiles; bets use credit; wins settle in Settlement center periods; no main-site wallet involved"]
],
"note": "Share settlement bills use the share rate recorded at bet time. Closed historical periods are not recalculated when agent share rates change later."
},
"roles": {
"title": "Roles & permissions",
"description": "The admin console manages access as account → role → feature permissions. Bound agent operator accounts only see data within their agent line. Site admins are bound to a single site and do not get platform technical menus such as draw odds configuration.",
"matrix": "Common roles",
"matrixRows": [
["Super admin", "Highest access across the system: integration sites, draw intervention, agent line provisioning, global configuration", "Built-in super admin; not limited by site roles"],
["Site admin", "Single-site credit-line operations: agent tree, players, settlement, tickets, reports", "Bound to one site; no platform play rules/odds configuration"],
["Bound agent operator", "Manage downstream agents and players under this line; period payments (bills where this account is the payee only)", "Bound to a specific agent node; cannot open or close periods"],
["Risk / finance / support", "View or operate assigned menus by job function", "Assigned by super admin in Role management"]
],
"accountModel": "Account and permission model",
"accountItems": [
"Admin users (Platform management): create admin accounts; bind roles or site/agent scope",
"Role management (Platform management): select menus and actions; accounts do not hold permissions directly",
"Site admins must choose an owning agent when creating players; cannot default to the root agent",
"A bound agent's main account can record settlement payments; payee rules and direct-player/bill-party rules still apply",
"With view-only access, buttons such as record payment, confirm bill, and open/close period are hidden (not shown as errors)"
],
"accountSetup": "Create an operations account (super admin)",
"accountSetupSteps": [
"Sign in → Platform management → Admin users → Create account",
"Enter login name, initial password, and display name",
"Choose binding: ① site role (site admin) ② agent node (agent operator) ③ role only (platform role)",
"Site admin: select the target integration site under Site role",
"Agent operator: select the agent node under Agent binding (data scope = that node and downstream)",
"After saving, ask the user to change the initial password; if expected menus are missing, check Role management for the assigned role"
],
"note": "With no integration site, site-dependent write actions prompt you to create a site (super admin only)."
},
"siteSetup": {
"title": "Integration sites (super admin)",
"description": "Wallet mode requires an integration site first. Save SSO and wallet secrets and have your technical team configure them on your main-site server. Credit-line agent line provisioning also creates a site.",
"path": "Create and configure",
"pathItems": [
"Sign in at https://lotteryadmin.tanumo.com/admin → Platform management → Integration sites",
"Click Create site; enter site code (must match site_code in your JWT), name, and status",
"After creation, copy immediately the one-time SSO secret and wallet API key shown on screen (they cannot be viewed again after closing)",
"Edit the site: enter your wallet gateway URL (HTTPS root, no path), lottery H5 entry, iframe allowlist (one origin per line)",
"Click Connectivity test and confirm your balance query API returns success",
"Give secrets and site code to your technical team for main-site server configuration; field reference and curl examples are in the API Integration Docs"
],
"fieldRows": [
["Site code", "Written into site_code in JWTs your organization issues; must match on both sides", "demo"],
["Wallet gateway URL", "Your HTTPS root URL (no path)", "https://wallet.your-domain.com"],
["Lottery H5 entry", "Redirect or iframe entry URL", "https://front.tanumo.com"],
["Iframe allowlist", "Domains allowed to embed the lottery; one per line", "https://www.your-domain.com"],
["SSO secret", "Shown once at creation; your main site uses it to sign JWTs", "—"],
["Wallet API key", "Shown once at creation; lottery uses it when calling your wallet APIs", "—"]
],
"fields": "Key fields",
"caution": "Important notes",
"cautionItems": [
"Keep site codes, secrets, and domains fully isolated between test and production",
"Production wallet gateway must be a public HTTPS URL",
"Secrets must be configured on your main-site server by your technical team; the admin console does not auto-sync them to your systems",
"Integration and go-live checklist: API Integration Docs → Delivery"
],
"apiLinkNote": "Wallet gateway fields, curl self-tests, and iframe protocol are in",
"apiLinkLabel": "API Integration Docs → Setup"
},
"draws": {
"title": "Draws & results",
"description": "A draw is the basic betting unit. List times are shown in local timezone; the server stores UTC. Whether the hall accepts bets is determined live; the list shows database status, which may differ slightly from the player-side countdown.",
"lifecycle": "Draw statuses",
"statusRows": [
["Not started", "Before start time", "Edit, delete (no bets)"],
["Open", "Accepting bets", "Edit some times, cancel (no bets)"],
["Closing", "No longer accepting new bets", "Wait for draw"],
["Pending draw / drawing", "Waiting for or generating result", "Proceed to review and publish"],
["Pending review", "Cooling period for verification", "Approve, redraw/rollback"],
["Settling", "Calculating wins and payouts", "—"],
["Settled", "All tickets for this draw processed", "View finance and risk"],
["Cancelled", "Cancelled by admin", "Record kept; placed bets refunded"]
],
"workflow": "Daily workflow (platform super admin)",
"workflowItems": [
"Daily operations → Draw list → Batch generate draw plan: auto-create upcoming draws on configured intervals",
"Manually create a draw or edit times for not-started/open draws when needed",
"After close time the system closes automatically; draw flow: generate result → cooling review → publish → auto settlement",
"Draw detail page shows: finance summary, number occupancy, result batches, related tickets"
],
"publishWalkthrough": "Publish draw results step by step",
"publishSteps": [
"Draw list → open the target draw",
"Confirm status is Pending draw or Pending review",
"If no result yet: click Generate draw result",
"During cooling period verify numbers → click Approve and publish",
"After publish the system moves to Settling → Settled; wallet-mode players receive instant payouts, credit-mode players get credit release recorded in period ledger",
"If issues are found after publish: super admin may Redraw during cooling (see next section)"
],
"reopenWalkthrough": "Redraw and rollback (super admin only, during cooling)",
"reopenSteps": [
"Draw detail → Redraw/rollback",
"System rolls back payouts/credit releases → regenerate result → review and publish again → settle again",
"Actions are written to audit log; confirm with your finance team before proceeding"
],
"rules": "Rules & risk (related modules, super admin)",
"rulesItems": [
"Platform management → Play rules: play switches, min/max stake",
"Platform management → Odds & rebate: affects new tickets only",
"Platform management → Limit versions: per-number caps; sold-out numbers block betting",
"Platform management → Risk center: view occupancy and payout pools by number; occupancy ledger defaults to ticket aggregation"
],
"note": "Times cannot be changed after close. Cancel a draw only when open/closing with no bets.",
"manualReviewLinkNote": "Draw manual review, cooling period, and payout settlement batches are covered in",
"manualReviewLinkLabel": "Manual review & payouts"
},
"settlementCenter": {
"title": "Settlement center (credit line)",
"description": "Manage agent settlement periods: open → close and generate bills → confirm → record payments. Bound agents can only view and operate bills for their own line. Wallet-mode players are not part of this module.",
"entry": "Entry & permissions",
"entryItems": [
"Agent organization → Settlement center: period list",
"Open / close period: site finance accounts not bound to an agent only (usually your finance role)",
"Bound agents: cannot open or close periods; can only handle bills where they are the payee",
"Settlement write permission is required to show record payment, confirm bill, and similar actions; with view-only access the action area is hidden"
],
"periodFlow": "Period lifecycle",
"periodItems": [
"Open period: create an in-progress period; start accumulating share ledger and player credit moves",
"During period: player bets use credit, win releases, rebates, etc. are written to ledger",
"Close period: aggregate period data into player bills and agent bills; irreversible",
"Related draws for the period must be settled before close; warnings appear if unsettled tickets remain",
"After close: agents/finance confirm bills → record actual payments → bills reach settled status"
],
"openWalkthrough": "Open a period (site finance)",
"openSteps": [
"Settlement center → period list → Open period",
"Enter period name and start/end dates (per your agreement with agents)",
"Confirm no other in-progress period conflicts → submit",
"After open, new bets and credit releases count toward this period"
],
"closeWalkthrough": "Close a period (site finance)",
"closeSteps": [
"Confirm all draws for this period are Settled",
"Settlement center → select in-progress period → Close period",
"System aggregates share ledger into player bills (direct players) and agent bills (share at each level)",
"After close, bill status is Pending confirm; historical share uses bet-time snapshot, not current agent profile"
],
"paymentWalkthrough": "Confirm and record payments",
"paymentSteps": [
"Settlement center → period detail → Bills tab → open target bill",
"Payee account clicks Confirm bill (status: Pending confirm → Confirmed)",
"Click Record payment; enter actual received/paid amount, method, and notes",
"Multiple payments allowed until Settled; partial payment shows Partial paid",
"Bad debt write-off and top-up reversal: site finance only (not bound to an agent)"
],
"detailTabs": "Three tabs on period detail",
"detailTabItems": [
"Bills: player bills and agent bills; open a bill to confirm or record payment",
"Payments & adjustments: operation log for payments, bad debt, top-ups, etc. (by operation record)",
"Credit ledger: player credit limit moves (bet hold, win release, period payment entries, etc.)"
],
"billStatusSection": "Bill statuses",
"billStatusRows": [
["Pending confirm", "After close; awaiting agent or finance confirmation", "Confirm bill"],
["Confirmed", "Amount confirmed", "Record payment"],
["Partial paid", "Outstanding balance remains", "Continue payment"],
["Overdue", "Past due and not fully paid", "Collect or bad debt handling"],
["Settled", "Payments complete", "Archive for reference"]
],
"operations": "Permissions and scope notes",
"operationItems": [
"Record payment: payee only",
"Bound agents: player bills include direct players only; agent bills include bills where this node is bill party or counterparty",
"Bills have no currency_code field; display currency uses player default_currency"
],
"note": "Closed share bills settle using bet-time snapshot; history is not recalculated from current agent profiles.",
"fundOpsLinkNote": "For credit ledger types and how they differ from wallet mode, see",
"fundOpsLinkLabel": "Fund operations guide"
},
"agents": {
"title": "Agent hierarchy",
"description": "The agent layer controls which data you can see and credit limits for downstream players; menu permissions still come from roles. Credit-line sites must maintain the agent tree before creating players under it.",
"structure": "Organization",
"structureItems": [
"Super admin: Agent organization → Agent lines → Provision line: create independent site + root node for external agents",
"Site ops: Agent organization → Agent list: maintain agent tree under existing site",
"Select an agent node to edit profile: share, credit, rebate, delegation permissions",
"Root agent profile editable by super admin only; downstream maintained by parent agent or site admin",
"Agent operator admin accounts can be created (bind agent node in Admin users)"
],
"provisionWalkthrough": "Provision credit-line agent line (super admin)",
"provisionSteps": [
"Agent organization → Agent lines → Provision line",
"Enter site info, root agent name and code",
"Set initial share, credit, and rebate for root agent",
"Submit to auto-create integration site and root agent; record returned site code",
"Create site admin or root agent operator account for the partner (see Roles & permissions)"
],
"dailyWalkthrough": "Daily agent maintenance (site ops)",
"dailySteps": [
"Agent list → select parent node → Create downstream agent",
"Enter name, code, share/credit/rebate; whether further delegation is allowed",
"After save the node appears in the tree; continue creating downstream agents or players",
"Share/credit changes affect new tickets and future periods only; closed history unchanged"
],
"profile": "Agent profile fields",
"profileRows": [
["Share rate", "This level's share of downstream turnover", "After close, counted in this level's share profit (win/loss)"],
["Credit line", "Credit ceiling for downstream players", "Total bet holds for downstream players must stay within chain credit rules"],
["Rebate rate", "Rebate layered on play configuration", "Combined with platform Odds & rebate settings"],
["Delegation", "Whether creating downstream agents/players is allowed", "Site admins are not limited by this switch; they follow their own role permissions"]
],
"siteAdmin": "Site admin notes",
"siteAdminItems": [
"Site admins are not blocked by delegation switches on the selected agent profile; they follow their own role permissions",
"When creating players under an agent you must choose the owning agent (cannot default to root)",
"Open/close period and bad debt write-off require a site finance account not bound to an agent"
],
"note": "Agent profile changes affect new tickets and future periods only; closed history stays unchanged."
},
"players": {
"title": "Player management",
"description": "Manage wallet-mode and credit-mode players in one place. List and detail views show different balances and ledger tabs by funding mode.",
"list": "Player list",
"listItems": [
"Daily operations → Player list",
"Search: username, nickname, main-site player ID, owning agent",
"Funding mode column distinguishes wallet mode vs credit mode",
"Balance column: credit mode shows credit limit (major); wallet mode shows in-lottery balance (minor)"
],
"createWalkthrough": "Create credit-mode player (site ops / agent)",
"createSteps": [
"Player list → Create player",
"Owning agent is required (site admins cannot skip)",
"Enter login name, password, nickname, default currency",
"Set initial credit limit (credit mode)",
"After save the player can sign in with lottery-native credentials; wallet-mode players are usually auto-created on first valid main-site SSO entry"
],
"freezeWalkthrough": "Freeze / unfreeze",
"freezeSteps": [
"Player list or player detail → Freeze",
"Frozen players cannot bet; existing tickets settle per rules",
"Unfreeze restores normal betting"
],
"modes": "Funding mode differences",
"modeRows": [
["Wallet mode", "Enter via your main-site SSO; first valid JWT auto-creates profile. Detail tabs: wallet ledger, transfer orders"],
["Credit mode", "Lottery-native account or created in agent admin. Detail tab: credit ledger; wins settle in agent periods"]
],
"detail": "Player detail page",
"detailItems": [
"Credit ledger: bet hold, win release, period payment entries, etc.",
"Wallet ledger / transfer orders: wallet mode only; transfer orders map to main-site transfer records",
"Ticket history: jump to ticket detail (including combo play breakdown)",
"Adjust credit: requires player/agent management write permission"
],
"note": "Wallet-mode usernames are generated by the lottery (e.g. nlotto******); nicknames are not synced from your main site."
},
"tickets": {
"title": "Ticket search",
"description": "Search player tickets across dimensions for operations, support, and agent reconciliation. Bound agents only see tickets for players in their agent subtree.",
"entry": "Entry",
"entryItems": [
"Daily operations → Ticket list",
"Or from player detail → Ticket history for a single player"
],
"filter": "Common filters",
"filterItems": [
"Draw, player, play type, ticket number, time range",
"Status: pending draw / won / lost / cancelled, etc.",
"Agent scope: super admin can filter by site; site ops see own site; bound agents see own subtree"
],
"detail": "Ticket detail",
"detailItems": [
"Shows bet content, odds, amount, share snapshot (credit mode)",
"Combo plays (e.g. combination bets) show combo breakdown on detail page",
"Related links: player detail, draw detail",
"Excel export supported (export permission required)"
],
"note": "Ticket amounts are snapshotted at bet time; status updates automatically after draw; no manual ticket edits."
},
"wallet": {
"title": "Wallet & reconciliation (wallet mode)",
"description": "Applies to wallet-mode players only. Shows transfer records between your main site and the lottery. Bound agent accounts usually do not see this menu.",
"walletSection": "Wallet ledger",
"walletItems": [
"Funds & reports → Wallet ledger",
"Filter by player, type, time: transfer in, transfer out, bet, payout, etc.",
"Amounts are minor integers; consistent with wallet gateway definitions in API Integration Docs",
"Export filtered results (permission required)"
],
"transferSection": "Transfer orders",
"transferItems": [
"Wallet module includes Transfer orders list (main site ↔ lottery)",
"Status: success / failed / pending",
"Long-pending orders can be handled in Reconciliation"
],
"reconcileSection": "Reconciliation (missing orders)",
"reconcileSteps": [
"Funds & reports → Reconciliation",
"List shows long-pending or abnormal transfer orders",
"Open an order → verify actual debit/credit result with your main site",
"Per verification: post entry, reverse, or close; actions are audited",
"Technical team should also check your wallet gateway logs and API Integration Docs → Troubleshooting"
],
"note": "Credit-mode players do not use wallet ledger or reconciliation; their fund moves are in Settlement center credit ledger.",
"fundOpsLinkNote": "For wallet vs credit fund lifecycle comparison, see",
"fundOpsLinkLabel": "Fund operations guide"
},
"config": {
"title": "Rules & risk control (platform super admin)",
"description": "Site-wide play, odds, limits, and risk parameters. Usually visible only to platform super admins or your organization's super admins with platform access; site ops and agents typically do not see these menus.",
"plays": "Play rules",
"playsItems": [
"Platform management → Play rules",
"Switch plays on/off: disabled plays cannot be bet on player side",
"Set min/max stake per bet"
],
"odds": "Odds & rebate",
"oddsItems": [
"Platform management → Odds & rebate",
"Adjust odds and base rebate; affects new tickets only",
"Combined with rebate rate in agent profiles"
],
"riskCap": "Limit versions",
"riskCapItems": [
"Platform management → Limit versions",
"Set per-number caps; number is sold out when cap is reached",
"New version applies to future draws only; open draws use the version active at open"
],
"risk": "Risk center",
"riskItems": [
"Platform management → Risk center",
"View number occupancy, payout pools, high-risk numbers",
"Occupancy ledger defaults to ticket aggregation; only numbers with occupancy or high risk are shown",
"Combo play breakdown is on ticket detail, not expanded in risk list"
],
"jackpot": "Jackpot",
"jackpotItems": [
"Platform management → Jackpot",
"Maintain jackpot amount and payout rules (when jackpot play is enabled)"
],
"note": "If your site operator account does not see these menus, that is normal permission isolation; contact our team or your super admin to adjust play settings."
},
"reports": {
"title": "Reports",
"description": "View P&L and risk by draw, player, play type, and more; async export supported. Data scope narrows automatically by account role.",
"entry": "Entry & filters",
"entryItems": [
"Funds & reports → Reports",
"Choose report type first, then set time, site, agent, draw, and other filters",
"Bound agent reports include own agent subtree only"
],
"types": "Main reports",
"reportRows": [
["Per-draw P&L", "Stake, payout, and profit summary for one draw"],
["Daily summary", "Aggregated by business date"],
["Player win/loss", "Ranking and detail by player"],
["Play dimension", "Stake and payout structure by play type"],
["Hot number risk", "Bet concentration and payout pool occupancy by number"],
["Rebate commission", "Rebate and commission accrual"],
["Audit log", "Trace key admin actions (super admin)"]
],
"export": "Export",
"exportItems": [
"Report export permission required for async export; download when complete",
"Ticket list and wallet ledger also support Excel export",
"Export jobs run asynchronously; narrow time range for large data sets"
],
"scope": "Data scope",
"scopeItems": [
"Super admin: all sites or filter by site",
"Site admin: own site",
"Bound agent: own agent subtree"
]
},
"fundOperations": {
"title": "Fund operations guide",
"description": "Explains how funds move during betting, draw settlement, and payment in wallet mode vs credit mode. Confirm the player's funding mode first, then check the matching ledger.",
"twoSystems": "Two parallel systems (do not confuse them)",
"twoSystemsItems": [
"Per-draw settlement (Daily operations → Settlement): calculates win/loss for the draw; wallet mode pays out here, credit mode records release/period ledger here",
"Credit-line period settlement (Agent organization → Settlement center): credit mode only; aggregates share by period and generates bills for agent/player payments",
"Main-site wallet transfers (Funds & reports → Wallet): wallet mode only; transfer in/out between main site and lottery"
],
"creditModel": "Credit mode: credit limit model",
"creditModelItems": [
"Credit limit (credit_limit): ceiling set by agent or ops for the player",
"Used credit (used_credit): total of bet holds plus settled losses",
"Available credit = credit limit used credit frozen amount; betting blocked when available is insufficient",
"Balance shown in player list/detail for credit mode is credit-related, not cash balance"
],
"creditLifecycle": "Credit mode: fund lifecycle of one ticket",
"creditLifecycleSteps": [
"① Bet placed: credit hold (ledger shows Bet hold). This locks available credit, not final deduction",
"② Pending draw: hold remains; ledger shows hold only, no duplicate settlement charge",
"③ After draw settlement: system releases the hold, then posts Draw settlement — loss increases used credit; win releases credit (available rises), no instant cash",
"④ During period: win/loss also written to share ledger for period close and bills",
"⑤ After period close: confirm bills and record payments in Settlement center; player may see Period settlement confirm or Settlement payment entry ledger types",
"⑥ Players with overdue unpaid bills: betting blocked until period payment is completed"
],
"creditLedger": "Credit mode: credit ledger type reference",
"creditLedgerRows": [
["Bet hold", "Player bet succeeds", "Available credit down; used credit up", "Settlement center → Credit ledger; player detail → Credit ledger"],
["Draw settlement", "Draw payout settlement complete", "Release hold and adjust used credit by win/loss; win = release", "Same; pending draw shows hold only; one settlement line per ticket after draw"],
["Period settlement confirm", "Period bill payment recorded (some cases)", "Used credit decreases", "Settlement center → Credit ledger"],
["Settlement payment entry", "Period payment bookkeeping", "Bookkeeping only; available credit unchanged", "Settlement center → Credit ledger"],
["Top-up / reversal / bad debt", "Site finance adjustment", "Adjust bill and ledger per adjustment type", "Settlement center → Payments & adjustments"]
],
"creditBill": "Credit mode: what bills and payments mean",
"creditBillItems": [
"Player bill after close = net receivable/payable for that player's win/loss in the period",
"Agent bill = share receivable/payable at each agent level",
"Positive net: bill party pays payee; negative net: payee pays bill party (admin shows payer/payee)",
"Confirm bill: both sides agree on amount; record payment: log actual funds, multiple entries until settled",
"Bad debt write-off: confirm unrecoverable debt; site finance only (not bound to agent)"
],
"creditAdjust": "Credit mode: manual credit limit adjustment",
"creditAdjustSteps": [
"Player detail → Adjust credit (requires player/agent management write permission)",
"Change credit limit ceiling; does not erase historical used credit",
"If new limit is below current used credit, player cannot bet until used credit drops",
"Period payments, bad debt, top-ups belong in Settlement center; do not duplicate manual entries on player detail"
],
"walletLifecycle": "Wallet mode: fund lifecycle",
"walletLifecycleSteps": [
"① Player enters lottery H5 from your main site (SSO + JWT)",
"② Transfer in: player initiates in H5 → lottery calls your wallet debit → in-lottery balance up (transfer order + wallet ledger)",
"③ Bet: deducted from in-lottery balance (wallet ledger type Bet debit)",
"④ Draw settlement: win amount to in-lottery balance (Payout credit); credited after Daily operations → Settlement batch review/payout",
"⑤ Transfer out (optional): lottery calls your wallet credit → in-lottery balance down",
"⑥ Exception: long-pending transfer orders handled manually in Reconciliation"
],
"walletTxn": "Wallet mode: wallet ledger types",
"walletTxnRows": [
["Main-site transfer in", "Player transfers from main site to lottery", "In-lottery balance increases"],
["Main-site transfer out", "Player transfers from lottery to main site", "In-lottery balance decreases"],
["Bet debit", "Bet succeeds", "Balance decreases"],
["Payout credit", "Draw payout settlement complete", "Balance increases (when won)"],
["Bet reversal / transfer-out failure refund", "Ticket cancelled or transfer-out rollback", "Balance restored"]
],
"walletReconcile": "Wallet mode: transfer order exceptions",
"walletReconcileRows": [
["Pending reconciliation", "Main site and lottery status mismatch or timeout", "Verify your gateway logs first, then choose action below"],
["Complete missing credit", "Main site debited but lottery not credited (transfer in)", "Post transfer in on lottery side and mark success"],
["Reversal", "Need to undo lottery-side entry", "Reverse lottery wallet balance"],
["Mark closed", "Resolved outside the system", "Change order status only, no balance move; not for pending transfer-out reconciliation"]
],
"compare": "Same stage: wallet mode vs credit mode",
"compareRows": [
["Entry", "Main-site SSO; first JWT auto-creates profile", "Lottery account or agent admin creates profile"],
["Must have funds to bet", "In-lottery balance ≥ stake", "Available credit ≥ stake"],
["When betting", "Balance deducted immediately", "Credit hold (freeze), not final loss"],
["After win", "Payout to lottery balance", "Credit release; cash settled in period bills"],
["Daily reconciliation", "Wallet ledger + transfer orders", "Settlement center credit ledger + bills"],
["Agent payments", "Not applicable", "Settlement center period bills"]
],
"note": "Credit-mode player UI uses release and period payment wording, not wallet-mode payout wording. The two modes cannot share one account."
},
"manualReview": {
"title": "Manual review & payouts",
"description": "Covers draw result review, cooling period, per-draw payout settlement batches, and related system switches. This is separate from Settlement center credit-line periods.",
"distinction": "Difference from credit-line period settlement",
"distinctionItems": [
"This guide: per-draw payout settlement batches (Daily operations → Settlement) — wallet mode pays out, credit mode posts draw ledger; runs every draw",
"Settlement center: credit-line period open/close/bill payments — aggregated by week/month etc.; not one-to-one with each draw",
"Approve and publish on draw detail: whether draw numbers take effect; settlement batch: how funds move after numbers are effective"
],
"drawReview": "Manual draw result review",
"drawReviewItems": [
"When System settings → Require manual review of draw results is on: after RNG generates numbers the draw is Pending review until someone clicks Approve and publish",
"When off: RNG results auto-publish (cooling period may still apply, see below)",
"Manual number entry: after submit, pending review or auto-publish depending on review switch",
"Entry: Daily operations → Draw list → draw detail → draw batch"
],
"drawPublishSteps": "Publish draw results (step by step)",
"drawPublishStepItems": [
"Open draw detail; confirm status is Pending draw or Pending review",
"If no result: click Generate draw result or enter numbers",
"Verify numbers match draw and play rules",
"Click Approve and publish",
"After publish, enters cooling period or Settling directly (see system settings)"
],
"cooldown": "Cooling period",
"cooldownItems": [
"System settings → Cooling period (minutes): wait after publish before payout settlement runs",
"During cooling, super admin may Redraw: rollback payouts/releases → regenerate → review and publish again",
"Set to 0 minutes: settlement runs immediately after publish, no cooling window",
"Cooling is the last window to verify numbers; unrelated to credit-line periods"
],
"settlementBatch": "Per-draw payout settlement batch",
"settlementBatchItems": [
"After each draw is published, the system auto (or manually) creates a settlement batch summarizing all ticket win/loss for that draw",
"Entry: Daily operations → Settlement → batch list; also from draw detail",
"Batch status: In progress → Pending review → Reviewed → Paid/Complete (automation may skip steps)",
"Wallet mode: Execute payout writes win amounts to player lottery balance",
"Credit mode: Execute payout writes draw settlement and share ledger; no cash balance increase"
],
"batchStatusSection": "Settlement batch statuses",
"batchStatusRows": [
["In progress", "Calculating win/loss for this draw", "Wait for completion"],
["Pending review", "Amounts calculated; awaiting finance/ops confirmation", "Approve / Reject"],
["Reviewed", "Amounts confirmed; awaiting payout", "Execute payout"],
["Paid", "Payout written to player account or credit ledger", "View detail, export"],
["Rejected", "Review failed; tickets return to pending settlement", "Fix and rerun settlement"],
["Failed", "Settlement process error", "Contact technical support"]
],
"batchWalkthrough": "Manual review and payout (step by step)",
"batchWalkthroughSteps": [
"Daily operations → Settlement → filter Pending review batches",
"Open batch detail; verify draw, ticket count, total stake, total payout, platform P&L",
"If consistent with draw result, click Approve; if not, Reject with notes",
"On Reviewed batch, click Execute payout — wallet balance credited, credit draw ledger created",
"After payout there is no one-click undo; wrong numbers require redraw during cooling on the draw"
],
"settings": "Related system switches (platform super admin)",
"settingRows": [
["Require manual review of draw results", "Platform management → System settings → Draw rhythm & review", "RNG results need manual publish", "RNG results auto-publish"],
["Cooling period duration", "Same as above", "Wait N minutes after publish before settlement", "Settlement immediately after publish"],
["Auto-run settlement", "System settings → Settlement automation", "Settlement batch runs automatically when due", "Settlement must be triggered manually"],
["Auto-approve settlement batch", "Same as above", "Batch auto-moves to Reviewed after settlement", "Manual approve required"],
["Auto payout credit", "Same as above", "Execute payout automatically after review", "Manual Execute payout required"]
],
"settingsNote": "Common production setup: manual draw review + cooling period + auto settlement run + manual batch review + auto or manual payout. Follow your organization's risk policy.",
"rejectNote": "Rejecting a settlement batch returns linked tickets to pending settlement; it does not rollback published draw numbers. If numbers are wrong, use draw redraw.",
"note": "Before closing a credit-line period, ensure all related draws in the period are Settled and payout batches complete; otherwise close will warn about unsettled tickets."
},
"faq": {
"title": "FAQ",
"description": "Typical admin issues and how to handle them.",
"faqRows": [
["Credit ledger confusing", "See Fund operations guide; pending draw shows Bet hold only; after draw one Draw settlement per ticket — no double charge"],
["Settlement batch vs Settlement center", "See Manual review & payouts; former is per-draw payout, latter is credit-line period bills"],
["Wrong draw result", "Draw detail → super admin redraw during cooling → rollback → review and publish again → settle again"],
["Player hall accepts bets but admin shows closed", "List status is DB snapshot; player hall is authoritative; check close_time and timezone"],
["Transfer not received (wallet mode)", "Check wallet ledger status; handle pending in Reconciliation; verify your gateway logs"],
["Integration site connectivity failed", "Confirm your wallet gateway is public HTTPS and implements balance/debit/credit APIs"],
["Close period button unavailable", "Confirm no other in-progress period, all draws for period are settled, account is not bound to an agent"],
["No record payment button", "Need settlement write permission; bill must be confirmed/partial paid/overdue with unpaid_amount > 0; must be payee"],
["Agent cannot see wallet menus", "Bound agent accounts hide wallet/reconciliation; use site finance or super admin account"],
["Cannot choose agent when creating player", "Site admin must choose owning agent; create downstream agent in Agent list first"],
["Insufficient permission", "Contact your super admin or our support to assign menus in Role management"]
],
"integration": "Technical integration",
"integrationItems": [
"SSO, iframe, wallet gateway: see API Integration Docs",
"Error codes 80018005: API Integration Docs → Troubleshooting"
],
"integrationLinkLabel": "View API Integration Docs"
}
}
}

View File

@@ -132,6 +132,8 @@
"validation": {
"shareRange": "Share rate must be between 0 and 100",
"creditInvalid": "Credit limit cannot be negative",
"creditBelowAllocated": "Credit limit cannot be below allocated credit (minimum {{min}})",
"creditExceedsParentWithMax": "Credit limit cannot exceed {{max}}",
"rebateLimitRange": "Rebate ceiling must be between 0 and 100%",
"defaultRebateRange": "Default player rebate must be between 0 and 100%",
"defaultExceedsLimit": "Default player rebate cannot exceed the rebate ceiling"

View File

@@ -74,6 +74,7 @@
"riskPools": { "filename": "risk-pools", "sheetName": "Risk pools" },
"riskIndex": { "filename": "risk-draws", "sheetName": "Risk center" },
"riskPoolDetail": { "filename": "risk-pool-{{number}}", "sheetName": "Risk pool detail" },
"ticketCombinations": { "filename": "ticket-combinations", "sheetName": "Combinations" },
"auditLogs": { "filename": "audit-logs", "sheetName": "Audit logs" },
"currencies": { "filename": "currencies", "sheetName": "Currencies" }
},
@@ -141,8 +142,12 @@
"notifications": "Notifications",
"notificationsComingSoon": "Notifications are under development",
"accountSettings": "Account settings",
"relatedDocs": "Documentation",
"loggedOut": "Signed out"
},
"docs": {
"learnMore": "Read full guide"
},
"nav": {
"home": "Home",
"dashboard": "Dashboard",

View File

@@ -42,6 +42,7 @@
"integrationSites": {
"title": "Integration sites",
"description": "Maintain partner integration settings in admin. site_code cannot be changed after creation.",
"pageGuide": "Wallet-mode partners need an integration site and SSO/wallet secrets; see API docs for technical setup.",
"create": "New site",
"edit": "Edit",
"save": "Save",

View File

@@ -1,6 +1,7 @@
{
"title": "Draws",
"statusListTitle": "Draw list",
"pageGuide": "Manage draw lifecycle: batch plans, close, result review, and settlement. Times cannot change after close; super admins may redraw during cooling.",
"generatePlan": "Generate draw plan",
"generating": "Generating…",
"generateSuccess": "Generated {{created}} draws, buffer {{upcoming}}/{{target}}",

View File

@@ -1,371 +1,467 @@
{
"shell": {
"title": "Integration API",
"admin": "Admin"
"title": "Lottery Integration Docs",
"admin": "Admin Console",
"adminLogin": "Admin Console"
},
"nav": {
"overview": "Overview",
"api": "API",
"ship": "Ship",
"ship": "Release",
"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"
"delivery": "Integration Delivery",
"quickstart": "Quick Start",
"fundamentals": "Fund Model",
"setup": "Integration Setup",
"sso": "Single Sign-On",
"iframe": "iframe Protocol",
"wallet": "Wallet Gateway",
"transfer": "Transfers (Reference)",
"errors": "Error Codes",
"troubleshooting": "Integration Troubleshooting",
"golive": "Go-Live Checklist",
"operations": "Operations Guide",
"adminGuide": "Admin Guide",
"apiReference": "API Integration Reference"
},
"headers": {
"component": ["Component", "Role", "Owner"],
"component": ["Component", "Responsibility", "Owner"],
"convention": ["Item", "Rule"],
"claim": ["Claim", "Type", "Req", "Note"],
"claim": ["Field", "Type", "Required", "Description"],
"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"]
"query": ["Parameter", "Type", ""],
"field": ["Field", "Type", "Description"],
"code": ["Error Code", "Description"],
"http": ["Status Code", "message", "Cause"],
"message": ["Direction", "Message Type", "Payload"],
"balance": ["Field", "Account", "Description"],
"call": ["Direction", "API", "Authentication"],
"sequence": ["Step", "Initiator", "Description"],
"envMap": ["Item", "Admin Integration Site", "Main-Site Config", "Description"],
"account": ["Account", "Password", "site_player_id"],
"contract": ["Scenario", "HTTP", "Response Body"],
"adminField": ["Field", "Description", "Example"],
"handoffTable": ["Item", "Description", "Owner"],
"env": ["Environment", "Example URL", "Description"],
"envelopeTable": ["Direction", "Message Fields", "Description"],
"faq": ["Symptom", "Troubleshooting Direction"]
},
"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",
"title": "Integration Overview",
"description": "For main-site developers and integration engineers. You implement: JWT issuance + wallet gateway; we provide H5 and API.",
"roles": "Responsibility Split",
"flow": "Business Flow",
"e2eSequence": "End-to-End Sequence",
"conventions": "General Conventions",
"readingOrder": "Recommended 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"]
["Main Site (Partner)", "User login; server-side JWT issuance; wallet gateway implementation", "Partner"],
["Lottery API (Us)", "JWT verification, transfers, betting, draw, settlement", "Us"],
["Lottery H5 (Us)", "Player UI; iframe embed or URL redirect entry", "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"
"User logs in on main site → main-site server issues short-lived JWT",
"Enter lottery H5 (iframe embed or URL ?token= redirect)",
"Player taps \"Transfer In\" in H5 → lottery callbacks main site to debit → credits lottery balance",
"Player bets / receives winnings in H5 (using in-lottery balance)",
"(Optional) Player taps \"Transfer Out\" in H5 → lottery callbacks main site to credit"
],
"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"],
["1", "Main Site", "User logs in; server issues JWT (includes site_code, site_player_id)"],
["2", "Main Site", "Embed lottery H5 in iframe, or redirect to lottery_h5_base_url/?token="],
["3", "Lottery H5", "Receives JWT; calls GET /api/v1/player/me to verify and auto-provision"],
["4", "Player", "Taps \"Transfer In\" in H5"],
["5", "Lottery API", "Server callbacks main site POST /wallet/debit-for-lottery"],
["6", "Main-Site Wallet", "Debits main_balance; returns success: true"],
["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"]
["8", "Player", "Bets / awaits winnings in H5"],
["9", "Player", "(Optional) Taps \"Transfer Out\" in H5"],
["10", "Lottery API", "Callbacks main site POST /wallet/credit-from-lottery"]
],
"conventionRows": [
["Amount", "Minor units (integer), e.g. 2000 = 20.00"],
["Amount", "Integer in minor currency units, e.g. 2000 = 20.00 NPR"],
["Encoding", "UTF-8 JSON"],
["Time", "JWT: Unix seconds (iat / exp)"],
["Auth", "Player API: Bearer JWT; gateway: Bearer wallet_api_key"]
["Time", "JWT uses Unix seconds (iat / exp); recommend exp - iat ≤ 300 seconds"],
["Player API Auth", "Authorization: Bearer {JWT} (issued by main site, verified by lottery)"],
["Wallet Gateway Auth", "Authorization: Bearer {wallet_api_key} (sent by lottery on callbacks)"]
],
"readingItems": ["Quickstart → Setup → SSO → iframe protocol → Wallet → Errors → Go-live"]
"readingItems": [
"Integration Delivery — confirm deliverables and environment URLs for both parties",
"Quick Start — complete first integration test step by step",
"Integration Setup — admin site provisioning and key mapping",
"Single Sign-On → iframe Protocol → Wallet Gateway",
"Integration Troubleshooting — common issues",
"Go-Live Checklist — production release checks"
]
},
"delivery": {
"title": "Integration Delivery",
"description": "Before integration testing begins, confirm the following deliverables with sales/technical support. Test and production environments must be fully isolated.",
"handoffScope": "Integration Scope (What You Need to Do)",
"weProvide": "We Provide",
"youProvide": "Partner Must Provide",
"environment": "Environment URLs",
"process": "Typical Integration Process",
"note": "Secrets (sso_jwt_secret, wallet_api_key) are shown only once at creation. Save them securely immediately. Secrets must be stored on the main-site server only — never in frontend or mobile apps. URLs below are Tanumos current defaults; partner-specific deployments follow sales delivery.",
"handoffRows": [
["JWT Issuance", "Main site issues HS256 JWT server-side after login; no \"login-for-token\" API", "Partner"],
["Wallet Gateway", "Implement three HTTPS endpoints: balance / debit / credit", "Partner"],
["iframe or URL Entry", "Embed lottery H5 or redirect with JWT", "Partner"],
["Lottery H5 + API", "Games, transfers, betting, draw", "Us"],
["Integration Site & Keys", "Create site_code and deliver secrets", "Us (Super Admin)"]
],
"provideRows": [
["site_code", "Site code, written into JWT"],
["sso_jwt_secret", "JWT signing secret (held and used by main site to sign)"],
["wallet_api_key", "Bearer secret when lottery callbacks wallet gateway"],
["lottery_h5_base_url", "Lottery H5 entry (iframe src or redirect); default https://front.tanumo.com"],
["lottery_api_base_url", "Lottery API base (curl); default https://lotterylaravel.tanumo.com"]
],
"submitRows": [
["wallet_api_url", "Partner wallet gateway HTTPS root URL (publicly reachable)"],
["iframe_allowed_origins", "Main-site origin allowlist (required for iframe mode, one per line)"],
["Test Accounts", "Several site_player_id values with initial main_balance (for integration testing)"],
["Egress IP (if needed)", "If gateway has IP allowlist, request lottery server egress IP"]
],
"environmentRows": [
["Lottery API", "https://lotterylaravel.tanumo.com", "curl: GET /api/v1/player/me"],
["Lottery H5 entry", "https://front.tanumo.com", "iframe / ?token=; wallet page example /wallet"],
["Integration docs", "https://lotteryadmin.tanumo.com/docs/integration", "This documentation (public)"],
["Admin console", "https://lotteryadmin.tanumo.com/admin", "Super admin; integration sites: Config → Integration sites"],
["Production Environment", "Separate domain and secrets", "site_code, secrets, and domains must not be shared with integration"]
],
"processSteps": [
"Sales enables integration → our super admin creates \"Integration Site\" and delivers secrets and H5 URL",
"Partner implements three wallet endpoints and deploys to public HTTPS (integration may use tunnel first)",
"Partner fills wallet_api_url and iframe_allowed_origins in admin, runs connectivity test",
"Partner implements JWT issuance and iframe postMessage (or URL redirect)",
"Complete integration testing per Quick Start acceptance checklist",
"Production: re-provision site, rotate secrets, full end-to-end retest before 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.",
"title": "Quick Start",
"description": "Assumes Integration Delivery is complete and you have site_code, secrets, and H5 URL. Follow these steps for first integration test.",
"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).",
"steps": "Integration Steps",
"acceptance": "Acceptance Checklist",
"note": "JWT must be issued on the main-site server — never hardcode sso_jwt_secret in frontend. Production wallet_api_url must be public HTTPS.",
"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"
"Received site_code, sso_jwt_secret, wallet_api_key, lottery_h5_base_url",
"Main site implements GET /wallet/balance, POST /wallet/debit-for-lottery, POST /wallet/credit-from-lottery",
"Admin \"Integration Site\" has wallet_api_url and iframe_allowed_origins filled; connectivity test passed",
"At least one test site_player_id prepared with sufficient main_balance"
],
"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"
"Main-site server implements JWT issuance (see Single Sign-On jsonwebtoken example)",
"Self-test with curl: Bearer JWT call GET https://lotterylaravel.tanumo.com/api/v1/player/me, should return code=0",
"Main-site page embeds <iframe src=\"https://front.tanumo.com\">, listens for postMessage",
"After LOTTERY_READY, send MAIN_INIT_TOKEN (token at message top level; see iframe page example)",
"After H5 enters hall, initiate \"Transfer In\" in H5",
"Confirm main site receives POST /wallet/debit-for-lottery and returns success: true",
"Confirm in-lottery balance increases in H5, attempt a bet",
"(Optional) Transfer out in H5, confirm POST /wallet/credit-from-lottery is called back",
"Check off each item in the acceptance checklist below"
],
"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"
"JWT self-test: curl https://lotterylaravel.tanumo.com/api/v1/player/me returns code=0, data.site_player_id correct",
"Wallet self-test: curl POST /wallet/debit-for-lottery returns success:true, main_balance debited correctly",
"Idempotency: replay same idempotent_key, response identical to first call, no double debit",
"iframe: after LOTTERY_READY MAIN_INIT_TOKEN, can enter H5 hall",
"Transfer In: H5 transfer succeeds, main-site gateway logs show debit-for-lottery record",
"Token refresh: after JWT near expiry or LOTTERY_TOKEN_NEEDED triggered, MAIN_REFRESH_TOKEN succeeds"
]
},
"fundamentals": {
"title": "Money model",
"balances": "Two balances",
"calls": "Call directions",
"note": "All amounts use minor integers. Credit-line players are out of scope.",
"title": "Fund Model",
"balances": "Two-Tier Balances",
"calls": "Call Directions",
"note": "All amounts use minor integer units. Credit-line (agent credit) is out of scope for this document; this document covers main-site wallet mode only.",
"balanceRows": [
["main_balance", "Main wallet", "Partner gateway; lottery calls back"],
["lottery balance", "In-lottery balance", "Used for betting after transfer-in"]
["main_balance", "Main-Site Wallet", "Partner implements gateway; lottery server callbacks to debit/credit"],
["lottery balance", "In-Lottery Balance", "Used for betting after transfer-in; displayed and operated in lottery H5"]
],
"callRows": [
["Lottery → main", "balance / debit / credit", "wallet_api_key"],
["Lottery H5 → lottery API", "me / transfers / bets", "Player JWT (not main site)"]
["Lottery → Main Site", "GET balance / POST debit / POST credit", "Bearer wallet_api_key"],
["Lottery H5 → Lottery API", "me / transfers / bets / balance query", "Bearer player JWT (main site not involved)"]
]
},
"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.",
"title": "Integration Setup",
"description": "Our super admin creates an \"Integration Site\" in the admin console. Secrets are shown only once after creation — save them immediately.",
"weProvide": "Provided After Site Creation",
"youProvide": "Partner Must Fill In / Provide",
"defaultPaths": "Default Wallet Gateway Paths",
"envMapping": "Configuration Mapping Table",
"adminSop": "Admin Site Provisioning Steps (Our Super Admin)",
"network": "Network Requirements",
"note": "Test and production site_code, secrets, and domains must be fully isolated. Secrets are written to main-site server config — they are not auto-synced from admin to partner systems.",
"receiveRows": [
["site_code", "Site code"],
["site_code", "Site code, written into JWT"],
["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"]
["lottery_h5_base_url", "Lottery H5 entry URL"]
],
"provideRows": [
["wallet_api_url", "HTTPS wallet base URL"],
["Test accounts", "site_player_id + initial balance"],
["iframe origin", "Parent origin when embedding"]
["wallet_api_url", "Partner wallet gateway HTTPS root URL (no path suffix)"],
["iframe_allowed_origins", "Main-site origin allowlist (iframe mode)"],
["Test Accounts", "site_player_id list + initial balance"]
],
"pathRows": [
["GET", "/wallet/balance", "Balance"],
["POST", "/wallet/debit-for-lottery", "Debit"],
["POST", "/wallet/credit-from-lottery", "Credit"]
["GET", "/wallet/balance", "Balance query"],
["POST", "/wallet/debit-for-lottery", "Debit (on transfer-in)"],
["POST", "/wallet/credit-from-lottery", "Credit (on transfer-out)"]
],
"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"]
["site_code", "code", "MAIN_SITE_CODE", "JWT and player provisioning identifier; must match on both sides"],
["SSO Secret", "sso_jwt_secret", "MAIN_SITE_SSO_JWT_SECRET", "Main site signs JWT; lottery verifies"],
["Wallet Auth", "wallet_api_key", "MAIN_SITE_WALLET_API_KEY", "Lottery sends as Bearer on callbacks to main site"],
["Wallet Root URL", "wallet_api_url", "(Main-site deployment)", "Partner HTTPS root URL; lottery appends /wallet/*"],
["Lottery API", "—", "—", "Default https://lotterylaravel.tanumo.com; player/wallet API root"],
["Lottery H5", "lottery_h5_base_url", "(Main-site iframe src)", "Default https://front.tanumo.com"],
["iframe Allowlist", "iframe_allowed_origins", "(Main-site origin)", "Must match actual main-site origin"]
],
"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"
"Super admin logs into admin console → left sidebar \"Config\"\"Integration Sites\"",
"Click Create: fill site code (site_code), name, default currency",
"Fill wallet_api_url (HTTPS root), lottery_h5_base_url, iframe_allowed_origins",
"After creation, immediately save sso_jwt_secret and wallet_api_key shown on page",
"Securely deliver secrets to partner; partner configures on main-site server",
"Run \"Connectivity Test\" in site list (probes GET /wallet/balance)"
],
"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", "—"]
["code", "Site code, written into JWT site_code", "partner_demo"],
["wallet_api_url", "Partner wallet gateway HTTPS root URL", "https://wallet.partner.com"],
["lottery_h5_base_url", "Lottery H5 entry", "https://front.tanumo.com"],
["iframe_allowed_origins", "Allowed main-site origins for embedding", "https://www.partner.com"],
["sso_jwt_secret", "Shown once at creation", "—"],
["wallet_api_key", "Shown once at creation", "—"]
],
"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"
"Wallet callbacks are initiated by lottery server (not player browser); partner gateway must be reachable from lottery servers",
"Production wallet_api_url must be public HTTPS (localhost / private IP not accepted)",
"Default paths /wallet/balance, /wallet/debit-for-lottery, /wallet/credit-from-lottery (path prefix configurable in admin)",
"Recommend endpoint timeout ≤ 10 seconds; timeout may cause transfer to enter pending reconciliation state"
]
},
"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.",
"title": "Single Sign-On (SSO)",
"description": "HS256 JWT. Main-site server signs; lottery verifies. Entry: URL ?token= or iframe postMessage.",
"claims": "JWT Fields",
"sign": "Signing Example (Node.js)",
"entryA": "Method A: URL Redirect",
"entryB": "Method B: iframe postMessage",
"noExchangeNote": "Lottery does not provide a \"login-for-token\" API. After main-site login, sign JWT yourself; all subsequent player APIs use Authorization: Bearer with the same JWT. First valid JWT call to GET /api/v1/player/me auto-provisions the player — no separate login API required.",
"entryApi": "Verification and Provisioning",
"entryApiNote": "Optional: main-site server may call GET /api/v1/player/me once after login for JWT verification pre-check. Day-to-day business (transfers, betting) is called by lottery H5 with JWT — main site not involved.",
"publicApis": "Public APIs (No Token Required)",
"h5ScopeNote": "Transfers, betting, in-lottery balance queries, etc. are called by our H5 with player JWT — out of main-site integration scope. Main site only needs to: ① issue JWT implement wallet gateway.",
"refreshNote": "iframe token refresh: after LOTTERY_TOKEN_NEEDED or LOTTERY_TOKEN_REFRESH_REQUEST, re-issue JWT and send MAIN_REFRESH_TOKEN. See \"iframe Protocol\".",
"authResponse": "Authentication Failure Example",
"errors": "SSO Error Codes",
"iframeNote": "iframe_allowed_origins must be configured. Token is passed via postMessage top-level field token — do not nest inside payload.",
"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"]
["site_code", "string", "Yes", "Integration site code, must match admin"],
["site_player_id", "string", "Yes", "Main-site user ID, stable and unique"],
["iat", "number", "Yes", "Issued at (Unix seconds)"],
["exp", "number", "Yes", "Expires at (Unix seconds); exp - iat ≤ 300"]
],
"messageRows": [
["→ main", "LOTTERY_READY", "Child ready"],
["→ main", "LOTTERY_TOKEN_NEEDED", "Refresh requested"],
["→ lottery", "MAIN_INIT_TOKEN", "{ token }"],
["→ lottery", "MAIN_REFRESH_TOKEN", "{ token }"]
["→ Main Site", "LOTTERY_READY", "Child page ready, requesting token"],
["→ Main Site", "LOTTERY_TOKEN_NEEDED", "Token expired, requesting refresh"],
["→ Lottery", "MAIN_INIT_TOKEN", "Top-level token field"],
["→ Lottery", "MAIN_REFRESH_TOKEN", "Top-level token field"]
],
"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"]
["GET", "/api/v1/integration/runtime-origins", "Allowed iframe embed origin list"]
],
"errorRows": [
["8001", "Missing Authorization"],
["8002", "JWT invalid or expired"],
["8003", "Player not provisioned"],
["8004", "SSO secret not configured"],
["8005", "Account suspended"]
["8001", "Missing Authorization header"],
["8002", "JWT invalid or expired (secret mismatch, exp timeout, signature error)"],
["8003", "Player not provisioned (SSO first me call auto-provisions; this code mostly seen in internal testing)"],
["8004", "SSO secret not configured (site-side issue, contact us)"],
["8005", "Account suspended (site disabled or player frozen)"]
]
},
"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.",
"title": "iframe Protocol",
"description": "postMessage contract when main site embeds lottery H5. Skip this chapter if using URL ?token= redirect only.",
"sequence": "Recommended Sequence",
"envelopeSection": "Message Format (Note Direction Differences)",
"childMessages": "Lottery → Main Site",
"parentMessages": "Main Site Lottery",
"example": "Main-Site Integration Example",
"targetOrigin": "targetOrigin Security",
"envelopeNote": "Common mistake: placing token in payload.token. Lottery H5 reads data.token at the message top level.",
"targetOriginNote": "postMessages second argument must be the lottery H5 origin (default https://front.tanumo.com), never *. Main site must validate event.origin; iframe_allowed_origins lists the main-site origin (not the lottery domain).",
"timingNote": "After MAIN_INIT_TOKEN, lottery child page no longer sends LOTTERY_READY. Refresh: LOTTERY_TOKEN_NEEDED → main site replies MAIN_REFRESH_TOKEN (top-level 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"
"Main site embeds <iframe src=\"{lottery_h5_base_url}\">",
"Lottery H5 validates allowlist then sends LOTTERY_READY",
"Main site listens for message, validates origin, sends MAIN_INIT_TOKEN (top-level token)",
"Lottery H5 stores token, calls GET /api/v1/player/me to enter",
"JWT nearing expiry: lottery sends LOTTERY_TOKEN_NEEDED → main site refreshes and sends MAIN_REFRESH_TOKEN"
],
"envelopeRows": [
["Lottery → Main Site", "type + payload + timestamp", "e.g. LOTTERY_READY, business data in payload"],
["Main Site → Lottery", "type + token + timestamp", "token must be at top level, not nested in payload"]
],
"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"]
["→ Main Site", "LOTTERY_READY", "Child page ready, requesting token"],
["→ Main Site", "LOTTERY_TOKEN_NEEDED", "Token expired, requesting refresh"],
["→ Main Site", "LOTTERY_TOKEN_REFRESH_REQUEST", "Active token refresh request"],
["→ Main Site", "LOTTERY_HEARTBEAT", "Heartbeat (may be ignored)"],
["→ Main Site", "LOTTERY_TOKEN_REFRESHED", "Refresh success notification (child → parent)"]
],
"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"]
["→ Lottery", "MAIN_INIT_TOKEN", "Initial delivery; top-level token field"],
["→ Lottery", "MAIN_REFRESH_TOKEN", "Refresh; top-level token field"],
["→ Lottery", "MAIN_REQUEST_STATUS", "Request child page status"],
["→ Lottery", "MAIN_NAVIGATE", "Navigate to specified path"]
]
},
"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.",
"title": "Wallet Gateway",
"description": "Implemented by partner. Called by lottery server (not player browser). Auth: Authorization: Bearer {wallet_api_key}.",
"balance": "Query Balance",
"debit": "Debit (Transfer In)",
"credit": "Credit (Transfer Out)",
"response": "Response Example",
"httpContract": "HTTP Contract",
"httpErrors": "HTTP Errors",
"creditNote": "Request body same as debit; used for transfer-out or rollback credit on failure.",
"idempotentNote": "idempotent_key: same key + same amount must return first JSON (HTTP 200), no duplicate posting; same key different amount → success: false.",
"queryRows": [
["site_code", "string", ""],
["site_player_id", "string", ""],
["currency_code", "string", ""]
["site_code", "string", "Site code"],
["site_player_id", "string", "Main-site user ID"],
["currency_code", "string", "Currency code"]
],
"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"]
["site_code", "string", "Site code"],
["site_player_id", "string", "Main-site user ID"],
["player_id", "number", "Lottery player ID (reference)"],
["currency_code", "string", "Currency code"],
["amount_minor", "integer", "Positive minor integer"],
["idempotent_key", "string", "Idempotency key, globally unique"]
],
"httpErrorRows": [
["401", "unauthorized", "Invalid API key"],
["422", "invalid request", "Invalid fields/amount"],
["409", "main balance insufficient", "Business rejection; may include data.main_balance"]
["401", "unauthorized", "wallet_api_key incorrect or missing"],
["422", "invalid request", "Missing fields or invalid amount_minor"],
["409", "main balance insufficient", "Insufficient balance or other business rejection"]
],
"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"]
["Debit/credit success", "200", "success: true; includes external_ref_no (recommended) and data.main_balance"],
["Balance query success", "200", "success: true; data.main_balance + currency_code"],
["Invalid parameters", "422", "success: false; message: invalid request"],
["Authentication failure", "401", "success: false; message: unauthorized"],
["Business rejection", "409", "success: false; message explains reason"],
["Idempotent replay", "200", "Identical to first success/rejection 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.",
"title": "Fund Transfers (Reference)",
"description": "This section explains how funds move between both sides — not a partner integration surface.",
"outOfScopeNote": "Partners do not implement APIs in this section. Transfer-in/out is called by our H5 with player JWT to lottery API; main site only implements wallet gateway debit/credit.",
"requestFields": "Request Fields",
"transferIn": "Transfer In (Main Site → Lottery)",
"transferOut": "Transfer Out (Lottery → Main Site)",
"transferResponse": "Success Response",
"errors": "Common Error Codes",
"inNote": "Flow: player H5 initiates transfer-in → lottery calls main site debit-for-lottery → credits in-lottery balance.",
"outNote": "Flow: player H5 initiates transfer-out → debits in-lottery balance → lottery calls main site credit-from-lottery.",
"responseNote": "Transfer-in and transfer-out share the same response structure; direction is in / out. Idempotent replay returns same data.",
"requestFieldRows": [
["amount", "integer", "Positive minor units"],
["amount", "integer", "Positive minor integer"],
["currency", "string", "Optional; defaults to player default_currency"],
["idempotent_key", "string", "Globally unique; retries return same result"]
["idempotent_key", "string", "Globally unique; retries must return same result"]
],
"errorRows": [
["1001", "Insufficient lottery balance (transfer-out)"],
["1009", "Main wallet operation failed"],
["1010", "Idempotency conflict (same key, different amount)"],
["1001", "Insufficient lottery balance (on transfer-out)"],
["1009", "Main-site wallet processing failed (gateway unreachable, 401, timeout, etc.)"],
["1010", "Idempotency key 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.",
"title": "Error Codes",
"sso": "SSO Authentication",
"lotteryWallet": "Lottery Wallet / Transfers",
"gateway": "Partner Wallet Gateway (HTTP)",
"idempotentNote": "Idempotency: same idempotent_key + same amount must return same result; same key different amount → 1010 or success:false.",
"ssoRows": [
["8001", "Missing Authorization"],
["8001", "Missing Authorization header"],
["8002", "JWT invalid or expired"],
["8003", "Player not provisioned"],
["8003", "Player not provisioned (SSO normal flow auto-provisions)"],
["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"]
["1009", "Main-site wallet processing failed"],
["1010", "Idempotency key conflict"],
["2003", "Transfer in before betting"]
],
"gatewayRows": [
["401", "unauthorized", "Invalid API key"],
["422", "invalid request", "Invalid fields/amount"],
["409", "—", "Business rejection"]
["401", "unauthorized", "API Key incorrect"],
["422", "invalid request", "Invalid fields or amount"],
["409", "—", "Business rejection (e.g. insufficient balance)"]
]
},
"troubleshooting": {
"title": "Integration Troubleshooting",
"description": "Match symptoms below for troubleshooting. If unresolved, contact integration technical support with site_code, timestamp, and request ID.",
"faq": "Common Issues",
"jwt": "JWT / Entry",
"iframe": "iframe",
"wallet": "Wallet Gateway",
"note": "During integration, first verify JWT (/player/me) and wallet gateway (/wallet/balance) separately with curl, then test full iframe flow.",
"faqRows": [
["curl /player/me returns 8002", "Check sso_jwt_secret matches admin site; site_code matches; exp not expired (≤300 seconds)"],
["iframe blank screen / won't enter hall", "Check token is at postMessage top level; MAIN_INIT_TOKEN sent after LOTTERY_READY; iframe_allowed_origins includes main-site origin"],
["Transfer-in fails 1009", "Can lottery server reach wallet_api_url; wallet_api_key correct; debit returns 200 + success:true"],
["Connectivity test fails", "wallet_api_url must be public HTTPS; GET /wallet/balance reachable with Bearer wallet_api_key"],
["8005 account suspended", "Check integration site enabled in admin; site_player_id not frozen"]
],
"jwtRows": [
["8002 Token invalid", "Secret mismatch / exp expired / site_code wrong / algorithm not HS256"],
["8004 SSO not configured", "Contact us to confirm site sso_jwt_secret is configured"],
["me returns code=0 but H5 still fails", "iframe token differs from curl test token, or token nested in payload instead of top level"]
],
"iframeRows": [
["Not receiving LOTTERY_READY", "Is iframe src lottery_h5_base_url; has H5 finished loading"],
["postMessage no response", "Is targetOrigin lottery H5 origin (not main-site origin)"],
["Repeated token refresh loop", "After MAIN_INIT_TOKEN, child page should not send LOTTERY_READY again"],
["Refresh fails", "Is main site listening for LOTTERY_TOKEN_NEEDED and replying MAIN_REFRESH_TOKEN"]
],
"walletRows": [
["401 unauthorized", "wallet_api_key does not match admin site"],
["409 insufficient balance", "Test account main_balance insufficient; check amount_minor units"],
["Duplicate debit", "idempotent_key not idempotent; same key must return first response"],
["Lottery not calling debit", "wallet_api_url unreachable or connectivity test not passed"]
]
},
"golive": {
"title": "Go-live",
"checklist": "Checklist",
"title": "Go-Live Checklist",
"description": "Before production release, confirm integration testing passed with account manager and complete the following checks.",
"deliveryChecklist": "Delivery and Process",
"checklist": "Technical Checklist",
"deliveryItems": [
"Production integration site created independently (site_code, secrets isolated from integration)",
"Secrets securely written to main-site production config (not frontend)",
"wallet_api_url is production public HTTPS, connectivity test passed",
"iframe_allowed_origins configured with production main-site origin",
"Full integration test records archived (transfer-in → bet → winnings → transfer-out)"
],
"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"
"JWT issued server-side only, validity ≤ 5 minutes",
"Three wallet endpoints on HTTPS, timeout ≤ 10 seconds, idempotency implemented",
"iframe: postMessage token at top level, origin validation enabled",
"Test vs production: site_code, secrets, domains fully separated",
"Monitoring: wallet gateway 4xx/5xx and debit/credit log alerts",
"Rollback plan: integration site can be temporarily disabled"
]
}
}

View File

@@ -1,5 +1,6 @@
{
"title": "Players",
"pageGuide": "Wallet and credit players in one list; detail tabs adapt to funding_mode.",
"detailTitle": "Player details",
"listTitle": "Player list",
"viewDetail": "View details",

View File

@@ -1,6 +1,7 @@
{
"title": "Reports",
"subtitle": "Centralized operational, finance, risk, and audit reports with unified export filters.",
"pageGuide": "P&L and risk by draw, player, and play type; export permission enables async jobs.",
"exportPanel": "Export setup",
"chooseReport": "Choose a report to export",
"libraryTitle": "Report types",

View File

@@ -17,6 +17,8 @@
"allPoolsPageTitle": "All risk pools",
"sourceReasonOptions": {
"ticket_place": "Bet placement",
"ticket_rollback": "Ticket rollback",
"ticket_failed_line": "Failed bet release",
"admin_manual_close": "Manual close",
"admin_manual_recover": "Manual recover"
},
@@ -25,6 +27,7 @@
"riskFilter": "Risk filter",
"sort": "Sort",
"filterAll": "All",
"filterActive": "Active / high risk",
"filterSoldOut": "Sold out",
"filterHighRisk": ">80%",
"sortUsageDesc": "Usage ratio ↓",
@@ -73,6 +76,15 @@
"playCode": "Play",
"loadLogsFailed": "Failed to load lock logs",
"lockLogsTitle": "Risk lock logs",
"lockLogsGroupedHint": "Grouped by ticket by default. Expanded plays (e.g. 2A) show combination detail on the ticket page.",
"groupBy": "View",
"groupByTicket": "By ticket",
"groupByEntry": "By number",
"combinationCount": "Combinations",
"lockReleaseSummary": "Lock / release rows",
"viewDetail": "Detail",
"viewTicket": "Ticket",
"number": "Bet number",
"drawInfoLoadFailed": "Failed to load draw info",
"loadingDraw": "Loading draw…",
"headerTitle": "Risk · Draw {{drawNo}}",

View File

@@ -1,6 +1,7 @@
{
"title": "Settlement center",
"subtitle": "Period close, bill confirm, and payments",
"pageGuide": "Credit-line agent periods: site finance opens/closes; bound agents view own bills and record payments as payee only.",
"subtitleList": "Period list: open/close periods; summary columns included — use row actions for bills and bet ledger.",
"period": {
"title": "Period",

View File

@@ -47,5 +47,16 @@
"settled_lose": "Settled loss",
"refunded": "Refunded"
},
"allTickets": "All tickets"
"allTickets": "All tickets",
"detailTitle": "Ticket detail",
"detailLoadFailed": "Failed to load ticket detail",
"loadingDetail": "Loading…",
"backToList": "Back to ticket list",
"viewTicketDetail": "View ticket detail",
"combinationCount": "Expanded combinations",
"combinationsTitle": "Combination breakdown",
"combinationsHint": "One bet line may expand into multiple 4D numbers for risk and settlement.",
"number4d": "4D number",
"comboBetAmount": "Combo bet",
"comboEstimatedPayout": "Est. payout reserve"
}

View File

@@ -0,0 +1,636 @@
{
"shell": {
"title": "प्रशासन सञ्चालन मार्गदर्शन",
"integrationDocs": "API एकीकरण कागजात",
"adminLogin": "प्रशासन कन्सोल"
},
"nav": {
"gettingStarted": "सुरुवात",
"operations": "मुख्य सञ्चालन",
"management": "व्यवस्थापन",
"finance": "कोष र टिकट",
"platform": "प्लेटफर्म कन्फिग",
"reference": "सन्दर्भ",
"overview": "अवलोकन",
"roles": "भूमिका र अनुमति",
"siteSetup": "एकीकरण साइट",
"draws": "ड्र र नतिजा",
"settlementCenter": "सेटलमेन्ट केन्द्र",
"agents": "एजेन्ट प्रणाली",
"players": "खेलाडी व्यवस्थापन",
"tickets": "टिकट खोज",
"wallet": "वालेट र मिलान",
"config": "नियम र जोखिम",
"reports": "रिपोर्ट केन्द्र",
"fundOperations": "कोष सञ्चालन विवरण",
"manualReview": "म्यानुअल समीक्षा र पुरस्कार",
"faq": "बारम्बार सोधिने"
},
"headers": {
"role": ["भूमिका", "मुख्य जिम्मेवारी", "विशेषता"],
"status": ["स्थिति", "अर्थ", "कार्य"],
"module": ["मोड्युल", "विवरण"],
"field": ["फिल्ड", "विवरण", "उदाहरण"],
"billStatusTable": ["बिल स्थिति", "अर्थ", "अर्को कदम"],
"faq": ["समस्या", "समाधान"],
"report": ["रिपोर्ट", "उद्देश्य"],
"menu": ["साइडबार समूह", "मेनु", "प्रयोगकर्ता", "विवरण"],
"ledger": ["लेजर प्रकार", "कहिले हुन्छ", "खेलाडी कोषमा प्रभाव", "कहाँ हेर्ने"],
"walletTxn": ["वालेट लेजर प्रकार", "कहिले हुन्छ", "विवरण"],
"compare": ["चरण", "वालेट मोड", "क्रेडिट मोड"],
"reconcile": ["अवस्था", "कार्य", "टिप्पणी"],
"setting": ["प्रणाली सेटिङ", "स्थान", "खुला हुँदा", "बन्द हुँदा"],
"batchStatus": ["ब्याच स्थिति", "अर्थ", "कार्य"]
},
"pages": {
"overview": {
"title": "प्रशासन सञ्चालन अवलोकन",
"description": "तपाईंको सुपर एडमिन, साइट अपरेटर र बाँधिएका एजेन्ट खाताका लागि। यो मार्गदर्शन वास्तविक मेनु अनुसार; वालेट मोड प्राविधिक एकीकरण API कागजातमा छ।",
"loginNote": "प्रशासन कन्सोल: https://lotteryadmin.tanumo.com/admin (तपाईंको वा हाम्रो तोकिएको खाता प्रयोग गर्नुहोस्)",
"scope": "प्रणाली क्षमता",
"scopeItems": [
"ड्र व्यवस्थापन: योजना, बन्द, नतिजा समीक्षा र सेटलमेन्ट",
"खेलाडी र टिकट: खोज, फ्रिज, लेजर र टिकट इतिहास",
"नियम कन्फिग: प्ले स्विच, ओड्स, सीमा, ज्याकपट (प्लेटफर्म सुपर एडमिन)",
"सेटलमेन्ट केन्द्र: क्रेडिट अवधि खोल/बन्द, बिल पुष्टि र भुक्तानी दर्ता",
"एजेन्ट प्रणाली: ट्री, शेयर/क्रेडिट/रिबेट, तल्लो खाता",
"वालेट र मिलान: मुख्य साइट ट्रान्सफर, पेन्डिङ ट्रान्सफर (वालेट मोड)",
"रिपोर्ट: P&L सारांश, जोखिम, असिंक निर्यात"
],
"menuMap": "मेनु नक्सा",
"menuMapNote": "देखिने मेनु भूमिका अनुसार फरक; बाँधिएका एजेन्टले वालेट/मिलान मेनु देख्दैनन्।",
"menuMapRows": [
["अवलोकन", "ड्यासबोर्ड", "सबै", "लगइन पछि होम; हालको सञ्चालन सारांश"],
["एजेन्ट संगठन", "एजेन्ट लाइन", "सुपर एडमिन", "क्रेडिट ग्राहकलाई साइट र मूल एजेन्ट (वालेटमा साइट मात्र)"],
["एजेन्ट संगठन", "एजेन्ट सूची", "साइट / एजेन्ट", "ट्री, शेयर/क्रेडिट/रिबेट, तल्लो खाता"],
["एजेन्ट संगठन", "सेटलमेन्ट केन्द्र", "साइट वित्त / एजेन्ट", "क्रेडिट अवधि, बिल, भुक्तानी"],
["दैनिक सञ्चालन", "ड्र सूची", "प्लेटफर्म सुपर एडमिन", "ड्र योजना, नतिजा समीक्षा, पुन: ड्र"],
["दैनिक सञ्चालन", "टिकट सूची", "अपरेटर / सपोर्ट / एजेन्ट", "खेलाडी, ड्र, प्ले अनुसार खोज"],
["दैनिक सञ्चालन", "खेलाडी सूची", "साइट / एजेन्ट", "सिर्जना, फ्रिज, लेजर र टिकट"],
["दैनिक सञ्चालन", "सेटलमेन्ट", "प्लेटफर्म अपरेटर", "एकल ड्र पछि पुरस्कार सेटलमेन्ट (क्रेडिट अवधि होइन)"],
["कोष र रिपोर्ट", "वालेट लेजर", "प्लेटफर्म / साइट वित्त", "वालेट: मुख्य साइट ट्रान्सफर र ब्यालेन्स"],
["कोष र रिपोर्ट", "मिलान", "प्लेटफर्म / साइट वित्त", "वालेट: लामो पेन्डिङ ट्रान्सफर"],
["कोष र रिपोर्ट", "रिपोर्ट केन्द्र", "अपरेटर / वित्त / एजेन्ट", "P&L, जोखिम, निर्यात"],
["प्लेटफर्म", "प्ले / ओड्स / सीमा / जोखिम / ज्याकपट", "सुपर एडमिन", "साइटव्यापी नियम (साइट अपरेटरमा छैन)"],
["प्लेटफर्म", "साइट / प्रशासक / भूमिका", "सुपर एडमिन", "साइट कुञ्जी, खाता र अनुमति"]
],
"readingOrder": "पढ्ने क्रम",
"readingItems": [
"भूमिका र अनुमति → खाताले के गर्न सक्छ जाँच",
"वालेट सुपर एडमिन: साइट → API कागजात → ड्र → वालेट/मिलान",
"क्रेडिट साइट: एजेन्ट → खेलाडी → सेटलमेन्ट केन्द्र (खोल/बन्द/भुक्तानी)",
"बाँधिएका एजेन्ट: एजेन्ट सूची → खेलाडी → सेटलमेन्ट (आफ्नो लाइन बिल मात्र)",
"कोष अर्थ र लेजर प्रकार → कोष सञ्चालन विवरण",
"ड्र समीक्षा र पुरस्कार ब्याच → म्यानुअल समीक्षा र पुरस्कार"
],
"modes": "दुई खेलाडी मोड",
"modeRows": [
["वालेट मोड", "मुख्य साइट SSO; वालेट गेटवे ट्रान्सफर; वालेट लेजर/मिलान हेर्नुहोस्। विवरण API कागजातमा"],
["क्रेडिट मोड", "लटरी खाता वा एजेन्टले सिर्जना; क्रेडिट कब्जा; सेटलमेन्ट केन्द्रमा अवधि सेटलमेन्ट"]
],
"note": "शेयर बिल भर्ना समयको शेयर दर प्रयोग गर्छ; बन्द अवधि पछि एजेन्ट परिवर्तनले पुनर्गणना गर्दैन।"
},
"roles": {
"title": "भूमिका र अनुमति",
"description": "पहुँच खाता → भूमिका → मेनु अनुमति मार्फत। बाँधिएका एजेन्टले आफ्नो लाइन मात्र; साइट एडमिनमा ड्र/ओड्स जस्ता प्लेटफर्म मेनु छैन।",
"matrix": "सामान्य भूमिका",
"matrixRows": [
["सुपर एडमिन", "उच्चतम पहुँच: साइट, ड्र, एजेन्ट लाइन, वैश्विक कन्फिग", "सिस्टम सुपर एडमिन; साइट भूमिकाले सीमित छैन"],
["साइट एडमिन", "एक साइट क्रेडिट सञ्चालन: एजेन्ट, खेलाडी, सेटलमेन्ट, टिकट, रिपोर्ट", "एक साइटमा बाँधिएको; प्ले/ओड्स कन्फिग छैन"],
["बाँधिएका एजेन्ट", "तल्लो र खेलाडी व्यवस्थापन; भुक्तानी (प्राप्तकर्ता बिल मात्र)", "एजेन्ट नोडमा बाँधिएको; अवधि खोल/बन्द गर्न सक्दैन"],
["जोखिम / वित्त / सपोर्ट", "काम अनुसार मोड्युल हेर्ने वा सञ्चालन", "भूमिका व्यवस्थापनबाट अनुमति तोकिन्छ"]
],
"accountModel": "खाता र अनुमति मोडेल",
"accountItems": [
"प्रशासक सूची (प्लेटफर्म): खाता सिर्जना, भूमिका वा साइट/एजेन्ट बाँध्ने",
"भूमिका व्यवस्थापन (प्लेटफर्म): मेनु र कार्य छान्ने; खातामा सिधै अनुमति होइन",
"साइट एडमिनले खेलाडी सिर्जनामा मालिक एजेन्ट छान्नुपर्छ; मूलमा स्वतः होइन",
"बाँधिएका एजेन्टले सेटलमेन्ट भुक्तानी गर्न सक्छ; प्राप्तकर्ता र सिधा खेलाडी/बिल नियम लागू",
"हेर्ने मात्र अनुमतिमा भुक्तानी, पुष्टि, खोल/बन्द बटन देखिँदैन (त्रुटि होइन)"
],
"accountSetup": "नयाँ अपरेटर खाता (सुपर एडमिन)",
"accountSetupSteps": [
"लगइन → प्लेटफर्म → प्रशासक सूची → नयाँ खाता",
"लगइन नाम, प्रारम्भिक पासवर्ड, प्रदर्शन नाम भर्नुहोस्",
"बाँध्ने तरिका: ① साइट भूमिका (साइट एडमिन) ② एजेन्ट नोड (एजेन्ट खाता) ③ भूमिका मात्र (प्लेटफर्म पद)",
"साइट एडमिन: साइट भूमिकामा लक्ष्य साइट छान्नुहोस्",
"एजेन्ट खाता: एजेन्ट बाँध्नमा नोड छान्नुहोस् (दायरा = नोड र तल्लो)",
"सुरक्षित राख्नुहोस्; मेनु नदेखिए भूमिका व्यवस्थापनमा मेनु जाँच गर्नुहोस्"
],
"note": "साइट नभएको अवस्थामा साइट चाहिने लेखनमा साइट सिर्जना सुझाव (सुपर एडमिन मात्र)।"
},
"siteSetup": {
"title": "एकीकरण साइट (सुपर एडमिन)",
"description": "वालेट मोडमा पहिले साइट सिर्जना, SSO र वालेट कुञ्जी सुरक्षित राखेर तपाईंको टेक टिमले मुख्य साइटमा राख्नुपर्छ। क्रेडिट लाइन खोल्दा साइट पनि बन्छ।",
"path": "सिर्जना र कन्फिग कदम",
"pathItems": [
"https://lotteryadmin.tanumo.com/admin मा लगइन → प्लेटफर्म → एकीकरण साइट",
"नयाँ साइट: साइट कोड (JWT site_code सँग मिल्ने), नाम, स्थिति",
"सिर्जना पछि तुरुन्त एक पटक देखाइने SSO र वालेट API कुञ्जी कपी गर्नुहोस् (पछि हेर्न मिल्दैन)",
"सम्पादन: वालेट गेटवे (HTTPS जडान, बिना path), लटरी H5 प्रवेश, iframe अनुमति (प्रति लाइन origin)",
"जडान परीक्षण: तपाईंको ब्यालेन्स API success फर्काउँछ जाँच",
"कुञ्जी र कोड टेक टिमलाई दिनुहोस्; फिल्ड र curl: API एकीकरण कागजात"
],
"fieldRows": [
["साइट कोड", "JWT site_code; दुवै पक्ष मिल्नुपर्छ", "demo"],
["वालेट गेटवे", "HTTPS जडान (बिना path)", "https://wallet.your-domain.com"],
["लटरी H5 प्रवेश", "रिडाइरेक्ट वा iframe", "https://front.tanumo.com"],
["iframe अनुमति", "अनुमत origin, प्रति लाइन", "https://www.your-domain.com"],
["SSO कुञ्जी", "एक पटक देखाइन्छ; JWT जारी गर्न", "—"],
["वालेट API कुञ्जी", "एक पटक देखाइन्छ; वालेट कलब्याक प्रमाणीकरण", "—"]
],
"fields": "मुख्य फिल्ड",
"caution": "सावधानी",
"cautionItems": [
"परीक्षण र उत्पादन कोड, कुञ्जी, डोमेन पूर्ण अलग",
"उत्पादन वालेट गेटवे सार्वजनिक HTTPS हुनुपर्छ",
"कुञ्जी तपाईंको मुख्य साइट सर्भरमा राख्नुपर्छ; प्रशासनबाट स्वतः सिंक हुँदैन",
"जोड र लाइभ चेकलिस्ट: API कागजात → डेलिभरी"
],
"apiLinkNote": "वालेट फिल्ड, curl र iframe प्रोटोकल:",
"apiLinkLabel": "API एकीकरण कागजात → सेटअप"
},
"draws": {
"title": "ड्र र नतिजा",
"description": "ड्र भर्ना एकाइ हो। समय स्थानीय देखाइन्छ; सर्वर UTC। हल खुला/बन्द रियल-टाइम; सूची DB status देखाउँछ, काउन्टडाउनसँग अलिक फरक हुन सक्छ।",
"lifecycle": "ड्र स्थिति",
"statusRows": [
["सुरु भएको छैन", "सुरु समय अघि", "सम्पादन, मेटाउने (टिकट छैन भने)"],
["खुला", "भर्ना स्वीकार", "समय सम्पादन, रद्द (टिकट छैन भने)"],
["बन्द हुँदै", "नयाँ भर्ना रोकियो", "नतिजा पर्खनुहोस्"],
["नतिजा पर्खँदै / ड्र हुँदै", "नतिजा बनाउँदै", "समीक्षा र प्रकाशन"],
["समीक्षामा", "शीतल अवधि पुनरावलोकन", "अनुमोदन, पुन: ड्र/रोलब्याक"],
["सेटल हुँदै", "जित र पुरस्कार गणना", "—"],
["सेटल भयो", "यो ड्र सम्पन्न", "वित्त र जोखिम हेर्नुहोस्"],
["रद्द", "प्रशासकले रद्द", "अभिलेख रहन्छ; भर्ना फिर्ता"]
],
"workflow": "दैनिक प्रवाह (प्लेटफर्म सुपर एडमिन)",
"workflowItems": [
"दैनिक सञ्चालन → ड्र सूची → ब्याच योजना: भविष्यका ड्र स्वतः",
"आवश्यक पर म्यानुअल ड्र वा खुला/सुरु नभएको ड्र समय सम्पादन",
"बन्द समयपछि स्वतः बन्द; नतिजा → शीतल समीक्षा → प्रकाशन → सेटलमेन्ट",
"विवरण: वित्त सारांश, नम्बर कब्जा, ड्र ब्याच, सम्बन्धित टिकट"
],
"publishWalkthrough": "नतिजा प्रकाशन कदम",
"publishSteps": [
"ड्र सूची → लक्ष्य ड्र विवरण",
"status «नतिजा पर्खँदै» वा «समीक्षामा» पुष्टि",
"नतिजा छैन भने «नतिजा उत्पादन»",
"शीतल अवधिमा नम्बर जाँच → «अनुमोदन र प्रकाशन»",
"प्रकाशन पछि सेटल हुँदै → सेटल भयो; वालेट तुरुन्त, क्रेडिट अवधि लेजर",
"समस्या भए शीतल अवधिमा पुन: ड्र (तल)"
],
"reopenWalkthrough": "पुन: ड्र र रोलब्याक (सुपर एडमिन, शीतल अवधि)",
"reopenSteps": [
"ड्र विवरण → «पुन: ड्र/रोलब्याक»",
"पुरस्कार/क्रेडिट रोलब्याक → नयाँ नतिजा → पुन: समीक्षा → पुन: सेटलमेन्ट",
"अडिट लग लेखिन्छ; वित्तसँग समन्वय गर्नुहोस्"
],
"rules": "नियम र जोखिम (सम्बन्धित, सुपर एडमिन)",
"rulesItems": [
"प्लेटफर्म → प्ले नियम: स्विच, न्यूनतम/अधिकतम भर्ना",
"प्लेटफर्म → ओड्स र रिबेट: नयाँ टिकट मात्र",
"प्लेटफर्म → सीमा संस्करण: नम्बर सीमा; सकिए पछि भर्ना रोक",
"प्लेटफर्म → जोखिम केन्द्र: कब्जा र पूल; टिकट अनुसार समूह"
],
"note": "बन्द पछि समय परिवर्तन हुँदैन। रद्द: खुला/बन्द र टिकट छैन भने मात्र।",
"manualReviewLinkNote": "ड्र म्यानुअल समीक्षा, शीतल अवधि, पुरस्कार ब्याच विवरण:",
"manualReviewLinkLabel": "म्यानुअल समीक्षा र पुरस्कार"
},
"settlementCenter": {
"title": "सेटलमेन्ट केन्द्र (क्रेडिट)",
"description": "एजेन्ट अवधि: खोल्ने → बन्द बिल → पुष्टि → भुक्तानी। बाँधिएका एजेन्टले आफ्नो लाइन बिल मात्र। वालेट खेलाडीमा यो मोड्युल छैन।",
"entry": "प्रवेश र अनुमति",
"entryItems": [
"एजेन्ट संगठन → सेटलमेन्ट केन्द्र: अवधि सूची",
"खोल/बन्द: एजेन्ट नबाँधिएको साइट वित्त खाता (सामान्यतया तपाईंको वित्त)",
"बाँधिएका एजेन्ट: खोल/बन्द गर्न सक्दैन; प्राप्तकर्ता बिल मात्र",
"लेख अनुमति चाहिन्छ भुक्तानी/पुष्टि बटनका लागि; हेर्ने मात्रमा लुक्छ"
],
"periodFlow": "अवधि जीवनचक्र",
"periodItems": [
"खोल्ने: «चलिरहेको» अवधि; शेयर र क्रेडिट लेजर सुरु",
"अवधिमा: भर्ना कब्जा, जित रिलिज, रिबेट लेजरमा",
"बन्द: सारांश → खेलाडी र एजेन्ट बिल; फिर्ता हुँदैन",
"बन्द अघि सम्बन्धित ड्र सेटल पूरा; नसेटल टिकट भए चेतावनी",
"बन्द पछि: पुष्टि → वास्तविक भुक्तानी → निपटान"
],
"openWalkthrough": "अवधि खोल्ने (साइट वित्त)",
"openSteps": [
"सेटलमेन्ट केन्द्र → अवधि सूची → «अवधि खोल्नुहोस्»",
"नाम, सुरु/अन्त्य मिति (एजेन्टसँग मिलाएर)",
"अर्को «चलिरहेको» अवधि छैन जाँच → पेश",
"सफल भए नयाँ भर्ना/रिलिज यो अवधिमा"
],
"closeWalkthrough": "अवधि बन्द (साइट वित्त)",
"closeSteps": [
"यो अवधिका सबै ड्र «सेटल भयो» पुष्टि",
"सेटलमेन्ट केन्द्र → चलिरहेको अवधि → «बन्द»",
"शेयर लेजर सारांश → खेलाडी बिल (सिधा) र एजेन्ट बिल",
"बिल «पुष्टि बाँकी»; शेयर भर्ना समय स्न्यापशट"
],
"paymentWalkthrough": "पुष्टि र भुक्तानी दर्ता",
"paymentSteps": [
"सेटलमेन्ट केन्द्र → अवधि विवरण → बिल ट्याब → लक्ष्य बिल",
"प्राप्तकर्ताले «बिल पुष्टि» (पुष्टि बाँकी → पुष्टि भयो)",
"«भुक्तानी दर्ता»: रकम, तरिका, टिप्पणी",
"बारम्बार दर्ता सम्भव; आंशिक भए «आंशिक भुक्तानी»",
"नराम्रो ऋण/समायोजन: साइट वित्त (एजेन्ट नबाँधिएको) मात्र"
],
"detailTabs": "अवधि विवरण तीन ट्याब",
"detailTabItems": [
"बिल: खेलाडी र एजेन्ट बिल; पुष्टि/भुक्तानी",
"भुक्तानी र समायोजन: दर्ता, नराम्रो ऋण, समायोजन सञ्चालन लग",
"लेजर: खेलाडी क्रेडिट परिवर्तन (कब्जा, रिलिज, अवधि भुक्तानी)"
],
"billStatusSection": "बिल स्थिति",
"billStatusRows": [
["पुष्टि बाँकी", "बन्द पछि पुष्टि पर्खँदै", "बिल पुष्टि"],
["पुष्टि भयो", "रकम मान्य", "भुक्तानी दर्ता"],
["आंशिक भुक्तानी", "बाँकी रकम", "जारी भुक्तानी"],
["म्याद नाघ्यो", "सम्झौता पछि निपटान भएन", "संग्रह वा नराम्रो ऋण"],
["निपटान", "भुक्तानी पूरा", "अभिलेख हेर्नुहोस्"]
],
"operations": "अनुमति र दायरा",
"operationItems": [
"भुक्तानी दर्ता: बिल प्राप्तकर्ता मात्र",
"बाँधिएका एजेन्ट: खेलाडी बिल सिधा खेलाडी; एजेन्ट बिल आफ्नो नोड सम्बन्धित",
"बिलमा currency_code छैन; मुद्रा खेलाडी default_currency"
],
"note": "बन्द बिल भर्ना समयको शेयर स्न्यापशट; हालको प्रोफाइलबाट पुनर्गणना हुँदैन।",
"fundOpsLinkNote": "क्रेडिट लेजर प्रकार र वालेट भिन्नता:",
"fundOpsLinkLabel": "कोष सञ्चालन विवरण"
},
"agents": {
"title": "एजेन्ट प्रणाली",
"description": "एजेन्टले डाटा दायरा र क्रेडिट सीमा नियन्त्रण; मेनु अनुमति भूमिकाबाट। क्रेडिट साइटमा पहिले ट्री, त्यसपछि खेलाडी।",
"structure": "संरचना",
"structureItems": [
"सुपर एडमिन: एजेन्ट लाइन → लाइन खोल्ने: बाह्य एजेन्टलाई साइट + मूल",
"साइट अपरेटर: एजेन्ट सूची: विद्यमान साइटमा ट्री",
"नोड छानेर प्रोफाइल: शेयर, क्रेडिट, रिबेट, प्रत्यायोजन",
"मूल प्रोफाइल मात्र सुपर एडमिन; तल्लो माथिल्लो वा साइट एडमिन",
"एजेन्ट खाता: प्रशासक सूचीमा नोड बाँध्ने"
],
"provisionWalkthrough": "क्रेडिट लाइन खोल्ने (सुपर एडमिन)",
"provisionSteps": [
"एजेन्ट संगठन → एजेन्ट लाइन → «लाइन खोल्नुहोस्»",
"साइट जानकारी, मूल नाम र कोड",
"प्रारम्भिक शेयर, क्रेडिट, रिबेट",
"पेश पछि साइट र मूल स्वतः; साइट कोड रेकर्ड",
"साइट एडमिन वा मूल एजेन्ट खाता सिर्जना (भूमिका खण्ड)"
],
"dailyWalkthrough": "दैनिक एजेन्ट व्यवस्थापन (साइट अपरेटर)",
"dailySteps": [
"एजेन्ट सूची → माथिल्लो नोड → «नयाँ तल्लो एजेन्ट»",
"नाम, कोड, शेयर/क्रेडिट/रिबेट; तल्लो सिर्जना अनुमति",
"सेभ पछि ट्रीमा देखिन्छ; तल्लो वा खेलाडी थप्न सकिन्छ",
"शेयर/क्रेडिट परिवर्तन पछि नयाँ टिकट र अवधिमा मात्र"
],
"profile": "एजेन्ट प्रोफाइल फिल्ड",
"profileRows": [
["शेयर दर", "तल्लो कारोबारबाट हिस्सा", "बन्द पछि share_profit (जित/हार)"],
["क्रेडिट सीमा", "तल्लो खेलाडी क्रेडिट सीमा", "लाइन कब्जा नियम"],
["रिबेट दर", "प्ले रिबेटसँग मिलेर", "प्लेटफर्म ओड्स/रिबेटसँग"],
["प्रत्यायोजन", "तल्लो एजेन्ट/खेलाडी सिर्जना", "साइट एडमिनमा स्विचले रोक्दैन"]
],
"siteAdmin": "साइट एडमिन सूचना",
"siteAdminItems": [
"साइट एडमिनलाई एजेन्ट प्रत्यायोजन स्विचले रोक्दैन; भूमिका अनुसार",
"खेलाडी सिर्जनामा मालिक एजेन्ट अनिवार्य (मूल स्वतः होइन)",
"अवधि खोल/बन्द, नराम्रो ऋण: एजेन्ट नबाँधिएको वित्त खाता"
],
"note": "प्रोफाइल परिवर्तन नयाँ टिकट र भविष्य अवधिमा मात्र; बन्द इतिहास अपरिवर्तित।"
},
"players": {
"title": "खेलाडी व्यवस्थापन",
"description": "वालेट र क्रेडिट खेलाडी एकै ठाउँ; सूची र विवरणमा कोष मोड अनुसार ब्यालेन्स र लेजर ट्याब।",
"list": "खेलाडी सूची",
"listItems": [
"दैनिक सञ्चालन → खेलाडी सूची",
"खोज: प्रयोगकर्ता नाम, उपनाम, मुख्य साइट ID, एजेन्ट",
"«कोष मोड» स्तम्भ: वालेट / क्रेडिट",
"ब्यालेन्स: क्रेडिट = क्रेडिट सीमा (major); वालेट = लटरी ब्यालेन्स (minor)"
],
"createWalkthrough": "क्रेडिट खेलाडी सिर्जना (साइट / एजेन्ट)",
"createSteps": [
"खेलाडी सूची → «नयाँ खेलाडी»",
"अनिवार्य मालिक एजेन्ट (साइट एडमिनले छोड्न मिल्दैन)",
"लगइन, पासवर्ड, उपनाम, पूर्वनिर्धारित मुद्रा",
"प्रारम्भिक क्रेडिट सीमा (क्रेडिट)",
"सेभ पछि लटरी खाताबाट लगइन; वालेट SSO पहिलो पटक स्वतः सिर्जना"
],
"freezeWalkthrough": "फ्रिज / अनफ्रिज",
"freezeSteps": [
"सूची वा विवरण → «फ्रिज»",
"फ्रिज पछि भर्ना रोकिन्छ; विद्यमान टिकट नियम अनुसार सेटल",
"अनफ्रिज पछि सामान्य भर्ना"
],
"modes": "कोष मोड भिन्नता",
"modeRows": [
["वालेट मोड", "मुख्य साइट SSO; पहिलो JWT मा स्वतः सिर्जना। ट्याब: वालेट लेजर, ट्रान्सफर"],
["क्रेडिट मोड", "लटरी खाता वा एजेन्टले सिर्जना। ट्याब: क्रेडिट लेजर; जित अवधि सेटलमेन्ट"]
],
"detail": "खेलाडी विवरण",
"detailItems": [
"क्रेडिट लेजर: कब्जा, रिलिज, अवधि भुक्तानी",
"वालेट लेजर / ट्रान्सफर: वालेट मात्र; मुख्य साइट ट्रान्सफर",
"टिकट इतिहास: विवरण (कम्बो प्ले समावेश)",
"क्रेडिट समायोजन: लेख अनुमति चाहिन्छ"
],
"note": "वालेट प्रयोगकर्ता नाम लटरीले उत्पादन (जस्तै nlotto******); मुख्य साइटबाट सिंक हुँदैन।"
},
"tickets": {
"title": "टिकट खोज",
"description": "बहु आयामले टिकट खोज; अपरेटर, सपोर्ट र एजेन्ट मिलानका लागि। बाँधिएका एजेन्टले उप-ट्री खेलाडी मात्र।",
"entry": "प्रवेश",
"entryItems": [
"दैनिक सञ्चालन → टिकट सूची",
"वा खेलाडी विवरण → टिकट इतिहासबाट एक खेलाडी"
],
"filter": "सामान्य फिल्टर",
"filterItems": [
"ड्र, खेलाडी, प्ले, टिकट नम्बर, समय दायरा",
"स्थिति: पर्खँदै / जित / हार / रद्द आदि",
"एजेन्ट दायरा: सुपर एडमिन साइट; साइट = आफ्नो साइट; एजेन्ट = उप-ट्री"
],
"detail": "टिकट विवरण",
"detailItems": [
"भर्ना, ओड्स, रकम, शेयर स्न्यापशट (क्रेडिट)",
"कम्बो प्ले (जस्तै लियनमा) कम्बो विवरण विवरण पृष्ठमा",
"लिङ्क: खेलाडी विवरण, ड्र विवरण",
"Excel निर्यात (निर्यात अनुमति चाहिन्छ)"
],
"note": "रकम भर्ना समयको स्न्यापशट; नतिजा पछि स्थिति स्वतः; हातले परिवर्तन गर्नुपर्दैन।"
},
"wallet": {
"title": "वालेट र मिलान (वालेट मोड)",
"description": "वालेट खेलाडी मात्र। मुख्य साइट र लटरी बीच ट्रान्सफर; बाँधिएका एजेन्टले सामान्यतया यो मेनु देख्दैनन्।",
"walletSection": "वालेट लेजर",
"walletItems": [
"कोष र रिपोर्ट → वालेट लेजर",
"खेलाडी, प्रकार, समय: भित्र, बाहिर, भर्ना, पुरस्कार आदि",
"रकम minor पूर्णांक; API कागजात वालेट गेटवे सँग मिल्छ",
"फिल्टर निर्यात (अनुमति चाहिन्छ)"
],
"transferSection": "ट्रान्सफर अर्डर",
"transferItems": [
"वालेट मोड्युलमा ट्रान्सफर सूची (मुख्य साइट ↔ लटरी)",
"स्थिति: सफल / असफल / पेन्डिङ",
"लामो पेन्डिङ भए मिलान मोड्युलमा"
],
"reconcileSection": "मिलान (हराएको ट्रान्सफर)",
"reconcileSteps": [
"कोष र रिपोर्ट → मिलान",
"लामो पेन्डिङ वा असामान्य ट्रान्सफर सूची",
"अर्डर खोलेर मुख्य साइटमा वास्तविक नतिजा पुष्टि",
"पुष्टि अनुसार: थप लेजर, रिभर्स वा बन्द; अडिट लग",
"टेक: मुख्य साइट वालेट लग र API कागजात → समस्या निवारण"
],
"note": "क्रेडिट खेलाडीमा वालेट/मिलान छैन; कोष परिवर्तन सेटलमेन्ट केन्द्र लेजरमा।",
"fundOpsLinkNote": "वालेट र क्रेडिट कोष जीवनचक्र तुलना:",
"fundOpsLinkLabel": "कोष सञ्चालन विवरण"
},
"config": {
"title": "नियम र जोखिम (प्लेटफर्म सुपर एडमिन)",
"description": "साइटव्यापी प्ले, ओड्स, सीमा र जोखिम। सामान्यतया प्लेटफर्म सुपर एडमिन मात्र; साइट/एजेन्टमा छैन।",
"plays": "प्ले नियम",
"playsItems": [
"प्लेटफर्म → प्ले नियम",
"प्ले स्विच: बन्द भए खेलाडी端मा भर्ना रोक",
"एकल भर्ना न्यूनतम/अधिकतम"
],
"odds": "ओड्स र रिबेट",
"oddsItems": [
"प्लेटफर्म → ओड्स र रिबेट",
"ओड्स, आधार रिबेट; नयाँ टिकट मात्र",
"एजेन्ट प्रोफाइल रिबेटसँग मिलेर"
],
"riskCap": "सीमा संस्करण",
"riskCapItems": [
"प्लेटफर्म → सीमा संस्करण",
"नम्बर सीमा; पुगे पछि सकियो",
"नयाँ संस्करण भविष्य ड्र मात्र; खुला ड्र त्यतिबेला संस्करण"
],
"risk": "जोखिम केन्द्र",
"riskItems": [
"प्लेटफर्म → जोखिम केन्द्र",
"नम्बर कब्जा, पूल, उच्च जोखिम",
"कब्जा टिकट अनुसार समूह; कब्जा/उच्च जोखिम मात्र",
"कम्बो विवरण टिकट विवरणमा; जोखिम सूचीमा होइन"
],
"jackpot": "ज्याकपट",
"jackpotItems": [
"प्लेटफर्म → ज्याकपट",
"ज्याकपट रकम र पुरस्कार नियम (प्ले सक्षम भए)"
],
"note": "साइट अपरेटरले मेनु नदेखे सामान्य; प्ले परिवर्तन सुपर एडमिन वा हामीलाई सम्पर्क।"
},
"reports": {
"title": "रिपोर्ट केन्द्र",
"description": "ड्र, खेलाडी, प्ले अनुसार P&L र जोखिम; असिंक निर्यात। दायरा भूमिका अनुसार स्वतः साँघुरिन्छ।",
"entry": "प्रवेश र फिल्टर",
"entryItems": [
"कोष र रिपोर्ट → रिपोर्ट केन्द्र",
"पहिले रिपोर्ट प्रकार, त्यसपछि समय, साइट, एजेन्ट, ड्र",
"बाँधिएका एजेन्ट: उप-ट्री मात्र"
],
"types": "मुख्य रिपोर्ट",
"reportRows": [
["प्रति ड्र P&L", "एकल ड्र भर्ना, पुरस्कार, P&L"],
["दैनिक सारांश", "व्यापार दिन अनुसार"],
["खेलाडी जित/हार", "खेलाडी क्रम र विवरण"],
["प्ले आयाम", "प्ले भर्ना र पुरस्कार संरचना"],
["तातो नम्बर जोखिम", "नम्बर केन्द्रितता र पूल कब्जा"],
["रिबेट कमिसन", "रिबेट र कमिसन"],
["अडिट लग", "प्रशासन महत्वपूर्ण कार्य (सुपर एडमिन)"]
],
"export": "निर्यात",
"exportItems": [
"रिपोर्ट निर्यात अनुमतिमा असिंक निर्यात; पूरा भए डाउनलोड",
"टिकट सूची, वालेट लेजर पनि Excel",
"ठूलो डाटामा समय दायरा साँघुरो बनाउनुहोस्"
],
"scope": "डाटा दायरा",
"scopeItems": [
"सुपर एडमिन: सबै वा साइट फिल्टर",
"साइट एडमिन: आफ्नो साइट",
"बाँधिएका एजेन्ट: उप-ट्री"
]
},
"fundOperations": {
"title": "कोष सञ्चालन विवरण",
"description": "वालेट र क्रेडिट मोडमा भर्ना, ड्र, भुक्तानीमा कोष कसरी बदलिन्छ। पहिले खेलाडी कोष मोड पुष्टि गर्नुहोस्, त्यसपछि सही लेजर हेर्नुहोस्।",
"twoSystems": "दुई समानान्तर प्रणाली (नभुल्नुहोस्)",
"twoSystemsItems": [
"एकल ड्र सेटलमेन्ट (दैनिक → सेटलमेन्ट): ड्र जित/हार; वालेटमा पुरस्कार, क्रेडिटमा रिलिज/अवधि लेजर",
"क्रेडिट अवधि सेटलमेन्ट (एजेन्ट → सेटलमेन्ट केन्द्र): क्रेडिट मात्र; शेयर सारांश र बिल",
"मुख्य साइट वालेट (कोष → वालेट): वालेट मात्र; मुख्य साइट ↔ लटरी ट्रान्सफर"
],
"creditModel": "क्रेडिट: सीमा मोडेल",
"creditModelItems": [
"क्रेडिट सीमा (credit_limit): एजेन्ट/अपरेटरले तोक्ने सीमा",
"प्रयोग क्रेडिट (used_credit): भर्ना कब्जा + सेटल हारको जोड",
"उपलब्ध = सीमा प्रयोग फ्रिज; अपर्याप्त भए भर्ना रोक",
"सूची/विवरण «ब्यालेन्स» क्रेडिटमा नगद होइन"
],
"creditLifecycle": "क्रेडिट: एक टिकटको कोष चक्र",
"creditLifecycleSteps": [
"① भर्ना: क्रेडिट कब्जा («भर्ना फ्रिज»); अन्तिम कटौती होइन",
"② पर्खँदै: कब्जा रहन्छ; सेटलसँग दोहोरो कटौती हुँदैन",
"③ ड्र सेटल: कब्जा रिलिज → «ड्र सेटलमेन्ट»; हार = प्रयोग बढ्छ, जित = रिलिज",
"④ अवधिमा: शेयर लेजरमा पनि लेखिन्छ",
"⑤ अवधि बन्द पछि: सेटलमेन्ट केन्द्रमा पुष्टि/भुक्तानी",
"⑥ म्याद नाघेको बिल: भर्ना रोक, पहिले भुक्तानी"
],
"creditLedger": "क्रेडिट: लेजर प्रकार",
"creditLedgerRows": [
["भर्ना फ्रिज", "भर्ना सफल", "उपलब्ध घट्छ; प्रयोग बढ्छ", "सेटलमेन्ट केन्द्र/खेलाडी विवरण"],
["ड्र सेटलमेन्ट", "ड्र पुरस्कार सेटल", "कब्जा रिलिज, जित/हार अनुसार", "पर्खँदैमा फ्रिज मात्र; पछि प्रति टिकट एक"],
["अवधि सेटल पुष्टि", "बिल भुक्तानी (केही)", "प्रयोग घट्छ", "सेटलमेन्ट केन्द्र लेजर"],
["सेटलमेन्ट भुक्तानी", "अवधि लेखा", "लेखा मात्र; उपलब्ध नबदलिने", "सेटलमेन्ट केन्द्र लेजर"],
["समायोजन/रिभर्स/नराम्रो ऋण", "साइट वित्त", "बिल र लेजर अनुसार", "भुक्तानी र समायोजन"]
],
"creditBill": "क्रेडिट: बिल र भुक्तानी अर्थ",
"creditBillItems": [
"खेलाडी बिल = अवधिको जित/हारको खुद प्राप्य/देय",
"एजेन्ट बिल = शेयर अनुसार हिस्सा",
"धनात्मक: बिल方 → प्राप्तकर्तालाई; ऋणात्मक: उल्टो (प्रशासनले देखाउँछ)",
"पुष्टि = रकम मान्य; भुक्तानी = वास्तविक कोष, बारम्बार सम्भव",
"नराम्रो ऋण: साइट वित्त (एजेन्ट नबाँधिएको) मात्र"
],
"creditAdjust": "क्रेडिट: म्यानुअल सीमा समायोजन",
"creditAdjustSteps": [
"खेलाडी विवरण → क्रेडिट समायोजन (लेख अनुमति)",
"सीमा परिवर्तन; पुरानो प्रयोग मेटिँदैन",
"सीमा घटाए प्रयोग बढी भए भर्ना रोक",
"अवधि भुक्तानी/नराम्रो ऋण सेटलमेन्ट केन्द्रमा; विवरणमा दोहोर्याउनुहोस्"
],
"walletLifecycle": "वालेट: कोष चक्र",
"walletLifecycleSteps": [
"① मुख्य साइट SSO → लटरी H5",
"② भित्र: H5 → वालेट debit → लटरी ब्यालेन्स बढ्छ",
"③ भर्ना: ब्यालेन्सबाट कटौती («भर्ना debit»)",
"④ ड्र: जित «पुरस्कार credit»; दैनिक → सेटलमेन्ट ब्याच पछि",
"⑤ बाहिर (वैकल्पिक): वालेट credit → ब्यालेन्स घट्छ",
"⑥ अपवाद: लामो पेन्डिङ → मिलान"
],
"walletTxn": "वालेट: लेजर प्रकार",
"walletTxnRows": [
["मुख्य साइट भित्र", "मुख्य साइटबाट लटरी", "ब्यालेन्स बढ्छ"],
["मुख्य साइट बाहिर", "लटरीबाट मुख्य साइट", "ब्यालेन्स घट्छ"],
["भर्ना debit", "भर्ना सफल", "ब्यालेन्स घट्छ"],
["पुरस्कार credit", "ड्र पुरस्कार सेटल", "ब्यालेन्स बढ्छ (जित)"],
["भर्ना रिभर्स / बाहिर असफल फिर्ता", "रद्द वा असफल", "ब्यालेन्स फिर्ता"]
],
"walletReconcile": "वालेट: ट्रान्सफर अपवाद",
"walletReconcileRows": [
["मिलान बाँकी", "स्थिति मेल खाएन वा टाइमआउट", "पहिले गेटवे लग जाँच"],
["थप credit", "मुख्य debit भयो, लटरी credit छैन", "लटरीमा भित्र थप"],
["रिभर्स", "लटरी लेजर रद्द", "उल्टो समायोजन"],
["बन्द चिन्ह", "बाहिर समाधान", "स्थिति मात्र; बाहिर pending मा प्रयोग नगर्नुहोस्"]
],
"compare": "एकै चरण: वालेट vs क्रेडिट",
"compareRows": [
["प्रवेश", "मुख्य SSO; पहिलो JWT", "लटरी खाता वा एजेन्ट"],
["भर्नाका लागि", "ब्यालेन्स ≥ रकम", "उपलब्ध ≥ रकम"],
["भर्ना", "ब्यालेन्स तुरुन्त कटौती", "क्रेडिट कब्जा (फ्रिज)"],
["जित पछि", "पुरस्कार ब्यालेन्समा", "रिलिज; नगद अवधि बिलमा"],
["दैनिक", "वालेट लेजर + ट्रान्सफर", "सेटलमेन्ट लेजर + बिल"],
["एजेन्ट भुक्तानी", "लागू छैन", "अवधि बिल"]
],
"note": "क्रेडिट UI मा «रिलिज»/«अवधि भुक्तानी»; वालेट «पुरस्कार» होइन। दुई मोड एउटै खातामा मिसाउनुहोस्।"
},
"manualReview": {
"title": "म्यानुअल समीक्षा र पुरस्कार",
"description": "ड्र नतिजा समीक्षा, शीतल अवधि, एकल ड्र पुरस्कार ब्याच र सेटिङ। सेटलमेन्ट केन्द्र क्रेडिट अवधिबाट अलग।",
"distinction": "क्रेडिट अवधि सेटलमेन्टबाट भिन्नता",
"distinctionItems": [
"यहाँ: एकल ड्र पुरस्कार ब्याच (दैनिक → सेटलमेन्ट); हरेक ड्रमा",
"सेटलमेन्ट केन्द्र: क्रेडिट अवधि खोल/बन्द/बिल; हप्ता/महिना सारांश",
"«अनुमोदन र प्रकाशन»: नम्बर लागू; ब्याच: लागू पछि कोष"
],
"drawReview": "ड्र नतिजा म्यानुअल समीक्षा",
"drawReviewItems": [
"«ड्र नतिजा म्यानुअल समीक्षा» खुला: RNG पछि «समीक्षामा», «अनुमोदन र प्रकाशन» चाहिन्छ",
"बन्द: RNG स्वतः प्रकाशन (शीतल अवधि हुन सक्छ)",
"म्यानुअल नम्बर: समीक्षा स्विच अनुसार",
"प्रवेश: दैनिक → ड्र सूची → विवरण → ड्र ब्याच"
],
"drawPublishSteps": "नतिजा प्रकाशन (कदम)",
"drawPublishStepItems": [
"विवरण खोल्नुहोस्; «नतिजा पर्खँदै» वा «समीक्षामा»",
"नतिजा छैन: उत्पादन वा नम्बर प्रविष्ट",
"नम्बर र नियम जाँच",
"«अनुमोदन र प्रकाशन»",
"पछि शीतल अवधि वा «सेटल हुँदै» (सेटिङ)"
],
"cooldown": "शीतल अवधि",
"cooldownItems": [
"«शीतल अवधि (मिनेट)»: प्रकाशन पछि पुरस्कार सेटल अघि पर्खाइ",
"शीतलमा सुपर एडमिन «पुन: ड्र»: रोलब्याक → पुन: नतिजा → पुन: प्रकाशन",
" मिनेट: तुरुन्त सेटल",
"नम्बर जाँचको अन्तिम窗口; क्रेडिट अवधि होइन"
],
"settlementBatch": "एकल ड्र पुरस्कार ब्याच",
"settlementBatchItems": [
"प्रकाशन पछि ब्याच: त्यो ड्रका सबै टिकट जित/हार",
"प्रवेश: दैनिक → सेटलमेन्ट; ड्र विवरणबाट पनि",
"स्थिति: चलिरहेको → समीक्षा → अनुमोदित → पुरस्कार/पूरा",
"वालेट: «पुरस्कार कार्यान्वयन» → ब्यालेन्स",
"क्रेडिट: «पुरस्कार कार्यान्वयन» → ड्र/शेयर लेजर; नगद होइन"
],
"batchStatusSection": "ब्याच स्थिति",
"batchStatusRows": [
["चलिरहेको", "जित/हार गणना", "पर्खनुहोस्"],
["समीक्षामा", "रकम तयार; पुष्टि बाँकी", "अनुमोदन / अस्वीकार"],
["अनुमोदित", "पुष्टि; पुरस्कार बाँकी", "पुरस्कार कार्यान्वयन"],
["पुरस्कार भयो", "खाता/लेजरमा लेखियो", "विवरण, निर्यात"],
["अस्वीकार", "समीक्षा असफल; पुन: सेटल", "सुधारेर पुन: चलाउनुहोस्"],
["असफल", "प्रक्रिया त्रुटि", "प्राविधिक समर्थन"]
],
"batchWalkthrough": "म्यानुअल समीक्षा र पुरस्कार (कदम)",
"batchWalkthroughSteps": [
"दैनिक → सेटलमेन्ट → «समीक्षामा» फिल्टर",
"विवरण: ड्र, टिकट संख्या, कुल भर्ना/पुरस्कार, P&L जाँच",
"मिल्यो «अनुमोदन»; समस्या «अस्वीकार» + टिप्पणी",
"«पुरस्कार कार्यान्वयन» — वालेट ब्यालेन्स, क्रेडिट लेजर",
"पछि एक-क्लिक फिर्ता छैन; गलत नम्बर → शीतलमा पुन: ड्र"
],
"settings": "सम्बन्धित सेटिङ (सुपर एडमिन)",
"settingRows": [
["ड्र म्यानुअल समीक्षा", "प्लेटफर्म → प्रणाली → ड्र", "RNG म्यानुअल", "RNG स्वतः"],
["शीतल अवधि", "उही", "N मिनेट पर्खाइ", "तुरुन्त सेटल"],
["स्वतः सेटलमेन्ट", "प्रणाली → सेटलमेन्ट स्वचालन", "स्वतः ब्याच", "म्यानुअल"],
["स्वतः ब्याच अनुमोदन", "उही", "स्वतः «अनुमोदित»", "म्यानुअल"],
["स्वतः पुरस्कार", "उही", "स्वतः कार्यान्वयन", "म्यानुअल"]
],
"settingsNote": "उत्पादन: म्यानुअल ड्र + शीतल + स्वतः सेटल + म्यानुअल ब्याच + स्व/म्यानुअल पुरस्कार। तपाईंको जोखिम नीति अनुसार।",
"rejectNote": "ब्याच अस्वीकार → टिकट पुन: सेटल; प्रकाशित नम्बर रोलब्याक हुँदैन। गलत नम्बर → पुन: ड्र।",
"note": "क्रेडिट अवधि बन्द अघि: सम्बन्धित ड्र «सेटल भयो» र पुरस्कार ब्याच पूरा; नभए चेतावनी।"
},
"faq": {
"title": "बारम्बार सोधिने",
"description": "प्रशासन सञ्चालनका सामान्य समस्या र समाधान।",
"faqRows": [
["क्रेडिट लेजर बुझ्न गाह्रो", "«कोष सञ्चालन विवरण»; पर्खँदै «भर्ना फ्रिज» मात्र; पछि प्रति टिकट «ड्र सेटलमेन्ट» — दोहोरो कटौती छैन"],
["सेटलमेन्ट ब्याच vs केन्द्र", "«म्यानुअल समीक्षा र पुरस्कार»; अघिल्लो एकल ड्र, पछिल्लो क्रेडिट अवधि बिल"],
["गलत नतिजा", "ड्र विवरण → शीतल अवधिमा पुन: ड्र → रोलब्याक → पुन: प्रकाशन → पुन: सेटलमेन्ट"],
["खेलाडी端 खुला, प्रशासन बन्द", "सूची DB status; हल रियल-टाइम; close_time र समय क्षेत्र जाँच"],
["ट्रान्सफर नआएको (वालेट)", "वालेट लेजर; मिलानमा पेन्डिङ; मुख्य साइट गेटवे लग"],
["साइट जडान असफल", "सार्वजनिक HTTPS; balance/debit/credit API कार्यान्वयन"],
["बन्द बटन अक्षम", "अर्को चलिरहेको अवधि छैन; ड्र सेटल; खाता एजेन्ट नबाँधिएको"],
["भुक्तानी बटन छैन", "सेटलमेन्ट लेख; बिल पुष्टि/आंशिक/म्याद र unpaid_amount > 0; प्राप्तकर्ता"],
["एजेन्टले वालेट मेनु छैन", "बाँधिएका एजेन्टमा लुक्छ; साइट वित्त वा सुपर एडमिन प्रयोग"],
["खेलाडी सिर्जनामा एजेन्ट छान्न मिल्दैन", "साइट एडमिनले मालिक एजेन्ट अनिवार्य; पहिले तल्लो एजेन्ट सिर्जना"],
["अनुमति अपर्याप्त", "सुपर एडमिन वा समर्थन; भूमिका व्यवस्थापनमा मेनु तोक्नुहोस्"]
],
"integration": "प्राविधिक एकीकरण",
"integrationItems": [
"SSO, iframe, वालेट: API एकीकरण कागजात",
"त्रुटि 80018005: कागजात → समस्या निवारण"
],
"integrationLinkLabel": "API एकीकरण कागजात हेर्नुहोस्"
}
}
}

View File

@@ -1,5 +1,6 @@
{
"title": "एजेन्ट लाइन",
"listTitle": "एजेन्ट सूची",
"sitesListHint": "कुञ्जी र कलब्याकको लागि",
"sitesListLink": "कन्फिग · साइट",
"subnav": {

View File

@@ -137,8 +137,12 @@
"notifications": "सूचना",
"notificationsComingSoon": "सूचना सुविधा विकासमा छ",
"accountSettings": "खाता सेटिङ",
"relatedDocs": "सम्बन्धित कागजात",
"loggedOut": "लगआउट भयो"
},
"docs": {
"learnMore": "पूर्ण मार्गदर्शन हेर्नुहोस्"
},
"nav": {
"home": "गृह",
"dashboard": "ड्यासबोर्ड",

View File

@@ -42,6 +42,7 @@
"integrationSites": {
"title": "मुख्य साइट एकीकरण साइटहरू",
"description": "एडमिनमा पार्टनर एकीकरण सेटिङ मिलाउनुहोस्। site_code सिर्जना पछि परिवर्तन गर्न मिल्दैन।",
"pageGuide": "वालेट ग्राहकलाई एकीकरण साइट र कुञ्जी चाहिन्छ; API कागजात हेर्नुहोस्।",
"create": "नयाँ साइट",
"edit": "सम्पादन",
"save": "बचत",

View File

@@ -1,6 +1,7 @@
{
"title": "ड्रअ",
"statusListTitle": "ड्रअ सूची",
"pageGuide": "ड्र जीवनचक्र व्यवस्थापन: योजना, बन्द, नतिजा समीक्षा र सेटलमेन्ट।",
"generatePlan": "ड्रअ योजना सिर्जना",
"generating": "सिर्जना हुँदैछ…",
"generateSuccess": "{{created}} ड्रअ सिर्जना भयो, बफर {{upcoming}}/{{target}}",

View File

@@ -1,13 +1,15 @@
{
"shell": {
"title": "Integration API",
"admin": "Admin"
"title": "लटरी इन्टिग्रेसन दस्तावेज",
"admin": "Admin",
"adminLogin": "Admin console"
},
"nav": {
"overview": "अवलोकन",
"api": "API",
"ship": "लाइभ",
"home": "सारांश",
"delivery": "इन्टिग्रेसन वितरण",
"quickstart": "छिटो सुरु",
"fundamentals": "रकम मोडेल",
"setup": "सेटअप",
@@ -16,7 +18,11 @@
"wallet": "वालेट गेटवे",
"transfer": "स्थानान्तरण (सन्दर्भ)",
"errors": "त्रुटि कोड",
"golive": "लाइभ सूची"
"troubleshooting": "समस्या निवारण",
"golive": "लाइभ सूची",
"operations": "सञ्चालन",
"adminGuide": "Admin Guide",
"apiReference": "API Reference"
},
"headers": {
"component": ["कम्पोनेन्ट", "भूमिका", "मालिक"],
@@ -35,337 +41,427 @@
"envMap": ["वस्तु", "Admin साइट", "मुख्य .env", "नोट"],
"account": ["प्रयोगकर्ता", "पासवर्ड", "site_player_id"],
"contract": ["परिदृश्य", "HTTP", "Body"],
"adminField": ["फिल्ड", "नोट", "उदाहरण"]
"adminField": ["फिल्ड", "नोट", "उदाहरण"],
"handoffTable": ["वस्तु", "विवरण", "जिम्मेवार"],
"env": ["Environment", "Address example", "Note"],
"envelopeTable": ["Dir", "Message fields", "Note"],
"faq": ["Symptom", "Troubleshooting"]
},
"pages": {
"overview": {
"title": "Integration",
"description": "मुख्य साइट SSO + वालेट गेटवे। पहिचान JWT; रकम मुख्य वालेट र लटरी भित्रको ब्यालेन्समा विभाजित।",
"roles": "भूमिका",
"flow": "प्रवाह",
"e2eSequence": "End-to-end क्रम",
"conventions": "सम्झौता",
"readingOrder": "पढ्ने क्रम",
"title": "Integration overview",
"description": "For main-site developers / integration engineers. You implement: JWT signing + wallet gateway; lottery provides H5 and API.",
"roles": "Roles",
"flow": "Business flow",
"e2eSequence": "End-to-end sequence",
"conventions": "Conventions",
"readingOrder": "Suggested reading order",
"matrix": [
["मुख्य साइट", "JWT जारी; वालेट गेटवे कार्यान्वयन", "साझेदार"],
["लटरी API", "JWT प्रमाणीकरण, खेल, स्थानान्तरण, बेट", "हामी"],
["लटरी H5", "H5 / iframe", "हामी"]
["Main site (partner)", "User login; server-side JWT; wallet gateway", "Partner"],
["Lottery API (us)", "JWT verify, transfer, bet, draw, settlement", "Us"],
["Lottery H5 (us)", "Player UI; iframe or URL entry", "Us"]
],
"flowItems": [
"मुख्य साइट लगइन → JWT जारी",
"लटरी प्रवेश (URL वा iframe)",
"transfer-in → मुख्य डेबिट + लटरी क्रेडिट",
"बेट / सेटल (लटरी ब्यालेन्स)",
"transfer-out → लटरी डेबिट + मुख्य क्रेडिट"
"User logs in on main site → main site server issues short-lived JWT",
"Enter lottery H5 (iframe embed or URL ?token= redirect)",
"Player taps transfer-in in H5 → lottery calls main site debit → lottery balance credited",
"Player bets / wins in H5 (uses lottery balance)",
"(Optional) Player taps transfer-out in H5 → lottery calls main site credit"
],
"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"]
["1", "Main site", "User login; server issues JWT (site_code, site_player_id)"],
["2", "Main site", "Embed lottery H5 iframe, or redirect to lottery_h5_base_url/?token="],
["3", "Lottery H5", "Receives JWT; calls GET /api/v1/player/me to verify and auto-provision"],
["4", "Player", "Taps transfer-in in H5"],
["5", "Lottery API", "Server calls main site POST /wallet/debit-for-lottery"],
["6", "Main wallet", "Debit main_balance; return success: true"],
["7", "Lottery API", "Credit lottery balance"],
["8", "Player", "Bets / waits for settlement in H5"],
["9", "Player", "(Optional) taps transfer-out in H5"],
["10", "Lottery API", "Calls main site 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"]
["Amount", "Minor integer units, e.g. 2000 = 20.00 NPR"],
["Encoding", "UTF-8 JSON"],
["Time", "JWT uses Unix seconds (iat / exp); recommend exp - iat ≤ 300 seconds"],
["Player API auth", "Authorization: Bearer {JWT} (main site signs, lottery verifies)"],
["Wallet gateway auth", "Authorization: Bearer {wallet_api_key} (lottery sends on callback)"]
],
"readingItems": ["छिटो सुरु → सेटअप → SSO → iframe → वालेट → त्रुटि → लाइभ"]
"readingItems": [
"Integration delivery — confirm deliverables and environment URLs",
"Quick start — first integration pass",
"Setup — admin site provisioning and key mapping",
"SSO → iframe protocol → wallet gateway",
"Troubleshooting — common issues",
"Go-live checklist — production release checks"
]
},
"delivery": {
"title": "Integration delivery",
"description": "Before integration testing, confirm the following deliverables with sales / support. Test and production environments must be fully isolated.",
"handoffScope": "Integration scope (what you need to do)",
"weProvide": "We provide",
"youProvide": "Partner provides",
"environment": "Environment URLs",
"process": "Typical integration process",
"note": "Secrets (sso_jwt_secret, wallet_api_key) are shown only once after creation — save them securely immediately. Store secrets on the main-site server only; never in frontend or mobile apps. URLs below are Tanumo current defaults; partner deployments follow sales delivery.",
"handoffRows": [
["JWT signing", "Main site server issues HS256 JWT after login; no token-exchange login API", "Partner"],
["Wallet gateway", "Implement balance / debit / credit over HTTPS", "Partner"],
["iframe or URL entry", "Embed lottery H5 or redirect with JWT", "Partner"],
["Lottery H5 + API", "Games, transfer, bet, draw", "Us"],
["Integration site & keys", "Create site_code and deliver secrets", "Us (super admin)"]
],
"provideRows": [
["site_code", "Site code written into JWT"],
["sso_jwt_secret", "JWT signing secret (held by main site)"],
["wallet_api_key", "Bearer secret when lottery calls wallet gateway"],
["lottery_h5_base_url", "Lottery H5 entry; default https://front.tanumo.com"],
["lottery_api_base_url", "Lottery API base; default https://lotterylaravel.tanumo.com"]
],
"submitRows": [
["wallet_api_url", "Partner wallet gateway HTTPS root (publicly reachable)"],
["iframe_allowed_origins", "Main-site origin allowlist (required for iframe; one per line)"],
["Test accounts", "Several site_player_id values with initial main_balance"],
["Egress IP (if needed)", "If gateway uses IP allowlist, request lottery server egress IP"]
],
"environmentRows": [
["Lottery API", "https://lotterylaravel.tanumo.com", "curl: GET /api/v1/player/me"],
["Lottery H5 entry", "https://front.tanumo.com", "iframe / ?token=; wallet example /wallet"],
["Integration docs", "https://lotteryadmin.tanumo.com/docs/integration", "Public documentation"],
["Admin console", "https://lotteryadmin.tanumo.com/admin", "Super admin; Config → Integration sites"],
["Production", "Separate domain and secrets", "site_code, secrets, domains not shared with staging"]
],
"processSteps": [
"Sales opens integration → our super admin creates integration site and delivers keys + H5 URL",
"Partner implements three wallet endpoints on public HTTPS (tunnel OK for staging)",
"Partner fills wallet_api_url, iframe_allowed_origins in admin; run connectivity test",
"Partner implements JWT signing and iframe postMessage (or URL redirect)",
"Complete integration using Quick start acceptance checklist",
"Production: new site, new secrets, full end-to-end retest before go-live"
]
},
"quickstart": {
"title": "छिटो सुरु",
"description": "स्थानीय इन्टिग्रेसन। repo मा main-site/ सन्दर्भ कार्यान्वयन; गोप्य कुञ्जी admin वा lottery .env सँग मिल्नुपर्छ।",
"description": "Assumes Integration delivery is done and you have site_code, secrets, and H5 URL. Follow these steps for first integration pass.",
"prereq": "पूर्वशर्त",
"steps": "इन्टिग्रेसन चरण",
"testAccounts": "परीक्षण खाता (main-site)",
"reference": "सन्दर्भ कार्यान्वयन",
"note": "प्रोडक्सनमा HTTPS र अलग site_code/गोप्य। स्थानीयमा wallet_api_url बिना lottery API stub हुन सक्छ (non-production)।",
"steps": "Integration steps",
"acceptance": "Acceptance checklist",
"note": "JWT must be signed on the main-site server only — never hard-code sso_jwt_secret in frontend. Production wallet_api_url must be public HTTPS.",
"prereqItems": [
"लटरी API (lotterLaravel) र lotteryfront चलिरहेको",
"main-site चलिरहेको (http://localhost:5173)",
"admin मा इन्टिग्रेसन साइट, वा lottery .env MAIN_SITE_* मिलेको"
"Received site_code, sso_jwt_secret, wallet_api_key, lottery_h5_base_url",
"Main site implements GET /wallet/balance, POST /wallet/debit-for-lottery, POST /wallet/credit-from-lottery",
"Admin integration site has wallet_api_url and iframe_allowed_origins; connectivity test passed",
"At least one test site_player_id with sufficient main_balance"
],
"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 जाँच"
"Main-site server implements JWT signing (see SSO jsonwebtoken example)",
"Self-test with curl: Bearer JWT on GET https://lotterylaravel.tanumo.com/api/v1/player/me → code=0",
"Embed <iframe src=\"https://front.tanumo.com\"> on main site; listen for postMessage",
"After LOTTERY_READY, send MAIN_INIT_TOKEN (token at message top level — see iframe page)",
"After H5 enters hall, initiate transfer-in in H5",
"Confirm main site receives POST /wallet/debit-for-lottery with success: true",
"Confirm lottery balance increases in H5; try a bet",
"(Optional) transfer-out in H5; confirm POST /wallet/credit-from-lottery callback",
"Check off acceptance checklist below"
],
"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 लग"
"JWT self-test: curl https://lotterylaravel.tanumo.com/api/v1/player/me returns code=0, data.site_player_id correct",
"Wallet self-test: curl POST /wallet/debit-for-lottery returns success:true, main_balance debited correctly",
"Idempotency: same idempotent_key replay returns same response, no double debit",
"iframe: LOTTERY_READY → MAIN_INIT_TOKEN → H5 hall loads",
"Transfer-in: success in H5; main-site gateway logs show debit-for-lottery",
"Refresh: after JWT near expiry or LOTTERY_TOKEN_NEEDED, MAIN_REFRESH_TOKEN succeeds"
]
},
"fundamentals": {
"title": "Money model",
"balances": "दुई तह ब्यालेन्स",
"calls": "कल दिशा",
"note": "सबै रकम minor पूर्णांक। क्रेडिट-लाइन प्लेयर यो दस्तावेज बाहिर।",
"balances": "Two balance layers",
"calls": "Call directions",
"note": "All amounts use minor integers. Credit-line (agent) players are out of scope; this doc covers main-site wallet mode only.",
"balanceRows": [
["main_balance", "मुख्य वालेट", "साझेदार गेटवे; लटरी कलब्याक"],
["lottery balance", "लटरी भित्रको ब्यालेन्स", "transfer-in पछि बेटिङ"]
["main_balance", "Main wallet", "Partner gateway; lottery server callbacks debit/credit"],
["lottery balance", "In-lottery balance", "Used for betting after transfer-in; shown in lottery H5"]
],
"callRows": [
["लटरी → मुख्य", "balance / debit / credit", "wallet_api_key"],
["लटरी H5 → लटरी API", "me / transfer / bet", "प्लेयर JWT (मुख्य होइन)"]
["Lottery → main", "GET balance / POST debit / POST credit", "Bearer wallet_api_key"],
["Lottery H5 → lottery API", "me / transfer / bet / balance", "Bearer player JWT (main site not involved)"]
]
},
"setup": {
"title": "Setup",
"description": "इन्टिग्रेसन साइट सिर्जना पछि गोप्य कुञ्जी एक पटक मात्र देखाइन्छ। तुरुन्त सुरक्षित राख्नुहोस्।",
"weProvide": "हामी दिन्छौं",
"youProvide": "साझेदारले दिन्छ",
"defaultPaths": "पूर्वनिर्धारित वालेट पथ",
"description": "Our super admin creates the integration site in admin. Secrets are shown only once — save immediately.",
"weProvide": "After site creation we provide",
"youProvide": "Partner fills / provides",
"defaultPaths": "Default wallet paths",
"envMapping": "Config mapping",
"note": "परीक्षण/प्रोडक्सन अलग। गोप्य मुख्य साइट .env मा म्यानुअल। स्थानीयमा lottery .env MAIN_SITE_* fallback।",
"adminSop": "Admin provisioning (our super admin)",
"network": "Network requirements",
"note": "Test and production site_code, secrets, and domains must be fully isolated. Secrets go in main-site server config — not auto-synced from admin.",
"receiveRows": [
["site_code", "साइट कोड"],
["sso_jwt_secret", "JWT हस्ताक्षर गोप्य (मुख्य साइट)"],
["wallet_api_key", "वालेट कलब्याक auth (मुख्य साइट जाँच)"],
["lottery_h5_base_url", "लटरी प्रवेश URL"]
["site_code", "Site code written into JWT"],
["sso_jwt_secret", "JWT signing secret (main site holds)"],
["wallet_api_key", "Wallet callback auth (main site verifies)"],
["lottery_h5_base_url", "Lottery H5 entry URL"]
],
"provideRows": [
["wallet_api_url", "HTTPS वालेट आधार URL"],
["परीक्षण खाता", "site_player_id + सुरु ब्यालेन्स"],
["iframe origin", "एम्बेड गर्दा मुख्य origin"]
["wallet_api_url", "Partner wallet gateway HTTPS root (no path suffix)"],
["iframe_allowed_origins", "Main-site origin allowlist (iframe mode)"],
["Test accounts", "site_player_id list + initial balance"]
],
"pathRows": [
["GET", "/wallet/balance", "ब्यालेन्स"],
["POST", "/wallet/debit-for-lottery", "डेबिट"],
["POST", "/wallet/credit-from-lottery", "क्रेडिट"]
["GET", "/wallet/balance", "Balance query"],
["POST", "/wallet/debit-for-lottery", "Debit (transfer-in)"],
["POST", "/wallet/credit-from-lottery", "Credit (transfer-out)"]
],
"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", "सन्दर्भ कार्यान्वयन मात्र"]
["site_code", "code", "MAIN_SITE_CODE", "JWT and player provisioning; must match both sides"],
["SSO secret", "sso_jwt_secret", "MAIN_SITE_SSO_JWT_SECRET", "Main site signs JWT; lottery verifies"],
["Wallet auth", "wallet_api_key", "MAIN_SITE_WALLET_API_KEY", "Lottery sends Bearer on callback"],
["Wallet root URL", "wallet_api_url", "(main site deploy)", "Partner HTTPS root; lottery appends /wallet/*"],
["Lottery API", "—", "—", "Default https://lotterylaravel.tanumo.com; player/wallet API root"],
["Lottery H5", "lottery_h5_base_url", "(main site iframe src)", "Default https://front.tanumo.com"],
["iframe allowlist", "iframe_allowed_origins", "(main site origin)", "Must match actual main-site origin"]
],
"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"
"Super admin → Config → Integration sites",
"Create site: site_code, name, default currency",
"Fill wallet_api_url (HTTPS root), lottery_h5_base_url, iframe_allowed_origins",
"Save sso_jwt_secret, wallet_api_key shown once after creation",
"Deliver secrets securely to partner; partner configures main-site server",
"Run connectivity test on site list (probes GET /wallet/balance)"
],
"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", "एक पटक", "—"]
["code", "Site code for JWT site_code", "partner_demo"],
["wallet_api_url", "Partner wallet gateway HTTPS root", "https://wallet.partner.com"],
["lottery_h5_base_url", "Lottery H5 entry", "https://front.tanumo.com"],
["iframe_allowed_origins", "Allowed parent origins", "https://www.partner.com"],
["sso_jwt_secret", "Shown once after creation", "—"],
["wallet_api_key", "Shown once after creation", "—"]
],
"network": "Network",
"networkItems": [
"वालेट कलब्याक server-to-server",
"प्रोडक्सन: HTTPS सार्वजनिक मात्र",
"पथ: /wallet/balance, debit, credit",
"timeout ≤ 10s"
"Wallet callbacks originate from lottery server (not player browser); gateway must be reachable from lottery servers",
"Production wallet_api_url must be public HTTPS (no localhost / private IP)",
"Default paths /wallet/balance, /wallet/debit-for-lottery, /wallet/credit-from-lottery (path prefix configurable in admin)",
"Recommend timeout ≤ 10 seconds; timeout may leave transfer in pending reconciliation"
]
},
"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 दोहोर्याउनुहोस्।",
"description": "HS256 JWT. Main site signs; lottery verifies. Entry: URL ?token= or iframe postMessage.",
"claims": "JWT claims",
"sign": "Signing example (Node.js)",
"entryA": "Method A — URL redirect",
"entryB": "Method B — iframe postMessage",
"noExchangeNote": "Lottery has no token-exchange login API. Main site issues JWT after login; player APIs use Authorization: Bearer with the same JWT. First valid GET /api/v1/player/me auto-provisions the player — no separate login call.",
"entryApi": "Verify and provision",
"entryApiNote": "Optional: main site may call GET /api/v1/player/me once server-side after login as a pre-check. Daily play (transfer, bet) is called by lottery H5 with JWT — main site does not integrate those APIs.",
"publicApis": "Public APIs (no token)",
"h5ScopeNote": "Transfer, bet, in-lottery balance queries are called by our H5 with JWT — outside main-site integration scope. Main site only: ① issue JWT ② implement wallet gateway.",
"refreshNote": "iframe refresh: on LOTTERY_TOKEN_NEEDED or LOTTERY_TOKEN_REFRESH_REQUEST, re-issue JWT and send MAIN_REFRESH_TOKEN. See iframe protocol page.",
"authResponse": "Auth failure example",
"errors": "SSO error codes",
"iframeNote": "Configure iframe_allowed_origins. Pass token via postMessage top-level field token — not inside payload.",
"claimRows": [
["site_code", "string", "Y", "इन्टिग्रेसन साइट कोड"],
["site_player_id", "string", "Y", "स्थिर मुख्य साइट प्रयोगकर्ता ID"],
["iat", "number", "Y", "जारी समय (सेकेन्ड)"],
["exp", "number", "Y", "म्याद (सेकेन्ड); ≤ 300s"]
["site_code", "string", "Y", "Integration site code matching admin"],
["site_player_id", "string", "Y", "Stable main-site user ID"],
["iat", "number", "Y", "Issued at (Unix seconds)"],
["exp", "number", "Y", "Expiry (Unix seconds); exp - iat ≤ 300"]
],
"messageRows": [
["→ मुख्य", "LOTTERY_READY", "चाइल्ड तयार"],
["→ मुख्य", "LOTTERY_TOKEN_NEEDED", "रिफ्रेस अनुरोध"],
["→ लटरी", "MAIN_INIT_TOKEN", "{ token }"],
["→ लटरी", "MAIN_REFRESH_TOKEN", "{ token }"]
["→ main", "LOTTERY_READY", "Child ready; requests token"],
["→ main", "LOTTERY_TOKEN_NEEDED", "Token invalid; requests refresh"],
["→ lottery", "MAIN_INIT_TOKEN", "Top-level token field"],
["→ lottery", "MAIN_REFRESH_TOKEN", "Top-level token field"]
],
"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"]
["GET", "/api/v1/player/ping", "Player API connectivity probe"],
["GET", "/api/v1/integration/runtime-origins", "iframe allowed embed origins"]
],
"errorRows": [
["8001", "Authorization छैन"],
["8002", "JWT अमान्य वा म्याद सकियो"],
["8003", "प्लेयर छैन"],
["8004", "SSO गोप्य सेट छैन"],
["8005", "खाता निलम्बित"]
["8001", "Missing Authorization header"],
["8002", "JWT invalid or expired (wrong secret, exp passed, bad signature)"],
["8003", "Player not provisioned (SSO me auto-provisions; rare in normal flow)"],
["8004", "SSO secret not configured (site issue — contact us)"],
["8005", "Account suspended (site disabled or player frozen)"]
]
},
"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।",
"description": "postMessage contract when main site embeds lottery H5. Skip this chapter if using URL ?token= redirect only.",
"sequence": "Recommended sequence",
"envelopeSection": "Message format (note direction differences)",
"childMessages": "Lottery → main",
"parentMessages": "Main → lottery",
"example": "Main-site integration example",
"targetOrigin": "targetOrigin security",
"envelopeNote": "Common mistake: putting token in payload.token. Lottery H5 reads data.token at the message top level.",
"targetOriginNote": "postMessage second argument must be lottery H5 origin (default https://front.tanumo.com), never *. Main site validates event.origin; iframe_allowed_origins is the main-site origin.",
"timingNote": "After MAIN_INIT_TOKEN the lottery child does not send LOTTERY_READY again. Refresh: LOTTERY_TOKEN_NEEDED → main sends MAIN_REFRESH_TOKEN (top-level token).",
"sequenceSteps": [
"iframe embed",
"LOTTERY_READY",
"MAIN_INIT_TOKEN",
"/player/me",
"LOTTERY_TOKEN_NEEDED → MAIN_REFRESH_TOKEN"
"Embed <iframe src=\"{lottery_h5_base_url}\">",
"Lottery H5 validates allowlist and sends LOTTERY_READY",
"Main listens for message, validates origin, sends MAIN_INIT_TOKEN (top-level token)",
"Lottery H5 stores token, calls GET /api/v1/player/me to enter",
"JWT nearing expiry: lottery sends LOTTERY_TOKEN_NEEDED → main sends MAIN_REFRESH_TOKEN"
],
"envelopeRows": [
["Lottery → main", "type + payload + timestamp", "e.g. LOTTERY_READY; business data in payload"],
["Main → lottery", "type + token + timestamp", "token must be top-level, not nested in payload"]
],
"childMessageRows": [
["→ मुख्य", "LOTTERY_READY", "तयार"],
["→ मुख्य", "LOTTERY_TOKEN_NEEDED", "रिफ्रेस"],
["→ मुख्य", "LOTTERY_TOKEN_REFRESH_REQUEST", "सक्रिय रिफ्रेस"],
["→ मुख्य", "LOTTERY_HEARTBEAT", "हार्टबिट"],
["→ मुख्य", "LOTTERY_TOKEN_REFRESHED", "रिफ्रेस सफल"]
["→ main", "LOTTERY_READY", "Child ready; requests token"],
["→ main", "LOTTERY_TOKEN_NEEDED", "Token invalid; requests refresh"],
["→ main", "LOTTERY_TOKEN_REFRESH_REQUEST", "Active refresh request"],
["→ main", "LOTTERY_HEARTBEAT", "Heartbeat (may ignore)"],
["→ main", "LOTTERY_TOKEN_REFRESHED", "Refresh succeeded (child → main)"]
],
"parentMessageRows": [
["→ लटरी", "MAIN_INIT_TOKEN", "{ token }"],
["→ लटरी", "MAIN_REFRESH_TOKEN", "{ token }"],
["→ लटरी", "MAIN_REQUEST_STATUS", "स्थिति"],
["→ लटरी", "MAIN_NAVIGATE", "{ path }"]
["→ lottery", "MAIN_INIT_TOKEN", "First delivery; top-level token field"],
["→ lottery", "MAIN_REFRESH_TOKEN", "Refresh; top-level token field"],
["→ lottery", "MAIN_REQUEST_STATUS", "Request child status"],
["→ lottery", "MAIN_NAVIGATE", "Navigate to 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",
"description": "Implemented by partner. Called by lottery server (not player browser). Auth: Authorization: Bearer {wallet_api_key}.",
"balance": "Query balance",
"debit": "Debit (transfer-in)",
"credit": "Credit (transfer-out)",
"response": "Response example",
"httpContract": "HTTP contract",
"httpErrors": "HTTP errors",
"creditNote": "Body debit जस्तै; transfer-out वा refund।",
"idempotentNote": "idempotent_key: एउटै key + operation ले पहिलो JSON (HTTP 200); दोहोरो लेखा निषेध।",
"creditNote": "Same body as debit; used for transfer-out or rollback credit.",
"idempotentNote": "idempotent_key: same key + same amount must return first JSON (HTTP 200), no double booking; same key different amount → success: false.",
"queryRows": [
["site_code", "string", ""],
["site_player_id", "string", ""],
["currency_code", "string", ""]
["site_code", "string", "Site code"],
["site_player_id", "string", "Main-site user ID"],
["currency_code", "string", "Currency code"]
],
"fieldRows": [
["site_code", "string", ""],
["site_player_id", "string", ""],
["player_id", "number", "लटरी प्लेयर ID"],
["currency_code", "string", ""],
["amount_minor", "integer", "धनात्मक minor"],
["idempotent_key", "string", "इडेम्पोटेन्सी"]
["site_code", "string", "Site code"],
["site_player_id", "string", "Main-site user ID"],
["player_id", "number", "Lottery player ID (reference)"],
["currency_code", "string", "Currency code"],
["amount_minor", "integer", "Positive minor integer"],
["idempotent_key", "string", "Idempotency key, globally unique"]
],
"httpErrorRows": [
["401", "unauthorized", "API Key गलत"],
["422", "invalid request", "फिल्ड/रकम गलत"],
["409", "main balance insufficient", "व्यापार अस्वीकार; data.main_balance हुन सक्छ"]
["401", "unauthorized", "wallet_api_key wrong or missing"],
["422", "invalid request", "Missing fields or invalid amount_minor"],
["409", "main balance insufficient", "Business rejection e.g. insufficient 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"]
["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"],
["Auth failure", "401", "success: false; message: unauthorized"],
["Business rejection", "409", "success: false; message explains reason"],
["Idempotent replay", "200", "Identical to first success/rejection response"]
]
},
"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",
"description": "For understanding how funds move between sides — not a partner integration surface.",
"outOfScopeNote": "Partner does not implement these APIs. Transfer-in/out is called by our H5 with player JWT; partner only implements wallet gateway debit/credit.",
"requestFields": "Request fields",
"transferIn": "Transfer-in (main → lottery)",
"transferOut": "Transfer-out (lottery → main)",
"transferResponse": "Success response",
"errors": "Common error codes",
"inNote": "Flow: player H5 initiates transfer-in → lottery calls main debit-for-lottery → lottery balance credited.",
"outNote": "Flow: player H5 initiates transfer-out → lottery debits → lottery calls main credit-from-lottery.",
"responseNote": "Transfer-in and transfer-out share the same response shape; direction is in / out. Idempotent replay returns same data.",
"requestFieldRows": [
["amount", "integer", "धनात्मक minor"],
["currency", "string", "वैकल्पिक; default_currency"],
["idempotent_key", "string", "अद्वितीय; retry एउटै नतिजा"]
["amount", "integer", "Positive minor integer"],
["currency", "string", "Optional; defaults to player default_currency"],
["idempotent_key", "string", "Globally unique; retries must return same result"]
],
"errorRows": [
["1001", "लटरी ब्यालेन्स अपर्याप्त (transfer-out)"],
["1009", "मुख्य वालेट असफल"],
["1010", "इडेम्पोटेन्सी द्वन्द्व"],
["2003", "पहिले transfer-in"]
["1001", "Insufficient lottery balance (transfer-out)"],
["1009", "Main wallet processing failed (gateway unreachable, 401, timeout, etc.)"],
["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": "एउटै idempotent_key ले एउटै नतिजा; फरक रकम → 1010।",
"sso": "SSO auth",
"lotteryWallet": "Lottery wallet / transfer",
"gateway": "Partner wallet gateway (HTTP)",
"idempotentNote": "Idempotency: same idempotent_key + same amount → same result; same key different amount → 1010 or success:false.",
"ssoRows": [
["8001", "Authorization छैन"],
["8002", "JWT अमान्य वा म्याद सकियो"],
["8003", "प्लेयर छैन"],
["8004", "SSO गोप्य सेट छैन"],
["8005", "खाता निलम्बित"]
["8001", "Missing Authorization header"],
["8002", "JWT invalid or expired"],
["8003", "Player not provisioned (SSO me auto-provisions in normal flow)"],
["8004", "SSO secret not configured"],
["8005", "Account suspended"]
],
"lotteryRows": [
["1001", "लटरी ब्यालेन्स अपर्याप्त"],
["1009", "मुख्य वालेट असफल"],
["1010", "इडेम्पोटेन्सी द्वन्द्व"],
["2003", "पहिले transfer-in"]
["1001", "Insufficient lottery balance"],
["1009", "Main wallet processing failed"],
["1010", "Idempotency conflict"],
["2003", "Transfer in before betting"]
],
"gatewayRows": [
["401", "unauthorized", "API Key गलत"],
["422", "invalid request", "फिल्ड/रकम गलत"],
["409", "—", "व्यापार अस्वीकार"]
["401", "unauthorized", "API Key wrong"],
["422", "invalid request", "Invalid field or amount"],
["409", "—", "Business rejection (e.g. insufficient balance)"]
]
},
"troubleshooting": {
"title": "Troubleshooting",
"description": "Match symptoms below. If still stuck, contact integration support with site_code, timestamp, and request ID.",
"faq": "FAQ",
"jwt": "JWT / entry",
"iframe": "iframe",
"wallet": "Wallet gateway",
"note": "During integration, verify JWT (/player/me) and wallet (/wallet/balance) with curl separately before full iframe flow.",
"faqRows": [
["curl /player/me returns 8002", "Check sso_jwt_secret matches admin site; site_code matches; exp not passed (≤300s)"],
["iframe blank / no hall", "Check token is postMessage top-level; MAIN_INIT_TOKEN after LOTTERY_READY; iframe_allowed_origins includes main origin"],
["Transfer-in fails 1009", "Can lottery server reach wallet_api_url; wallet_api_key correct; debit returns 200 + success:true"],
["Connectivity test fails", "wallet_api_url must be public HTTPS; GET /wallet/balance reachable with Bearer wallet_api_key"],
["8005 account frozen", "Check integration site enabled in admin; site_player_id not frozen"]
],
"jwtRows": [
["8002 Token invalid", "Wrong secret / exp passed / wrong site_code / algorithm not HS256"],
["8004 SSO not configured", "Contact us to confirm site sso_jwt_secret is set"],
["me code=0 but H5 still fails", "iframe token differs from curl test token, or token nested in payload not top-level"]
],
"iframeRows": [
["No LOTTERY_READY", "iframe src is lottery_h5_base_url; H5 finished loading"],
["postMessage no response", "targetOrigin is lottery H5 origin (not main-site origin)"],
["Repeated token spam", "After MAIN_INIT_TOKEN child should not send LOTTERY_READY again"],
["Refresh fails", "Main listens for LOTTERY_TOKEN_NEEDED and replies MAIN_REFRESH_TOKEN"]
],
"walletRows": [
["401 unauthorized", "wallet_api_key does not match admin site"],
["409 insufficient balance", "Test account main_balance too low; check amount_minor units"],
["Double debit", "idempotent_key not idempotent; same key must return first response"],
["Lottery never calls debit", "wallet_api_url unreachable or connectivity test not passed"]
]
},
"golive": {
"title": "Go-live",
"checklist": "Checklist",
"description": "Before production release, confirm integration passed with account manager and complete checks below.",
"deliveryChecklist": "Delivery and process",
"checklist": "Technical checklist",
"deliveryItems": [
"Production integration site created separately (site_code, secrets isolated from staging)",
"Secrets stored securely in main-site production config (not frontend)",
"wallet_api_url is production public HTTPS; connectivity test passed",
"iframe_allowed_origins includes production main-site origin",
"Full integration log archived (transfer-in → bet → settlement → transfer-out)"
],
"items": [
"परीक्षण/प्रोडक्सन site_code, गोप्य, डोमेन अलग",
"JWT सर्वर-साइड मात्र, TTL ≤ 5min",
"वालेट HTTPS, timeout ≤ 10s",
"idempotent_key इडेम्पोटेन्सी",
"iframe: iframe_allowed_origins",
"पूर्ण: transfer-in → bet → settle → transfer-out"
"JWT server-side only, TTL ≤ 5 minutes",
"Wallet three endpoints HTTPS, timeout ≤ 10s, idempotency implemented",
"iframe: postMessage token top-level, origin validation enabled",
"Test vs production: site_code, secrets, domains fully separated",
"Monitoring: wallet gateway 4xx/5xx and debit/credit log alerts",
"Rollback plan: can temporarily disable integration site"
]
}
}

View File

@@ -1,5 +1,6 @@
{
"title": "खेलाडी",
"pageGuide": "वालेट र क्रेडिट खेलाडी एकै सूची; विवरण funding_mode अनुसार।",
"detailTitle": "खेलाडी विवरण",
"listTitle": "खेलाडी सूची",
"viewDetail": "विवरण हेर्नुहोस्",

View File

@@ -1,6 +1,7 @@
{
"title": "रिपोर्ट",
"subtitle": "सञ्चालन, वित्त, जोखिम र अडिट रिपोर्टहरू एउटै ठाउँबाट फिल्टर गरी निर्यात गर्नुहोस्।",
"pageGuide": "ड्र, खेलाडी, प्ले अनुसार P&L र जोखिम; निर्यात अनुमति चाहिन्छ।",
"exportPanel": "निर्यात सेटअप",
"chooseReport": "निर्यात गर्ने रिपोर्ट छान्नुहोस्",
"libraryTitle": "रिपोर्ट प्रकार",

View File

@@ -17,6 +17,8 @@
"allPoolsPageTitle": "सबै जोखिम पूल",
"sourceReasonOptions": {
"ticket_place": "बेट अकुपेन्सी",
"ticket_rollback": "टिकट रोलब्याक",
"ticket_failed_line": "असफल बेट रिलिज",
"admin_manual_close": "म्यानुअल बन्द",
"admin_manual_recover": "म्यानुअल पुनर्स्थापना"
},
@@ -25,6 +27,7 @@
"riskFilter": "जोखिम फिल्टर",
"sort": "क्रमबद्ध",
"filterAll": "सबै",
"filterActive": "लक / उच्च जोखिम",
"filterSoldOut": "बिक्री समाप्त",
"filterHighRisk": ">80%",
"sortUsageDesc": "प्रयोग अनुपात ↓",
@@ -67,6 +70,15 @@
"playCode": "प्ले",
"loadLogsFailed": "लक लग लोड असफल भयो",
"lockLogsTitle": "जोखिम लक लग",
"lockLogsGroupedHint": "पूर्वनिर्धारित रूपमा टिकट अनुसार समूह। विस्तारित प्लेको संयोजन टिकट विवरणमा हेर्नुहोस्।",
"groupBy": "दृश्य",
"groupByTicket": "टिकट अनुसार",
"groupByEntry": "नम्बर अनुसार",
"combinationCount": "संयोजन",
"lockReleaseSummary": "लक / रिलिज पङ्क्ति",
"viewDetail": "विवरण",
"viewTicket": "टिकट",
"number": "बेट नम्बर",
"drawInfoLoadFailed": "ड्रअ जानकारी लोड असफल भयो",
"loadingDraw": "ड्रअ लोड हुँदैछ…",
"headerTitle": "जोखिम · ड्रअ {{drawNo}}",

View File

@@ -1,6 +1,7 @@
{
"title": "सेटलमेन्ट केन्द्र",
"subtitle": "अवधि बन्द, बिल पुष्टि र भुक्तानी",
"pageGuide": "क्रेडिट एजेन्ट अवधि: साइट वित्तले खोल/बन्द; बाँधिएका एजेन्टले आफ्नो बिल मात्र।",
"subtitleList": "अवधि सूची: खोल्नुहोस्/बन्द गर्नुहोस्; सारांश स्तम्भहरू समावेश — बिल र बेट लेजरका लागि पङ्क्ति कार्य प्रयोग गर्नुहोस्।",
"period": {
"title": "Period",

View File

@@ -43,5 +43,16 @@
"settled_lose": "हार सेटल भयो",
"refunded": "फिर्ता भयो"
},
"allTickets": "सबै टिकट"
"allTickets": "सबै टिकट",
"detailTitle": "टिकट विवरण",
"detailLoadFailed": "टिकट विवरण लोड असफल",
"loadingDetail": "लोड हुँदै…",
"backToList": "टिकट सूचीमा फर्कनुहोस्",
"viewTicketDetail": "टिकट विवरण हेर्नुहोस्",
"combinationCount": "विस्तारित संयोजन",
"combinationsTitle": "संयोजन विवरण",
"combinationsHint": "एक बेट लाइन धेरै 4D नम्बरमा विस्तार हुन सक्छ।",
"number4d": "4D नम्बर",
"comboBetAmount": "संयोजन बेट",
"comboEstimatedPayout": "अनुमानित भुक्तानी रिजर्भ"
}

View File

@@ -0,0 +1,637 @@
{
"shell": {
"title": "后台运营手册",
"integrationDocs": "API 对接文档",
"adminLogin": "管理后台"
},
"nav": {
"gettingStarted": "入门",
"operations": "核心运营",
"management": "经营管理",
"finance": "资金与注单",
"platform": "平台配置",
"reference": "参考",
"overview": "总览",
"roles": "角色与权限",
"siteSetup": "接入站点",
"draws": "期号与开奖",
"settlementCenter": "结算中心",
"agents": "代理体系",
"players": "玩家管理",
"tickets": "注单查询",
"wallet": "钱包与对账",
"config": "规则与风控",
"reports": "报表中心",
"fundOperations": "资金操作详解",
"manualReview": "人工审核与派彩",
"faq": "常见问题"
},
"headers": {
"role": ["角色", "主要职责", "典型权限"],
"status": ["状态", "含义", "可执行操作"],
"module": ["模块", "说明"],
"field": ["字段", "说明", "示例"],
"billStatusTable": ["账单状态", "含义", "后续操作"],
"faq": ["问题", "处理方式"],
"report": ["报表", "用途"],
"menu": ["侧栏分组", "菜单", "典型使用者", "说明"],
"ledger": ["流水类型", "何时发生", "对玩家资金的影响", "在哪里查看"],
"walletTxn": ["钱包流水类型", "何时发生", "说明"],
"compare": ["环节", "钱包盘", "信用盘"],
"reconcile": ["情况", "处理方式", "说明"],
"setting": ["系统设置项", "位置", "开启时", "关闭时"],
"batchStatus": ["批次状态", "含义", "可执行操作"]
},
"pages": {
"overview": {
"title": "后台运营总览",
"description": "面向贵司超级管理员、站点运营与代理经营人员。本手册按后台实际菜单组织,含逐步操作说明;钱包盘技术对接请参阅 API 对接文档。",
"loginNote": "后台登录https://lotteryadmin.tanumo.com/admin请使用贵司或我方分配的运营账号",
"scope": "系统能力",
"scopeItems": [
"期号管理:生成计划、封盘、开奖审核与派彩结算",
"玩家与注单:搜索、冻结、查看流水与注单历史",
"规则配置:玩法开关、赔率、封顶、奖池(平台超管)",
"结算中心:信用盘账期开账/关账、账单确认与收付登记",
"代理体系:代理树、占成/授信/回水、下级经营账号",
"钱包与对账:主站划转流水、掉单处理(钱包盘)",
"报表:盈亏汇总、风险占用、异步导出"
],
"menuMap": "后台菜单地图",
"menuMapNote": "实际可见菜单取决于账号角色;绑定代理账号看不到「钱包流水」「对账」等平台财务菜单。",
"menuMapRows": [
["总览", "仪表盘", "全员", "登录后首页,查看当期运营概览"],
["代理组织", "代理线路", "超管", "为信用盘客户开通独立站点与根代理(钱包盘通常只需「接入站点」)"],
["代理组织", "代理列表", "站点运营 / 代理", "维护代理树、占成/授信/回水、下级账号"],
["代理组织", "结算中心", "站点财务 / 绑定代理", "信用盘账期、账单确认、登记收付"],
["日常运营", "期号列表", "平台超管", "期号计划、开奖审核、重开(钱包/信用共用)"],
["日常运营", "注单列表", "运营 / 客服 / 代理", "按玩家、期号、玩法检索注单"],
["日常运营", "玩家列表", "站点运营 / 代理", "建档、冻结、查看流水与注单"],
["日常运营", "结算", "平台运营", "单期开奖后的派彩结算批次(非信用账期)"],
["资金与报表", "钱包流水", "平台 / 站点财务", "钱包盘:主站转入转出与彩票内余额变动"],
["资金与报表", "对账", "平台 / 站点财务", "钱包盘:处理长时间 pending 的转账单"],
["资金与报表", "报表中心", "运营 / 财务 / 代理", "盈亏、玩家输赢、风险、导出"],
["平台管理", "投注规则 / 赔率 / 限额 / 风控 / 奖池", "超管", "全站玩法与风险参数(站点运营通常无此菜单)"],
["平台管理", "接入站点 / 管理列表 / 角色管理", "超管", "站点密钥、后台账号与权限分配"]
],
"readingOrder": "建议阅读顺序",
"readingItems": [
"角色与权限 → 确认贵司账号能操作哪些功能",
"钱包盘超管:接入站点 → API 对接文档联调 → 期号与开奖 → 钱包与对账",
"信用盘站点运营:代理体系 → 玩家管理 → 结算中心(开账/关账/收付)",
"绑定代理经营账号:代理列表 → 玩家管理 → 结算中心(仅本线账单收付)",
"资金语义与流水类型 → 资金操作详解",
"开奖审核、派彩批次 → 人工审核与派彩"
],
"modes": "两种玩家模式",
"modeRows": [
["钱包盘", "贵司主站 SSO 进场;资金经贵司钱包网关划转;运营查「钱包流水」「对账」。技术细节见 API 对接文档"],
["信用盘", "彩票端账号或代理建档;下注占用授信;输赢在「结算中心」账期结算,不涉及主站钱包"]
],
"note": "信用占成账单按下注时记录的占成比例结算;已关账的历史账期不会因后来改代理占成而重算。"
},
"roles": {
"title": "角色与权限",
"description": "后台按「账号 → 角色 → 功能权限」管理。绑定代理的经营账号只能看自己代理线下的数据;站点管理员绑定单站,不含开奖赔率等平台技术菜单。",
"matrix": "常见角色",
"matrixRows": [
["超级管理员", "全库最高权限:接入站点、开奖干预、代理线路开通、全局配置", "系统内置超管;不按站点角色限制"],
["站点管理员", "单站信用盘运营:代理树、玩家、结算、注单、报表", "绑定某站点;不含投注规则/赔率等平台配置"],
["绑定代理经营账号", "管理本代理线下的下级与玩家;账期收付(仅作为收款方的账单)", "绑定到具体代理节点;不能开账/关账"],
["风控 / 财务 / 客服", "按岗位查看或操作对应菜单", "由超管在「角色管理」中分配功能权限"]
],
"accountModel": "账号与权限模型",
"accountItems": [
"「管理列表」(平台管理):创建后台账号,绑定角色或站点/代理",
"「角色管理」(平台管理):勾选菜单与操作权限;账号不直接挂权限",
"站点管理员创建玩家时须选择所属代理,不能默认挂在根代理下",
"绑定代理的主账号可做本线结算收付;须同时满足「账单收款方」「直属玩家/账单方」等规则",
"仅查看权限时,登记收付、确认账单、开账关账等按钮不会显示(非报错)"
],
"accountSetup": "新建运营账号(超管操作)",
"accountSetupSteps": [
"登录后台 → 平台管理 →「管理列表」→「新建账号」",
"填写登录名、初始密码、显示名称",
"选择绑定方式:① 绑定站点角色(站点管理员)② 绑定代理节点(代理经营账号)③ 仅绑定角色(平台岗位)",
"站点管理员:在「站点角色」中选择目标接入站",
"代理经营账号:在「代理绑定」中选择具体代理节点(数据范围=该节点及下级)",
"保存后通知对方修改初始密码;若看不到预期菜单,到「角色管理」检查角色是否勾选对应菜单"
],
"note": "无接入站点时,依赖站点的写操作会提示创建站点(仅超管可见入口)。"
},
"siteSetup": {
"title": "接入站点(超管)",
"description": "钱包盘须先创建接入站点,保存 SSO 与钱包密钥,并由贵司技术写入主站服务端。信用盘代理线路开通时会同步创建站点。",
"path": "创建与配置步骤",
"pathItems": [
"登录 https://lotteryadmin.tanumo.com/admin → 平台管理 →「接入站点」",
"点击「新建站点」,填写站点编码(与贵司 JWT 中 site_code 一致)、名称、状态",
"创建成功后**立即复制**页面一次性展示的 SSO 密钥、钱包 API 密钥(关闭后无法再次查看)",
"编辑站点填写贵司钱包网关地址HTTPS 根地址,不含路径)、彩票 H5 入口、iframe 白名单(每行一个 origin",
"点击「连通性测试」,确认贵司余额查询接口返回 success",
"将密钥与站点编码交给贵司技术,写入主站服务端配置;字段对照与 curl 示例见 API 对接文档"
],
"fieldRows": [
["站点编码", "写入贵司签发 JWT 的 site_code双方须一致", "demo"],
["钱包网关地址", "贵司 HTTPS 根地址(不含路径)", "https://wallet.贵司域名.com"],
["彩票 H5 入口", "跳转或 iframe 嵌入地址", "https://front.tanumo.com"],
["iframe 白名单", "允许嵌入贵司页面的域名,每行一个", "https://www.贵司域名.com"],
["SSO 密钥", "创建后仅展示一次;贵司主站用来签发 JWT", "—"],
["钱包 API 密钥", "创建后仅展示一次;彩票回调贵司钱包接口时校验", "—"]
],
"fields": "关键字段",
"caution": "注意事项",
"cautionItems": [
"测试与生产的站点编码、密钥、域名须完全隔离",
"生产环境钱包网关须为公网 HTTPS 地址",
"密钥须由贵司技术自行写入主站服务端,后台不会自动同步到贵司系统",
"联调与上线清单见 API 对接文档 → 接入交付"
],
"apiLinkNote": "钱包网关字段、curl 自测与 iframe 协议详见",
"apiLinkLabel": "API 对接文档 → 接入配置"
},
"draws": {
"title": "期号与开奖",
"description": "期号是下注的基本单位。列表时间按本地时区展示,服务器按 UTC 存储。大厅是否可下注由系统实时判定;列表展示数据库 status可能与玩家端倒计时略有差异。",
"lifecycle": "期号状态",
"statusRows": [
["未开始", "尚未到达开始时间", "编辑、删除(无注单时)"],
["可下注", "玩家可下注", "编辑部分时间、取消(无注单时)"],
["封盘中", "已停止接收新注单", "等待开奖"],
["待开奖 / 开奖中", "等待或正在生成结果", "进入审核与发布"],
["待审核", "冷静期内可复核", "审核通过、重开/回滚"],
["结算中", "正在计算中奖与派彩", "—"],
["已结算", "本期注单处理完毕", "查看财务与风控"],
["已取消", "管理员取消", "记录保留,已下注退本"]
],
"workflow": "日常流程(平台超管)",
"workflowItems": [
"日常运营 →「期号列表」→「批量生成期开奖计划」:按配置间隔自动生成未来期号",
"必要时「手动创建期号」或编辑未开始/可下注期的时间",
"到达封盘时间后系统自动封盘;进入开奖:生成结果 → 冷静期审核 → 发布 → 自动结算",
"期号详情页可查看:财务汇总、号码占用、开奖批次、关联注单"
],
"publishWalkthrough": "开奖发布逐步操作",
"publishSteps": [
"期号列表 → 点击目标期号进入详情",
"确认 status 为「待开奖」或「待审核」",
"若尚未生成结果:点击「生成开奖结果」",
"冷静期内复核号码无误 → 点击「审核通过并发布」",
"发布后系统自动进入「结算中」→「已结算」;钱包盘玩家余额即时派彩,信用盘玩家释额记入账期流水",
"若发布后发现问题:冷静期内超管可「重开」(见下节)"
],
"reopenWalkthrough": "重开与回滚(仅超管,冷静期内)",
"reopenSteps": [
"期号详情 →「重开/回滚」",
"系统回滚已派彩/已释额 → 重新生成结果 → 再次审核发布 → 重新结算",
"操作会写入审计日志;请与贵司财务确认后再执行"
],
"rules": "规则与风控(关联模块,超管)",
"rulesItems": [
"平台管理 → 投注规则:玩法开关、最小/最大下注额",
"平台管理 → 赔率与回水:仅影响后续新注单",
"平台管理 → 限额版本:号码封顶;售罄后禁止下注",
"平台管理 → 风控中心:按号码查看占用与赔付池;占用流水默认按注单聚合"
],
"note": "封盘后不可再修改时间。取消期号仅适用于可下注/封盘且无注单的情况。",
"manualReviewLinkNote": "开奖人工审核、冷静期、派彩结算批次详见",
"manualReviewLinkLabel": "人工审核与派彩"
},
"settlementCenter": {
"title": "结算中心(信用盘)",
"description": "管理代理账期:开账 → 关账生成账单 → 确认 → 登记收付。绑定代理仅可查看与操作本线相关账单。钱包盘玩家不涉及此模块。",
"entry": "入口与权限",
"entryItems": [
"代理组织 →「结算中心」:账期列表",
"开账 / 关账:仅**未绑定代理**的站点财务账号(通常为贵司财务岗)",
"绑定代理:不能开账/关账;只能处理自己作为**收款方**的账单",
"须具备结算「写」权限,才显示登记收付、确认账单等按钮;仅查看时操作区隐藏"
],
"periodFlow": "账期生命周期",
"periodItems": [
"开账:创建「进行中」账期,开始累计占成流水与玩家信用变动",
"账期内:玩家下注占用授信、中奖释额、回水等写入流水",
"关账:汇总账期内数据,生成玩家账单与代理账单;**不可撤销**",
"关账前须完成本期相关期号开奖结算;仍有未结算注单时会提示",
"关账后:代理/财务确认账单 → 登记实收实付 → 账单结清"
],
"openWalkthrough": "开账步骤(站点财务)",
"openSteps": [
"结算中心 → 账期列表 →「开账」",
"填写账期名称、起止日期(按贵司与代理约定)",
"确认当前无其他「进行中」账期冲突 → 提交",
"开账成功后,新下注与释额会计入本账期"
],
"closeWalkthrough": "关账步骤(站点财务)",
"closeSteps": [
"确认本期所有期号均已「已结算」",
"结算中心 → 选中进行中账期 →「关账」",
"系统汇总占成流水,生成玩家账单(直属玩家)与代理账单(各级占成)",
"关账完成后账单状态为「待确认」;历史占成按**下注时快照**,不按当前代理档案重算"
],
"paymentWalkthrough": "确认与登记收付",
"paymentSteps": [
"结算中心 → 账期详情 →「账单」Tab → 打开目标账单",
"收款方账号点击「确认账单」(状态:待确认 → 已确认)",
"点击「登记收付」,填写实收/实付金额、方式、备注",
"可多次登记直至「已结清」;部分付款时状态为「部分已付」",
"坏账核销、补差冲正仅站点财务(未绑定代理)可操作"
],
"detailTabs": "账期详情三个 Tab",
"detailTabItems": [
"账单:玩家账单与代理账单列表,可进入单张账单确认/收付",
"收付与调账:登记收付、坏账、补差等**操作台账**(按操作记录)",
"账务流水:玩家**信用额度**变动(下注占用、中奖释额、账期收付记账等)"
],
"billStatusSection": "账单状态",
"billStatusRows": [
["待确认", "关账后待代理或财务确认", "确认账单"],
["已确认", "金额已确认", "登记收付"],
["部分已付", "尚有未付金额", "继续收付"],
["已逾期", "超过约定未结清", "催收或坏账处理"],
["已结清", "收付完成", "归档查阅"]
],
"operations": "权限与范围补充",
"operationItems": [
"登记收付:仅账单**收款方**可操作",
"绑定代理:玩家账单仅含**直属玩家**;代理账单仅含本节点为账单方或收付对方",
"账单无 currency_code 字段;展示币种取玩家 default_currency"
],
"note": "已关账的占成账单按下注时快照结算,不会按代理当前档案重新计算历史。",
"fundOpsLinkNote": "信用盘账务流水各类型的含义与钱包盘差异,请参阅",
"fundOpsLinkLabel": "资金操作详解"
},
"agents": {
"title": "代理体系",
"description": "代理层决定「能看哪些数据、能给下级多少额度」;菜单操作权限仍由角色控制。信用盘站点须先维护代理树,再在其下创建玩家。",
"structure": "组织结构",
"structureItems": [
"超管:代理组织 →「代理线路」→「开通线路」:为外部代理开通独立站点 + 根节点",
"站点运营:代理组织 →「代理列表」:在已有站点下维护代理树",
"选中代理节点后可编辑档案:占成、授信、回水、下放权限",
"根代理档案仅超管可改;下级由上级代理或站点管理员维护",
"可为代理开通经营后台账号(管理列表中绑定代理节点)"
],
"provisionWalkthrough": "开通信用盘代理线路(超管)",
"provisionSteps": [
"代理组织 → 代理线路 →「开通线路」",
"填写站点信息、根代理名称与编码",
"设置根代理初始占成、授信、回水",
"提交后自动创建接入站点与根代理;记录返回的站点编码",
"为对方创建站点管理员或根代理经营账号(见「角色与权限」)"
],
"dailyWalkthrough": "日常维护代理(站点运营)",
"dailySteps": [
"代理列表 → 选中上级节点 →「新建下级代理」",
"填写名称、编码、占成/授信/回水;是否允许继续下放",
"保存后在树形列表中可见;可继续为其创建下级或玩家",
"修改占成/授信仅影响**之后**的新注单与账期,已关账历史不变"
],
"profile": "代理档案字段",
"profileRows": [
["占成比例", "本级从下级流水中的分成", "关账后计入本级 share_profit输赢"],
["授信额度", "可给下级玩家使用的信用上限", "下级玩家下注占用总和不可超过链路授信规则"],
["回水比例", "按玩法配置叠加的回水", "与平台「赔率与回水」配置叠加"],
["下放权限", "是否允许创建下级代理/玩家", "站点管理员不受此开关限制,按自身角色权限操作"]
],
"siteAdmin": "站点管理员注意",
"siteAdminItems": [
"站点管理员不受「选中代理档案上的下放开关」限制,按自身角色权限操作",
"在代理下创建玩家时必须选择所属代理(不可默认根代理)",
"开/关账期、坏账核销等须使用未绑定代理的站点财务账号"
],
"note": "修改代理档案只影响之后的新注单与账期;已关账历史不变。"
},
"players": {
"title": "玩家管理",
"description": "统一管理钱包盘与信用盘玩家;列表与详情会按资金模式展示不同余额与流水 Tab。",
"list": "玩家列表",
"listItems": [
"日常运营 →「玩家列表」",
"搜索:用户名、昵称、主站玩家 ID、所属代理",
"列表「资金模式」列区分钱包盘 / 信用盘",
"余额列信用盘显示授信额度major钱包盘显示彩票内余额minor"
],
"createWalkthrough": "创建信用盘玩家(站点运营 / 代理)",
"createSteps": [
"玩家列表 →「新建玩家」",
"**必选**所属代理(站点管理员不可省略)",
"填写登录名、密码、昵称、默认币种",
"设置初始授信额度(信用盘)",
"保存后玩家可用彩票端账号密码登录;钱包盘玩家通常由主站 SSO 首次进场自动建档"
],
"freezeWalkthrough": "冻结 / 解冻",
"freezeSteps": [
"玩家列表或玩家详情 →「冻结」",
"冻结后无法下注;已有注单按规则继续结算",
"解冻后恢复正常下注"
],
"modes": "资金模式差异",
"modeRows": [
["钱包盘", "从贵司主站 SSO 进入,首次有效 JWT 自动建档。详情 Tab钱包流水、转账单"],
["信用盘", "彩票端账号或代理后台创建。详情 Tab信用流水中奖走账期结算"]
],
"detail": "玩家详情页",
"detailItems": [
"信用流水:下注占用、中奖释额、账期收付记账等",
"钱包流水 / 转账单:仅钱包盘;转账单对应主站划转记录",
"注单历史:跳转注单详情(含组合玩法明细)",
"调整授信:须具备玩家/代理管理写权限"
],
"note": "钱包盘玩家登录名由彩票端自动生成(如 nlotto******),不从主站同步昵称。"
},
"tickets": {
"title": "注单查询",
"description": "按多维度检索玩家注单,供运营、客服与代理对账使用。绑定代理仅能看到本代理子树玩家的注单。",
"entry": "入口",
"entryItems": [
"日常运营 →「注单列表」",
"也可从玩家详情 → 注单历史进入单玩家注单"
],
"filter": "常用筛选",
"filterItems": [
"期号、玩家、玩法、注单号、时间范围",
"状态:待开奖 / 已中奖 / 未中奖 / 已取消等",
"代理范围:超管可选站点;站点运营为本站;绑定代理为本子树"
],
"detail": "注单详情",
"detailItems": [
"展示下注内容、赔率、金额、占成快照(信用盘)",
"组合类玩法(如连码)在详情页查看**组合明细**",
"可关联跳转:玩家详情、期号详情",
"支持 Excel 导出(须具备导出权限)"
],
"note": "注单金额为下注时快照;开奖后状态由系统自动更新,无需手工改单。"
},
"wallet": {
"title": "钱包与对账(钱包盘)",
"description": "仅适用于钱包盘玩家。展示主站与彩票之间的划转记录;绑定代理账号通常看不到此菜单。",
"walletSection": "钱包流水",
"walletItems": [
"资金与报表 →「钱包流水」",
"按玩家、类型、时间筛选:转入、转出、下注、派彩等",
"金额为 minor 整数;与 API 对接文档中钱包网关口径一致",
"可导出筛选结果(须具备权限)"
],
"transferSection": "转账单",
"transferItems": [
"钱包模块内可查看「转账单」列表(主站 ↔ 彩票)",
"状态:成功 / 失败 / 处理中pending",
"pending 过久时可到「对账」模块处理"
],
"reconcileSection": "对账(掉单处理)",
"reconcileSteps": [
"资金与报表 →「对账」",
"列表展示长时间 pending 或状态异常的转账单",
"打开单据 → 向贵司主站核实实际扣款/加款结果",
"按核实结果:补记账、冲正或关闭;操作写入审计",
"技术侧须同时检查贵司钱包网关日志与 API 对接文档 → 联调排错"
],
"note": "信用盘玩家不涉及钱包流水与对账;其资金变动在「结算中心」账务流水中查看。",
"fundOpsLinkNote": "钱包盘与信用盘资金生命周期对照详见",
"fundOpsLinkLabel": "资金操作详解"
},
"config": {
"title": "规则与风控(平台超管)",
"description": "全站玩法、赔率、限额与风险参数。一般仅我方平台超管或贵司获得平台权限的超管可见;站点运营/代理通常无此菜单。",
"plays": "投注规则",
"playsItems": [
"平台管理 →「投注规则」",
"按玩法开关:关闭后玩家端不可下注该玩法",
"设置单注最小/最大金额"
],
"odds": "赔率与回水",
"oddsItems": [
"平台管理 →「赔率与回水」",
"调整赔率、基础回水;**仅影响之后的新注单**",
"与代理档案中的回水比例叠加计算"
],
"riskCap": "限额版本",
"riskCapItems": [
"平台管理 →「限额版本」",
"按号码设置封顶;达到上限后该号码售罄",
"须发布新版本才对**未来期号**生效;已开盘期号按当时版本执行"
],
"risk": "风控中心",
"riskItems": [
"平台管理 →「风控中心」",
"查看号码占用、赔付池、高风险号码",
"占用流水默认按注单聚合;仅显示有占用或高风险的号码",
"组合玩法明细在注单详情查看,不在风控列表展开"
],
"jackpot": "奖池",
"jackpotItems": [
"平台管理 →「奖池」",
"维护奖池金额与派奖规则(若玩法启用奖池)"
],
"note": "若贵司站点运营账号看不到上述菜单,属正常权限隔离;需调整玩法请联系我方或贵司超管。"
},
"reports": {
"title": "报表中心",
"description": "按期号、玩家、玩法等维度查看盈亏与风险,支持异步导出。数据范围随账号角色自动收窄。",
"entry": "入口与筛选",
"entryItems": [
"资金与报表 →「报表中心」",
"先选报表类型,再设时间、站点、代理、期号等筛选",
"绑定代理报表仅含本代理子树"
],
"types": "主要报表",
"reportRows": [
["每期盈亏", "单期下注、派彩、盈亏汇总"],
["每日汇总", "按业务日聚合"],
["玩家输赢", "玩家维度排行与明细"],
["玩法维度", "各玩法投注与派彩结构"],
["热门号码风险", "号码投注集中度与赔付池占用"],
["佣金回水", "回水与佣金计提"],
["审计日志", "后台关键操作追溯(超管)"]
],
"export": "导出",
"exportItems": [
"具备「报表导出」权限可发起异步导出;完成后下载",
"注单列表、钱包流水亦支持 Excel 导出",
"导出任务在后台异步执行,大量数据请缩小时间范围"
],
"scope": "数据范围",
"scopeItems": [
"超管:全站或按站点筛选",
"站点管理员:本站点",
"绑定代理:本代理子树"
]
},
"fundOperations": {
"title": "资金操作详解",
"description": "说明钱包盘与信用盘在「下注、开奖、收付」各环节资金如何变动。请先确认玩家资金模式,再查对应流水。",
"twoSystems": "两套并行体系(勿混淆)",
"twoSystemsItems": [
"单期开奖结算(日常运营 → 结算):计算本期注单输赢;钱包盘在此派彩入账,信用盘在此释额/记账期流水",
"信用盘账期结算(代理组织 → 结算中心):仅信用盘;按账期汇总占成并生成账单,与代理/玩家收付",
"钱包主站划转(资金与报表 → 钱包):仅钱包盘;主站与彩票之间的转入/转出"
],
"creditModel": "信用盘:额度模型",
"creditModelItems": [
"授信额度:代理或运营为玩家设置的上限",
"已用授信:当前被下注占用与已结算亏损占用的合计",
"可用授信 = 授信额度 已用授信 冻结额度;可用不足时无法下注",
"玩家详情与列表中的「余额」对信用盘即授信相关数值,不是现金余额"
],
"creditLifecycle": "信用盘:一笔注单的资金生命周期",
"creditLifecycleSteps": [
"① 下注成功:占用授信(流水显示「下注冻结」)。此时只是锁定可用额度,并非最终扣款",
"② 待开奖:占用一直存在;账务流水中仅见冻结,不会与结算重复扣款",
"③ 开奖结算后:系统释放该注占用,再记一条「开奖结算」—— 未中奖则亏损计入已用授信;中奖则释额(可用额度回升),不即时发现金",
"④ 账期内:上述输赢同时写入占成流水,供关账生成代理/玩家账单",
"⑤ 账期关账后:在「结算中心」确认账单并登记收付;玩家侧可能见「账期结算确认」「结算收付入账」类流水",
"⑥ 有逾期账单未结清的玩家:系统禁止继续下注,须先完成账期收付"
],
"creditLedger": "信用盘:账务流水类型对照",
"creditLedgerRows": [
["下注冻结", "玩家下注成功", "可用授信减少;已用授信增加", "结算中心 → 账务流水;玩家详情 → 信用流水"],
["开奖结算", "期号派彩结算完成", "释放占用并按输赢调整已用授信;中奖为释额", "同上;待开奖仅显示冻结,开奖后每注单一条结算"],
["账期结算确认", "账期账单登记收付(部分场景)", "减少已用授信", "结算中心 → 账务流水"],
["结算收付入账", "账期收付记账", "仅记账,不改变可用授信", "结算中心 → 账务流水"],
["补差 / 冲正 / 坏账", "站点财务调账", "按调账类型调整账单与流水", "结算中心 → 收付与调账"]
],
"creditBill": "信用盘:账单与收付在说什么",
"creditBillItems": [
"关账后生成的「玩家账单」= 本账期该玩家输赢汇总后的应收/应付净额",
"「代理账单」= 各级代理占成后的分成应收/应付",
"净额为正:账单方应向收款方付款;净额为负:收款方应向账单方付款(后台会标明付款方/收款方)",
"确认账单:双方认可金额;登记收付:记录实际资金往来,可多次登记直至结清",
"坏账核销:确认无法收回的欠款,仅站点财务(未绑定代理)可操作"
],
"creditAdjust": "信用盘:人工调整授信",
"creditAdjustSteps": [
"玩家详情 → 调整授信(须具备玩家/代理管理写权限)",
"修改授信额度上限;不影响已用授信的历史占用",
"调低额度时若已用超过新上限,玩家将无法继续下注直至占用下降",
"账期收付、坏账、补差请在「结算中心」操作,不要在玩家详情重复手工改账"
],
"walletLifecycle": "钱包盘:资金生命周期",
"walletLifecycleSteps": [
"① 玩家从贵司主站进入彩票 H5SSO + JWT",
"② 转入:玩家在 H5 发起 → 彩票回调贵司钱包扣款 → 彩票内余额增加(主站转账单 + 钱包流水)",
"③ 下注:直接从彩票内余额扣减(钱包流水类型「下注扣款」)",
"④ 开奖结算:中奖金额写入彩票内余额(「派彩入账」);在「日常运营 → 结算」批次审核/派彩后到账",
"⑤ 转出(可选):彩票回调贵司钱包加款 → 彩票内余额减少",
"⑥ 异常:长时间 pending 的转账单在「对账」中人工处理"
],
"walletTxn": "钱包盘:钱包流水类型",
"walletTxnRows": [
["主站转入", "玩家从主站划入彩票", "彩票内余额增加"],
["主站转出", "玩家从彩票划回主站", "彩票内余额减少"],
["下注扣款", "下注成功", "余额减少"],
["派彩入账", "期号结算派彩完成", "余额增加(中奖时)"],
["下注冲正 / 转出失败回补", "注单取消或转出失败回滚", "余额回补"]
],
"walletReconcile": "钱包盘:转账单异常处理",
"walletReconcileRows": [
["待对账", "主站与彩票状态不一致或超时", "先核实贵司网关日志,再选下方操作"],
["补完成入账", "主站已扣款但彩票未记账(转入)", "在彩票侧补记转入并标记成功"],
["冲正", "需撤销已执行的彩票侧记账", "反向调整彩票钱包余额"],
["标记结案", "已在系统外处理完毕", "仅改单据状态,不动余额;转出待对账单不可用此操作"]
],
"compare": "同一环节:钱包盘 vs 信用盘",
"compareRows": [
["进场", "主站 SSO首次 JWT 自动建档", "彩票账号或代理后台创建"],
["有余额才能下注", "彩票内余额 ≥ 下注额", "可用授信 ≥ 下注额"],
["下注时", "余额立即扣减", "占用授信(冻结),非最终亏损"],
["中奖后", "派彩入账到彩票余额", "释额;现金在账期账单收付"],
["日常查账", "钱包流水 + 转账单", "结算中心账务流水 + 账单"],
["与代理收付", "不涉及", "结算中心账期账单"]
],
"note": "信用盘玩家端文案使用「释额」「账期收付」,不用钱包盘「派彩」口径。两种模式玩家不可混用同一账号。"
},
"manualReview": {
"title": "人工审核与派彩",
"description": "说明开奖结果审核、冷静期、单期派彩结算批次,以及相关系统开关。与「结算中心」信用账期是不同流程。",
"distinction": "与信用账期结算的区别",
"distinctionItems": [
"本文涵盖:单期开奖后的派彩结算批次(菜单:日常运营 → 结算)—— 钱包盘派彩入账、信用盘记开奖流水,每期开奖都会走",
"结算中心:信用盘代理账期开账/关账/账单收付 —— 按周/月等业务账期汇总,与是否开奖无一对一关系",
"期号详情里的「审核发布」:开奖号码要不要生效;结算批次:号码生效后资金如何变动"
],
"drawReview": "开奖结果人工审核",
"drawReviewItems": [
"系统设置 →「开奖结果必须人工审核」开启时RNG 生成号码后期号进入「待审核」,须人工点击「审核通过并发布」",
"关闭时RNG 结果自动发布(仍可有冷静期,见下)",
"人工录入号码:提交后同样进入待审核或直接发布,取决于审核开关",
"入口:日常运营 → 期号列表 → 期号详情 → 开奖批次"
],
"drawPublishSteps": "发布开奖结果(逐步)",
"drawPublishStepItems": [
"打开期号详情,确认状态为「待开奖」或「待审核」",
"若无结果:点击「生成开奖结果」或录入号码",
"复核号码与期号、玩法规则无误",
"点击「审核通过并发布」",
"发布后进入冷静期或直接进入「结算中」(见系统设置)"
],
"cooldown": "冷静期",
"cooldownItems": [
"系统设置 →「冷静期时长(分钟)」:结果发布后等待一段时间再跑派彩结算",
"冷静期内超管可「重开」:回滚已派彩/已释额 → 重新生成结果 → 再审核发布",
"设为 0 分钟:发布后立即进入结算,无冷静窗口",
"冷静期是给运营复核号码的最后窗口,与信用账期无关"
],
"settlementBatch": "单期派彩结算批次",
"settlementBatchItems": [
"每期开奖并发布后,系统自动(或人工触发)生成结算批次,汇总该期所有注单的输赢",
"入口:日常运营 →「结算」→ 批次列表;也可从期号详情跳转",
"批次状态:进行中 → 待审核 → 已审核 → 已派奖/已完成(依自动化开关可能跳过中间步骤)",
"钱包盘:「执行派彩」把中奖金额写入玩家彩票余额",
"信用盘:「执行派彩」写入开奖结算流水与占成流水,不增加现金余额"
],
"batchStatusSection": "结算批次状态",
"batchStatusRows": [
["进行中", "正在计算本期注单输赢", "等待完成"],
["待审核", "金额已算出,等待财务/运营确认", "审核通过 / 驳回"],
["已审核", "金额已确认,等待派彩入账", "执行派彩"],
["已派奖", "派彩已写入玩家账户或信用流水", "查看明细、导出"],
["已驳回", "审核不通过,注单回到待重新结算", "修正后重新跑结算"],
["失败", "结算过程异常", "联系技术支持"]
],
"batchWalkthrough": "人工审核并派彩(逐步)",
"batchWalkthroughSteps": [
"日常运营 → 结算 → 筛选「待审核」批次",
"打开批次详情,核对:期号、注单数、总实扣、派彩合计、平台盈亏",
"确认与期号开奖结果一致后点击「审核通过」;有问题点击「驳回」并填写备注",
"已审核批次点击「执行派彩」—— 钱包盘玩家余额到账,信用盘生成开奖流水",
"执行派彩后不可一键撤回;若号码错误须在冷静期内重开期号"
],
"settings": "相关系统开关(平台超管)",
"settingRows": [
["开奖结果必须人工审核", "平台管理 → 系统设置 → 开奖节奏与审核", "RNG 结果须人工发布", "RNG 结果自动发布"],
["冷静期时长", "同上", "发布后等待 N 分钟再结算", "发布后立即结算"],
["自动执行结算", "系统设置 → 结算自动化", "到期自动跑结算批次", "须人工触发结算"],
["自动审核结算批次", "同上", "结算完成后自动「已审核」", "须人工点审核通过"],
["自动派彩入账", "同上", "审核后自动执行派彩", "须人工点执行派彩"]
],
"settingsNote": "生产环境常见组合:开奖人工审核 + 有冷静期 + 结算自动跑 + 批次人工审核 + 派彩自动或人工。具体以贵司风控要求为准。",
"rejectNote": "驳回结算批次会把关联注单回到待结算状态,不会自动回滚已发布开奖号码;若号码本身有误,请走期号重开。",
"note": "信用盘站点关账前,须确保本账期内相关期号均已「已结算」且派彩批次已完成,否则关账会提示未结算注单。"
},
"faq": {
"title": "常见问题",
"description": "后台操作中的典型问题与处理思路。",
"faqRows": [
["信用盘流水看不懂", "见「资金操作详解」;待开奖只有「下注冻结」,开奖后每注一条「开奖结算」,不会重复扣两次"],
["结算批次与结算中心区别", "见「人工审核与派彩」;前者是单期派彩,后者是信用账期账单"],
["号码售罄无法下注", "风控中心查看占用;在限额版本中提高未来期封顶并发布新版本"],
["开奖结果有误", "期号详情 → 超管冷静期内重开 → 回滚 → 重新审核发布 → 重新结算"],
["玩家端可下注但后台显示已封盘", "列表 status 为 DB 快照;以玩家端大厅为准;检查 close_time 与时区"],
["转账未到账(钱包盘)", "钱包流水查状态;对账模块处理 pending核实贵司网关日志"],
["接入站点连通失败", "确认贵司钱包网关为公网 HTTPS且已实现 balance/debit/credit 接口"],
["关账按钮不可用", "确认无其他进行中账期、本期期号均已结算、当前账号未绑定代理"],
["看不到登记收付按钮", "需结算写权限;账单须为已确认/部分已付/逾期且 unpaid_amount > 0且须为收款方"],
["代理看不到钱包菜单", "绑定代理账号隐藏钱包/对账;请用站点财务或超管账号"],
["创建玩家时无法选代理", "站点管理员必须选所属代理;先在代理列表创建下级代理"],
["权限不足", "联系贵司超管或我方支持,在「角色管理」中勾选对应菜单"]
],
"integration": "技术对接",
"integrationItems": [
"SSO、iframe、钱包网关请参阅 API 对接文档",
"错误码 80018005 见对接文档 → 联调排错"
],
"integrationLinkLabel": "查看 API 对接文档"
}
}
}

View File

@@ -16,7 +16,7 @@
"deleteSuccess": "已删除 {{name}}",
"deleteFailed": "删除失败",
"roleListTitle": "平台角色管理",
"roleListHint": "平台仅保留「超级管理员」与「代理」两个内置角色超级管理员自动拥有全部权限。",
"roleListHint": "可新增自定义角色并配置权限;内置角色超级管理员、站点管理员、代理)不可删除。",
"createRole": "新增平台角色",
"roleCreateSuccess": "已创建角色 {{name}}",
"roleUpdateSuccess": "已更新角色 {{name}}",

View File

@@ -158,6 +158,8 @@
"validation": {
"shareRange": "占成比例须在 0100 之间",
"creditInvalid": "授信额度不能为负数",
"creditBelowAllocated": "授信额度不能低于已下发给下级/玩家的总额(当前至少 {{min}}",
"creditExceedsParentWithMax": "授信额度不能超过 {{max}}",
"rebateLimitRange": "回水上限须在 0100% 之间",
"defaultRebateRange": "默认玩家回水须在 0100% 之间",
"defaultExceedsLimit": "默认玩家回水不能超过回水上限"

View File

@@ -74,6 +74,7 @@
"riskPools": { "filename": "风险池", "sheetName": "风险池" },
"riskIndex": { "filename": "风控中心期号列表", "sheetName": "风控中心" },
"riskPoolDetail": { "filename": "风险池详情-{{number}}", "sheetName": "风险池详情" },
"ticketCombinations": { "filename": "注单组合明细", "sheetName": "组合明细" },
"auditLogs": { "filename": "审计日志", "sheetName": "审计日志" },
"currencies": { "filename": "币种管理", "sheetName": "币种管理" }
},
@@ -141,8 +142,12 @@
"notifications": "通知",
"notificationsComingSoon": "通知功能开发中",
"accountSettings": "账号设置",
"relatedDocs": "相关文档",
"loggedOut": "已退出登录"
},
"docs": {
"learnMore": "查看完整说明"
},
"nav": {
"home": "首页",
"dashboard": "仪表盘",

View File

@@ -42,6 +42,7 @@
"integrationSites": {
"title": "接入站点",
"description": "由运营在后台维护各主站对接参数并通过权限控制谁能查看或修改。site_code 创建后不可修改。",
"pageGuide": "钱包盘客户须先创建接入站点并保存 SSO/钱包密钥;技术联调见 API 对接文档。",
"create": "新建站点",
"edit": "编辑",
"save": "保存",

View File

@@ -1,6 +1,7 @@
{
"title": "期号",
"statusListTitle": "期号列表",
"pageGuide": "管理期号生命周期:批量生成计划、封盘、开奖审核与结算。封盘后不可改时间;超管可在冷静期内重开。",
"generatePlan": "批量生成期开奖计划",
"generating": "生成中…",
"generateSuccess": "已生成 {{created}} 期,当前缓冲 {{upcoming}}/{{target}}",

View File

@@ -1,13 +1,15 @@
{
"shell": {
"title": "彩票接入文档",
"admin": "管理后台"
"admin": "管理后台",
"adminLogin": "管理后台"
},
"nav": {
"overview": "概览",
"api": "接口",
"ship": "发布",
"home": "总览",
"delivery": "接入交付",
"quickstart": "快速开始",
"fundamentals": "资金模型",
"setup": "接入配置",
@@ -16,7 +18,11 @@
"wallet": "钱包网关",
"transfer": "划转(参考)",
"errors": "错误码",
"golive": "上线清单"
"troubleshooting": "联调排错",
"golive": "上线清单",
"operations": "运营手册",
"adminGuide": "后台管理手册",
"apiReference": "API 对接参考"
},
"headers": {
"component": ["组件", "职责", "实现方"],
@@ -32,271 +38,319 @@
"balance": ["字段", "账户", "说明"],
"call": ["方向", "接口", "鉴权"],
"sequence": ["步骤", "发起方", "说明"],
"envMap": ["项", "后台接入站点", "主站 .env", "说明"],
"envMap": ["项", "后台接入站点", "主站配置项", "说明"],
"account": ["账号", "密码", "site_player_id"],
"contract": ["场景", "HTTP", "响应体"],
"adminField": ["字段", "说明", "示例"]
"adminField": ["字段", "说明", "示例"],
"handoffTable": ["项", "说明", "负责方"],
"env": ["环境", "地址示例", "说明"],
"envelopeTable": ["方向", "消息字段", "说明"],
"faq": ["现象", "排查方向"]
},
"pages": {
"overview": {
"title": "接入总览",
"description": "主站 SSO + 钱包网关。身份用 JWT资金分主站钱包与彩票内余额。",
"roles": "职责",
"flow": "链路",
"description": "面向主站开发/集成工程师。您需实现JWT 签发 + 钱包网关;彩票提供 H5 与 API。",
"roles": "职责划分",
"flow": "业务链路",
"e2eSequence": "端到端时序",
"conventions": "约定",
"readingOrder": "阅读顺序",
"conventions": "通用约定",
"readingOrder": "建议阅读顺序",
"matrix": [
["主站", "签发 JWT实现钱包网关", "客户"],
["彩票 API", "验签、玩法、划转、下注", "我方"],
["彩票前端", "H5 / iframe 承载", "我方"]
["主站(客户)", "用户登录;服务端签发 JWT实现钱包网关", "客户"],
["彩票 API(我方)", "验签 JWT、划转、下注、开奖、结算", "我方"],
["彩票 H5我方", "玩家界面;iframe 或 URL 跳转入场", "我方"]
],
"flowItems": [
"主站登录 → 签发 JWT",
"进入彩票URL 跳转或 iframe",
"转入:主站扣款 + 彩票加款",
"下注 / 派奖(彩票内余额)",
"转出:彩票扣款 + 主站加款"
"用户在主站登录 → 主站服务端签发短效 JWT",
"进入彩票 H5iframe 嵌入或 URL ?token= 跳转",
"玩家在 H5 内「转入」→ 彩票回调主站扣款 彩票加款",
"玩家在 H5 内下注 / 派奖(使用彩票内余额)",
"(可选)玩家在 H5 内「转出」→ 彩票回调主站加款"
],
"e2eRows": [
["1", "主站", "用户登录;服务端签发 JWT"],
["2", "主站", "iframe 嵌入彩票 H5或跳转 ?token="],
["3", "彩票 H5", "收到 token;调用 GET /api/v1/player/me 验签建档"],
["4", "玩家", "在彩票 H5 内点「转入」"],
["1", "主站", "用户登录;服务端签发 JWT(含 site_code、site_player_id"],
["2", "主站", "iframe 嵌入彩票 H5或跳转 lottery_h5_base_url/?token="],
["3", "彩票 H5", "收到 JWT;调用 GET /api/v1/player/me 验签并自动建档"],
["4", "玩家", "在 H5 内点「转入」"],
["5", "彩票 API", "服务端回调主站 POST /wallet/debit-for-lottery"],
["6", "主站钱包", "扣减 main_balance返回 success"],
["7", "彩票 API", "彩票内加"],
["8", "玩家", "在 H5 内下注 / 派奖"],
["9", "玩家", "(可选)在 H5 内转出"],
["6", "主站钱包", "扣减 main_balance返回 success: true"],
["7", "彩票 API", "彩票内余额增加"],
["8", "玩家", "在 H5 内下注 / 等待派奖"],
["9", "玩家", "(可选)在 H5 内点击「转出"],
["10", "彩票 API", "回调主站 POST /wallet/credit-from-lottery"]
],
"conventionRows": [
["金额", "最小货币单位整数minor如 2000 = 20.00"],
["金额", "最小货币单位整数minor如 2000 = 20.00 NPR"],
["编码", "UTF-8 JSON"],
["时间", "JWTUnix 秒iat / exp"],
["鉴权", "玩家 APIBearer JWT钱包网关Bearer wallet_api_key"]
["时间", "JWT 使用 Unix 秒iat / exp,建议 exp - iat ≤ 300 秒"],
["玩家 API 鉴权", "Authorization: Bearer {JWT}(主站签发,彩票验签)"],
["钱包网关鉴权", "Authorization: Bearer {wallet_api_key}(彩票回调时携带)"]
],
"readingItems": ["快速开始 → 接入配置 → 单点登录 → iframe 协议 → 钱包网关 → 错误码 → 上线清单"]
"readingItems": [
"接入交付 — 确认双方交付物与环境地址",
"快速开始 — 按步骤完成首次联调",
"接入配置 — 后台建站与密钥映射",
"单点登录 → iframe 协议 → 钱包网关",
"联调排错 — 常见问题",
"上线清单 — 生产发布检查"
]
},
"delivery": {
"title": "接入交付",
"description": "联调开始前,请与商务/技术支持确认以下交付物。测试与生产环境须完全隔离。",
"handoffScope": "接入范围(您需要做什么)",
"weProvide": "我方提供",
"youProvide": "客户需提供",
"environment": "环境地址",
"process": "典型接入流程",
"note": "密钥sso_jwt_secret、wallet_api_key创建后仅展示一次请立即安全保存。密钥仅保存在主站服务端禁止写入前端或移动端。下列地址为 Tanumo 当前默认环境;客户独立部署时以商务交付为准。",
"handoffRows": [
["JWT 签发", "主站登录后由服务端签发 HS256 JWT无「登录换票」接口", "客户"],
["钱包网关", "实现 balance / debit / credit 三个 HTTPS 接口", "客户"],
["iframe 或 URL 入场", "嵌入彩票 H5 或跳转并携带 JWT", "客户"],
["彩票 H5 + API", "玩法、划转、下注、开奖", "我方"],
["接入站点与密钥", "创建 site_code 并下发密钥", "我方(超管)"]
],
"provideRows": [
["site_code", "站点编码,写入 JWT"],
["sso_jwt_secret", "JWT 签名密钥(主站持有并签发)"],
["wallet_api_key", "彩票回调钱包网关时的 Bearer 密钥"],
["lottery_h5_base_url", "彩票 H5 入口iframe src 或跳转地址);当前默认 https://front.tanumo.com"],
["lottery_api_base_url", "彩票 API 根地址(联调 curl当前默认 https://lotterylaravel.tanumo.com"]
],
"submitRows": [
["wallet_api_url", "客户钱包网关 HTTPS 根地址(公网可达)"],
["iframe_allowed_origins", "主站 origin 白名单iframe 模式必填,每行一个)"],
["测试账号", "若干 site_player_id 及初始 main_balance联调用"],
["出口 IP如需", "若网关有 IP 白名单,请索取彩票服务端出口 IP"]
],
"environmentRows": [
["彩票 API", "https://lotterylaravel.tanumo.com", "联调 curlGET /api/v1/player/me"],
["彩票 H5 入口", "https://front.tanumo.com", "iframe / URL ?token= 跳转;钱包页示例 /wallet"],
["接入文档", "https://lotteryadmin.tanumo.com/docs/integration", "本文档(公开,无需登录)"],
["管理后台", "https://lotteryadmin.tanumo.com/admin", "超管登录;接入站点:配置 → 接入站点"],
["生产环境", "独立域名与密钥", "site_code、密钥、域名均不与联调共用"]
],
"processSteps": [
"商务开通接入 → 我方超管创建「接入站点」并交付密钥与 H5 地址",
"客户实现钱包三接口并部署到公网 HTTPS联调可先内网穿透",
"客户在后台填入 wallet_api_url、iframe_allowed_origins执行连通性测试",
"客户实现 JWT 签发与 iframe postMessage或 URL 跳转)",
"按「快速开始」验收清单完成联调",
"生产环境重新建站、换密钥、全链路复测后上线"
]
},
"quickstart": {
"title": "快速开始",
"description": "本地联调参考。仓库内 main-site/ 为可运行示例,密钥须与后台接入站点或彩票 .env 一致。",
"description": "假设已完成「接入交付」并拿到 site_code、密钥与 H5 地址。按下列步骤完成首次联调。",
"prereq": "前置条件",
"steps": "联调步骤",
"testAccounts": "测试账号main-site",
"reference": "参考实现",
"note": "生产环境须使用 HTTPS、独立 site_code 与密钥。本地未配置 wallet_api_url 时,彩票 API 可能以 stub 模式跳过主站扣款(仅非 production。",
"acceptance": "验收清单",
"note": "JWT 必须在主站服务端签发,禁止在前端硬编码 sso_jwt_secret。生产环境 wallet_api_url 须为公网 HTTPS。",
"prereqItems": [
"彩票 APIlotterLaravel与彩票前端lotteryfront已启动",
"main-site 已启动(默认 http://localhost:5173",
"后台已创建接入站点,或彩票 .env 配置 MAIN_SITE_* 与主站 .env 对齐"
"已收到 site_code、sso_jwt_secret、wallet_api_key、lottery_h5_base_url",
"主站已实现 GET /wallet/balance、POST /wallet/debit-for-lottery、POST /wallet/credit-from-lottery",
"后台接入站点」已填入 wallet_api_url 与 iframe_allowed_origins连通性测试通过",
"已准备至少一个测试 site_player_id 及足够 main_balance"
],
"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 与钱包网关"
"主站服务端实现 JWT 签发见「单点登录」jsonwebtoken 示例)",
"用 curl 自测Bearer JWT 调用 GET https://lotterylaravel.tanumo.com/api/v1/player/me应返回 code=0",
"主站页面嵌入 <iframe src=\"https://front.tanumo.com\">,监听 postMessage",
"收到 LOTTERY_READY 后,发送 MAIN_INIT_TOKENtoken 放在消息顶层,见 iframe 页示例)",
"H5 进入大厅后,在 H5 内发起转入",
"确认主站收到 POST /wallet/debit-for-lottery 且返回 success: true",
"确认 H5 内彩票余额增加,尝试下注",
"可选H5 内转出,确认 POST /wallet/credit-from-lottery 被回调",
"按下方验收清单逐项勾选"
],
"accountRows": [
["alice", "alice123", "10001"],
["bob", "bob123", "10002"],
["demo", "demo123", "10003"]
],
"referenceItems": [
"代码monorepo 内 main-site/Next.js 测试主站壳)",
"主站http://localhost:5173彩票 H5http://localhost:3800",
"主站 README 含环境变量与消息协议说明",
"配置字段对照见「接入配置」页的映射表"
],
"acceptance": "验收清单",
"acceptanceItems": [
"签发 JWT curl GET /api/v1/player/me 返回 code=0",
"自测钱包网关 debit返回 success:truemain_balance 正确",
"相同 idempotent_key 重放响应与首次一致,余额不重复扣",
"iframe子页 LOTTERY_READY 后收到 MAIN_INIT_TOKEN可进入大厅",
"H5 内转入成功主站网关日志有 debit-for-lottery 记录"
"JWT 自测:curl https://lotterylaravel.tanumo.com/api/v1/player/me 返回 code=0data.site_player_id 正确",
"钱包自测curl POST /wallet/debit-for-lottery 返回 success:truemain_balance 正确扣减",
"幂等:相同 idempotent_key 重放响应与首次一致,余额不重复扣",
"iframeLOTTERY_READY MAIN_INIT_TOKEN可进入 H5 大厅",
"转入:H5 内转入成功主站网关日志有 debit-for-lottery 记录",
"续签:等待 JWT 临近过期或触发 LOTTERY_TOKEN_NEEDED 后MAIN_REFRESH_TOKEN 可续签成功"
]
},
"fundamentals": {
"title": "资金模型",
"balances": "两层余额",
"calls": "调用方向",
"note": "金额一律使用 minor 整数。信用盘(代理授信)不在本文档范围。",
"note": "金额一律使用 minor 整数。信用盘(代理授信)不在本文档范围;本文档仅覆盖主站钱包盘。",
"balanceRows": [
["main_balance", "主站钱包", "客户实现网关;彩票回调"],
["lottery balance", "彩票内余额", "转入后用于下注"]
["main_balance", "主站钱包", "客户实现网关;彩票服务端回调扣款/加款"],
["lottery balance", "彩票内余额", "玩家转入后用于下注;由彩票 H5 展示与操作"]
],
"callRows": [
["彩票 → 主站", "balance / debit / credit", "wallet_api_key"],
["彩票 H5 → 彩票 API", "me / 划转 / 下注", "玩家 JWT主站接)"]
["彩票 → 主站", "GET balance / POST debit / POST credit", "Bearer wallet_api_key"],
["彩票 H5 → 彩票 API", "me / 划转 / 下注 / 查余额", "Bearer 玩家 JWT主站无需对接)"]
]
},
"setup": {
"title": "接入配置",
"description": "接入站点创建后一次性下发密钥,请立即保存。",
"weProvide": "我方提供",
"youProvide": "客户需提供",
"defaultPaths": "默认钱包路径",
"envMapping": "配置映射",
"note": "测试与生产环境的 site_code、密钥、域名须隔离。密钥写入主站 .env不会从后台自动同步。本地开发可在彩票 .env 用 MAIN_SITE_* 作兜底。",
"description": "由我方超管在管理后台创建「接入站点」。创建成功后密钥仅展示一次,请立即保存。",
"weProvide": "创建站点后我方提供",
"youProvide": "客户需回填/提供",
"defaultPaths": "钱包网关默认路径",
"envMapping": "配置映射",
"adminSop": "后台建站步骤(我方超管)",
"network": "网络要求",
"note": "测试与生产的 site_code、密钥、域名须完全隔离。密钥写入主站服务端配置不会从后台自动同步到客户系统。",
"receiveRows": [
["site_code", "站点编码"],
["site_code", "站点编码,写入 JWT"],
["sso_jwt_secret", "JWT 签名密钥(主站持有)"],
["wallet_api_key", "钱包回调鉴权(主站校验)"],
["lottery_h5_base_url", "彩票入口地址"]
["lottery_h5_base_url", "彩票 H5 入口地址"]
],
"provideRows": [
["wallet_api_url", "HTTPS 钱包根地址"],
["测试账号", "site_player_id + 初始余额"],
["iframe origin", "嵌入时提供主站 origin"]
["wallet_api_url", "客户钱包网关 HTTPS 根地址(无 path 后缀)"],
["iframe_allowed_origins", "主站 origin 白名单iframe 模式)"],
["测试账号", "site_player_id 列表 + 初始余额"]
],
"pathRows": [
["GET", "/wallet/balance", "余额查询"],
["POST", "/wallet/debit-for-lottery", "扣款"],
["POST", "/wallet/credit-from-lottery", "加款"]
["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", "仅参考实现需要"]
["site_code", "code", "MAIN_SITE_CODE", "JWT 与玩家建档标识;双方须一致"],
["SSO 密钥", "sso_jwt_secret", "MAIN_SITE_SSO_JWT_SECRET", "主站签发 JWT;彩票验签"],
["钱包鉴权", "wallet_api_key", "MAIN_SITE_WALLET_API_KEY", "彩票回调主站时 Bearer 携带"],
["钱包根地址", "wallet_api_url", "(主站部署)", "客户 HTTPS 根地址;彩票拼接 /wallet/*"],
["彩票 API", "—", "—", "当前默认 https://lotterylaravel.tanumo.com玩家/钱包业务 API 根地址"],
["彩票 H5", "lottery_h5_base_url", "(主站 iframe src", "当前默认 https://front.tanumo.com"],
["iframe 白名单", "iframe_allowed_origins", "(主站 origin", "须与主站实际 origin 一致"]
],
"adminSop": "后台建站步骤",
"adminSopSteps": [
"超管登录管理后台 → 配置 → 接入站点",
"新建站点填写站点编码site_code、名称、币种",
"填写 wallet_api_urlHTTPS 根地址,无 path、lottery_h5_base_url、iframe_allowed_origins(主站 origin每行一个",
"创建成功后立即保存一次性展示的 sso_jwt_secret、wallet_api_key",
"将密钥写入主站 .env在列表中对站点执行「连通性测试」探测 GET /wallet/balance",
"本地联调可用 main-site/ 参考实现;生产 wallet_api_url 须公网 HTTPS"
"超管登录管理后台 → 左侧「配置」→「接入站点",
"点击新建填写站点编码site_code、名称、默认币种",
"填写 wallet_api_urlHTTPS 根地址、lottery_h5_base_url、iframe_allowed_origins",
"创建成功后立即保存页面展示的 sso_jwt_secret、wallet_api_key",
"将密钥安全交付客户;客户在主站服务端配置",
"在站点列表执行「连通性测试」(探测 GET /wallet/balance"
],
"adminFieldRows": [
["code", "站点编码,写入 JWT site_code", "demo"],
["code", "站点编码,写入 JWT site_code", "partner_demo"],
["wallet_api_url", "客户钱包网关 HTTPS 根地址", "https://wallet.partner.com"],
["lottery_h5_base_url", "彩票 H5 入口", "https://lottery.partner.com"],
["lottery_h5_base_url", "彩票 H5 入口", "https://front.tanumo.com"],
["iframe_allowed_origins", "允许嵌入的主站 origin", "https://www.partner.com"],
["sso_jwt_secret", "创建后一次性下发", "—"],
["wallet_api_key", "创建后一次性下发", "—"]
["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 秒;超时可能进入待对账状态"
"钱包回调由彩票服务端发起非玩家浏览器客户网关须对彩票服务器网络可达",
"生产环境 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。",
"description": "HS256 JWT。主站服务端签发,彩票验签。入场URL ?token= 或 iframe postMessage。",
"claims": "JWT 字段",
"sign": "签示例",
"sign": "签示例Node.js",
"entryA": "方式 AURL 跳转",
"entryB": "方式 Biframe 嵌入",
"noExchangeNote": "彩票不提供「登录换票」接口。主站登录后自行签发 JWT玩家 API 统一用 Authorization: Bearer 携带。首次有效 JWT 调用 GET /api/v1/player/me 时自动建档。",
"entryApi": "入场接口(彩票)",
"entryApiNote": "可选:主站登录后服务端代调一次,用于验签与建档(参考 main-site。日常业务由彩票 H5 自行调用玩家 API。",
"entryB": "方式 Biframe postMessage",
"noExchangeNote": "彩票不提供「登录换票」接口。主站登录后自行签发 JWT后续玩家 API 统一用 Authorization: Bearer 携带同一份 JWT。首次有效 JWT 调用 GET /api/v1/player/me 时自动建档,无需先调登录接口。",
"entryApi": "验签与建档",
"entryApiNote": "可选:主站登录后服务端代调一次 GET /api/v1/player/me 做验签预检。日常业务(划转、下注)由彩票 H5 自行携带 JWT 调用,主站无需对接。",
"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。",
"h5ScopeNote": "划转、下注、彩票内余额查询等由我方 H5 携带 JWT 调用,不在主站接入范围内。主站只需:① 签发 JWT 实现钱包网关。",
"refreshNote": "iframe 续签:主站收到 LOTTERY_TOKEN_NEEDED 或 LOTTERY_TOKEN_REFRESH_REQUEST 后,重新签发 JWT 并发送 MAIN_REFRESH_TOKEN。详见「iframe 协议」。",
"authResponse": "鉴权失败示例",
"errors": "SSO 错误码",
"iframeNote": "须配置 iframe_allowed_origins。token 通过 postMessage 顶层字段 token 传递,不要放在 payload 内。",
"claimRows": [
["site_code", "string", "是", "接入站点编码"],
["site_code", "string", "是", "接入站点编码,与后台一致"],
["site_player_id", "string", "是", "主站用户 ID稳定唯一"],
["iat", "number", "是", "签发时间(秒)"],
["exp", "number", "是", "过期时间(秒);≤ 300"]
["iat", "number", "是", "签发时间(Unix 秒)"],
["exp", "number", "是", "过期时间(Unix 秒exp - iat ≤ 300"]
],
"messageRows": [
["→ 主站", "LOTTERY_READY", "子页就绪"],
["→ 主站", "LOTTERY_TOKEN_NEEDED", "请求续签"],
["→ 彩票", "MAIN_INIT_TOKEN", "{ token }"],
["→ 彩票", "MAIN_REFRESH_TOKEN", "{ token }"]
["→ 主站", "LOTTERY_READY", "子页就绪,请求 token"],
["→ 主站", "LOTTERY_TOKEN_NEEDED", "token 失效,请求续签"],
["→ 彩票", "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", "账号已冻结"]
["8002", "JWT 无效或已过期密钥不一致、exp 超时、签名错误)"],
["8003", "玩家未建档SSO 首次 me 会自动建档;此码多见于内部测试)"],
["8004", "SSO 密钥未配置(站点侧问题,联系我方)"],
["8005", "账号已冻结(站点停用或玩家冻结)"]
]
},
"iframe": {
"title": "iframe 协议",
"description": "主站嵌入彩票 H5 时的 postMessage 约定。URL 跳转模式可跳过本章。",
"description": "主站嵌入彩票 H5 时的 postMessage 约定。若使用 URL ?token= 跳转,可跳过本章。",
"sequence": "推荐时序",
"envelope": "消息结构",
"envelopeSection": "消息格式(注意方向差异)",
"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。",
"example": "主站集成示例",
"targetOrigin": "targetOrigin 安全",
"envelopeNote": "常见错误:把 token 放在 payload.token。彩票 H5 读取的是消息顶层的 data.token。",
"targetOriginNote": "postMessage 第二参数须为彩票 H5 的 origin当前默认 https://front.tanumo.com禁止使用 *。主站须校验 event.originiframe_allowed_origins 填入主站 origin非彩票域名。",
"timingNote": "收到 MAIN_INIT_TOKEN 后彩票子页不再发送 LOTTERY_READY。续签LOTTERY_TOKEN_NEEDED → 主站回 MAIN_REFRESH_TOKEN顶层 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"
"主站嵌入 <iframe src=\"{lottery_h5_base_url}\">",
"彩票 H5 校验白名单后发送 LOTTERY_READY",
"主站监听 message校验 origin 后发送 MAIN_INIT_TOKEN(顶层 token",
"彩票 H5 保存 token调用 GET /api/v1/player/me 入场",
"JWT 将过期:彩票发 LOTTERY_TOKEN_NEEDED → 主站续签后发 MAIN_REFRESH_TOKEN"
],
"envelopeRows": [
["彩票 → 主站", "type + payload + timestamp", "如 LOTTERY_READY业务数据在 payload"],
["主站 → 彩票", "type + token + timestamp", "token 必须在顶层,不要嵌套在 payload"]
],
"childMessageRows": [
["→ 主站", "LOTTERY_READY", "子页就绪,请求下发 token"],
["→ 主站", "LOTTERY_TOKEN_NEEDED", "token 失效,请求续签"],
["→ 主站", "LOTTERY_TOKEN_REFRESH_REQUEST", "主动请求刷新 token"],
["→ 主站", "LOTTERY_HEARTBEAT", "心跳(可忽略)"],
["→ 主站", "LOTTERY_TOKEN_REFRESHED", "续签成功通知"]
["→ 主站", "LOTTERY_TOKEN_REFRESHED", "续签成功通知(子 → 主)"]
],
"parentMessageRows": [
["→ 彩票", "MAIN_INIT_TOKEN", "{ token } 首次下发"],
["→ 彩票", "MAIN_REFRESH_TOKEN", "{ token } 续签"],
["→ 彩票", "MAIN_INIT_TOKEN", "首次下发;顶层 token 字段"],
["→ 彩票", "MAIN_REFRESH_TOKEN", "续签;顶层 token 字段"],
["→ 彩票", "MAIN_REQUEST_STATUS", "请求子页状态"],
["→ 彩票", "MAIN_NAVIGATE", "{ path } 导航"]
["→ 彩票", "MAIN_NAVIGATE", "导航到指定 path"]
]
},
"wallet": {
"title": "钱包网关",
"description": "由客户实现。彩票服务端调用。鉴权:Bearer wallet_api_key。",
"description": "由客户实现。彩票服务端调用非玩家浏览器。鉴权Authorization: Bearer {wallet_api_key}。",
"balance": "查询余额",
"debit": "扣款",
"credit": "加款",
"debit": "扣款(转入)",
"credit": "加款(转出)",
"response": "响应示例",
"httpContract": "HTTP 契约",
"httpErrors": "HTTP 错误",
"creditNote": "请求体与扣款相同;用于转出或失败回滚加款。",
"idempotentNote": "idempotent_key相同键 + 相同操作须返回首次 JSONHTTP 200禁止重复记账同键不同操作/金额 → success: false。",
"idempotentNote": "idempotent_key相同键 + 相同金额须返回首次 JSONHTTP 200禁止重复记账同键不同金额 → success: false。",
"queryRows": [
["site_code", "string", ""],
["site_player_id", "string", ""],
["currency_code", "string", ""]
["site_code", "string", "站点编码"],
["site_player_id", "string", "主站用户 ID"],
["currency_code", "string", "币种代码"]
],
"fieldRows": [
["site_code", "string", ""],
["site_player_id", "string", ""],
["player_id", "number", "彩票玩家 ID"],
["currency_code", "string", ""],
["site_code", "string", "站点编码"],
["site_player_id", "string", "主站用户 ID"],
["player_id", "number", "彩票玩家 ID(参考)"],
["currency_code", "string", "币种代码"],
["amount_minor", "integer", "minor 正整数"],
["idempotent_key", "string", "幂等键"]
["idempotent_key", "string", "幂等键,全局唯一"]
],
"httpErrorRows": [
["401", "unauthorized", "API Key 错误"],
["422", "invalid request", "字段或金额非法"],
["409", "main balance insufficient", "业务拒绝(如余额不足);可含 data.main_balance"]
["401", "unauthorized", "wallet_api_key 错误或缺失"],
["422", "invalid request", "字段缺失或 amount_minor 非法"],
["409", "main balance insufficient", "余额不足等业务拒绝"]
],
"httpContractRows": [
["扣款/加款成功", "200", "success: true含 external_ref_no建议与 data.main_balance"],
@@ -309,24 +363,24 @@
},
"transfer": {
"title": "资金划转(参考)",
"description": "内部说明:由彩票 H5 调用,非主站接入面。",
"outOfScopeNote": "客户无需实现本节 API。转入/转出由我方 H5 携带玩家 JWT 调用;主站只需实现钱包网关 debit/credit。本节仅供理解资金如何在两端移动。",
"description": "本节供理解资金如何在两端移动,非客户接入面。",
"outOfScopeNote": "客户无需实现本节 API。转入/转出由我方 H5 携带玩家 JWT 调用彩票 API;主站只需实现钱包网关 debit/credit。",
"requestFields": "请求字段",
"transferIn": "转入(主站 → 彩票)",
"transferOut": "转出(彩票 → 主站)",
"transferResponse": "成功响应",
"errors": "常见错误码",
"inNote": "流程:彩票调主站 debit-for-lottery → 彩票内加款。",
"outNote": "流程:彩票内扣款 → 彩票调主站 credit-from-lottery。",
"responseNote": "转入与转出结构相同direction 为 in / out。幂等重放返回相同 data。",
"inNote": "流程:玩家 H5 发起转入 → 彩票调主站 debit-for-lottery → 彩票内加款。",
"outNote": "流程:玩家 H5 发起转出 → 彩票内扣款 → 彩票调主站 credit-from-lottery。",
"responseNote": "转入与转出响应结构相同direction 为 in / out。幂等重放返回相同 data。",
"requestFieldRows": [
["amount", "integer", "minor 正整数"],
["currency", "string", "可选;默认玩家 default_currency"],
["idempotent_key", "string", "全局唯一;重复须返回相同结果"]
],
"errorRows": [
["1001", "彩票余额不足(转出)"],
["1009", "主站钱包处理失败"],
["1001", "彩票余额不足(转出"],
["1009", "主站钱包处理失败网关不可达、401、超时等"],
["1010", "幂等键冲突(同键不同金额)"],
["2003", "请先转入后再下注"]
]
@@ -336,11 +390,11 @@
"sso": "SSO 鉴权",
"lotteryWallet": "彩票钱包 / 划转",
"gateway": "客户钱包网关HTTP",
"idempotentNote": "幂等:同一 idempotent_key 须返回相同结果;同键不同金额 → 1010。",
"idempotentNote": "幂等:同一 idempotent_key + 相同金额须返回相同结果;同键不同金额 → 1010 或 success:false。",
"ssoRows": [
["8001", "缺少 Authorization 头"],
["8002", "JWT 无效或已过期"],
["8003", "玩家未建档"],
["8003", "玩家未建档SSO 正常流程会自动建档)"],
["8004", "SSO 密钥未配置"],
["8005", "账号已冻结"]
],
@@ -356,16 +410,58 @@
["409", "—", "业务拒绝(如余额不足)"]
]
},
"troubleshooting": {
"title": "联调排错",
"description": "按现象对照排查。仍无法解决请联系对接技术支持,并提供 site_code、时间戳与请求 ID。",
"faq": "常见问题",
"jwt": "JWT / 入场",
"iframe": "iframe",
"wallet": "钱包网关",
"note": "联调时先用 curl 分别验证 JWT/player/me与钱包网关/wallet/balance再测 iframe 全链路。",
"faqRows": [
["curl /player/me 返回 8002", "检查 sso_jwt_secret 是否与后台站点一致site_code 是否匹配exp 是否已过期≤300 秒)"],
["iframe 白屏 / 不进大厅", "检查 token 是否在 postMessage 顶层;是否收到 LOTTERY_READY 后发送 MAIN_INIT_TOKENiframe_allowed_origins 是否含主站 origin"],
["转入失败 1009", "彩票服务端能否访问 wallet_api_urlwallet_api_key 是否正确debit 是否返回 200 + success:true"],
["连通性测试失败", "wallet_api_url 须公网 HTTPSGET /wallet/balance 可访问且 Bearer wallet_api_key 正确"],
["8005 账号冻结", "后台检查接入站点是否启用;该 site_player_id 是否被冻结"]
],
"jwtRows": [
["8002 Token 无效", "密钥不一致 / exp 过期 / site_code 写错 / 算法非 HS256"],
["8004 SSO 未配置", "联系我方确认站点 sso_jwt_secret 已配置"],
["me 返回 code=0 但 H5 仍失败", "iframe 传的 token 与 curl 测试的不是同一份,或 token 在 payload 内而非顶层"]
],
"iframeRows": [
["收不到 LOTTERY_READY", "iframe src 是否为 lottery_h5_base_urlH5 是否加载完成"],
["postMessage 无响应", "targetOrigin 是否为彩票 H5 的 origin非主站 origin"],
["重复刷 token", "收到 MAIN_INIT_TOKEN 后子页不应再发 LOTTERY_READY"],
["续签失败", "主站是否监听 LOTTERY_TOKEN_NEEDED 并回 MAIN_REFRESH_TOKEN"]
],
"walletRows": [
["401 unauthorized", "wallet_api_key 与后台站点不一致"],
["409 余额不足", "测试账号 main_balance 不足;检查 amount_minor 单位"],
["重复扣款", "idempotent_key 未做幂等;相同 key 须返回首次响应"],
["彩票未回调 debit", "wallet_api_url 不可达或连通性测试未通过"]
]
},
"golive": {
"title": "上线清单",
"checklist": "检查",
"description": "生产发布前,请与客户经理确认联调已通过,并完成下列检查",
"deliveryChecklist": "交付与流程",
"checklist": "技术检查项",
"deliveryItems": [
"生产环境已独立创建接入站点site_code、密钥与联调隔离",
"密钥已安全写入主站生产配置(非前端)",
"wallet_api_url 为生产 HTTPS 公网地址,连通性测试通过",
"iframe_allowed_origins 已配置生产主站 origin",
"全链路联调记录已归档(转入 → 下注 → 派奖 → 转出)"
],
"items": [
"测试与生产site_code、密钥、域名完全分离",
"JWT 仅服务端签发,有效期 ≤ 5 分钟",
"钱包接口 HTTPS超时建议 ≤ 10 秒",
"idempotent_key 幂等处理",
"iframe 模式:配置 iframe_allowed_origins",
"全链路:转入 → 下注 → 派奖 → 转出"
"钱包接口 HTTPS超时 ≤ 10 秒,幂等已实现",
"iframepostMessage token 在顶层origin 校验已启用",
"测试与生产site_code、密钥、域名完全分离",
"监控:钱包网关 4xx/5xx 与 debit/credit 日志告警",
"回滚预案:可临时停用接入站点"
]
}
}

View File

@@ -1,5 +1,6 @@
{
"title": "玩家",
"pageGuide": "钱包盘与信用盘玩家统一管理;详情页按 funding_mode 展示不同流水与操作。",
"detailTitle": "玩家详情",
"listTitle": "玩家列表",
"viewDetail": "查看详情",

View File

@@ -1,6 +1,7 @@
{
"title": "报表中心",
"subtitle": "集中查看运营、资金、风控与审计报表,统一按维度筛选后导出。",
"pageGuide": "按期号、玩家、玩法等维度查看盈亏与风险;具备导出权限可发起异步导出任务。",
"exportPanel": "导出设置",
"chooseReport": "选择要导出的报表",
"libraryTitle": "报表类型",

View File

@@ -17,6 +17,8 @@
"allPoolsPageTitle": "全部风险池",
"sourceReasonOptions": {
"ticket_place": "下注占用",
"ticket_rollback": "注单回滚",
"ticket_failed_line": "下注失败释池",
"admin_manual_close": "人工关闭",
"admin_manual_recover": "人工恢复"
},
@@ -25,6 +27,7 @@
"riskFilter": "风险筛选",
"sort": "排序",
"filterAll": "全部",
"filterActive": "有占用/高风险",
"filterSoldOut": "售罄",
"filterHighRisk": ">80%",
"sortUsageDesc": "占用比 ↓(热门)",
@@ -73,6 +76,15 @@
"playCode": "玩法",
"loadLogsFailed": "加载占用流水失败",
"lockLogsTitle": "风险占用流水",
"lockLogsGroupedHint": "默认按注单汇总;展开类玩法(如 2A在注单详情查看组合明细。",
"groupBy": "展示方式",
"groupByTicket": "按注单",
"groupByEntry": "按号码明细",
"combinationCount": "组合数",
"lockReleaseSummary": "占用/释放条数",
"viewDetail": "详情",
"viewTicket": "注单",
"number": "下注号码",
"drawInfoLoadFailed": "无法加载期号信息",
"loadingDraw": "加载期号…",
"headerTitle": "风控 · 第 {{drawNo}} 期",

View File

@@ -1,6 +1,7 @@
{
"title": "结算中心",
"subtitle": "账期关账、账单确认与收付登记",
"pageGuide": "信用盘代理账期:站点财务开账/关账;绑定代理仅可查看本线账单并作为收款方登记收付。",
"subtitleList": "账期列表:开账、关账;列表已含账期汇总,行内「查看详情」进入账单与下注流水。",
"period": {
"title": "账期",

View File

@@ -47,5 +47,16 @@
"settled_lose": "已结算(未中奖)",
"refunded": "已退款"
},
"allTickets": "全部注单"
"allTickets": "全部注单",
"detailTitle": "注单详情",
"detailLoadFailed": "加载注单详情失败",
"loadingDetail": "加载中…",
"backToList": "返回注单列表",
"viewTicketDetail": "查看注单详情",
"combinationCount": "展开组合数",
"combinationsTitle": "展开组合明细",
"combinationsHint": "系统按玩法规则将一注展开为多个 4D 号码;此处为风控与结算用的组合列表。",
"number4d": "4D 号码",
"comboBetAmount": "单组合下注",
"comboEstimatedPayout": "预估赔付占用"
}

View File

@@ -0,0 +1,11 @@
export const ADMIN_DOC_LINKS = {
overview: "/docs/admin",
roles: "/docs/admin/roles",
siteSetup: "/docs/admin/site-setup",
draws: "/docs/admin/draws",
settlementCenter: "/docs/admin/settlement-center",
agents: "/docs/admin/agents",
players: "/docs/admin/players",
reports: "/docs/admin/reports",
faq: "/docs/admin/faq",
} as const;

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

@@ -0,0 +1,44 @@
import type { DocsNavGroup } from "@/lib/docs-nav";
export const ADMIN_DOCS_NAV_GROUPS: DocsNavGroup[] = [
{
titleKey: "nav.gettingStarted",
items: [
{ href: "/docs/admin", titleKey: "nav.overview" },
{ href: "/docs/admin/roles", titleKey: "nav.roles" },
],
},
{
titleKey: "nav.operations",
items: [
{ href: "/docs/admin/site-setup", titleKey: "nav.siteSetup" },
{ href: "/docs/admin/draws", titleKey: "nav.draws" },
{ href: "/docs/admin/manual-review", titleKey: "nav.manualReview" },
{ href: "/docs/admin/settlement-center", titleKey: "nav.settlementCenter" },
],
},
{
titleKey: "nav.management",
items: [
{ href: "/docs/admin/agents", titleKey: "nav.agents" },
{ href: "/docs/admin/players", titleKey: "nav.players" },
],
},
{
titleKey: "nav.finance",
items: [
{ href: "/docs/admin/tickets", titleKey: "nav.tickets" },
{ href: "/docs/admin/wallet", titleKey: "nav.wallet" },
{ href: "/docs/admin/fund-operations", titleKey: "nav.fundOperations" },
{ href: "/docs/admin/reports", titleKey: "nav.reports" },
],
},
{
titleKey: "nav.platform",
items: [{ href: "/docs/admin/config", titleKey: "nav.config" }],
},
{
titleKey: "nav.reference",
items: [{ href: "/docs/admin/faq", titleKey: "nav.faq" }],
},
];

View File

@@ -16,7 +16,7 @@ const EXACT_ROUTES: Record<string, PageTitleSpec> = {
"/admin/admin-users": { ns: "adminUsers", key: "title" },
"/admin/admin-roles": { ns: "adminRoles", key: "title" },
"/admin/agents": { ns: "agents", key: "title" },
"/admin/agents/list": { ns: "agents", key: "directoryTitle" },
"/admin/agents/list": { ns: "agents", key: "listTitle" },
"/admin/agents/provision": { ns: "agents", key: "subnav.provision" },
"/admin/agents/sites": { ns: "config", key: "integrationSites.title" },
"/admin/settlement-center": { ns: "settlementCenter", key: "title" },
@@ -32,6 +32,15 @@ const EXACT_ROUTES: Record<string, PageTitleSpec> = {
"/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" },
"/docs/admin": { ns: "adminDocs", key: "shell.title" },
"/docs/admin/roles": { ns: "adminDocs", key: "nav.roles" },
"/docs/admin/site-setup": { ns: "adminDocs", key: "nav.siteSetup" },
"/docs/admin/draws": { ns: "adminDocs", key: "nav.draws" },
"/docs/admin/settlement-center": { ns: "adminDocs", key: "nav.settlementCenter" },
"/docs/admin/agents": { ns: "adminDocs", key: "nav.agents" },
"/docs/admin/players": { ns: "adminDocs", key: "nav.players" },
"/docs/admin/reports": { ns: "adminDocs", key: "nav.reports" },
"/docs/admin/faq": { ns: "adminDocs", key: "nav.faq" },
"/admin/docs/integration-guide": { ns: "config", key: "integrationGuide.title" },
"/admin/wallet": { ns: "wallet", key: "title" },
"/admin/wallet/transactions": { ns: "wallet", key: "walletTransactions" },

View File

@@ -178,6 +178,32 @@ export function validateAgentProfileScalars(
return null;
}
/** 授信额度相对 min/max 的越界类型(用于区分错误文案)。 */
export function creditLimitRangeIssue(
value: string,
options: { min: number; max?: number },
): "below_min" | "above_max" | null {
const trimmed = value.trim();
if (trimmed === "") {
return null;
}
const parsed = Number.parseInt(trimmed, 10);
if (!Number.isFinite(parsed)) {
return null;
}
if (parsed < options.min) {
return "below_min";
}
if (options.max !== undefined && parsed > options.max) {
return "above_max";
}
return null;
}
export function isNumericStepperOutOfRange(
value: string,
options: { min?: number; max?: number; integer?: boolean },

View File

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

View File

@@ -13,6 +13,7 @@ export const DOCS_NAV_GROUPS: DocsNavGroup[] = [
titleKey: "nav.overview",
items: [
{ href: "/docs/integration", titleKey: "nav.home" },
{ href: "/docs/integration/delivery", titleKey: "nav.delivery" },
{ href: "/docs/integration/quickstart", titleKey: "nav.quickstart" },
{ href: "/docs/integration/fundamentals", titleKey: "nav.fundamentals" },
{ href: "/docs/integration/preparation", titleKey: "nav.setup" },
@@ -30,6 +31,9 @@ export const DOCS_NAV_GROUPS: DocsNavGroup[] = [
},
{
titleKey: "nav.ship",
items: [{ href: "/docs/integration/go-live", titleKey: "nav.golive" }],
items: [
{ href: "/docs/integration/troubleshooting", titleKey: "nav.troubleshooting" },
{ href: "/docs/integration/go-live", titleKey: "nav.golive" },
],
},
];

View File

@@ -1,6 +1,6 @@
import { createHighlighter, type Highlighter } from "shiki";
export type HighlightLang = "json" | "bash" | "typescript" | "http";
export type HighlightLang = "json" | "bash" | "typescript" | "http" | "html";
let highlighterPromise: Promise<Highlighter> | null = null;
@@ -8,7 +8,7 @@ function getHighlighter(): Promise<Highlighter> {
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
themes: ["github-light"],
langs: ["json", "bash", "typescript", "http"],
langs: ["json", "bash", "typescript", "http", "html"],
});
}

View File

@@ -13,6 +13,7 @@ import {
deleteAdminRole,
getAdminRoles,
getAdminUserPermissionCatalog,
postAdminRole,
putAdminRole,
putAdminRolePermissions,
} from "@/api/admin-users";
@@ -116,6 +117,15 @@ export function AdminRolesConsole(): React.ReactElement {
void load();
}, []);
function openCreateRole(): void {
setEditingRoleId(null);
setRoleSlug("");
setRoleName("");
setRoleDescription("");
setRoleStatus(1);
setRoleDialogOpen(true);
}
function openEditRole(role: AdminRoleRow): void {
if (isPlatformSuperAdminRole(role)) {
return;
@@ -172,13 +182,29 @@ export function AdminRolesConsole(): React.ReactElement {
async function submitRole(): Promise<void> {
const name = roleName.trim();
const slug = roleSlug.trim().toLowerCase();
if (name === "" || slug === "" || editingRoleId === null) {
if (name === "" || slug === "") {
toast.error(t("roleFormRequired"));
return;
}
setRoleFormSaving(true);
try {
if (editingRoleId === null) {
const created = await postAdminRole({
slug,
name,
description: roleDescription.trim() === "" ? null : roleDescription.trim(),
status: roleStatus,
});
setRoles((prev) => [...prev, created].sort((a, b) => a.sort_order - b.sort_order || a.id - b.id));
setCatalog((prev) =>
prev ? { ...prev, roles: [...prev.roles, created].sort((a, b) => a.slug.localeCompare(b.slug)) } : prev,
);
toast.success(t("roleCreateSuccess", { name: created.name }));
handleRoleDialogOpenChange(false);
return;
}
const updated = await putAdminRole(editingRoleId, {
slug,
name,
@@ -228,6 +254,11 @@ export function AdminRolesConsole(): React.ReactElement {
<CardTitle>{t("roleListTitle", { defaultValue: "平台角色管理" })}</CardTitle>
</div>
<div className="admin-list-actions">
{canManageRoles ? (
<Button type="button" size="sm" onClick={() => openCreateRole()}>
{t("createRole")}
</Button>
) : null}
<AdminTableExportButton
tableId="admin-roles-table"
filename={exportLabels.filename}
@@ -241,7 +272,7 @@ export function AdminRolesConsole(): React.ReactElement {
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
{t("roleListHint", {
defaultValue: "平台仅保留「超级管理员」与「代理」两个内置角色超级管理员自动拥有全部权限。",
defaultValue: "可新增自定义角色并配置权限;内置角色超级管理员、站点管理员、代理)不可删除。",
})}
</p>
{err ? <p className="text-sm text-red-600 dark:text-red-400">{err}</p> : null}
@@ -256,7 +287,7 @@ export function AdminRolesConsole(): React.ReactElement {
<TableHead>{t("roleTable.status")}</TableHead>
<TableHead>{t("roleTable.users")}</TableHead>
<TableHead>{t("roleTable.permissions")}</TableHead>
<TableHead className="sticky right-0 z-10 bg-card text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">{t("roleTable.actions")}</TableHead>
<TableHead className="sticky right-0 z-20 bg-muted w-14 text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">{t("roleTable.actions")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -273,10 +304,7 @@ export function AdminRolesConsole(): React.ReactElement {
<TableRow key={role.id}>
<TableCell>{role.id}</TableCell>
<TableCell>
<div className="flex flex-col">
<span className="font-medium">{role.name}</span>
<span className="text-xs text-muted-foreground">{role.description ?? ""}</span>
</div>
<span className="font-medium">{role.name}</span>
</TableCell>
<TableCell>{role.slug}</TableCell>
<TableCell>
@@ -390,7 +418,9 @@ export function AdminRolesConsole(): React.ReactElement {
<Dialog open={roleDialogOpen} onOpenChange={handleRoleDialogOpenChange}>
<DialogContent showCloseButton className="max-w-lg gap-4">
<DialogHeader>
<DialogTitle>{t("roleDialog.editTitle")}</DialogTitle>
<DialogTitle>
{editingRoleId === null ? t("roleDialog.createTitle") : t("roleDialog.editTitle")}
</DialogTitle>
<DialogDescription>{t("roleDialog.description")}</DialogDescription>
</DialogHeader>
<div className="space-y-3">
@@ -400,7 +430,7 @@ export function AdminRolesConsole(): React.ReactElement {
value={roleSlug}
placeholder={t("roleDialog.slugPlaceholder")}
onChange={(e) => setRoleSlug(e.target.value)}
disabled
disabled={editingRoleId !== null}
/>
</div>
<div className="space-y-1.5">
@@ -444,7 +474,10 @@ export function AdminRolesConsole(): React.ReactElement {
onClick={() =>
requestConfirm({
title: t("confirmSaveRoleTitle"),
description: t("confirmSaveRoleEditDescription", { name: roleName || "—" }),
description:
editingRoleId === null
? t("confirmSaveRoleCreateDescription", { name: roleName || "—" })
: t("confirmSaveRoleEditDescription", { name: roleName || "—" }),
confirmLabel: t("confirm.confirmSave", { ns: "common" }),
onConfirm: () => submitRole(),
})

View File

@@ -179,8 +179,9 @@ export function AgentLineDetailPanel({
detailTab === "players" ? playerActionHint : childActionHint;
return (
<div className="flex min-h-[28rem] min-w-0 flex-1 flex-col bg-background">
<header className="border-b border-border/60 bg-card px-5 py-4 sm:px-6">
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden bg-background">
<div className="shrink-0 bg-card shadow-[0_1px_0_rgb(216_230_251_/_35%)]">
<header className="border-b border-border/60 px-5 py-4 sm:px-6">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2.5">
@@ -232,7 +233,7 @@ export function AgentLineDetailPanel({
<AdminSubnav
aria-label={t("detailTabs", { defaultValue: "代理详情" })}
className="overflow-x-auto bg-card px-4 sm:px-5"
className="overflow-x-auto border-b border-border/60 px-4 sm:px-5"
>
{tabs
.filter((tab) => tab.visible)
@@ -247,8 +248,9 @@ export function AgentLineDetailPanel({
</AdminSubnavButton>
))}
</AdminSubnav>
</div>
<div className="min-h-0 flex-1 overflow-y-auto bg-muted/15 px-5 py-5 sm:px-6 sm:py-6">
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain bg-muted/15 px-5 py-5 sm:px-6 sm:py-6">
{detailTab === "overview" ? (
<OverviewTab
profile={profile}
@@ -392,47 +394,42 @@ function OverviewTab({
</div>
{!profileLoading && profile ? (
<>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<MetricCard
label={t("profile.rebateLimit", { defaultValue: "回水上限 (%)" })}
value={`${rebateCap ?? "0"}%`}
/>
<MetricCard
label={t("profile.defaultPlayerRebate", { defaultValue: "默认玩家回水 (%)" })}
value={`${percentValueToUi(profile.default_player_rebate ?? 0)}%`}
/>
<MetricCard
label={t("profile.riskTags", { defaultValue: "风控标签" })}
value={
(profile.risk_tags?.length ?? 0) > 0
? profile.risk_tags!.join(", ")
: t("common:states.none", { defaultValue: "无" })
}
/>
</div>
<div className="grid gap-3 sm:grid-cols-3">
<CapabilityMetric
label={t("profile.canGrantExtraRebate", { defaultValue: "允许额外回水" })}
enabled={profile.can_grant_extra_rebate === true}
yesLabel={yesLabel}
noLabel={noLabel}
/>
<CapabilityMetric
label={t("profile.canCreatePlayer", { defaultValue: "允许创建玩家" })}
enabled={profile.can_create_player !== false}
yesLabel={yesLabel}
noLabel={noLabel}
/>
<CapabilityMetric
label={t("profile.canCreateChildAgent", { defaultValue: "允许创建下级代理" })}
enabled={profile.can_create_child_agent === true}
yesLabel={yesLabel}
noLabel={noLabel}
/>
</div>
</>
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
<MetricCard
label={t("profile.rebateLimit", { defaultValue: "回水上限 (%)" })}
value={`${rebateCap ?? "0"}%`}
/>
<MetricCard
label={t("profile.defaultPlayerRebate", { defaultValue: "默认玩家回水 (%)" })}
value={`${percentValueToUi(profile.default_player_rebate ?? 0)}%`}
/>
<MetricCard
label={t("profile.riskTags", { defaultValue: "风控标签" })}
value={
(profile.risk_tags?.length ?? 0) > 0
? profile.risk_tags!.join(", ")
: t("common:states.none", { defaultValue: "无" })
}
/>
<CapabilityMetric
label={t("profile.canGrantExtraRebate", { defaultValue: "允许额外回水" })}
enabled={profile.can_grant_extra_rebate === true}
yesLabel={yesLabel}
noLabel={noLabel}
/>
<CapabilityMetric
label={t("profile.canCreatePlayer", { defaultValue: "允许创建玩家" })}
enabled={profile.can_create_player !== false}
yesLabel={yesLabel}
noLabel={noLabel}
/>
<CapabilityMetric
label={t("profile.canCreateChildAgent", { defaultValue: "允许创建下级代理" })}
enabled={profile.can_create_child_agent === true}
yesLabel={yesLabel}
noLabel={noLabel}
/>
</div>
) : null}
</div>
);
@@ -454,7 +451,7 @@ function CapabilityMetric({
<p className="text-xs font-medium text-muted-foreground">{label}</p>
<p
className={cn(
"mt-1.5 text-lg font-semibold",
"mt-1.5 text-2xl font-semibold tracking-tight",
enabled ? "text-foreground" : "text-muted-foreground",
)}
>

View File

@@ -231,7 +231,7 @@ export function AgentLineSidebar({
const hasAnyAgent = displayForest.length > 0;
return (
<aside className="flex h-full min-h-[28rem] w-full flex-col bg-muted/10 lg:w-[18rem] lg:shrink-0 lg:border-r lg:border-border/70">
<aside className="flex min-h-0 h-full w-full flex-col bg-muted/10 lg:w-[18rem] lg:shrink-0 lg:border-r lg:border-border/70">
<div className="space-y-3 border-b border-border/60 bg-card px-4 py-4">
{siteLabel ? (
<p className="truncate text-xs font-medium text-foreground/80" title={siteLabel}>

View File

@@ -23,6 +23,7 @@ import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import {
AGENT_PERCENT_HARD_MAX,
actualShareRateFromRelative,
creditLimitRangeIssue,
isNumericStepperOutOfRange,
maxCreditLimitFromParent,
maxDefaultRebatePercent,
@@ -104,6 +105,10 @@ export function AgentProfileFields({
const maxDefaultRebate = maxDefaultRebatePercent(rebateLimit, parentCaps);
const maxCreditLimit = maxCreditLimitFromParent(parentCaps, baselineCreditLimit);
const actualShare = actualShareRateFromRelative(Number.parseFloat(shareRate) || 0, parentCaps);
const creditRangeIssue = creditLimitRangeIssue(creditLimit, {
min: minCreditLimit,
max: maxCreditLimit,
});
return (
<div className="space-y-6">
@@ -224,19 +229,19 @@ export function AgentProfileFields({
})}
</p>
) : null}
{profileScalarsEditable &&
isNumericStepperOutOfRange(creditLimit, {
min: minCreditLimit,
max: maxCreditLimit,
integer: true,
}) ? (
{profileScalarsEditable && creditRangeIssue === "below_min" ? (
<p className="text-xs text-destructive">
{t("profile.validation.creditBelowAllocated", {
defaultValue: "授信额度不能低于已下发给下级/玩家的总额(当前至少 {{min}}",
min: formatAdminCreditMajorDecimal(minCreditLimit, currencyCode),
})}
</p>
) : null}
{profileScalarsEditable && creditRangeIssue === "above_max" && maxCreditLimit !== undefined ? (
<p className="text-xs text-destructive">
{t("profile.validation.creditExceedsParentWithMax", {
defaultValue: "授信额度不能超过 {{max}}",
max:
maxCreditLimit !== undefined
? formatAdminCreditMajorDecimal(maxCreditLimit, currencyCode)
: creditLimit,
max: formatAdminCreditMajorDecimal(maxCreditLimit, currencyCode),
})}
</p>
) : null}

View File

@@ -21,6 +21,8 @@ import { AgentLineProvisionWizard } from "@/modules/agents/agent-line-provision-
import { AgentLineSidebar } from "@/modules/agents/agent-line-sidebar";
import { AgentProfileFields } from "@/modules/agents/agent-profile-fields";
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
import { AdminPageGuide } from "@/components/admin/admin-page-guide";
import { ADMIN_DOC_LINKS } from "@/lib/admin-doc-links";
import { AdminNoIntegrationSiteState } from "@/components/admin/admin-no-integration-site-state";
import { Button } from "@/components/ui/button";
import {
@@ -952,7 +954,7 @@ export function AgentsConsole(): React.ReactElement {
if (showProvisionEmpty) {
return (
<div className="flex min-h-[32rem] flex-col gap-0">
<div className="flex min-h-0 flex-1 flex-col gap-0">
<AgentLineProvisionWizard
embedded
defaultSiteCode={activeSiteCode}
@@ -965,13 +967,14 @@ export function AgentsConsole(): React.ReactElement {
}
return (
<div className="flex min-h-[32rem] flex-col gap-0">
<div className="flex min-h-0 flex-1 flex-col gap-0">
<AdminPageGuide guide={t("pageGuide")} docHref={ADMIN_DOC_LINKS.agents} className="mb-4 px-1" />
<ConfirmDialog />
{canViewAgents && err ? <p className="px-1 text-sm text-destructive">{err}</p> : null}
{canViewAgents ? (
<div className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-2xl border border-border/70 bg-card shadow-sm lg:flex-row">
<div className="flex max-h-[calc(100dvh-14rem)] min-h-[28rem] flex-1 flex-col overflow-hidden rounded-2xl border border-border/70 bg-card shadow-sm lg:flex-row">
{showAgentSidebar ? (
<AgentLineSidebar
siteLabel={selectedSiteLabel}

View File

@@ -0,0 +1,418 @@
"use client";
import Link from "next/link";
import {
DocList,
DocNote,
DocOrderedList,
DocPage,
DocPageHeader,
DocParagraph,
DocSection,
DocTable,
} from "@/components/docs/doc-ui";
import { useAdminDoc } from "@/modules/docs/admin/use-admin-doc";
export function AdminOverviewDocScreen(): React.ReactElement {
const { p, rows, list, header } = useAdminDoc("overview");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocNote>{p("loginNote")}</DocNote>
<DocSection title={p("scope")}>
<DocList items={list("scopeItems")} />
</DocSection>
<DocSection title={p("menuMap")}>
<DocParagraph>{p("menuMapNote")}</DocParagraph>
<DocTable compact headers={header("menu")} rows={rows("menuMapRows")} />
</DocSection>
<DocSection title={p("modes")}>
<DocTable compact headers={header("module")} rows={rows("modeRows")} />
</DocSection>
<DocSection title={p("readingOrder")}>
<DocOrderedList items={list("readingItems")} />
</DocSection>
<DocNote>{p("note")}</DocNote>
</DocPage>
);
}
export function AdminRolesDocScreen(): React.ReactElement {
const { p, rows, list, header } = useAdminDoc("roles");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("matrix")}>
<DocTable compact headers={header("role")} rows={rows("matrixRows")} />
</DocSection>
<DocSection title={p("accountModel")}>
<DocList items={list("accountItems")} />
</DocSection>
<DocSection title={p("accountSetup")}>
<DocOrderedList items={list("accountSetupSteps")} />
</DocSection>
<DocNote>{p("note")}</DocNote>
</DocPage>
);
}
export function AdminSiteSetupDocScreen(): React.ReactElement {
const { p, rows, list, header } = useAdminDoc("siteSetup");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("path")}>
<DocOrderedList items={list("pathItems")} />
</DocSection>
<DocSection title={p("fields")}>
<DocTable compact headers={header("field")} rows={rows("fieldRows")} />
</DocSection>
<DocSection title={p("caution")}>
<DocList items={list("cautionItems")} />
</DocSection>
<DocNote>
{p("apiLinkNote")}{" "}
<Link
href="/docs/integration/preparation"
className="font-medium text-slate-900 underline decoration-slate-300 underline-offset-2 hover:decoration-slate-500"
>
{p("apiLinkLabel")}
</Link>
</DocNote>
</DocPage>
);
}
export function AdminDrawsDocScreen(): React.ReactElement {
const { p, rows, list, header } = useAdminDoc("draws");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("lifecycle")}>
<DocTable compact headers={header("status")} rows={rows("statusRows")} />
</DocSection>
<DocSection title={p("workflow")}>
<DocOrderedList items={list("workflowItems")} />
</DocSection>
<DocSection title={p("publishWalkthrough")}>
<DocOrderedList items={list("publishSteps")} />
</DocSection>
<DocSection title={p("reopenWalkthrough")}>
<DocOrderedList items={list("reopenSteps")} />
</DocSection>
<DocSection title={p("rules")}>
<DocList items={list("rulesItems")} />
</DocSection>
<DocNote>{p("note")}</DocNote>
<DocParagraph>
{p("manualReviewLinkNote")}{" "}
<Link
href="/docs/admin/manual-review"
className="font-medium text-slate-900 underline decoration-slate-300 underline-offset-2 hover:decoration-slate-500"
>
{p("manualReviewLinkLabel")}
</Link>
</DocParagraph>
</DocPage>
);
}
export function AdminSettlementCenterDocScreen(): React.ReactElement {
const { p, rows, list, header } = useAdminDoc("settlementCenter");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("entry")}>
<DocList items={list("entryItems")} />
</DocSection>
<DocSection title={p("periodFlow")}>
<DocOrderedList items={list("periodItems")} />
</DocSection>
<DocSection title={p("openWalkthrough")}>
<DocOrderedList items={list("openSteps")} />
</DocSection>
<DocSection title={p("closeWalkthrough")}>
<DocOrderedList items={list("closeSteps")} />
</DocSection>
<DocSection title={p("paymentWalkthrough")}>
<DocOrderedList items={list("paymentSteps")} />
</DocSection>
<DocSection title={p("detailTabs")}>
<DocList items={list("detailTabItems")} />
</DocSection>
<DocSection title={p("billStatusSection")}>
<DocTable compact headers={header("billStatusTable")} rows={rows("billStatusRows")} />
</DocSection>
<DocSection title={p("operations")}>
<DocList items={list("operationItems")} />
</DocSection>
<DocNote>{p("note")}</DocNote>
<DocParagraph>
{p("fundOpsLinkNote")}{" "}
<Link
href="/docs/admin/fund-operations"
className="font-medium text-slate-900 underline decoration-slate-300 underline-offset-2 hover:decoration-slate-500"
>
{p("fundOpsLinkLabel")}
</Link>
</DocParagraph>
</DocPage>
);
}
export function AdminAgentsDocScreen(): React.ReactElement {
const { p, rows, list, header } = useAdminDoc("agents");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("structure")}>
<DocOrderedList items={list("structureItems")} />
</DocSection>
<DocSection title={p("provisionWalkthrough")}>
<DocOrderedList items={list("provisionSteps")} />
</DocSection>
<DocSection title={p("dailyWalkthrough")}>
<DocOrderedList items={list("dailySteps")} />
</DocSection>
<DocSection title={p("profile")}>
<DocTable compact headers={header("field")} rows={rows("profileRows")} />
</DocSection>
<DocSection title={p("siteAdmin")}>
<DocList items={list("siteAdminItems")} />
</DocSection>
<DocNote>{p("note")}</DocNote>
</DocPage>
);
}
export function AdminPlayersDocScreen(): React.ReactElement {
const { p, rows, list, header } = useAdminDoc("players");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("list")}>
<DocList items={list("listItems")} />
</DocSection>
<DocSection title={p("createWalkthrough")}>
<DocOrderedList items={list("createSteps")} />
</DocSection>
<DocSection title={p("freezeWalkthrough")}>
<DocOrderedList items={list("freezeSteps")} />
</DocSection>
<DocSection title={p("modes")}>
<DocTable compact headers={header("module")} rows={rows("modeRows")} />
</DocSection>
<DocSection title={p("detail")}>
<DocList items={list("detailItems")} />
</DocSection>
<DocNote>{p("note")}</DocNote>
</DocPage>
);
}
export function AdminTicketsDocScreen(): React.ReactElement {
const { p, list } = useAdminDoc("tickets");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("entry")}>
<DocList items={list("entryItems")} />
</DocSection>
<DocSection title={p("filter")}>
<DocList items={list("filterItems")} />
</DocSection>
<DocSection title={p("detail")}>
<DocList items={list("detailItems")} />
</DocSection>
<DocNote>{p("note")}</DocNote>
</DocPage>
);
}
export function AdminWalletDocScreen(): React.ReactElement {
const { p, list } = useAdminDoc("wallet");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("walletSection")}>
<DocList items={list("walletItems")} />
</DocSection>
<DocSection title={p("transferSection")}>
<DocList items={list("transferItems")} />
</DocSection>
<DocSection title={p("reconcileSection")}>
<DocOrderedList items={list("reconcileSteps")} />
</DocSection>
<DocNote>{p("note")}</DocNote>
<DocParagraph>
{p("fundOpsLinkNote")}{" "}
<Link
href="/docs/admin/fund-operations"
className="font-medium text-slate-900 underline decoration-slate-300 underline-offset-2 hover:decoration-slate-500"
>
{p("fundOpsLinkLabel")}
</Link>
</DocParagraph>
</DocPage>
);
}
export function AdminConfigDocScreen(): React.ReactElement {
const { p, list } = useAdminDoc("config");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("plays")}>
<DocList items={list("playsItems")} />
</DocSection>
<DocSection title={p("odds")}>
<DocList items={list("oddsItems")} />
</DocSection>
<DocSection title={p("riskCap")}>
<DocList items={list("riskCapItems")} />
</DocSection>
<DocSection title={p("risk")}>
<DocList items={list("riskItems")} />
</DocSection>
<DocSection title={p("jackpot")}>
<DocList items={list("jackpotItems")} />
</DocSection>
<DocNote>{p("note")}</DocNote>
</DocPage>
);
}
export function AdminFundOperationsDocScreen(): React.ReactElement {
const { p, rows, list, header } = useAdminDoc("fundOperations");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("twoSystems")}>
<DocList items={list("twoSystemsItems")} />
</DocSection>
<DocSection title={p("creditModel")}>
<DocList items={list("creditModelItems")} />
</DocSection>
<DocSection title={p("creditLifecycle")}>
<DocOrderedList items={list("creditLifecycleSteps")} />
</DocSection>
<DocSection title={p("creditLedger")}>
<DocTable compact headers={header("ledger")} rows={rows("creditLedgerRows")} />
</DocSection>
<DocSection title={p("creditBill")}>
<DocList items={list("creditBillItems")} />
</DocSection>
<DocSection title={p("creditAdjust")}>
<DocOrderedList items={list("creditAdjustSteps")} />
</DocSection>
<DocSection title={p("walletLifecycle")}>
<DocOrderedList items={list("walletLifecycleSteps")} />
</DocSection>
<DocSection title={p("walletTxn")}>
<DocTable compact headers={header("walletTxn")} rows={rows("walletTxnRows")} />
</DocSection>
<DocSection title={p("walletReconcile")}>
<DocTable compact headers={header("reconcile")} rows={rows("walletReconcileRows")} />
</DocSection>
<DocSection title={p("compare")}>
<DocTable compact headers={header("compare")} rows={rows("compareRows")} />
</DocSection>
<DocNote>{p("note")}</DocNote>
</DocPage>
);
}
export function AdminManualReviewDocScreen(): React.ReactElement {
const { p, rows, list, header } = useAdminDoc("manualReview");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("distinction")}>
<DocList items={list("distinctionItems")} />
</DocSection>
<DocSection title={p("drawReview")}>
<DocList items={list("drawReviewItems")} />
</DocSection>
<DocSection title={p("drawPublishSteps")}>
<DocOrderedList items={list("drawPublishStepItems")} />
</DocSection>
<DocSection title={p("cooldown")}>
<DocList items={list("cooldownItems")} />
</DocSection>
<DocSection title={p("settlementBatch")}>
<DocList items={list("settlementBatchItems")} />
</DocSection>
<DocSection title={p("batchStatusSection")}>
<DocTable compact headers={header("batchStatus")} rows={rows("batchStatusRows")} />
</DocSection>
<DocSection title={p("batchWalkthrough")}>
<DocOrderedList items={list("batchWalkthroughSteps")} />
</DocSection>
<DocSection title={p("settings")}>
<DocTable compact headers={header("setting")} rows={rows("settingRows")} />
</DocSection>
<DocNote>{p("settingsNote")}</DocNote>
<DocNote>{p("rejectNote")}</DocNote>
<DocNote>{p("note")}</DocNote>
</DocPage>
);
}
export function AdminReportsDocScreen(): React.ReactElement {
const { p, rows, list, header } = useAdminDoc("reports");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("entry")}>
<DocList items={list("entryItems")} />
</DocSection>
<DocSection title={p("types")}>
<DocTable compact headers={header("report")} rows={rows("reportRows")} />
</DocSection>
<DocSection title={p("export")}>
<DocList items={list("exportItems")} />
</DocSection>
<DocSection title={p("scope")}>
<DocList items={list("scopeItems")} />
</DocSection>
</DocPage>
);
}
export function AdminFaqDocScreen(): React.ReactElement {
const { p, rows, list, header } = useAdminDoc("faq");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection>
<DocTable compact headers={header("faq")} rows={rows("faqRows")} />
</DocSection>
<DocSection title={p("integration")}>
<DocList items={list("integrationItems")} />
<DocParagraph>
<Link
href="/docs/integration"
className="font-medium text-slate-900 underline decoration-slate-300 underline-offset-2 hover:decoration-slate-500"
>
{p("integrationLinkLabel")}
</Link>
</DocParagraph>
</DocSection>
</DocPage>
);
}

View File

@@ -0,0 +1,46 @@
"use client";
import { useTranslation } from "react-i18next";
export type AdminDocPageKey =
| "overview"
| "roles"
| "siteSetup"
| "draws"
| "settlementCenter"
| "agents"
| "players"
| "tickets"
| "wallet"
| "config"
| "fundOperations"
| "manualReview"
| "reports"
| "faq";
const ADMIN_DOCS_NS = "adminDocs";
function asStringArray(value: unknown): string[] {
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
}
function asStringMatrix(value: unknown): string[][] {
return Array.isArray(value)
? value.filter((row): row is string[] => Array.isArray(row) && row.every((cell) => typeof cell === "string"))
: [];
}
export function useAdminDoc(page: AdminDocPageKey) {
const { t } = useTranslation(ADMIN_DOCS_NS);
return {
t,
p: (key: string) => t(`pages.${page}.${key}`, { ns: ADMIN_DOCS_NS }),
rows: (key: string) =>
asStringMatrix(t(`pages.${page}.${key}`, { returnObjects: true, ns: ADMIN_DOCS_NS })),
list: (key: string) =>
asStringArray(t(`pages.${page}.${key}`, { returnObjects: true, ns: ADMIN_DOCS_NS })),
header: (key: string) =>
asStringArray(t(`headers.${key}`, { returnObjects: true, ns: ADMIN_DOCS_NS })),
};
}

View File

@@ -0,0 +1,373 @@
"use client";
import {
DocCode,
DocEndpoint,
DocPage,
DocPageHeader,
DocParagraph,
DocSection,
DocTable,
DocList,
DocOrderedList,
DocNote,
} from "@/components/docs/doc-ui";
const CURL_PLAYER_ME = `curl -sS "https://{lottery_api}/api/v1/player/me" \\
-H "Authorization: Bearer {JWT}" \\
-H "Accept: application/json"`;
const CURL_WALLET_BALANCE = `curl -sS "https://{wallet_host}/wallet/balance?site_code=demo&site_player_id=100001&currency_code=NPR" \\
-H "Authorization: Bearer {wallet_api_key}"`;
const CURL_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"
}'`;
const IFRAME_EXAMPLE = `<!DOCTYPE html>
<html>
<head><title>主站集成</title></head>
<body>
<iframe id="lotteryFrame" src="https://lottery.example.com/"></iframe>
<script>
const LOTTERY_ORIGIN = "https://lottery.example.com";
window.addEventListener("message", (event) => {
if (event.origin !== LOTTERY_ORIGIN) return;
const { data } = event;
switch (data.type) {
case "LOTTERY_READY":
fetchNewToken().then(token => {
sendToIframe("MAIN_INIT_TOKEN", { token });
});
break;
case "LOTTERY_TOKEN_NEEDED":
case "LOTTERY_TOKEN_REFRESH_REQUEST":
fetchNewToken().then(token => {
sendToIframe("MAIN_REFRESH_TOKEN", { token });
});
break;
}
});
function sendToIframe(type, payload) {
const iframe = document.getElementById("lotteryFrame");
iframe.contentWindow.postMessage(
{ type, payload, timestamp: Date.now(), source: "main-site" },
LOTTERY_ORIGIN
);
}
async function fetchNewToken() {
const res = await fetch("/api/auth/lottery-token", {
method: "POST",
credentials: "include",
});
const data = await res.json();
return data.token;
}
</script>
</body>
</html>`;
const JWT_SIGN_TS = `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 function ApiReferenceDocScreen(): React.ReactElement {
return (
<DocPage>
<DocPageHeader
title="API 对接参考"
description="面向主站开发/集成工程师的技术文档SSO、iframe、钱包网关、错误码与上线清单。"
/>
<DocSection title="1. 接入总览">
<DocTable
compact
headers={["组件", "职责", "实现方"]}
rows={[
["主站", "签发 JWT实现钱包网关余额查询 / 扣款 / 加款)", "客户"],
["彩票 API", "验签、玩法、划转、下注、结算、开奖", "我方"],
["彩票前端", "H5 / iframe 承载,玩家交互界面", "我方"],
]}
/>
<DocNote>
使minor 2000 = 20.00 UTF-8 JSON
</DocNote>
<DocOrderedList
items={[
"主站登录 → 服务端签发 JWT",
"进入彩票URL 跳转或 iframe 嵌入)",
"转入:主站扣款 + 彩票加款",
"下注 / 派奖(彩票内余额)",
"转出:彩票扣款 + 主站加款",
]}
/>
</DocSection>
<DocSection title="2. 快速开始">
<DocParagraph></DocParagraph>
<DocCode language="bash">{CURL_PLAYER_ME}</DocCode>
<DocCode language="bash">{CURL_WALLET_BALANCE}</DocCode>
<DocCode language="bash">{CURL_WALLET_DEBIT}</DocCode>
</DocSection>
<DocSection title="3. 接入配置">
<DocOrderedList
items={[
"超管登录后台 → 平台管理 → 接入配置 → 新建站点",
"填写站点编码site_code、名称、默认币种、wallet_api_url、lottery_h5_base_url、iframe_allowed_origins",
"创建成功后立即保存一次性展示的 sso_jwt_secret 和 wallet_api_key",
"将密钥写入主站 .env执行「连通性测试」",
]}
/>
<DocTable
compact
headers={["项", "后台字段", "主站 .env", "说明"]}
rows={[
["站点编码", "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彩票允许嵌入"],
]}
/>
<DocNote>
site_code wallet_api_url HTTPS localhost / IP
</DocNote>
</DocSection>
<DocSection title="4. 单点登录SSO">
<DocParagraph> HS256 JWT</DocParagraph>
<DocTable
compact
headers={["字段", "类型", "必填", "说明"]}
rows={[
["site_code", "string", "是", "接入站点编码"],
["site_player_id", "string", "是", "主站用户 ID稳定唯一"],
["iat", "number", "是", "签发时间Unix 秒)"],
["exp", "number", "是", "过期时间Unix 秒);建议 ≤ 300 秒"],
]}
/>
<DocCode language="typescript">{JWT_SIGN_TS}</DocCode>
<DocSection title="入场方式">
<DocParagraph> A URL </DocParagraph>
<DocCode>{`https://{lottery_h5_base_url}/?token={JWT}`}</DocCode>
<DocParagraph> B iframe </DocParagraph>
<DocCode>{`<iframe id="lotteryFrame" src="https://lottery.example.com/"></iframe>`}</DocCode>
<DocNote>
JWT GET /api/v1/player/me username / nickname
</DocNote>
</DocSection>
<DocSection title="入场接口">
<DocEndpoint method="GET" path="/api/v1/player/me" />
<DocCode language="http">{`GET /api/v1/player/me
Authorization: Bearer {JWT}
Accept-Language: zh`}</DocCode>
<DocNote>
JWT API Authorization: Bearer
</DocNote>
</DocSection>
<DocSection title="SSO 错误码">
<DocTable
compact
headers={["错误码", "说明"]}
rows={[
["8001", "缺少 Authorization 头"],
["8002", "JWT 无效或已过期"],
["8003", "玩家未建档"],
["8004", "SSO 密钥未配置"],
["8005", "账号已冻结(站点不存在/停用或玩家冻结)"],
]}
/>
</DocSection>
</DocSection>
<DocSection title="5. iframe 协议">
<DocOrderedList
items={[
"主站页面嵌入 <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",
]}
/>
<DocNote>
postMessage origin https://www.partner.com禁止使用 *。
MAIN_INIT_TOKEN LOTTERY_READY token
</DocNote>
<DocSection title="彩票 → 主站">
<DocTable
compact
headers={["方向", "消息类型", "说明"]}
rows={[
["→ 主站", "LOTTERY_READY", "子页就绪,请求下发 token"],
["→ 主站", "LOTTERY_TOKEN_NEEDED", "token 失效,请求续签"],
["→ 主站", "LOTTERY_TOKEN_REFRESH_REQUEST", "主动请求刷新 token"],
["→ 主站", "LOTTERY_HEARTBEAT", "心跳(可忽略)"],
["→ 主站", "LOTTERY_TOKEN_REFRESHED", "续签成功通知"],
]}
/>
</DocSection>
<DocSection title="主站 → 彩票">
<DocTable
compact
headers={["方向", "消息类型", "载荷", "说明"]}
rows={[
["→ 彩票", "MAIN_INIT_TOKEN", "{ token }", "首次下发 JWT"],
["→ 彩票", "MAIN_REFRESH_TOKEN", "{ token }", "续签 JWT"],
["→ 彩票", "MAIN_REQUEST_STATUS", "—", "请求子页状态"],
["→ 彩票", "MAIN_NAVIGATE", "{ path }", "导航到指定路径"],
]}
/>
</DocSection>
<DocSection title="完整示例">
<DocCode>{IFRAME_EXAMPLE}</DocCode>
</DocSection>
</DocSection>
<DocSection title="6. 钱包网关">
<DocParagraph>
Authorization: Bearer {"{wallet_api_key}"}
</DocParagraph>
<DocSection title="查询余额">
<DocEndpoint method="GET" path="/wallet/balance" />
<DocTable
compact
headers={["参数", "类型", "说明"]}
rows={[
["site_code", "string", "站点编码"],
["site_player_id", "string", "主站用户 ID"],
["currency_code", "string", "币种代码"],
]}
/>
<DocCode>{`{
"success": true,
"data": { "main_balance": 500000, "currency_code": "NPR" }
}`}</DocCode>
</DocSection>
<DocSection title="扣款(转入时调用)">
<DocEndpoint method="POST" path="/wallet/debit-for-lottery" />
<DocTable
compact
headers={["字段", "类型", "说明"]}
rows={[
["site_code", "string", "站点编码"],
["site_player_id", "string", "主站用户 ID"],
["player_id", "number", "彩票玩家 ID参考"],
["currency_code", "string", "币种"],
["amount_minor", "integer", "minor 正整数"],
["idempotent_key", "string", "幂等键"],
]}
/>
<DocCode>{`{
"success": true,
"external_ref_no": "MW-001",
"data": { "main_balance": 498000, "currency_code": "NPR" }
}`}</DocCode>
</DocSection>
<DocSection title="加款(转出时调用)">
<DocEndpoint method="POST" path="/wallet/credit-from-lottery" />
<DocParagraph></DocParagraph>
</DocSection>
<DocSection title="HTTP 契约">
<DocTable
compact
headers={["场景", "HTTP 状态码", "响应体"]}
rows={[
["扣款/加款成功", "200", "success: true含 external_ref_no 与 data.main_balance"],
["余额查询成功", "200", "success: truedata.main_balance + currency_code"],
["参数非法", "422", "success: falsemessage: invalid request"],
["鉴权失败", "401", "success: falsemessage: unauthorized"],
["业务拒绝", "409", "success: falsemessage 说明原因(如余额不足)"],
["幂等重放", "200", "与首次成功/拒绝响应完全一致"],
]}
/>
</DocSection>
<DocNote>
idempotent_key + JSONHTTP 200/ success: false
</DocNote>
</DocSection>
<DocSection title="7. 错误码汇总">
<DocSection title="SSO 鉴权">
<DocTable
compact
headers={["错误码", "说明"]}
rows={[
["8001", "缺少 Authorization 头"],
["8002", "JWT 无效或已过期"],
["8003", "玩家未建档"],
["8004", "SSO 密钥未配置"],
["8005", "账号已冻结"],
]}
/>
</DocSection>
<DocSection title="彩票钱包 / 划转">
<DocTable
compact
headers={["错误码", "说明"]}
rows={[
["1001", "彩票余额不足(转出时)"],
["1009", "主站钱包处理失败"],
["1010", "幂等键冲突(同键不同金额)"],
["2003", "请先转入后再下注"],
]}
/>
</DocSection>
<DocSection title="客户钱包网关 HTTP">
<DocTable
compact
headers={["HTTP", "message", "原因"]}
rows={[
["401", "unauthorized", "API Key 错误"],
["422", "invalid request", "字段或金额非法"],
["409", "—", "业务拒绝(如余额不足)"],
]}
/>
</DocSection>
</DocSection>
<DocSection title="8. 上线清单">
<DocList
items={[
"测试与生产site_code、密钥、域名完全隔离",
"JWT 仅服务端签发,有效期 ≤ 5 分钟",
"钱包接口走 HTTPS超时建议 ≤ 10 秒",
"idempotent_key 幂等处理已正确实现",
"iframe 模式:已配置 iframe_allowed_origins",
"全链路联调通过:转入 → 下注 → 派奖 → 转出",
]}
/>
</DocSection>
</DocPage>
);
}

View File

@@ -1,3 +1,13 @@
/** 文档默认环境地址(当前 Tanumo 部署;客户独立环境以商务交付为准) */
export const DOC_ENV = {
lotteryH5Origin: "https://front.tanumo.com",
lotteryH5Wallet: "https://front.tanumo.com/wallet",
lotteryApiBase: "https://lotterylaravel.tanumo.com",
adminBase: "https://lotteryadmin.tanumo.com",
integrationDocs: "https://lotteryadmin.tanumo.com/docs/integration",
integrationSitesAdmin: "https://lotteryadmin.tanumo.com/admin/config/integration-sites",
} as const;
/** 代码示例(语言无关,三语共用) */
export const SSO_JWT_PAYLOAD_EXAMPLE = `{
"site_code": "demo",
@@ -6,24 +16,28 @@ export const SSO_JWT_PAYLOAD_EXAMPLE = `{
"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_JWT_SIGN_EXAMPLE = `import jwt from "jsonwebtoken";
export const SSO_ENTRY_URL = `https://{lottery_host}/?token={JWT}`;
const token = jwt.sign(
{
site_code: "demo",
site_player_id: "100001",
},
process.env.MAIN_SITE_SSO_JWT_SECRET!,
{
algorithm: "HS256",
expiresIn: 300,
},
);`;
export const SSO_ENTRY_URL = `${DOC_ENV.lotteryH5Origin}/?token={JWT}`;
export const SSO_POSTMESSAGE = `iframe.contentWindow.postMessage(
{ type: "MAIN_INIT_TOKEN", token: jwt, source: "main-site" },
"https://lottery.example.com"
"${DOC_ENV.lotteryH5Origin}"
);`;
export const PLAYER_ME_REQUEST = `GET /api/v1/player/me
export const PLAYER_ME_REQUEST = `GET ${DOC_ENV.lotteryApiBase}/api/v1/player/me
Authorization: Bearer {JWT}
Accept-Language: zh`;
@@ -48,7 +62,7 @@ export const PLAYER_ME_SUCCESS = `{
export const IFRAME_CHILD_READY = `{
"type": "LOTTERY_READY",
"payload": { "url": "https://lottery.example.com/", "userAgent": "..." },
"payload": { "url": "${DOC_ENV.lotteryH5Origin}/", "userAgent": "..." },
"timestamp": 1718000000000,
"source": "lottery-iframe"
}`;
@@ -60,7 +74,33 @@ export const IFRAME_PARENT_INIT = `{
"source": "main-site"
}`;
export const ACCEPTANCE_PLAYER_ME = `curl -sS "https://{lottery_api}/api/v1/player/me" \\
export const IFRAME_INTEGRATION_EXAMPLE = `<iframe id="lotteryFrame" src="${DOC_ENV.lotteryH5Origin}/"></iframe>
<script>
const LOTTERY_ORIGIN = "${DOC_ENV.lotteryH5Origin}";
window.addEventListener("message", (event) => {
if (event.origin !== LOTTERY_ORIGIN) return;
const { type } = event.data ?? {};
if (type === "LOTTERY_READY" || type === "LOTTERY_TOKEN_NEEDED" || type === "LOTTERY_TOKEN_REFRESH_REQUEST") {
issueLotteryJwt().then((token) => sendToken(type === "LOTTERY_READY" ? "MAIN_INIT_TOKEN" : "MAIN_REFRESH_TOKEN", token));
}
});
function sendToken(type, token) {
document.getElementById("lotteryFrame").contentWindow.postMessage(
{ type, token, timestamp: Date.now(), source: "main-site" },
LOTTERY_ORIGIN,
);
}
async function issueLotteryJwt() {
const res = await fetch("/api/your-server/sign-lottery-jwt", { method: "POST", credentials: "include" });
const { token } = await res.json();
return token;
}
</script>`;
export const ACCEPTANCE_PLAYER_ME = `curl -sS "${DOC_ENV.lotteryApiBase}/api/v1/player/me" \\
-H "Authorization: Bearer {JWT}" \\
-H "Accept: application/json"`;

View File

@@ -6,6 +6,7 @@ import {
DocList,
DocNote,
DocOrderedList,
DocPage,
DocPageHeader,
DocSection,
DocTable,
@@ -18,6 +19,7 @@ import {
ACCEPTANCE_PLAYER_ME,
ACCEPTANCE_WALLET_DEBIT,
IFRAME_CHILD_READY,
IFRAME_INTEGRATION_EXAMPLE,
IFRAME_PARENT_INIT,
PLAYER_AUTH_ERROR,
PLAYER_ME_REQUEST,
@@ -33,10 +35,6 @@ import {
} 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");
@@ -62,8 +60,34 @@ export function OverviewDocScreen(): React.ReactElement {
);
}
export function DeliveryDocScreen(): React.ReactElement {
const { p, rows, list, header } = useIntegrationDoc("delivery");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("handoffScope")}>
<DocTable compact headers={header("handoffTable")} rows={rows("handoffRows")} />
</DocSection>
<DocSection title={p("weProvide")}>
<DocTable compact headers={header("param")} rows={rows("provideRows")} />
</DocSection>
<DocSection title={p("youProvide")}>
<DocTable compact headers={header("param")} rows={rows("submitRows")} />
</DocSection>
<DocSection title={p("environment")}>
<DocTable compact headers={header("env")} rows={rows("environmentRows")} />
</DocSection>
<DocSection title={p("process")}>
<DocOrderedList items={list("processSteps")} />
</DocSection>
<DocNote>{p("note")}</DocNote>
</DocPage>
);
}
export function QuickstartDocScreen(): React.ReactElement {
const { p, rows, list, header } = useIntegrationDoc("quickstart");
const { p, list } = useIntegrationDoc("quickstart");
return (
<DocPage>
@@ -74,12 +98,6 @@ export function QuickstartDocScreen(): React.ReactElement {
<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>
@@ -150,6 +168,7 @@ export function SsoDocScreen(): React.ReactElement {
<DocSection title={p("sign")}>
<DocCode language="typescript">{SSO_JWT_SIGN_EXAMPLE}</DocCode>
</DocSection>
<DocNote>{p("noExchangeNote")}</DocNote>
<DocSection title={p("entryA")}>
<DocCode>{SSO_ENTRY_URL}</DocCode>
</DocSection>
@@ -158,7 +177,6 @@ export function SsoDocScreen(): React.ReactElement {
<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>
@@ -169,10 +187,7 @@ export function SsoDocScreen(): React.ReactElement {
<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>
<DocNote>{p("refreshNote")}</DocNote>
<DocSection title={p("authResponse")}>
<DocCode language="http">{PLAYER_AUTH_ERROR}</DocCode>
</DocSection>
@@ -192,7 +207,8 @@ export function IframeDocScreen(): React.ReactElement {
<DocSection title={p("sequence")}>
<DocOrderedList items={list("sequenceSteps")} />
</DocSection>
<DocSection title={p("envelope")}>
<DocSection title={p("envelopeSection")}>
<DocTable compact headers={header("envelopeTable")} rows={rows("envelopeRows")} />
<DocNote>{p("envelopeNote")}</DocNote>
</DocSection>
<DocSection title={p("childMessages")}>
@@ -203,6 +219,9 @@ export function IframeDocScreen(): React.ReactElement {
<DocTable compact headers={header("message")} rows={rows("parentMessageRows")} />
<DocCode>{IFRAME_PARENT_INIT}</DocCode>
</DocSection>
<DocSection title={p("example")}>
<DocCode language="html">{IFRAME_INTEGRATION_EXAMPLE}</DocCode>
</DocSection>
<DocSection title={p("targetOrigin")}>
<DocNote>{p("targetOriginNote")}</DocNote>
</DocSection>
@@ -305,10 +324,36 @@ export function GoLiveDocScreen(): React.ReactElement {
return (
<DocPage>
<DocPageHeader title={p("title")} />
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("deliveryChecklist")}>
<DocList items={list("deliveryItems")} />
</DocSection>
<DocSection title={p("checklist")}>
<DocList items={list("items")} />
</DocSection>
</DocPage>
);
}
export function TroubleshootingDocScreen(): React.ReactElement {
const { p, rows, header } = useIntegrationDoc("troubleshooting");
return (
<DocPage>
<DocPageHeader title={p("title")} description={p("description")} />
<DocSection title={p("faq")}>
<DocTable compact headers={header("faq")} rows={rows("faqRows")} />
</DocSection>
<DocSection title={p("jwt")}>
<DocTable compact headers={header("faq")} rows={rows("jwtRows")} />
</DocSection>
<DocSection title={p("iframe")}>
<DocTable compact headers={header("faq")} rows={rows("iframeRows")} />
</DocSection>
<DocSection title={p("wallet")}>
<DocTable compact headers={header("faq")} rows={rows("walletRows")} />
</DocSection>
<DocNote>{p("note")}</DocNote>
</DocPage>
);
}

View File

@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
type DocPageKey =
| "overview"
| "delivery"
| "quickstart"
| "fundamentals"
| "setup"
@@ -12,16 +13,32 @@ type DocPageKey =
| "wallet"
| "transfer"
| "errors"
| "troubleshooting"
| "golive";
const INTEGRATION_DOCS_NS = "integrationDocs";
function asStringArray(value: unknown): string[] {
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
}
function asStringMatrix(value: unknown): string[][] {
return Array.isArray(value)
? value.filter((row): row is string[] => Array.isArray(row) && row.every((cell) => typeof cell === "string"))
: [];
}
export function useIntegrationDoc(page: DocPageKey) {
const { t } = useTranslation("integrationDocs");
const { t } = useTranslation(INTEGRATION_DOCS_NS);
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[],
p: (key: string) => t(`pages.${page}.${key}`, { ns: INTEGRATION_DOCS_NS }),
rows: (key: string) =>
asStringMatrix(t(`pages.${page}.${key}`, { returnObjects: true, ns: INTEGRATION_DOCS_NS })),
list: (key: string) =>
asStringArray(t(`pages.${page}.${key}`, { returnObjects: true, ns: INTEGRATION_DOCS_NS })),
header: (key: string) =>
asStringArray(t(`headers.${key}`, { returnObjects: true, ns: INTEGRATION_DOCS_NS })),
};
}

View File

@@ -17,6 +17,8 @@ import {
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { LOTTERY_SCHEDULE_TIMEZONE } from "@/lib/lottery-schedule-timezone";
import { AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import { AdminPageGuide } from "@/components/admin/admin-page-guide";
import { ADMIN_DOC_LINKS } from "@/lib/admin-doc-links";
import { AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
import { Button } from "@/components/ui/button";
@@ -249,6 +251,7 @@ export function DrawsIndexConsole() {
return (
<>
<AdminPageGuide guide={t("pageGuide")} docHref={ADMIN_DOC_LINKS.draws} className="mb-4" />
<Card className="admin-list-card">
<CardHeader className="admin-list-header flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<CardTitle className="admin-list-title">{t("statusListTitle")}</CardTitle>

View File

@@ -19,6 +19,8 @@ import {
putAdminIntegrationSite,
} from "@/api/admin-integration-sites";
import { AdminPageCard } from "@/components/admin/admin-page-card";
import { AdminPageGuide } from "@/components/admin/admin-page-guide";
import { ADMIN_DOC_LINKS } from "@/lib/admin-doc-links";
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
@@ -527,6 +529,11 @@ export function IntegrationSitesConsole({
return (
<>
<AdminPageGuide
guide={t("integrationSites.pageGuide")}
docHref={ADMIN_DOC_LINKS.siteSetup}
className="mb-4"
/>
<AdminPageCard
title={t("integrationSites.title")}
description={t("integrationSites.description")}

View File

@@ -23,6 +23,8 @@ import {
import { flattenAgentTree, type FlatAgentOption } from "@/lib/admin-agent-tree";
import { useAdminSiteCodeOptions } from "@/hooks/use-admin-site-code-options";
import { AdminAgentCell, AdminAgentHead } from "@/components/admin/admin-agent-columns";
import { AdminPageGuide } from "@/components/admin/admin-page-guide";
import { ADMIN_DOC_LINKS } from "@/lib/admin-doc-links";
import { AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
@@ -425,6 +427,7 @@ export function PlayersConsole(): React.ReactElement {
return (
<div className="flex w-full max-w-none flex-col gap-6">
<AdminPageGuide guide={t("pageGuide")} docHref={ADMIN_DOC_LINKS.players} />
<Card className="admin-list-card">
<CardHeader className="admin-list-header flex flex-col gap-4">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">

View File

@@ -112,7 +112,7 @@ export function ReportJobsPanel({ canExport, refreshToken = 0, reportType }: Rep
<TableHead>{t("tasks.columns.format")}</TableHead>
<TableHead>{t("tasks.columns.status")}</TableHead>
<TableHead>{t("tasks.columns.createdAt")}</TableHead>
<TableHead className="sticky right-0 z-10 bg-card text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">{t("tasks.columns.actions")}</TableHead>
<TableHead className="sticky right-0 z-20 bg-muted w-14 text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">{t("tasks.columns.actions")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>

View File

@@ -46,6 +46,8 @@ import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { PRD_REPORT_EXPORT, PRD_REPORT_VIEW } from "@/lib/admin-prd";
import { useAdminProfile } from "@/stores/admin-session";
import { adminAgentDisplayLabel } from "@/components/admin/admin-agent-columns";
import { AdminPageGuide } from "@/components/admin/admin-page-guide";
import { ADMIN_DOC_LINKS } from "@/lib/admin-doc-links";
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
import { AdminDateRangeField } from "@/components/admin/admin-date-range-field";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
@@ -1620,6 +1622,7 @@ export function ReportsConsole({ initialCategory }: { initialCategory?: ReportCa
return (
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4">
<AdminPageGuide guide={t("pageGuide")} docHref={ADMIN_DOC_LINKS.reports} />
<Card className="admin-list-card">
<CardHeader className="admin-list-header pb-3">
<div className="flex flex-col gap-3">

View File

@@ -184,7 +184,7 @@ export function RiskIndexConsole() {
<TableHead>{t("drawNo")}</TableHead>
<TableHead>{t("status")}</TableHead>
<TableHead>{t("closeTime")}</TableHead>
<TableHead className="sticky right-0 z-10 bg-card text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">{t("table.actions", { ns: "common" })}</TableHead>
<TableHead className="sticky right-0 z-20 bg-muted w-14 text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">{t("table.actions", { ns: "common" })}</TableHead>
</TableRow>
</TableHeader>
<TableBody>

View File

@@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import { useCallback, useState } from "react";
import { useExportLabels } from "@/hooks/use-export-labels";
import { useTranslation } from "react-i18next";
@@ -10,10 +11,10 @@ import { getAdminRiskPoolLockLogs } from "@/api/admin-risk";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import { AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import {
Select,
SelectContent,
@@ -35,20 +36,29 @@ import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"
import { riskActionTypeLabel, riskSourceReasonLabel } from "@/modules/risk/risk-display";
import { formatAdminMinorUnits } from "@/lib/money";
import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminRiskLockLogListData, AdminRiskLockLogRow } from "@/types/api/admin-risk";
import type {
AdminRiskLockLogListData,
AdminRiskLockLogRow,
AdminRiskLockLogTicketRow,
} from "@/types/api/admin-risk";
const ACTION_ALL = "__all__";
const GROUP_TICKET = "ticket";
const GROUP_ENTRY = "entry";
function riskActionFilterLabel(
value: string,
t: (key: string) => string,
): string {
function riskActionFilterLabel(value: string, t: (key: string) => string): string {
if (value === ACTION_ALL) {
return t("noLimit");
}
return riskActionTypeLabel(value, t);
}
function isTicketGroupData(
data: AdminRiskLockLogListData | null,
): data is AdminRiskLockLogListData & { group_by: "ticket"; items: AdminRiskLockLogTicketRow[] } {
return data?.group_by === "ticket";
}
export function RiskLockLogsConsole({ drawId }: { drawId: number }) {
const { t } = useTranslation(["risk", "common"]);
const tRef = useTranslationRef(["risk", "common"]);
@@ -66,6 +76,8 @@ export function RiskLockLogsConsole({ drawId }: { drawId: number }) {
const [appliedNumber, setAppliedNumber] = useState("");
const [draftAction, setDraftAction] = useState<string>(ACTION_ALL);
const [appliedAction, setAppliedAction] = useState<string>(ACTION_ALL);
const [draftGroupBy, setDraftGroupBy] = useState<"ticket" | "entry">(GROUP_TICKET);
const [appliedGroupBy, setAppliedGroupBy] = useState<"ticket" | "entry">(GROUP_TICKET);
const load = useCallback(async () => {
setLoading(true);
@@ -74,34 +86,58 @@ export function RiskLockLogsConsole({ drawId }: { drawId: number }) {
const d = await getAdminRiskPoolLockLogs(drawId, {
page,
per_page: perPage,
group_by: appliedGroupBy,
normalized_number: appliedNumber.trim() === "" ? undefined : appliedNumber.trim(),
action_type:
appliedAction === ACTION_ALL
? undefined
: (appliedAction as "lock" | "release"),
appliedAction === ACTION_ALL ? undefined : (appliedAction as "lock" | "release"),
});
setData(d);
} catch (e) {
const msg =
e instanceof LotteryApiBizError ? e.message : tRef.current("loadLogsFailed");
const msg = e instanceof LotteryApiBizError ? e.message : tRef.current("loadLogsFailed");
setError(msg);
setData(null);
} finally {
setLoading(false);
}
}, [drawId, page, perPage, appliedAction, appliedNumber]);
}, [drawId, page, perPage, appliedAction, appliedNumber, appliedGroupBy, tRef]);
useAsyncEffect(() => {
void load();
}, [drawId, page, perPage, appliedAction, appliedNumber]);
}, [drawId, page, perPage, appliedAction, appliedNumber, appliedGroupBy]);
const ticketGrouped = isTicketGroupData(data);
const currencyCode = data?.currency_code ?? "NPR";
return (
<Card className="admin-list-card">
<CardHeader className="admin-list-header">
<CardHeader className="admin-list-header space-y-1">
<CardTitle className="admin-list-title">{t("lockLogsTitle")}</CardTitle>
<CardDescription className="text-xs">{t("lockLogsGroupedHint")}</CardDescription>
</CardHeader>
<CardContent className="admin-list-content">
<div className="admin-list-toolbar">
<div className="admin-list-field">
<Label htmlFor="risk-log-group" className="sm:w-20 sm:shrink-0">
{t("groupBy")}
</Label>
<Select
modal={false}
value={draftGroupBy}
onValueChange={(v) => {
if (v === GROUP_TICKET || v === GROUP_ENTRY) setDraftGroupBy(v);
}}
>
<SelectTrigger id="risk-log-group" size="sm" className="h-8 w-full sm:w-44">
<SelectValue>
{draftGroupBy === GROUP_TICKET ? t("groupByTicket") : t("groupByEntry")}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value={GROUP_TICKET}>{t("groupByTicket")}</SelectItem>
<SelectItem value={GROUP_ENTRY}>{t("groupByEntry")}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="admin-list-field">
<Label htmlFor="risk-log-number" className="sm:w-20 sm:shrink-0">
{t("number4d")}
@@ -149,6 +185,7 @@ export function RiskLockLogsConsole({ drawId }: { drawId: number }) {
onClick={() => {
setAppliedNumber(draftNumber);
setAppliedAction(draftAction);
setAppliedGroupBy(draftGroupBy);
setPage(1);
}}
>
@@ -159,23 +196,71 @@ export function RiskLockLogsConsole({ drawId }: { drawId: number }) {
{error ? <p className="text-sm text-destructive">{error}</p> : null}
<>
<div className="admin-table-shell">
<Table id={`risk-lock-logs-table-${drawId}`}>
<TableHeader>
<TableRow>
<TableHead>{t("time")}</TableHead>
<TableHead>{t("searchNumber")}</TableHead>
<TableHead>{t("action")}</TableHead>
<TableHead className="text-center">{t("amount")}</TableHead>
<TableHead>{t("source")}</TableHead>
<TableHead>{t("ticketNo")}</TableHead>
<TableHead>{t("playCode")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading && !data ? <AdminTableLoadingRow colSpan={7} /> : null}
{(data?.items ?? []).map((row: AdminRiskLockLogRow) => (
<div className="admin-table-shell">
<Table id={`risk-lock-logs-table-${drawId}`}>
<TableHeader>
{ticketGrouped ? (
<TableRow>
<TableHead>{t("time")}</TableHead>
<TableHead>{t("ticketNo")}</TableHead>
<TableHead>{t("playCode")}</TableHead>
<TableHead>{t("number")}</TableHead>
<TableHead className="text-center">{t("combinationCount")}</TableHead>
<TableHead className="text-center">{t("lockReleaseSummary")}</TableHead>
<TableHead className="text-center">{t("amount")}</TableHead>
<TableHead className="text-center">{t("viewDetail")}</TableHead>
</TableRow>
) : (
<TableRow>
<TableHead>{t("time")}</TableHead>
<TableHead>{t("searchNumber")}</TableHead>
<TableHead>{t("action")}</TableHead>
<TableHead className="text-center">{t("amount")}</TableHead>
<TableHead>{t("source")}</TableHead>
<TableHead>{t("ticketNo")}</TableHead>
<TableHead>{t("playCode")}</TableHead>
</TableRow>
)}
</TableHeader>
<TableBody>
{loading && !data ? (
<AdminTableLoadingRow colSpan={ticketGrouped ? 8 : 7} />
) : null}
{ticketGrouped
? data.items.map((row: AdminRiskLockLogTicketRow) => (
<TableRow key={row.ticket_item_id}>
<TableCell className="whitespace-nowrap text-sm text-muted-foreground">
{row.last_at ? formatDt(row.last_at) : "—"}
</TableCell>
<TableCell className="font-mono text-sm">{row.ticket_no}</TableCell>
<TableCell className="text-sm">{playCodeLabel(row.play_code)}</TableCell>
<TableCell className="font-mono text-sm">{row.original_number}</TableCell>
<TableCell className="text-center text-sm tabular-nums">
{row.combination_count}
{row.number_count !== row.combination_count ? (
<span className="text-muted-foreground"> / {row.number_count}</span>
) : null}
</TableCell>
<TableCell className="text-center text-xs tabular-nums">
{t("lock")} {row.lock_entry_count} · {t("release")} {row.release_entry_count}
</TableCell>
<TableCell className="text-center text-xs tabular-nums">
<div>{formatAdminMinorUnits(row.total_lock_amount, currencyCode)}</div>
<div className="text-muted-foreground">
{formatAdminMinorUnits(row.total_release_amount, currencyCode)}
</div>
</TableCell>
<TableCell className="text-center">
<Link
href={`/admin/tickets/${encodeURIComponent(row.ticket_no)}`}
className="text-xs font-medium text-primary hover:underline"
>
{t("viewTicket")}
</Link>
</TableCell>
</TableRow>
))
: (data?.items as AdminRiskLockLogRow[] | undefined)?.map((row) => (
<TableRow key={row.id}>
<TableCell className="whitespace-nowrap text-sm text-muted-foreground">
{row.created_at ? formatDt(row.created_at) : "—"}
@@ -187,35 +272,45 @@ export function RiskLockLogsConsole({ drawId }: { drawId: number }) {
{riskActionTypeLabel(row.action_type, t)}
</TableCell>
<TableCell className="text-center text-sm font-semibold">
{formatAdminMinorUnits(row.amount, data?.currency_code ?? "NPR")}
{formatAdminMinorUnits(row.amount, currencyCode)}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{riskSourceReasonLabel(row.source_reason, t)}
</TableCell>
<TableCell className="font-mono text-sm">{row.ticket_no ?? "—"}</TableCell>
<TableCell className="font-mono text-sm">
{row.ticket_no ? (
<Link
href={`/admin/tickets/${encodeURIComponent(row.ticket_no)}`}
className="text-primary hover:underline"
>
{row.ticket_no}
</Link>
) : (
"—"
)}
</TableCell>
<TableCell className="text-sm">{playCodeLabel(row.play_code)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</TableBody>
</Table>
</div>
{data ? (
<AdminListPaginationFooter
selectId={`risk-logs-${drawId}`}
total={data.meta.total}
page={data.meta.current_page}
lastPage={data.meta.last_page}
perPage={data.meta.per_page}
loading={loading}
onPerPageChange={(n) => {
setPerPage(n);
setPage(1);
}}
onPageChange={setPage}
/>
) : null}
</>
{data ? (
<AdminListPaginationFooter
selectId={`risk-logs-${drawId}-${appliedGroupBy}`}
total={data.meta.total}
page={data.meta.current_page}
lastPage={data.meta.last_page}
perPage={data.meta.per_page}
loading={loading}
onPerPageChange={(n) => {
setPerPage(n);
setPage(1);
}}
onPageChange={setPage}
/>
) : null}
</CardContent>
</Card>
);

View File

@@ -63,7 +63,7 @@ function riskSortLabel(
return option ? t(option.label) : value;
}
export type RiskPoolListFilter = "all" | "sold_out" | "high_risk";
export type RiskPoolListFilter = "all" | "active" | "sold_out" | "high_risk";
type RiskPoolsConsoleProps = {
drawId: number;
@@ -87,7 +87,7 @@ function resolveInitialFilter(
if (soldOutOnly) {
return "sold_out";
}
return "all";
return "active";
}
export function RiskPoolsConsole({
@@ -96,7 +96,7 @@ export function RiskPoolsConsole({
titleKey,
soldOutOnly,
initialFilter: initialFilterProp,
defaultSort = "number_asc",
defaultSort = "usage_desc",
allowSortChange = true,
}: RiskPoolsConsoleProps) {
const { t } = useTranslation(["risk", "common"]);
@@ -130,6 +130,7 @@ export function RiskPoolsConsole({
per_page: perPage,
sold_out_only: filter === "sold_out",
high_risk_only: filter === "high_risk",
active_only: filter === "active" && number.trim() === "",
normalized_number: number.trim(),
sort,
});
@@ -216,12 +217,15 @@ export function RiskPoolsConsole({
<SelectValue>
{filter === "all"
? t("filterAll")
: filter === "sold_out"
? t("filterSoldOut")
: t("filterHighRisk")}
: filter === "active"
? t("filterActive")
: filter === "sold_out"
? t("filterSoldOut")
: t("filterHighRisk")}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="active">{t("filterActive")}</SelectItem>
<SelectItem value="all">{t("filterAll")}</SelectItem>
<SelectItem value="sold_out">{t("filterSoldOut")}</SelectItem>
<SelectItem value="high_risk">{t("filterHighRisk")}</SelectItem>

View File

@@ -10,6 +10,8 @@ import { toast } from "sonner";
import { getSettlementPeriods, type SettlementPeriodRow } from "@/api/admin-agent-settlement";
import { getAdminIntegrationSites } from "@/api/admin-integration-sites";
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
import { AdminPageGuide } from "@/components/admin/admin-page-guide";
import { ADMIN_DOC_LINKS } from "@/lib/admin-doc-links";
import { AdminNoIntegrationSiteState } from "@/components/admin/admin-no-integration-site-state";
import { AgentBillDetail } from "@/modules/settlement/agent-bill-detail";
import { SettlementCenterPeriodDetail } from "@/modules/settlement/settlement-center-period-detail";
@@ -58,6 +60,7 @@ export function SettlementCenterShell(): React.ReactElement {
const canFinanceAdjustments = canOperateBills && boundAgent === null;
const [siteOptions, setSiteOptions] = useState<SiteOption[]>([]);
const [sitesReady, setSitesReady] = useState(() => boundAgent?.admin_site_id != null);
const [adminSiteId, setAdminSiteId] = useState<number | null>(null);
const [sitePickerOpen, setSitePickerOpen] = useState(false);
const [siteKeyword, setSiteKeyword] = useState("");
@@ -80,27 +83,36 @@ export function SettlementCenterShell(): React.ReactElement {
currency_code: "NPR",
}]);
setAdminSiteId(boundAgent.admin_site_id);
setSitesReady(true);
return;
}
void getAdminIntegrationSites().then((sites) => {
const options = (sites.items ?? []).map((site) => ({
id: site.id,
label: formatAdminSiteLabel(site.name, site.code),
code: site.code,
currency_code: site.currency_code ?? "NPR",
}));
setSiteOptions(options);
setAdminSiteId((current) => {
if (siteFromUrl !== null && options.some((site) => site.id === siteFromUrl)) {
return siteFromUrl;
}
if (current !== null && options.some((site) => site.id === current)) {
return current;
}
return options[0]?.id ?? null;
setSitesReady(false);
void getAdminIntegrationSites()
.then((sites) => {
const options = (sites.items ?? []).map((site) => ({
id: site.id,
label: formatAdminSiteLabel(site.name, site.code),
code: site.code,
currency_code: site.currency_code ?? "NPR",
}));
setSiteOptions(options);
setAdminSiteId((current) => {
if (siteFromUrl !== null && options.some((site) => site.id === siteFromUrl)) {
return siteFromUrl;
}
if (current !== null && options.some((site) => site.id === current)) {
return current;
}
return options[0]?.id ?? null;
});
})
.catch(() => {
setSiteOptions([]);
})
.finally(() => {
setSitesReady(true);
});
});
}, [boundAgent, siteFromUrl]);
const siteId = adminSiteId ?? siteOptions[0]?.id ?? null;
@@ -283,14 +295,18 @@ export function SettlementCenterShell(): React.ReactElement {
};
}, [activePeriod, activePeriodId, activeView, periodsReady, router, siteId]);
const shellBootstrapping =
!sitesReady || (siteId !== null && !periodsReady);
return (
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4">
{siteId === null && siteOptions.length === 0 && boundAgent === null ? (
<AdminPageGuide guide={t("pageGuide")} docHref={ADMIN_DOC_LINKS.settlementCenter} />
{shellBootstrapping ? (
<AdminLoadingState />
) : siteId === null && siteOptions.length === 0 && boundAgent === null ? (
<AdminNoIntegrationSiteState canCreate={profile?.is_super_admin === true} />
) : siteId === null ? (
<p className="text-sm text-muted-foreground">{t("empty.noSite", { defaultValue: "请选择站点。" })}</p>
) : !periodsReady ? (
<AdminLoadingState />
) : isListMode ? (
<SettlementPeriodWorkbench
adminSiteId={siteId}

View File

@@ -140,7 +140,7 @@ export function SettlementCreditLedgerPanel({
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(20);
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
setLoading(true);

View File

@@ -401,18 +401,24 @@ export function PlayerTicketsConsole(): React.ReactElement {
<TableCell className="text-xs">{formatTs(row.updated_at)}</TableCell>
<TableCell className="sticky right-0 z-10 bg-card text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">
<AdminRowActionsMenu
actions={
row.player_id
actions={[
{
key: "view-ticket",
label: t("viewTicketDetail"),
icon: Eye,
href: `/admin/tickets/${encodeURIComponent(row.ticket_no)}`,
},
...(row.player_id
? [
{
key: "view-player",
label: t("viewPlayer", { ns: "tickets" }),
label: t("viewPlayer"),
icon: Eye,
href: adminPlayerDetailPath(row.player_id),
},
]
: []
}
: []),
]}
/>
</TableCell>
</TableRow>

View File

@@ -0,0 +1,230 @@
"use client";
import Link from "next/link";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useTranslationRef } from "@/hooks/use-translation-ref";
import { getAdminTicketItem } from "@/api/admin-ticket-detail";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { PlayerFundingModeBadge } from "@/components/admin/player-funding-badges";
import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog";
import { useExportLabels } from "@/hooks/use-export-labels";
import { adminPlayerDetailPath } from "@/lib/admin-player-paths";
import { formatAdminMinorUnits } from "@/lib/money";
import { cn } from "@/lib/utils";
import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminTicketItemDetail } from "@/types/api/admin-tickets";
function ticketStatusText(value: string, t: (key: string) => string): string {
const key = `statusOptions.${value}`;
const translated = t(key);
return translated === key ? value : translated;
}
export function TicketDetailConsole({ ticketNo }: { ticketNo: string }) {
const { t } = useTranslation(["tickets", "common"]);
const tRef = useTranslationRef(["tickets", "common"]);
const playCodeLabel = useAdminPlayCodeLabel();
const formatDt = useAdminDateTimeFormatter();
const exportLabels = useExportLabels("ticketCombinations");
const [data, setData] = useState<AdminTicketItemDetail | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [comboPage, setComboPage] = useState(1);
const [comboPerPage, setComboPerPage] = useState(20);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
const d = await getAdminTicketItem(ticketNo);
setData(d);
} catch (e) {
const msg = e instanceof LotteryApiBizError ? e.message : tRef.current("detailLoadFailed");
setError(msg);
setData(null);
} finally {
setLoading(false);
}
}, [ticketNo, tRef]);
useAsyncEffect(() => {
void load();
}, [ticketNo]);
const combinations = data?.combinations ?? [];
const comboLastPage = Math.max(1, Math.ceil(combinations.length / comboPerPage));
const comboPageSafe = Math.min(comboPage, comboLastPage);
const comboSlice = useMemo(() => {
const start = (comboPageSafe - 1) * comboPerPage;
return combinations.slice(start, start + comboPerPage);
}, [combinations, comboPageSafe, comboPerPage]);
if (error && !data) {
return (
<Card className="border-destructive/40">
<CardHeader>
<CardTitle>{t("detailTitle")}</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-sm text-destructive">{error}</p>
<Link href="/admin/tickets" className={cn(buttonVariants({ variant: "outline", size: "sm" }))}>
{t("backToList")}
</Link>
</CardContent>
</Card>
);
}
const currencyCode = data?.currency_code ?? "NPR";
return (
<div className="flex flex-col gap-4">
<Card>
<CardHeader className="space-y-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<CardTitle className="text-lg">{t("detailTitle")}</CardTitle>
<Link href="/admin/tickets" className={cn(buttonVariants({ variant: "outline", size: "sm" }))}>
{t("backToList")}
</Link>
</div>
<CardDescription className="font-mono text-xs">
{data?.ticket_no ?? ticketNo}
{data?.order_no ? ` · ${data.order_no}` : ""}
</CardDescription>
</CardHeader>
<CardContent className="space-y-3 text-sm">
{loading && !data ? (
<p className="text-muted-foreground">{t("loadingDetail")}</p>
) : data ? (
<div className="grid gap-2 sm:grid-cols-2">
<p>
<span className="text-muted-foreground">{t("drawNo")}</span>
<span className="font-mono font-medium">{data.draw_no ?? "—"}</span>
</p>
<p>
<span className="text-muted-foreground">{t("playCode")}</span>
<span>{playCodeLabel(data.play_code)}</span>
</p>
<p>
<span className="text-muted-foreground">{t("number")}</span>
<span className="font-mono font-medium">{data.original_number ?? "—"}</span>
</p>
<p>
<span className="text-muted-foreground">{t("combinationCount")}</span>
<span className="font-semibold tabular-nums">{data.combination_count}</span>
</p>
<p>
<span className="text-muted-foreground">{t("betAmount")}</span>
<span className="font-semibold tabular-nums">{data.total_bet_amount_formatted}</span>
</p>
<p>
<span className="text-muted-foreground">{t("actualDeduct")}</span>
<span className="font-semibold tabular-nums">{data.actual_deduct_amount_formatted}</span>
</p>
<p className="flex flex-wrap items-center gap-2">
<span className="text-muted-foreground">{t("status")}</span>
<AdminStatusBadge status={data.status}>{ticketStatusText(data.status, t)}</AdminStatusBadge>
</p>
<p className="flex flex-wrap items-center gap-2">
<span className="text-muted-foreground">{t("player", { ns: "tickets" })}</span>
<PlayerFundingModeBadge row={data} />
{data.player_id ? (
<Link href={adminPlayerDetailPath(data.player_id)} className="font-mono text-xs text-primary hover:underline">
{data.username ?? data.site_player_id ?? data.player_id}
</Link>
) : (
"—"
)}
</p>
<p>
<span className="text-muted-foreground">{t("placedAt")}</span>
<span>{data.placed_at ? formatDt(data.placed_at) : "—"}</span>
</p>
{data.fail_reason_text || data.fail_reason_code ? (
<p className="sm:col-span-2 text-destructive">
<span className="text-muted-foreground">{t("failReason")}</span>
{data.fail_reason_text ?? data.fail_reason_code}
</p>
) : null}
</div>
) : null}
</CardContent>
</Card>
<Card className="admin-list-card">
<CardHeader className="admin-list-header">
<CardTitle className="admin-list-title">{t("combinationsTitle")}</CardTitle>
<CardDescription className="text-xs">{t("combinationsHint")}</CardDescription>
</CardHeader>
<CardContent className="admin-list-content">
<div className="admin-list-actions mb-3 justify-end">
<AdminTableExportButton
tableId={`ticket-combinations-${ticketNo}`}
filename={exportLabels.filename}
sheetName={exportLabels.sheetName}
/>
</div>
<div className="admin-table-shell">
<Table id={`ticket-combinations-${ticketNo}`}>
<TableHeader>
<TableRow>
<TableHead className="w-16 text-center">No.</TableHead>
<TableHead>{t("number4d")}</TableHead>
<TableHead className="text-center">{t("comboBetAmount")}</TableHead>
<TableHead className="text-center">{t("comboEstimatedPayout")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading && !data ? <AdminTableLoadingRow colSpan={4} /> : null}
{comboSlice.map((row) => (
<TableRow key={`${row.combination_no}-${row.number_4d}`}>
<TableCell className="text-center font-mono text-xs">{row.combination_no}</TableCell>
<TableCell className="font-mono font-medium">{row.number_4d}</TableCell>
<TableCell className="text-center tabular-nums text-sm">
{formatAdminMinorUnits(row.bet_amount, currencyCode)}
</TableCell>
<TableCell className="text-center tabular-nums text-sm">
{formatAdminMinorUnits(row.estimated_payout, currencyCode)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{combinations.length > 0 ? (
<AdminListPaginationFooter
selectId={`ticket-combo-${ticketNo}`}
total={combinations.length}
page={comboPageSafe}
lastPage={comboLastPage}
perPage={comboPerPage}
loading={loading}
onPerPageChange={(n) => {
setComboPerPage(n);
setComboPage(1);
}}
onPageChange={setComboPage}
/>
) : null}
</CardContent>
</Card>
</div>
);
}

View File

@@ -37,11 +37,27 @@ export type AdminRiskLockLogRow = {
created_at: string | null;
};
export type AdminRiskLockLogTicketRow = {
ticket_item_id: number;
ticket_no: string;
play_code: string;
original_number: string;
combination_count: number;
player_id: number;
number_count: number;
lock_entry_count: number;
release_entry_count: number;
total_lock_amount: number;
total_release_amount: number;
last_at: string | null;
};
export type AdminRiskLockLogListData = {
draw_id: number;
draw_no: string;
currency_code: string | null;
items: AdminRiskLockLogRow[];
group_by: "ticket" | "entry";
items: AdminRiskLockLogRow[] | AdminRiskLockLogTicketRow[];
meta: AdminRiskPoolListMeta;
};

View File

@@ -39,6 +39,52 @@ export type AdminTicketItemRow = {
updated_at: string | null;
};
export type AdminTicketCombinationRow = {
combination_no: number;
number_4d: string;
bet_amount: number;
estimated_payout: number;
};
export type AdminTicketItemDetail = {
id: number;
ticket_no: string;
order_no: string | null;
order_status: string | null;
draw_id: number;
draw_no: string | null;
currency_code: string | null;
player_id: number;
site_code: string | null;
site_player_id: string | null;
username: string | null;
nickname: string | null;
funding_mode: string | null;
agent_node_id: number | null;
agent_code: string | null;
agent_name: string | null;
play_code: string;
dimension: number | null;
digit_slot: number | null;
original_number: string | null;
normalized_number: string | null;
combination_count: number;
unit_bet_amount: number;
total_bet_amount_minor: number;
total_bet_amount_formatted: string;
actual_deduct_amount_minor: number;
actual_deduct_amount_formatted: string;
risk_locked_amount: number;
status: string;
fail_reason_code: string | null;
fail_reason_text: string | null;
win_amount_minor: number;
win_amount_formatted: string;
placed_at: string | null;
updated_at: string | null;
combinations: AdminTicketCombinationRow[];
};
export type AdminTicketItemsData = {
items: AdminTicketItemRow[];
total: number;