diff --git a/.env.example b/.env.example
index d1b1413..cbd6815 100644
--- a/.env.example
+++ b/.env.example
@@ -22,7 +22,7 @@ LOTTERY_API_UPSTREAM=http://127.0.0.1:8000
# 可选:入口授权失败时返回主站(build 前设置,会打进 JS)
# NEXT_PUBLIC_MAIN_SITE_URL=http://localhost:5173
-# 可选:代理玩家登录时隐式携带的站点编号(不配则后端用 LOTTERY_DEFAULT_SITE_CODE)
+# 多站点部署时可选:玩家端登录请求附带 site_code 以优先匹配该站(玩家界面无需填写)
# NEXT_PUBLIC_PLAYER_SITE_CODE=default_site
# Reverb:本地全栈联调时取消注释,并 php artisan reverb:start;不配则走轮询
diff --git a/AGENTS.md b/AGENTS.md
index 7e2b30f..ec133f5 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -11,3 +11,4 @@ This version has breaking changes — APIs, conventions, and file structure may
## Learned Workspace Facts
- 信用盘流水对外类型:`win_credit`(中奖释额)、`bill_settlement`(账期收付),与钱包 `prize`/派彩文案分离。
+- 信用流水结余:`game_settlement_win`/`settlement_confirm`/`bet_hold_release` 会减 `used_credit` 并倒推结余;`settlement_payout`(账期收付)仅记账,`affects_available_credit: false`。
diff --git a/src/features/hall/hall-bet-preview-dialog.tsx b/src/features/hall/hall-bet-preview-dialog.tsx
index e20f3f5..da35ee2 100644
--- a/src/features/hall/hall-bet-preview-dialog.tsx
+++ b/src/features/hall/hall-bet-preview-dialog.tsx
@@ -116,6 +116,10 @@ export function HallBetPreviewDialog({
const { t } = useTranslation("player");
const summary = data?.summary;
const lines = data?.lines ?? [];
+ const periodRebate = summary?.instant_rebate_applied === false;
+ const rebateLabel = periodRebate
+ ? t("hall.preview.periodRebate", { defaultValue: "账期回水" })
+ : t("hall.preview.rebate");
useEffect(() => {
if (open && !placing && !data) {
@@ -216,7 +220,7 @@ export function HallBetPreviewDialog({
{t("hall.preview.amount")}
- {t("hall.preview.rebate")}
+ {rebateLabel}
|
{t("hall.preview.estimatedMax")}
@@ -245,7 +249,8 @@ export function HallBetPreviewDialog({
{formatMinorAsCurrency(ln.total_bet_amount, currencyCode)}
|
- -{formatMinorAsCurrency(ln.rebate_amount, currencyCode).replace(`${currencyCode} `, "")}
+ {periodRebate ? "" : "-"}
+ {formatMinorAsCurrency(ln.rebate_amount, currencyCode).replace(`${currencyCode} `, "")}
|
{formatMinorAsCurrency(ln.estimated_max_payout, currencyCode)}
@@ -268,9 +273,9 @@ export function HallBetPreviewDialog({
- {t("hall.preview.rebate")}
+ {rebateLabel}
- -
+ {periodRebate ? "" : "-"}
{formatMinorAsCurrency(summary.total_rebate_amount, currencyCode).replace(
`${currencyCode} `,
"",
@@ -292,6 +297,15 @@ export function HallBetPreviewDialog({
) : null}
+ {periodRebate ? (
+
+ {t("hall.preview.periodRebateHint", {
+ defaultValue:
+ "信用盘回水在账期结算入账,下注时按全额占用可用信用,此处为账期回水预估。",
+ })}
+
+ ) : null}
+
{data.warnings.length > 0 ? (
) : (
diff --git a/src/features/player/player-login-screen.tsx b/src/features/player/player-login-screen.tsx
index 380c361..e8075ad 100644
--- a/src/features/player/player-login-screen.tsx
+++ b/src/features/player/player-login-screen.tsx
@@ -15,10 +15,12 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { usePlayerSessionStore } from "@/stores/player-session-store";
+import {
+ validatePlayerLoginPassword,
+ validatePlayerLoginUsername,
+} from "@/lib/player-input-validation";
import { LotteryApiBizError } from "@/types/api/errors";
-const DEPLOY_SITE_CODE = process.env.NEXT_PUBLIC_PLAYER_SITE_CODE?.trim() ?? "";
-
function stripSearchParamFromBrowserUrl(name: string): void {
if (typeof window === "undefined") return;
const url = new URL(window.location.href);
@@ -58,10 +60,27 @@ export function PlayerLoginScreen(): React.ReactElement {
return;
}
+ const usernameIssue = validatePlayerLoginUsername(username);
+ if (usernameIssue === "invalid_charset") {
+ toast.error(
+ t("login.usernameInvalidCharset", {
+ defaultValue: "账号只能使用字母、数字、点(.)、下划线和连字符",
+ }),
+ );
+ return;
+ }
+
+ const passwordIssue = validatePlayerLoginPassword(password);
+ if (passwordIssue === "too_short") {
+ toast.error(
+ t("login.passwordMinLength", { defaultValue: "密码至少需要 6 个字符" }),
+ );
+ return;
+ }
+
setLoading(true);
try {
const data = await postPlayerAuthLogin({
- ...(DEPLOY_SITE_CODE !== "" ? { site_code: DEPLOY_SITE_CODE } : {}),
username: username.trim(),
password,
});
diff --git a/src/lib/player-input-validation.ts b/src/lib/player-input-validation.ts
new file mode 100644
index 0000000..8c5798f
--- /dev/null
+++ b/src/lib/player-input-validation.ts
@@ -0,0 +1,30 @@
+/** 彩票端登录账号:字母、数字、点、下划线、连字符。 */
+export const PLAYER_ACCOUNT_PATTERN = /^[a-zA-Z0-9._-]+$/;
+
+export const PLAYER_PASSWORD_MIN_LENGTH = 6;
+
+export type AccountValidationIssue = "empty" | "invalid_charset";
+export type PasswordValidationIssue = "empty" | "too_short";
+
+export function validatePlayerLoginUsername(value: string): AccountValidationIssue | null {
+ const trimmed = value.trim();
+ if (trimmed === "") {
+ return "empty";
+ }
+ if (!PLAYER_ACCOUNT_PATTERN.test(trimmed)) {
+ return "invalid_charset";
+ }
+
+ return null;
+}
+
+export function validatePlayerLoginPassword(value: string): PasswordValidationIssue | null {
+ if (value === "") {
+ return "empty";
+ }
+ if (value.length < PLAYER_PASSWORD_MIN_LENGTH) {
+ return "too_short";
+ }
+
+ return null;
+}
diff --git a/src/types/api/ticket.ts b/src/types/api/ticket.ts
index eff822f..bf0f280 100644
--- a/src/types/api/ticket.ts
+++ b/src/types/api/ticket.ts
@@ -53,6 +53,7 @@ export type TicketPreviewData = {
total_rebate_amount: number;
total_actual_deduct: number;
total_estimated_payout: number;
+ instant_rebate_applied?: boolean;
};
lines: TicketPreviewLine[];
warnings: TicketPreviewWarning[];
|