feat: enhance player login validation and update betting preview

- Added username and password validation to the player login screen, improving user feedback for input errors.
- Updated the betting preview dialog to conditionally display rebate information based on the applied instant rebate status.
- Enhanced documentation in AGENTS.md to clarify credit flow and settlement processes.
- Modified .env.example to provide clearer instructions for multi-site deployment configurations.
This commit is contained in:
2026-06-14 21:13:24 +08:00
parent 7c283627d3
commit 0b14e77f64
6 changed files with 73 additions and 8 deletions

View File

@@ -22,7 +22,7 @@ LOTTERY_API_UPSTREAM=http://127.0.0.1:8000
# 可选入口授权失败时返回主站build 前设置,会打进 JS # 可选入口授权失败时返回主站build 前设置,会打进 JS
# NEXT_PUBLIC_MAIN_SITE_URL=http://localhost:5173 # NEXT_PUBLIC_MAIN_SITE_URL=http://localhost:5173
# 可选:代理玩家登录时隐式携带的站点编号(不配则后端用 LOTTERY_DEFAULT_SITE_CODE # 多站点部署时可选:玩家登录请求附带 site_code 以优先匹配该站(玩家界面无需填写
# NEXT_PUBLIC_PLAYER_SITE_CODE=default_site # NEXT_PUBLIC_PLAYER_SITE_CODE=default_site
# Reverb本地全栈联调时取消注释并 php artisan reverb:start不配则走轮询 # Reverb本地全栈联调时取消注释并 php artisan reverb:start不配则走轮询

View File

@@ -11,3 +11,4 @@ This version has breaking changes — APIs, conventions, and file structure may
## Learned Workspace Facts ## Learned Workspace Facts
- 信用盘流水对外类型:`win_credit`(中奖释额)、`bill_settlement`(账期收付),与钱包 `prize`/派彩文案分离。 - 信用盘流水对外类型:`win_credit`(中奖释额)、`bill_settlement`(账期收付),与钱包 `prize`/派彩文案分离。
- 信用流水结余:`game_settlement_win`/`settlement_confirm`/`bet_hold_release` 会减 `used_credit` 并倒推结余;`settlement_payout`(账期收付)仅记账,`affects_available_credit: false`

View File

@@ -116,6 +116,10 @@ export function HallBetPreviewDialog({
const { t } = useTranslation("player"); const { t } = useTranslation("player");
const summary = data?.summary; const summary = data?.summary;
const lines = data?.lines ?? []; const lines = data?.lines ?? [];
const periodRebate = summary?.instant_rebate_applied === false;
const rebateLabel = periodRebate
? t("hall.preview.periodRebate", { defaultValue: "账期回水" })
: t("hall.preview.rebate");
useEffect(() => { useEffect(() => {
if (open && !placing && !data) { if (open && !placing && !data) {
@@ -216,7 +220,7 @@ export function HallBetPreviewDialog({
{t("hall.preview.amount")} {t("hall.preview.amount")}
</th> </th>
<th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black"> <th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black">
{t("hall.preview.rebate")} {rebateLabel}
</th> </th>
<th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black"> <th className="border-r border-[#dfe8f6] px-2 py-3 text-center font-black">
{t("hall.preview.estimatedMax")} {t("hall.preview.estimatedMax")}
@@ -245,7 +249,8 @@ export function HallBetPreviewDialog({
{formatMinorAsCurrency(ln.total_bet_amount, currencyCode)} {formatMinorAsCurrency(ln.total_bet_amount, currencyCode)}
</td> </td>
<td className="border-r border-[#e8eef7] px-2 py-3 text-center font-semibold tabular-nums text-emerald-600"> <td className="border-r border-[#e8eef7] px-2 py-3 text-center font-semibold tabular-nums text-emerald-600">
-{formatMinorAsCurrency(ln.rebate_amount, currencyCode).replace(`${currencyCode} `, "")} {periodRebate ? "" : "-"}
{formatMinorAsCurrency(ln.rebate_amount, currencyCode).replace(`${currencyCode} `, "")}
</td> </td>
<td className="border-r border-[#e8eef7] px-2 py-3 text-center font-black tabular-nums text-[#e5002c]"> <td className="border-r border-[#e8eef7] px-2 py-3 text-center font-black tabular-nums text-[#e5002c]">
{formatMinorAsCurrency(ln.estimated_max_payout, currencyCode)} {formatMinorAsCurrency(ln.estimated_max_payout, currencyCode)}
@@ -268,9 +273,9 @@ export function HallBetPreviewDialog({
</p> </p>
</div> </div>
<div className="border-r border-[#dfe8f6] px-2 py-3"> <div className="border-r border-[#dfe8f6] px-2 py-3">
<p className="font-bold text-[#304f86]">{t("hall.preview.rebate")}</p> <p className="font-bold text-[#304f86]">{rebateLabel}</p>
<p className="mt-1 font-black tabular-nums text-emerald-600"> <p className="mt-1 font-black tabular-nums text-emerald-600">
- {periodRebate ? "" : "-"}
{formatMinorAsCurrency(summary.total_rebate_amount, currencyCode).replace( {formatMinorAsCurrency(summary.total_rebate_amount, currencyCode).replace(
`${currencyCode} `, `${currencyCode} `,
"", "",
@@ -292,6 +297,15 @@ export function HallBetPreviewDialog({
</div> </div>
) : null} ) : null}
{periodRebate ? (
<p className="text-xs leading-relaxed text-slate-500">
{t("hall.preview.periodRebateHint", {
defaultValue:
"信用盘回水在账期结算入账,下注时按全额占用可用信用,此处为账期回水预估。",
})}
</p>
) : null}
{data.warnings.length > 0 ? ( {data.warnings.length > 0 ? (
<WarningsBlock warnings={data.warnings} /> <WarningsBlock warnings={data.warnings} />
) : ( ) : (

View File

@@ -15,10 +15,12 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { usePlayerSessionStore } from "@/stores/player-session-store"; import { usePlayerSessionStore } from "@/stores/player-session-store";
import {
validatePlayerLoginPassword,
validatePlayerLoginUsername,
} from "@/lib/player-input-validation";
import { LotteryApiBizError } from "@/types/api/errors"; import { LotteryApiBizError } from "@/types/api/errors";
const DEPLOY_SITE_CODE = process.env.NEXT_PUBLIC_PLAYER_SITE_CODE?.trim() ?? "";
function stripSearchParamFromBrowserUrl(name: string): void { function stripSearchParamFromBrowserUrl(name: string): void {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
const url = new URL(window.location.href); const url = new URL(window.location.href);
@@ -58,10 +60,27 @@ export function PlayerLoginScreen(): React.ReactElement {
return; 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); setLoading(true);
try { try {
const data = await postPlayerAuthLogin({ const data = await postPlayerAuthLogin({
...(DEPLOY_SITE_CODE !== "" ? { site_code: DEPLOY_SITE_CODE } : {}),
username: username.trim(), username: username.trim(),
password, password,
}); });

View File

@@ -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;
}

View File

@@ -53,6 +53,7 @@ export type TicketPreviewData = {
total_rebate_amount: number; total_rebate_amount: number;
total_actual_deduct: number; total_actual_deduct: number;
total_estimated_payout: number; total_estimated_payout: number;
instant_rebate_applied?: boolean;
}; };
lines: TicketPreviewLine[]; lines: TicketPreviewLine[];
warnings: TicketPreviewWarning[]; warnings: TicketPreviewWarning[];