diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index dfa211b..03f8222 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -15,14 +15,17 @@ const path = require("path"); /** 改成服务器上的绝对路径,例如 /www/wwwroot/lottery-admin.cjdhr.top */ -const APP_CWD = path.resolve(__dirname); +const DEPLOY_ROOT = path.resolve(__dirname); +// deploy.sh uploads the standalone bundle to app/. Keep runtime and static assets +// on the same build so Next.js chunk references cannot become mismatched. +const APP_CWD = path.join(DEPLOY_ROOT, "app"); module.exports = { apps: [ { name: "lotteryadmin", cwd: APP_CWD, - script: ".next/standalone/server.js", + script: "server.js", interpreter: "node", exec_mode: "fork", instances: 1, @@ -31,8 +34,8 @@ module.exports = { max_memory_restart: "1G", time: true, merge_logs: true, - out_file: path.join(APP_CWD, "logs/pm2-out.log"), - error_file: path.join(APP_CWD, "logs/pm2-error.log"), + out_file: path.join(DEPLOY_ROOT, "logs/pm2-out.log"), + error_file: path.join(DEPLOY_ROOT, "logs/pm2-error.log"), env: { NODE_ENV: "production", @@ -41,7 +44,7 @@ module.exports = { }, // LOTTERY_API_UPSTREAM 只写在 .env,勿在此硬编码(会覆盖 .env 导致 API 502) - env_file: path.join(APP_CWD, ".env"), + env_file: path.join(DEPLOY_ROOT, ".env"), }, ], }; diff --git a/public/logo_副本.png b/public/logo_副本.png new file mode 100644 index 0000000..2294dc4 Binary files /dev/null and b/public/logo_副本.png differ diff --git a/src/api/admin-draws.ts b/src/api/admin-draws.ts index 2acb434..dd343d5 100644 --- a/src/api/admin-draws.ts +++ b/src/api/admin-draws.ts @@ -23,6 +23,7 @@ export type AdminDrawListQuery = { draw_no?: string; status?: string; agent_node_id?: number; + has_player_bets?: boolean; }; export async function getAdminDraws(q: AdminDrawListQuery = {}): Promise { diff --git a/src/app/admin/(shell)/draws/[drawId]/publish/[batchId]/page.tsx b/src/app/admin/(shell)/draws/[drawId]/publish/[batchId]/page.tsx index fb99837..00e116f 100644 --- a/src/app/admin/(shell)/draws/[drawId]/publish/[batchId]/page.tsx +++ b/src/app/admin/(shell)/draws/[drawId]/publish/[batchId]/page.tsx @@ -1,6 +1,4 @@ -import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; -import { DrawPublishConsole } from "@/modules/draws/draw-publish-console"; -import { PRD_DRAW_RESULT_MANAGE } from "@/lib/admin-prd"; +import { redirect } from "next/navigation"; import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; @@ -9,10 +7,6 @@ export const metadata: Metadata = buildPageMetadata("draws", "publishTitle"); export default async function AdminDrawPublishBatchPage(props: { params: Promise<{ drawId: string; batchId: string }>; }) { - const { drawId, batchId } = await props.params; - return ( - - - - ); + const { drawId } = await props.params; + redirect(`/admin/draws/${drawId}/results`); } diff --git a/src/app/admin/(shell)/draws/[drawId]/results/page.tsx b/src/app/admin/(shell)/draws/[drawId]/results/page.tsx index e17201e..1a32374 100644 --- a/src/app/admin/(shell)/draws/[drawId]/results/page.tsx +++ b/src/app/admin/(shell)/draws/[drawId]/results/page.tsx @@ -1,6 +1,7 @@ import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; import { DrawResultsConsole } from "@/modules/draws/draw-results-console"; -import { PRD_DRAW_ACCESS_ANY } from "@/lib/admin-prd"; +import { DrawReviewConsole } from "@/modules/draws/draw-review-console"; +import { PRD_DRAW_ACCESS_ANY, PRD_DRAW_RESULT_MANAGE } from "@/lib/admin-prd"; export default async function AdminDrawResultsPage(props: { params: Promise<{ drawId: string }>; @@ -8,7 +9,12 @@ export default async function AdminDrawResultsPage(props: { const { drawId } = await props.params; return ( - +
+ + + + +
); } diff --git a/src/app/admin/(shell)/draws/[drawId]/review/page.tsx b/src/app/admin/(shell)/draws/[drawId]/review/page.tsx index f15f648..8684067 100644 --- a/src/app/admin/(shell)/draws/[drawId]/review/page.tsx +++ b/src/app/admin/(shell)/draws/[drawId]/review/page.tsx @@ -1,14 +1,8 @@ -import { AdminPermissionGate } from "@/components/admin/admin-permission-gate"; -import { DrawReviewConsole } from "@/modules/draws/draw-review-console"; -import { PRD_DRAW_RESULT_MANAGE } from "@/lib/admin-prd"; +import { redirect } from "next/navigation"; export default async function AdminDrawReviewPage(props: { params: Promise<{ drawId: string }>; }) { const { drawId } = await props.params; - return ( - - - - ); + redirect(`/admin/draws/${drawId}/results`); } diff --git a/src/app/favicon.ico b/src/app/favicon.ico index 718d6fe..d03a18b 100644 Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ diff --git a/src/i18n/locales/en/draws.json b/src/i18n/locales/en/draws.json index 9e3cfca..1891547 100644 --- a/src/i18n/locales/en/draws.json +++ b/src/i18n/locales/en/draws.json @@ -32,6 +32,7 @@ "queryDraw": "Search draw", "reset": "Reset", "fuzzyDrawNo": "Fuzzy draw no.", + "hasPlayerBets": "Has player bets", "viewDetails": "View draw details", "editDraw": { "action": "Edit draw", @@ -82,6 +83,10 @@ "rng": "RNG auto-generated", "manual": "Manual entry" }, + "resultGroups": { + "starter": "Starter", + "consolation": "Consolation" + }, "batchStatusOptions": { "pending_review": "Pending review", "published": "Published" diff --git a/src/i18n/locales/ne/draws.json b/src/i18n/locales/ne/draws.json index 787da23..7ed6456 100644 --- a/src/i18n/locales/ne/draws.json +++ b/src/i18n/locales/ne/draws.json @@ -116,6 +116,7 @@ "financeOverview": "ड्रअ वित्तीय सारांश", "finishedAt": "समाप्त समय", "fuzzyDrawNo": "फजी ड्रअ नं.", + "hasPlayerBets": "खेलाडीको दांव भएको", "generateFailed": "सिर्जना असफल भयो", "generatePlan": "ड्रअ योजना सिर्जना", "generateSuccess": "{{created}} ड्रअ सिर्जना भयो, बफर {{upcoming}}/{{target}}", @@ -176,6 +177,10 @@ "manual": "म्यानुअल प्रविष्टि", "rng": "RNG स्वचालित" }, + "resultGroups": { + "starter": "विशेष", + "consolation": "सान्त्वना" + }, "resultsTitle": "परिणाम", "reviewAndPublish": "समीक्षा / प्रकाशित", "reviewAndPublishAction": "जाँचेर प्रकाशित गर्नुहोस्", diff --git a/src/i18n/locales/zh/draws.json b/src/i18n/locales/zh/draws.json index 47c5ceb..8de05e0 100644 --- a/src/i18n/locales/zh/draws.json +++ b/src/i18n/locales/zh/draws.json @@ -32,6 +32,7 @@ "queryDraw": "查询期号", "reset": "重置", "fuzzyDrawNo": "模糊匹配期号", + "hasPlayerBets": "有玩家下注", "viewDetails": "查看期号详情", "editDraw": { "action": "编辑期号", @@ -82,6 +83,10 @@ "rng": "RNG 自动生成", "manual": "人工录入" }, + "resultGroups": { + "starter": "特别奖", + "consolation": "安慰奖" + }, "batchStatusOptions": { "pending_review": "待审核", "published": "已发布" diff --git a/src/modules/config/bet-provider-settings-panel.tsx b/src/modules/config/bet-provider-settings-panel.tsx index d6d8f82..bbedcba 100644 --- a/src/modules/config/bet-provider-settings-panel.tsx +++ b/src/modules/config/bet-provider-settings-panel.tsx @@ -41,6 +41,7 @@ import type { AdminBetProviderRow } from "@/types/api/admin-bet-provider"; type ProviderFormState = { code: string; name: string; + short_code: string; is_enabled: boolean; sort_order: string; }; @@ -48,6 +49,7 @@ type ProviderFormState = { const EMPTY_FORM: ProviderFormState = { code: "", name: "", + short_code: "", is_enabled: true, sort_order: "0", }; @@ -56,6 +58,7 @@ function toFormState(row: AdminBetProviderRow): ProviderFormState { return { code: row.code, name: row.name, + short_code: row.short_code, is_enabled: row.is_enabled, sort_order: String(row.sort_order), }; @@ -121,8 +124,9 @@ export function BetProviderSettingsPanel() { async function handleSubmit(): Promise { const name = form.name.trim(); const sortOrder = parseSortOrder(form.sort_order); - if (name === "" || (mode === "create" && form.code.trim() === "")) { - toast.error(t("betProviders.form.required", { ns: "config", defaultValue: "请填写代码和名称" })); + const shortCode = form.short_code.trim().toUpperCase(); + if (name === "" || shortCode.length !== 1 || !/^[A-Z]$/.test(shortCode) || (mode === "create" && form.code.trim() === "")) { + toast.error(t("betProviders.form.required", { ns: "config", defaultValue: "请填写代码、名称和一个字母缩写" })); return; } @@ -132,6 +136,7 @@ export function BetProviderSettingsPanel() { await postAdminBetProvider({ code: form.code.trim().toUpperCase(), name, + short_code: shortCode, is_enabled: form.is_enabled, sort_order: sortOrder, }); @@ -139,6 +144,7 @@ export function BetProviderSettingsPanel() { } else if (editingId !== null) { await putAdminBetProvider(editingId, { name, + short_code: shortCode, is_enabled: form.is_enabled, sort_order: sortOrder, }); @@ -173,6 +179,7 @@ export function BetProviderSettingsPanel() { {t("betProviders.table.code", { ns: "config", defaultValue: "代码" })} {t("betProviders.table.name", { ns: "config", defaultValue: "名称" })} + {t("betProviders.table.shortCode", { ns: "config", defaultValue: "缩写" })} {t("betProviders.table.enabled", { ns: "config", defaultValue: "状态" })} {t("betProviders.table.sort", { ns: "config", defaultValue: "排序" })} @@ -183,12 +190,12 @@ export function BetProviderSettingsPanel() { {loading ? ( - + {t("betProviders.loading", { ns: "config", defaultValue: "加载中..." })} ) : items.length === 0 ? ( - + ) : ( items.map((row) => ( @@ -201,6 +208,7 @@ export function BetProviderSettingsPanel() { ) : null} + {row.short_code} {row.is_enabled @@ -267,6 +275,17 @@ export function BetProviderSettingsPanel() { disabled={saving} /> +
+ + updateForm("short_code", e.target.value.toUpperCase().replace(/[^A-Z]/g, ""))} + disabled={saving} + /> +
- - - {t("odds.table.prizeScope", { ns: "config" })} - {t("odds.table.multiplier", { ns: "config" })} - - - - {scopeEditorRows.map(({ scope, row, hint, sourceLabel, sourceClassName }) => ( - - -
- {prizeScopeLabel(scope, t)} - {hint ? {hint} : null} - {row ? ( - - {sourceLabel} - - ) : null} -
-
- - {row ? ( - canEditDraft ? ( - - updateOddsForScope(scope, { - odds_value: parseOddsMultiplierInput(e.target.value), - }) - } - /> - ) : ( - - {oddsMultiplierLabel(row.odds_value)} - - ) - ) : ( - - {t("odds.missingScopeRow", { ns: "config", scope })} - - )} - -
- ))} - - {t("odds.rebateRate", { ns: "config" })} - - {canEditDraft ? ( - setRebateForPlayPercent(e.target.value)} - /> - ) : ( - - {rebatePercentUi} - - )} - - -
- - ); - const classicOddsGrid = ( -
+
{scopeEditorRows.map(({ scope, row, hint, sourceLabel, sourceClassName }) => ( -
+
))} -
+
@@ -896,7 +813,7 @@ export function OddsConfigDocScreen({ /> ) : resolvedPlayCode ? (
- {mergedLayout ? mergedOddsTable : classicOddsGrid} + {classicOddsGrid}
) : null} @@ -976,8 +893,8 @@ export function OddsConfigDocScreen({ return (
{toolbarBlock} -
-