积分收益政策h5
This commit is contained in:
parent
a4dd49c33d
commit
2f384687b5
@ -46,6 +46,8 @@ export const PERMISSIONS = {
|
||||
coinSellerSubApplicationView: "coin-seller-sub-application:view",
|
||||
coinSellerSubApplicationAudit: "coin-seller-sub-application:audit",
|
||||
hostWithdrawalView: "host-withdrawal:view",
|
||||
pointWithdrawalConfigView: "point-withdrawal-config:view",
|
||||
pointWithdrawalConfigUpdate: "point-withdrawal-config:update",
|
||||
financeView: "finance:view",
|
||||
financeApplicationCreate: "finance-application:create",
|
||||
financeApplicationAudit: "finance-application:audit",
|
||||
@ -294,6 +296,7 @@ export const MENU_CODES = {
|
||||
hostOrgCoinSellers: "host-org-coin-sellers",
|
||||
hostOrgCoinSellerSubApplications: "host-org-coin-seller-sub-applications",
|
||||
hostWithdrawals: "host-withdrawals",
|
||||
hostOrgPointWithdrawalConfig: "host-org-point-withdrawal-config",
|
||||
payment: "payment",
|
||||
paymentBillList: "payment-bill-list",
|
||||
paymentRechargeProducts: "payment-recharge-products",
|
||||
|
||||
@ -260,7 +260,7 @@
|
||||
}
|
||||
|
||||
.genderAgeFemale {
|
||||
background: var(--danger);
|
||||
background: var(--gender-female);
|
||||
}
|
||||
|
||||
.genderAgeMale {
|
||||
|
||||
275
src/features/host-org/pages/PointWithdrawalConfigPage.jsx
Normal file
275
src/features/host-org/pages/PointWithdrawalConfigPage.jsx
Normal file
@ -0,0 +1,275 @@
|
||||
import AddOutlined from "@mui/icons-material/AddOutlined";
|
||||
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
||||
import EditOutlined from "@mui/icons-material/EditOutlined";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useAppScope } from "@/app/app-scope/AppScopeProvider.jsx";
|
||||
import { useAuth } from "@/app/auth/AuthProvider.jsx";
|
||||
import { PERMISSIONS } from "@/app/permissions";
|
||||
import {
|
||||
deletePointWithdrawalCoinSeller,
|
||||
listPointWithdrawalCoinSellers,
|
||||
upsertPointWithdrawalCoinSeller,
|
||||
} from "@/features/host-org/pointWithdrawalApi";
|
||||
import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js";
|
||||
import {
|
||||
AdminActionIconButton,
|
||||
AdminListBody,
|
||||
AdminListPage,
|
||||
AdminListToolbar,
|
||||
AdminRowActions,
|
||||
} from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { Button } from "@/shared/ui/Button.jsx";
|
||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
|
||||
const emptyForm = {
|
||||
sellerUserId: "",
|
||||
sortOrder: "0",
|
||||
pointAmount: "",
|
||||
sellerCoinAmount: "",
|
||||
serviceCountryCodes: "",
|
||||
status: "active",
|
||||
};
|
||||
|
||||
export function PointWithdrawalConfigPage() {
|
||||
const { appCode } = useAppScope();
|
||||
const { can } = useAuth();
|
||||
const { showToast } = useToast();
|
||||
const canUpdate = can(PERMISSIONS.pointWithdrawalConfigUpdate);
|
||||
const [editing, setEditing] = useState(null);
|
||||
const [form, setForm] = useState(emptyForm);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const queryFn = useCallback(() => listPointWithdrawalCoinSellers(), []);
|
||||
const query = useAdminQuery(queryFn, {
|
||||
errorMessage: "获取币商提现白名单失败",
|
||||
queryKey: ["point-withdrawal-coin-sellers", appCode],
|
||||
});
|
||||
|
||||
const openCreate = () => {
|
||||
setEditing({ mode: "create" });
|
||||
setForm(emptyForm);
|
||||
};
|
||||
const openEdit = (item) => {
|
||||
setEditing({ mode: "edit", sellerUserId: item.sellerUserId });
|
||||
setForm({
|
||||
sellerUserId: item.sellerUserId,
|
||||
sortOrder: String(item.sortOrder),
|
||||
pointAmount: String(item.pointAmount),
|
||||
sellerCoinAmount: String(item.sellerCoinAmount),
|
||||
serviceCountryCodes: (item.serviceCountryCodes || []).join(", "),
|
||||
status: item.status || "active",
|
||||
});
|
||||
};
|
||||
const closeDialog = () => {
|
||||
if (!saving) setEditing(null);
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
const sellerUserId = Number(form.sellerUserId);
|
||||
const sortOrder = Number(form.sortOrder);
|
||||
const pointAmount = Number(form.pointAmount);
|
||||
const sellerCoinAmount = Number(form.sellerCoinAmount);
|
||||
if (
|
||||
![sellerUserId, sortOrder, pointAmount, sellerCoinAmount].every(Number.isSafeInteger) ||
|
||||
sellerUserId <= 0 ||
|
||||
sortOrder < 0 ||
|
||||
pointAmount <= 0 ||
|
||||
sellerCoinAmount <= 0
|
||||
) {
|
||||
showToast("币商 ID、排序和兑换比例必须填写有效整数", "error");
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
// 国家码只负责展示/准入范围;去重和大写会在 Admin API 再做一次,避免不同客户端写出不同格式。
|
||||
const serviceCountryCodes = [
|
||||
...new Set(
|
||||
form.serviceCountryCodes
|
||||
.split(/[,,\s]+/)
|
||||
.map((value) => value.trim().toUpperCase())
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
await upsertPointWithdrawalCoinSeller({
|
||||
sellerUserId,
|
||||
sortOrder,
|
||||
pointAmount,
|
||||
sellerCoinAmount,
|
||||
serviceCountryCodes,
|
||||
status: form.status,
|
||||
});
|
||||
showToast("币商提现配置已保存", "success");
|
||||
setEditing(null);
|
||||
await query.reload();
|
||||
} catch (error) {
|
||||
showToast(error.message || "保存币商提现配置失败", "error");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (item) => {
|
||||
if (!window.confirm(`确认删除币商 ${item.displayUserId || item.sellerUserId} 的提现配置?`)) return;
|
||||
try {
|
||||
await deletePointWithdrawalCoinSeller(item.sellerUserId);
|
||||
showToast("币商提现配置已删除", "success");
|
||||
await query.reload();
|
||||
} catch (error) {
|
||||
showToast(error.message || "删除币商提现配置失败", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const columns = (() => {
|
||||
const base = [
|
||||
{ key: "sortOrder", label: "排序", render: (item) => item.sortOrder, width: "90px" },
|
||||
{
|
||||
key: "seller",
|
||||
label: "币商",
|
||||
render: (item) => <SellerCell item={item} />,
|
||||
width: "minmax(220px, 1.2fr)",
|
||||
},
|
||||
{
|
||||
key: "countries",
|
||||
label: "服务国家",
|
||||
render: (item) => (item.serviceCountryCodes?.length ? item.serviceCountryCodes.join(", ") : "全部国家"),
|
||||
width: "minmax(160px, 1fr)",
|
||||
},
|
||||
{
|
||||
key: "ratio",
|
||||
label: "主播 POINT : 币商金币",
|
||||
render: (item) => `${formatNumber(item.pointAmount)} : ${formatNumber(item.sellerCoinAmount)}`,
|
||||
width: "minmax(210px, 1fr)",
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "状态",
|
||||
render: (item) => (item.status === "active" ? "启用" : "停用"),
|
||||
width: "100px",
|
||||
},
|
||||
{ key: "updatedAtMs", label: "更新时间", render: (item) => formatMillis(item.updatedAtMs), width: "170px" },
|
||||
];
|
||||
if (canUpdate) {
|
||||
base.push({
|
||||
key: "actions",
|
||||
label: "操作",
|
||||
width: "120px",
|
||||
render: (item) => (
|
||||
<AdminRowActions>
|
||||
<AdminActionIconButton label="编辑" onClick={() => openEdit(item)}>
|
||||
<EditOutlined fontSize="small" />
|
||||
</AdminActionIconButton>
|
||||
<AdminActionIconButton label="删除" onClick={() => remove(item)}>
|
||||
<DeleteOutlineOutlined fontSize="small" />
|
||||
</AdminActionIconButton>
|
||||
</AdminRowActions>
|
||||
),
|
||||
});
|
||||
}
|
||||
return base;
|
||||
})();
|
||||
|
||||
return (
|
||||
<AdminListPage>
|
||||
<AdminListToolbar
|
||||
actions={
|
||||
canUpdate ? (
|
||||
<Button startIcon={<AddOutlined />} variant="primary" onClick={openCreate}>
|
||||
添加币商
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
filters={<span>当前 App:{appCode} · 配置为空时 H5 不展示币商提现选项</span>}
|
||||
/>
|
||||
<AdminListBody>
|
||||
<DataState error={query.error} loading={query.loading} onRetry={query.reload}>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
items={query.data?.items || []}
|
||||
minWidth="980px"
|
||||
rowKey={(item) => item.sellerUserId}
|
||||
/>
|
||||
</DataState>
|
||||
</AdminListBody>
|
||||
<Dialog fullWidth maxWidth="sm" open={Boolean(editing)} onClose={closeDialog}>
|
||||
<DialogTitle>{editing?.mode === "edit" ? "编辑币商提现配置" : "添加币商提现配置"}</DialogTitle>
|
||||
<DialogContent sx={{ display: "grid", gap: 2, paddingTop: "16px !important" }}>
|
||||
<TextField
|
||||
disabled={editing?.mode === "edit"}
|
||||
label="币商用户 ID"
|
||||
required
|
||||
value={form.sellerUserId}
|
||||
onChange={(event) => setForm({ ...form, sellerUserId: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
label="排序"
|
||||
required
|
||||
type="number"
|
||||
value={form.sortOrder}
|
||||
onChange={(event) => setForm({ ...form, sortOrder: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
label="主播 POINT 数量"
|
||||
required
|
||||
type="number"
|
||||
value={form.pointAmount}
|
||||
onChange={(event) => setForm({ ...form, pointAmount: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
label="币商金币数量"
|
||||
required
|
||||
type="number"
|
||||
value={form.sellerCoinAmount}
|
||||
onChange={(event) => setForm({ ...form, sellerCoinAmount: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
helperText="使用 ISO 国家码,逗号分隔;留空表示全部国家"
|
||||
label="服务国家"
|
||||
value={form.serviceCountryCodes}
|
||||
onChange={(event) => setForm({ ...form, serviceCountryCodes: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
select
|
||||
label="状态"
|
||||
value={form.status}
|
||||
onChange={(event) => setForm({ ...form, status: event.target.value })}
|
||||
>
|
||||
<MenuItem value="active">启用</MenuItem>
|
||||
<MenuItem value="disabled">停用</MenuItem>
|
||||
</TextField>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button disabled={saving} onClick={closeDialog}>
|
||||
取消
|
||||
</Button>
|
||||
<Button disabled={saving} variant="primary" onClick={submit}>
|
||||
保存
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</AdminListPage>
|
||||
);
|
||||
}
|
||||
|
||||
function SellerCell({ item }) {
|
||||
return (
|
||||
<div>
|
||||
<strong>{item.nickname || "未找到用户"}</strong>
|
||||
<div>
|
||||
{item.displayUserId || "-"} · UID {item.sellerUserId}
|
||||
</div>
|
||||
<small>{item.countryName || "-"}</small>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
return new Intl.NumberFormat("en-US").format(Number(value || 0));
|
||||
}
|
||||
47
src/features/host-org/pointWithdrawalApi.test.ts
Normal file
47
src/features/host-org/pointWithdrawalApi.test.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { setAccessToken, setSelectedAppCode } from "@/shared/api/request";
|
||||
import {
|
||||
deletePointWithdrawalCoinSeller,
|
||||
listPointWithdrawalCoinSellers,
|
||||
upsertPointWithdrawalCoinSeller,
|
||||
} from "./pointWithdrawalApi";
|
||||
|
||||
afterEach(() => {
|
||||
setAccessToken("");
|
||||
setSelectedAppCode("lalu");
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test("point withdrawal whitelist APIs use selected app scope and server-owned ratio payload", async () => {
|
||||
setSelectedAppCode("fami");
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => new Response(JSON.stringify({ code: 0, data: { items: [] } }))),
|
||||
);
|
||||
|
||||
await listPointWithdrawalCoinSellers();
|
||||
await upsertPointWithdrawalCoinSeller({
|
||||
sellerUserId: 90001,
|
||||
sortOrder: 1,
|
||||
pointAmount: 100000,
|
||||
sellerCoinAmount: 92000,
|
||||
serviceCountryCodes: ["SA"],
|
||||
status: "active",
|
||||
});
|
||||
await deletePointWithdrawalCoinSeller("90001");
|
||||
|
||||
const [listUrl, listInit] = vi.mocked(fetch).mock.calls[0];
|
||||
const [putUrl, putInit] = vi.mocked(fetch).mock.calls[1];
|
||||
const [deleteUrl, deleteInit] = vi.mocked(fetch).mock.calls[2];
|
||||
expect(String(listUrl)).toContain("/api/v1/admin/point-withdrawal/coin-sellers");
|
||||
expect(listInit?.headers).toMatchObject({ "X-App-Code": "fami" });
|
||||
expect(String(putUrl)).toContain("/api/v1/admin/point-withdrawal/coin-sellers");
|
||||
expect(putInit?.method).toBe("PUT");
|
||||
expect(JSON.parse(String(putInit?.body))).toMatchObject({
|
||||
pointAmount: 100000,
|
||||
sellerCoinAmount: 92000,
|
||||
serviceCountryCodes: ["SA"],
|
||||
});
|
||||
expect(String(deleteUrl)).toContain("/api/v1/admin/point-withdrawal/coin-sellers/90001");
|
||||
expect(deleteInit?.method).toBe("DELETE");
|
||||
});
|
||||
42
src/features/host-org/pointWithdrawalApi.ts
Normal file
42
src/features/host-org/pointWithdrawalApi.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { apiRequest } from "@/shared/api/request";
|
||||
|
||||
export interface PointWithdrawalCoinSellerDto {
|
||||
appCode: string;
|
||||
sellerUserId: string;
|
||||
displayUserId: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
countryName: string;
|
||||
sortOrder: number;
|
||||
pointAmount: number;
|
||||
sellerCoinAmount: number;
|
||||
serviceCountryCodes: string[];
|
||||
status: "active" | "disabled";
|
||||
createdAtMs: number;
|
||||
updatedAtMs: number;
|
||||
}
|
||||
|
||||
export interface PointWithdrawalCoinSellerPayload {
|
||||
sellerUserId: number;
|
||||
sortOrder: number;
|
||||
pointAmount: number;
|
||||
sellerCoinAmount: number;
|
||||
serviceCountryCodes: string[];
|
||||
status: "active" | "disabled";
|
||||
}
|
||||
|
||||
export function listPointWithdrawalCoinSellers(): Promise<{ items: PointWithdrawalCoinSellerDto[] }> {
|
||||
return apiRequest("/v1/admin/point-withdrawal/coin-sellers", { method: "GET" });
|
||||
}
|
||||
|
||||
export function upsertPointWithdrawalCoinSeller(
|
||||
payload: PointWithdrawalCoinSellerPayload,
|
||||
): Promise<PointWithdrawalCoinSellerDto> {
|
||||
return apiRequest("/v1/admin/point-withdrawal/coin-sellers", { body: payload, method: "PUT" });
|
||||
}
|
||||
|
||||
export function deletePointWithdrawalCoinSeller(sellerUserId: string): Promise<{ deleted: boolean }> {
|
||||
return apiRequest(`/v1/admin/point-withdrawal/coin-sellers/${encodeURIComponent(sellerUserId)}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
@ -84,4 +84,13 @@ export const hostOrgRoutes = [
|
||||
path: "/host/withdrawals",
|
||||
permission: PERMISSIONS.hostWithdrawalView,
|
||||
},
|
||||
{
|
||||
label: "币商提现白名单",
|
||||
loader: () =>
|
||||
import("./pages/PointWithdrawalConfigPage.jsx").then((module) => module.PointWithdrawalConfigPage),
|
||||
menuCode: MENU_CODES.hostOrgPointWithdrawalConfig,
|
||||
pageKey: "host-org-point-withdrawal-config",
|
||||
path: "/host/point-withdrawal-config",
|
||||
permission: PERMISSIONS.pointWithdrawalConfigView,
|
||||
},
|
||||
];
|
||||
|
||||
@ -71,14 +71,51 @@ function TemplateBaseFields({ disabled, form, setForm }) {
|
||||
<h3>模板基础</h3>
|
||||
</div>
|
||||
<div className={styles.formGrid}>
|
||||
<TextField required disabled={disabled} label="模板名称" size="small" value={form.name} onChange={(event) => patch({ name: event.target.value })} />
|
||||
<TextField required disabled={disabled} label="模板编码" size="small" value={form.templateCode} onChange={(event) => patch({ templateCode: event.target.value })} />
|
||||
<TextField required disabled={disabled} label="模板版本" size="small" value={form.templateVersion} onChange={(event) => patch({ templateVersion: event.target.value })} />
|
||||
<TextField select disabled={disabled} label="状态" size="small" value={form.status} onChange={(event) => patch({ status: event.target.value })}>
|
||||
<TextField
|
||||
required
|
||||
disabled={disabled}
|
||||
label="模板名称"
|
||||
size="small"
|
||||
value={form.name}
|
||||
onChange={(event) => patch({ name: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
disabled={disabled}
|
||||
label="模板编码"
|
||||
size="small"
|
||||
value={form.templateCode}
|
||||
onChange={(event) => patch({ templateCode: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
disabled={disabled}
|
||||
label="模板版本"
|
||||
size="small"
|
||||
value={form.templateVersion}
|
||||
onChange={(event) => patch({ templateVersion: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
select
|
||||
disabled={disabled}
|
||||
label="状态"
|
||||
size="small"
|
||||
value={form.status}
|
||||
onChange={(event) => patch({ status: event.target.value })}
|
||||
>
|
||||
<MenuItem value="active">启用</MenuItem>
|
||||
<MenuItem value="disabled">停用</MenuItem>
|
||||
</TextField>
|
||||
<TextField className={styles.fieldWide} disabled={disabled} label="说明" maxRows={3} multiline size="small" value={form.description} onChange={(event) => patch({ description: event.target.value })} />
|
||||
<TextField
|
||||
className={styles.fieldWide}
|
||||
disabled={disabled}
|
||||
label="说明"
|
||||
maxRows={3}
|
||||
multiline
|
||||
size="small"
|
||||
value={form.description}
|
||||
onChange={(event) => patch({ description: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@ -91,8 +128,18 @@ function GlobalRuleSection({ disabled, form, setForm }) {
|
||||
<h3>全局口径</h3>
|
||||
</div>
|
||||
<div className={styles.formGrid}>
|
||||
<NumberField disabled={disabled} label="POINT/USD" value={form.pointsPerUsd} onChange={(pointsPerUsd) => updateRule(setForm, { pointsPerUsd })} />
|
||||
<NumberField disabled={disabled} label="Google 金币/USD" value={form.googleCoinPerUsd} onChange={(googleCoinPerUsd) => updateRule(setForm, { googleCoinPerUsd })} />
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="POINT/USD"
|
||||
value={form.pointsPerUsd}
|
||||
onChange={(pointsPerUsd) => updateRule(setForm, { pointsPerUsd })}
|
||||
/>
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="Google 金币/USD"
|
||||
value={form.googleCoinPerUsd}
|
||||
onChange={(googleCoinPerUsd) => updateRule(setForm, { googleCoinPerUsd })}
|
||||
/>
|
||||
<SwitchField
|
||||
checked={form.allowSelfBrushing}
|
||||
disabled={disabled}
|
||||
@ -111,9 +158,33 @@ function HostRuleSection({ disabled, form, setForm }) {
|
||||
<h3>主播收益</h3>
|
||||
</div>
|
||||
<div className={styles.formGrid}>
|
||||
<NumberField disabled={disabled} label="主播收益比例" suffix="%" value={form.host.pointRatioPercent} onChange={(pointRatioPercent) => updateSection(setForm, "host", { pointRatioPercent })} />
|
||||
<NumberField disabled={disabled} label="最低提现积分" value={form.host.minimumWithdrawPoints} onChange={(minimumWithdrawPoints) => updateSection(setForm, "host", { minimumWithdrawPoints })} />
|
||||
<NumberField disabled={disabled} label="提现手续费" suffix="bps" value={form.host.withdrawFeeBps} onChange={(withdrawFeeBps) => updateSection(setForm, "host", { withdrawFeeBps })} />
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="主播收益比例"
|
||||
suffix="%"
|
||||
value={form.host.pointRatioPercent}
|
||||
onChange={(pointRatioPercent) => updateSection(setForm, "host", { pointRatioPercent })}
|
||||
/>
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="Agency 分成比例"
|
||||
suffix="%"
|
||||
value={form.agency.pointRatioPercent}
|
||||
onChange={(pointRatioPercent) => updateSection(setForm, "agency", { pointRatioPercent })}
|
||||
/>
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="最低提现积分"
|
||||
value={form.host.minimumWithdrawPoints}
|
||||
onChange={(minimumWithdrawPoints) => updateSection(setForm, "host", { minimumWithdrawPoints })}
|
||||
/>
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="提现手续费"
|
||||
suffix="bps"
|
||||
value={form.host.withdrawFeeBps}
|
||||
onChange={(withdrawFeeBps) => updateSection(setForm, "host", { withdrawFeeBps })}
|
||||
/>
|
||||
<SwitchField
|
||||
checked={form.host.affectsRoomHeat}
|
||||
disabled={disabled}
|
||||
@ -131,18 +202,60 @@ function AgentRuleSection({ disabled, form, setForm }) {
|
||||
<section className={styles.formSection}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h3>Agent 规则</h3>
|
||||
<Button disabled={disabled} startIcon={<AddOutlined fontSize="small" />} onClick={() => addAgentLevel(setForm)}>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
startIcon={<AddOutlined fontSize="small" />}
|
||||
onClick={() => addAgentLevel(setForm)}
|
||||
>
|
||||
添加等级
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.formGrid}>
|
||||
<SelectField disabled={disabled} label="周期" options={agentPeriods} value={agent.period} onChange={(period) => updateSection(setForm, "agent", { period })} />
|
||||
<NumberField disabled={disabled} label="首月扶持比例" suffix="%" value={agent.supportRatioPercent} onChange={(supportRatioPercent) => updateSection(setForm, "agent", { supportRatioPercent })} />
|
||||
<NumberField disabled={disabled} label="扶持天数" value={agent.supportDays} onChange={(supportDays) => updateSection(setForm, "agent", { supportDays })} />
|
||||
<NumberField disabled={disabled} label="合格主播数" value={agent.qualifiedHostMin} onChange={(qualifiedHostMin) => updateSection(setForm, "agent", { qualifiedHostMin })} />
|
||||
<SwitchField checked={agent.differential.enabled} disabled={disabled} label="跨级差值" onChange={(enabled) => updateAgentDifferential(setForm, { enabled })} />
|
||||
<NumberField disabled={disabled} label="差值深度" value={agent.differential.maxDepth} onChange={(maxDepth) => updateAgentDifferential(setForm, { maxDepth })} />
|
||||
<TextField disabled={disabled} label="差值公式" size="small" value={agent.differential.crossLevelFormula} onChange={(event) => updateAgentDifferential(setForm, { crossLevelFormula: event.target.value })} />
|
||||
<SelectField
|
||||
disabled={disabled}
|
||||
label="周期"
|
||||
options={agentPeriods}
|
||||
value={agent.period}
|
||||
onChange={(period) => updateSection(setForm, "agent", { period })}
|
||||
/>
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="首月扶持比例"
|
||||
suffix="%"
|
||||
value={agent.supportRatioPercent}
|
||||
onChange={(supportRatioPercent) => updateSection(setForm, "agent", { supportRatioPercent })}
|
||||
/>
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="扶持天数"
|
||||
value={agent.supportDays}
|
||||
onChange={(supportDays) => updateSection(setForm, "agent", { supportDays })}
|
||||
/>
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="合格主播数"
|
||||
value={agent.qualifiedHostMin}
|
||||
onChange={(qualifiedHostMin) => updateSection(setForm, "agent", { qualifiedHostMin })}
|
||||
/>
|
||||
<SwitchField
|
||||
checked={agent.differential.enabled}
|
||||
disabled={disabled}
|
||||
label="跨级差值"
|
||||
onChange={(enabled) => updateAgentDifferential(setForm, { enabled })}
|
||||
/>
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="差值深度"
|
||||
value={agent.differential.maxDepth}
|
||||
onChange={(maxDepth) => updateAgentDifferential(setForm, { maxDepth })}
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="差值公式"
|
||||
size="small"
|
||||
value={agent.differential.crossLevelFormula}
|
||||
onChange={(event) => updateAgentDifferential(setForm, { crossLevelFormula: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.levelTable}>
|
||||
<div className={styles.agentLevelHeader}>
|
||||
@ -157,14 +270,56 @@ function AgentRuleSection({ disabled, form, setForm }) {
|
||||
</div>
|
||||
{agent.levels.map((level, index) => (
|
||||
<div className={styles.agentLevelRow} key={`${level.level}-${index}`}>
|
||||
<NumberField bare disabled={disabled} value={level.level} onChange={(value) => updateAgentLevel(setForm, index, { level: value })} />
|
||||
<TextField disabled={disabled} size="small" value={level.name} onChange={(event) => updateAgentLevel(setForm, index, { name: event.target.value })} />
|
||||
<NumberField bare disabled={disabled} value={level.minUsd} onChange={(value) => updateAgentLevel(setForm, index, { minUsd: value })} />
|
||||
<NumberField bare disabled={disabled} value={level.maxUsd} onChange={(value) => updateAgentLevel(setForm, index, { maxUsd: value })} />
|
||||
<NumberField bare disabled={disabled} suffix="%" value={level.ratioPercent} onChange={(value) => updateAgentLevel(setForm, index, { ratioPercent: value })} />
|
||||
<NumberField bare disabled={disabled} value={level.qualifiedHostMin} onChange={(value) => updateAgentLevel(setForm, index, { qualifiedHostMin: value })} />
|
||||
<AdminSwitch checked={level.manualReview} disabled={disabled} label={`Agent 等级 ${index + 1} 人工审核`} onChange={(event) => updateAgentLevel(setForm, index, { manualReview: event.target.checked })} />
|
||||
<RemoveButton disabled={disabled || agent.levels.length <= 1} label="删除 Agent 等级" onClick={() => removeAgentLevel(setForm, index)} />
|
||||
<NumberField
|
||||
bare
|
||||
disabled={disabled}
|
||||
value={level.level}
|
||||
onChange={(value) => updateAgentLevel(setForm, index, { level: value })}
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
value={level.name}
|
||||
onChange={(event) => updateAgentLevel(setForm, index, { name: event.target.value })}
|
||||
/>
|
||||
<NumberField
|
||||
bare
|
||||
disabled={disabled}
|
||||
value={level.minUsd}
|
||||
onChange={(value) => updateAgentLevel(setForm, index, { minUsd: value })}
|
||||
/>
|
||||
<NumberField
|
||||
bare
|
||||
disabled={disabled}
|
||||
value={level.maxUsd}
|
||||
onChange={(value) => updateAgentLevel(setForm, index, { maxUsd: value })}
|
||||
/>
|
||||
<NumberField
|
||||
bare
|
||||
disabled={disabled}
|
||||
suffix="%"
|
||||
value={level.ratioPercent}
|
||||
onChange={(value) => updateAgentLevel(setForm, index, { ratioPercent: value })}
|
||||
/>
|
||||
<NumberField
|
||||
bare
|
||||
disabled={disabled}
|
||||
value={level.qualifiedHostMin}
|
||||
onChange={(value) => updateAgentLevel(setForm, index, { qualifiedHostMin: value })}
|
||||
/>
|
||||
<AdminSwitch
|
||||
checked={level.manualReview}
|
||||
disabled={disabled}
|
||||
label={`Agent 等级 ${index + 1} 人工审核`}
|
||||
onChange={(event) =>
|
||||
updateAgentLevel(setForm, index, { manualReview: event.target.checked })
|
||||
}
|
||||
/>
|
||||
<RemoveButton
|
||||
disabled={disabled || agent.levels.length <= 1}
|
||||
label="删除 Agent 等级"
|
||||
onClick={() => removeAgentLevel(setForm, index)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -178,15 +333,43 @@ function BDRuleSection({ disabled, form, setForm }) {
|
||||
<section className={styles.formSection}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h3>BD 规则</h3>
|
||||
<Button disabled={disabled} startIcon={<AddOutlined fontSize="small" />} onClick={() => addBDLevel(setForm)}>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
startIcon={<AddOutlined fontSize="small" />}
|
||||
onClick={() => addBDLevel(setForm)}
|
||||
>
|
||||
添加等级
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.formGrid}>
|
||||
<SelectField disabled={disabled} label="周期" options={bdPeriods} value={bd.period} onChange={(period) => updateSection(setForm, "bd", { period })} />
|
||||
<TextField disabled={disabled} label="结算基准" size="small" value={bd.base} onChange={(event) => updateSection(setForm, "bd", { base: event.target.value })} />
|
||||
<NumberField disabled={disabled} label="合格 Agency 数" value={bd.qualifiedAgencyMin} onChange={(qualifiedAgencyMin) => updateSection(setForm, "bd", { qualifiedAgencyMin })} />
|
||||
<NumberField disabled={disabled} label="每 Agency 主播数" value={bd.qualifiedHostPerAgencyMin} onChange={(qualifiedHostPerAgencyMin) => updateSection(setForm, "bd", { qualifiedHostPerAgencyMin })} />
|
||||
<SelectField
|
||||
disabled={disabled}
|
||||
label="周期"
|
||||
options={bdPeriods}
|
||||
value={bd.period}
|
||||
onChange={(period) => updateSection(setForm, "bd", { period })}
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="结算基准"
|
||||
size="small"
|
||||
value={bd.base}
|
||||
onChange={(event) => updateSection(setForm, "bd", { base: event.target.value })}
|
||||
/>
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="合格 Agency 数"
|
||||
value={bd.qualifiedAgencyMin}
|
||||
onChange={(qualifiedAgencyMin) => updateSection(setForm, "bd", { qualifiedAgencyMin })}
|
||||
/>
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="每 Agency 主播数"
|
||||
value={bd.qualifiedHostPerAgencyMin}
|
||||
onChange={(qualifiedHostPerAgencyMin) =>
|
||||
updateSection(setForm, "bd", { qualifiedHostPerAgencyMin })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.levelTable}>
|
||||
<div className={styles.bdLevelHeader}>
|
||||
@ -199,12 +382,43 @@ function BDRuleSection({ disabled, form, setForm }) {
|
||||
</div>
|
||||
{bd.levels.map((level, index) => (
|
||||
<div className={styles.bdLevelRow} key={`${level.level}-${index}`}>
|
||||
<NumberField bare disabled={disabled} value={level.level} onChange={(value) => updateBDLevel(setForm, index, { level: value })} />
|
||||
<TextField disabled={disabled} size="small" value={level.name} onChange={(event) => updateBDLevel(setForm, index, { name: event.target.value })} />
|
||||
<NumberField bare disabled={disabled} value={level.thresholdUsd} onChange={(value) => updateBDLevel(setForm, index, { thresholdUsd: value })} />
|
||||
<NumberField bare disabled={disabled} prefix="$" value={level.salaryUsd} onChange={(value) => updateBDLevel(setForm, index, { salaryUsd: value })} />
|
||||
<NumberField bare disabled={disabled} suffix="%" value={level.commissionPercent} onChange={(value) => updateBDLevel(setForm, index, { commissionPercent: value })} />
|
||||
<RemoveButton disabled={disabled || bd.levels.length <= 1} label="删除 BD 等级" onClick={() => removeBDLevel(setForm, index)} />
|
||||
<NumberField
|
||||
bare
|
||||
disabled={disabled}
|
||||
value={level.level}
|
||||
onChange={(value) => updateBDLevel(setForm, index, { level: value })}
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
value={level.name}
|
||||
onChange={(event) => updateBDLevel(setForm, index, { name: event.target.value })}
|
||||
/>
|
||||
<NumberField
|
||||
bare
|
||||
disabled={disabled}
|
||||
value={level.thresholdUsd}
|
||||
onChange={(value) => updateBDLevel(setForm, index, { thresholdUsd: value })}
|
||||
/>
|
||||
<NumberField
|
||||
bare
|
||||
disabled={disabled}
|
||||
prefix="$"
|
||||
value={level.salaryUsd}
|
||||
onChange={(value) => updateBDLevel(setForm, index, { salaryUsd: value })}
|
||||
/>
|
||||
<NumberField
|
||||
bare
|
||||
disabled={disabled}
|
||||
suffix="%"
|
||||
value={level.commissionPercent}
|
||||
onChange={(value) => updateBDLevel(setForm, index, { commissionPercent: value })}
|
||||
/>
|
||||
<RemoveButton
|
||||
disabled={disabled || bd.levels.length <= 1}
|
||||
label="删除 BD 等级"
|
||||
onClick={() => removeBDLevel(setForm, index)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -219,9 +433,33 @@ function ManagerRuleSection({ disabled, form, setForm }) {
|
||||
<h3>经理规则</h3>
|
||||
</div>
|
||||
<div className={styles.formGrid}>
|
||||
<NumberField disabled={disabled} label="币商充值门槛" prefix="$" value={form.manager.coinSellerRechargeThresholdUsd} onChange={(coinSellerRechargeThresholdUsd) => updateSection(setForm, "manager", { coinSellerRechargeThresholdUsd })} />
|
||||
<NumberField disabled={disabled} label="币商充值提成" suffix="%" value={form.manager.coinSellerRechargeCommissionPercent} onChange={(coinSellerRechargeCommissionPercent) => updateSection(setForm, "manager", { coinSellerRechargeCommissionPercent })} />
|
||||
<NumberField disabled={disabled} label="主播收益提成" suffix="%" value={form.manager.hostPointCommissionPercent} onChange={(hostPointCommissionPercent) => updateSection(setForm, "manager", { hostPointCommissionPercent })} />
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="币商充值门槛"
|
||||
prefix="$"
|
||||
value={form.manager.coinSellerRechargeThresholdUsd}
|
||||
onChange={(coinSellerRechargeThresholdUsd) =>
|
||||
updateSection(setForm, "manager", { coinSellerRechargeThresholdUsd })
|
||||
}
|
||||
/>
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="币商充值提成"
|
||||
suffix="%"
|
||||
value={form.manager.coinSellerRechargeCommissionPercent}
|
||||
onChange={(coinSellerRechargeCommissionPercent) =>
|
||||
updateSection(setForm, "manager", { coinSellerRechargeCommissionPercent })
|
||||
}
|
||||
/>
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="主播收益提成"
|
||||
suffix="%"
|
||||
value={form.manager.hostPointCommissionPercent}
|
||||
onChange={(hostPointCommissionPercent) =>
|
||||
updateSection(setForm, "manager", { hostPointCommissionPercent })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@ -233,16 +471,55 @@ function CoinSellerRuleSection({ disabled, form, setForm }) {
|
||||
<section className={styles.formSection}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h3>币商规则</h3>
|
||||
<Button disabled={disabled} startIcon={<AddOutlined fontSize="small" />} onClick={() => addCoinSellerLevel(setForm)}>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
startIcon={<AddOutlined fontSize="small" />}
|
||||
onClick={() => addCoinSellerLevel(setForm)}
|
||||
>
|
||||
添加档位
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.formGrid}>
|
||||
<SwitchField checked={coinSeller.withdrawOrderEnabled} disabled={disabled} label="提现订单" onChange={(withdrawOrderEnabled) => updateSection(setForm, "coinSeller", { withdrawOrderEnabled })} />
|
||||
<NumberField disabled={disabled} label="订单超时" suffix="秒" value={coinSeller.orderTimeoutSeconds} onChange={(orderTimeoutSeconds) => updateSection(setForm, "coinSeller", { orderTimeoutSeconds })} />
|
||||
<NumberField disabled={disabled} label="币商结算" suffix="%" value={coinSeller.sellerPointSettlePercent} onChange={(sellerPointSettlePercent) => updateSection(setForm, "coinSeller", { sellerPointSettlePercent })} />
|
||||
<NumberField disabled={disabled} label="币商奖励" suffix="%" value={coinSeller.sellerPointRewardPercent} onChange={(sellerPointRewardPercent) => updateSection(setForm, "coinSeller", { sellerPointRewardPercent })} />
|
||||
<NumberField disabled={disabled} label="成功率关闭门槛" suffix="%" value={coinSeller.successRateCloseThresholdPercent} onChange={(successRateCloseThresholdPercent) => updateSection(setForm, "coinSeller", { successRateCloseThresholdPercent })} />
|
||||
<SwitchField
|
||||
checked={coinSeller.withdrawOrderEnabled}
|
||||
disabled={disabled}
|
||||
label="提现订单"
|
||||
onChange={(withdrawOrderEnabled) => updateSection(setForm, "coinSeller", { withdrawOrderEnabled })}
|
||||
/>
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="订单超时"
|
||||
suffix="秒"
|
||||
value={coinSeller.orderTimeoutSeconds}
|
||||
onChange={(orderTimeoutSeconds) => updateSection(setForm, "coinSeller", { orderTimeoutSeconds })}
|
||||
/>
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="币商结算"
|
||||
suffix="%"
|
||||
value={coinSeller.sellerPointSettlePercent}
|
||||
onChange={(sellerPointSettlePercent) =>
|
||||
updateSection(setForm, "coinSeller", { sellerPointSettlePercent })
|
||||
}
|
||||
/>
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="币商奖励"
|
||||
suffix="%"
|
||||
value={coinSeller.sellerPointRewardPercent}
|
||||
onChange={(sellerPointRewardPercent) =>
|
||||
updateSection(setForm, "coinSeller", { sellerPointRewardPercent })
|
||||
}
|
||||
/>
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="成功率关闭门槛"
|
||||
suffix="%"
|
||||
value={coinSeller.successRateCloseThresholdPercent}
|
||||
onChange={(successRateCloseThresholdPercent) =>
|
||||
updateSection(setForm, "coinSeller", { successRateCloseThresholdPercent })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.levelTable}>
|
||||
<div className={styles.coinSellerLevelHeader}>
|
||||
@ -255,12 +532,47 @@ function CoinSellerRuleSection({ disabled, form, setForm }) {
|
||||
</div>
|
||||
{coinSeller.levels.map((level, index) => (
|
||||
<div className={styles.coinSellerLevelRow} key={`${level.name}-${index}`}>
|
||||
<TextField disabled={disabled} size="small" value={level.name} onChange={(event) => updateCoinSellerLevel(setForm, index, { name: event.target.value })} />
|
||||
<NumberField bare disabled={disabled} prefix="$" value={level.thresholdUsd} onChange={(value) => updateCoinSellerLevel(setForm, index, { thresholdUsd: value })} />
|
||||
<NumberField bare disabled={disabled} prefix="$" value={level.singleRechargeThresholdUsd} onChange={(value) => updateCoinSellerLevel(setForm, index, { singleRechargeThresholdUsd: value })} />
|
||||
<NumberField bare disabled={disabled} value={level.coinPerUsd} onChange={(value) => updateCoinSellerLevel(setForm, index, { coinPerUsd: value })} />
|
||||
<AdminSwitch checked={level.withdrawOrderEnabled} disabled={disabled} label={`币商档位 ${index + 1} 提现订单`} onChange={(event) => updateCoinSellerLevel(setForm, index, { withdrawOrderEnabled: event.target.checked })} />
|
||||
<RemoveButton disabled={disabled || coinSeller.levels.length <= 1} label="删除币商档位" onClick={() => removeCoinSellerLevel(setForm, index)} />
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
value={level.name}
|
||||
onChange={(event) => updateCoinSellerLevel(setForm, index, { name: event.target.value })}
|
||||
/>
|
||||
<NumberField
|
||||
bare
|
||||
disabled={disabled}
|
||||
prefix="$"
|
||||
value={level.thresholdUsd}
|
||||
onChange={(value) => updateCoinSellerLevel(setForm, index, { thresholdUsd: value })}
|
||||
/>
|
||||
<NumberField
|
||||
bare
|
||||
disabled={disabled}
|
||||
prefix="$"
|
||||
value={level.singleRechargeThresholdUsd}
|
||||
onChange={(value) =>
|
||||
updateCoinSellerLevel(setForm, index, { singleRechargeThresholdUsd: value })
|
||||
}
|
||||
/>
|
||||
<NumberField
|
||||
bare
|
||||
disabled={disabled}
|
||||
value={level.coinPerUsd}
|
||||
onChange={(value) => updateCoinSellerLevel(setForm, index, { coinPerUsd: value })}
|
||||
/>
|
||||
<AdminSwitch
|
||||
checked={level.withdrawOrderEnabled}
|
||||
disabled={disabled}
|
||||
label={`币商档位 ${index + 1} 提现订单`}
|
||||
onChange={(event) =>
|
||||
updateCoinSellerLevel(setForm, index, { withdrawOrderEnabled: event.target.checked })
|
||||
}
|
||||
/>
|
||||
<RemoveButton
|
||||
disabled={disabled || coinSeller.levels.length <= 1}
|
||||
label="删除币商档位"
|
||||
onClick={() => removeCoinSellerLevel(setForm, index)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -275,10 +587,32 @@ function TaskAndGameRuleSection({ disabled, form, setForm }) {
|
||||
<h3>任务与游戏邀请</h3>
|
||||
</div>
|
||||
<div className={styles.formGrid}>
|
||||
<SelectField disabled={disabled} label="任务奖励资产" options={taskAssets} value={form.tasks.rewardAssetType} onChange={(rewardAssetType) => updateSection(setForm, "tasks", { rewardAssetType })} />
|
||||
<NumberField disabled={disabled} label="新主播 7 日任务上限" value={form.tasks.newHost7dMaxPoints} onChange={(newHost7dMaxPoints) => updateSection(setForm, "tasks", { newHost7dMaxPoints })} />
|
||||
<SwitchField checked={form.gameInvite.enabled} disabled={disabled} label="游戏邀请返利" onChange={(enabled) => updateSection(setForm, "gameInvite", { enabled })} />
|
||||
<SelectField disabled={disabled} label="游戏邀请周期" options={gameInvitePeriods} value={form.gameInvite.period} onChange={(period) => updateSection(setForm, "gameInvite", { period })} />
|
||||
<SelectField
|
||||
disabled={disabled}
|
||||
label="任务奖励资产"
|
||||
options={taskAssets}
|
||||
value={form.tasks.rewardAssetType}
|
||||
onChange={(rewardAssetType) => updateSection(setForm, "tasks", { rewardAssetType })}
|
||||
/>
|
||||
<NumberField
|
||||
disabled={disabled}
|
||||
label="新主播 7 日任务上限"
|
||||
value={form.tasks.newHost7dMaxPoints}
|
||||
onChange={(newHost7dMaxPoints) => updateSection(setForm, "tasks", { newHost7dMaxPoints })}
|
||||
/>
|
||||
<SwitchField
|
||||
checked={form.gameInvite.enabled}
|
||||
disabled={disabled}
|
||||
label="游戏邀请返利"
|
||||
onChange={(enabled) => updateSection(setForm, "gameInvite", { enabled })}
|
||||
/>
|
||||
<SelectField
|
||||
disabled={disabled}
|
||||
label="游戏邀请周期"
|
||||
options={gameInvitePeriods}
|
||||
value={form.gameInvite.period}
|
||||
onChange={(period) => updateSection(setForm, "gameInvite", { period })}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@ -306,7 +640,14 @@ function NumberField({ bare = false, disabled, label, onChange, prefix, suffix,
|
||||
|
||||
function SelectField({ disabled, label, onChange, options, value }) {
|
||||
return (
|
||||
<TextField select disabled={disabled} label={label} size="small" value={value} onChange={(event) => onChange(event.target.value)}>
|
||||
<TextField
|
||||
select
|
||||
disabled={disabled}
|
||||
label={label}
|
||||
size="small"
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
>
|
||||
{options.map(([optionValue, optionLabel]) => (
|
||||
<MenuItem key={optionValue} value={optionValue}>
|
||||
{optionLabel}
|
||||
@ -320,7 +661,14 @@ function SwitchField({ checked, disabled, label, onChange }) {
|
||||
return (
|
||||
<div className={styles.switchField}>
|
||||
<span>{label}</span>
|
||||
<AdminSwitch checked={checked} checkedLabel="开" disabled={disabled} label={label} uncheckedLabel="关" onChange={(event) => onChange(event.target.checked)} />
|
||||
<AdminSwitch
|
||||
checked={checked}
|
||||
checkedLabel="开"
|
||||
disabled={disabled}
|
||||
label={label}
|
||||
uncheckedLabel="关"
|
||||
onChange={(event) => onChange(event.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -338,7 +686,10 @@ function updateRule(setForm, patch) {
|
||||
}
|
||||
|
||||
function updateSection(setForm, section, patch) {
|
||||
setForm((current) => ({ ...current, rule: { ...current.rule, [section]: { ...current.rule[section], ...patch } } }));
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
rule: { ...current.rule, [section]: { ...current.rule[section], ...patch } },
|
||||
}));
|
||||
}
|
||||
|
||||
function updateAgentDifferential(setForm, patch) {
|
||||
@ -373,7 +724,9 @@ function updateArrayItem(setForm, section, key, index, patch) {
|
||||
...current.rule,
|
||||
[section]: {
|
||||
...current.rule[section],
|
||||
[key]: current.rule[section][key].map((item, itemIndex) => (itemIndex === index ? { ...item, ...patch } : item)),
|
||||
[key]: current.rule[section][key].map((item, itemIndex) =>
|
||||
itemIndex === index ? { ...item, ...patch } : item,
|
||||
),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@ -125,23 +125,42 @@ function TemplatePanel({ page }) {
|
||||
const items = page.templates.data.items || [];
|
||||
const total = page.templates.data.total || 0;
|
||||
const columns = templateColumns.map((column) =>
|
||||
column.key === "actions" ? { ...column, render: (item) => <TemplateActions item={item} page={page} /> } : column,
|
||||
column.key === "actions"
|
||||
? { ...column, render: (item) => <TemplateActions item={item} page={page} /> }
|
||||
: column,
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<AdminListToolbar
|
||||
actions={
|
||||
page.abilities.canSaveTemplate ? (
|
||||
<AdminActionIconButton label="新增收益政策模板" primary onClick={() => page.openTemplateEditor(null)}>
|
||||
<AdminActionIconButton
|
||||
label="新增收益政策模板"
|
||||
primary
|
||||
onClick={() => page.openTemplateEditor(null)}
|
||||
>
|
||||
<AddOutlined fontSize="small" />
|
||||
</AdminActionIconButton>
|
||||
) : null
|
||||
}
|
||||
filters={
|
||||
<div className={styles.toolbarFilters}>
|
||||
<AdminSearchBox label="模板" placeholder="搜索模板名称或编码" value={page.templateFilters.keyword} onChange={page.changeTemplateKeyword} />
|
||||
<AdminFilterSelect label="状态" options={statusOptions} value={page.templateFilters.status} onChange={page.changeTemplateStatus} />
|
||||
<AdminFilterResetButton disabled={!page.templateFilters.keyword && !page.templateFilters.status} onClick={page.resetTemplateFilters} />
|
||||
<AdminSearchBox
|
||||
label="模板"
|
||||
placeholder="搜索模板名称或编码"
|
||||
value={page.templateFilters.keyword}
|
||||
onChange={page.changeTemplateKeyword}
|
||||
/>
|
||||
<AdminFilterSelect
|
||||
label="状态"
|
||||
options={statusOptions}
|
||||
value={page.templateFilters.status}
|
||||
onChange={page.changeTemplateStatus}
|
||||
/>
|
||||
<AdminFilterResetButton
|
||||
disabled={!page.templateFilters.keyword && !page.templateFilters.status}
|
||||
onClick={page.resetTemplateFilters}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
@ -151,7 +170,16 @@ function TemplatePanel({ page }) {
|
||||
columns={columns}
|
||||
items={items}
|
||||
minWidth="1240px"
|
||||
pagination={total > 0 ? { page: page.templatePage, pageSize: page.templates.data.pageSize || 50, total, onPageChange: page.setTemplatePage } : undefined}
|
||||
pagination={
|
||||
total > 0
|
||||
? {
|
||||
page: page.templatePage,
|
||||
pageSize: page.templates.data.pageSize || 50,
|
||||
total,
|
||||
onPageChange: page.setTemplatePage,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
rowKey={(item) => `${item.templateCode}:${item.templateVersion}`}
|
||||
/>
|
||||
</AdminListBody>
|
||||
@ -177,16 +205,33 @@ function InstancePanel({ page }) {
|
||||
<AdminListToolbar
|
||||
actions={
|
||||
page.abilities.canCreateInstance ? (
|
||||
<AdminActionIconButton label="创建策略实例" primary onClick={() => page.openInstanceCreator(null)}>
|
||||
<AdminActionIconButton
|
||||
label="创建策略实例"
|
||||
primary
|
||||
onClick={() => page.openInstanceCreator(null)}
|
||||
>
|
||||
<AddOutlined fontSize="small" />
|
||||
</AdminActionIconButton>
|
||||
) : null
|
||||
}
|
||||
filters={
|
||||
<div className={styles.toolbarFilters}>
|
||||
<AdminSearchBox label="实例" placeholder="搜索实例或模板编码" value={page.instanceFilters.keyword} onChange={page.changeInstanceKeyword} />
|
||||
<AdminFilterSelect label="状态" options={statusOptions} value={page.instanceFilters.status} onChange={page.changeInstanceStatus} />
|
||||
<AdminFilterResetButton disabled={!page.instanceFilters.keyword && !page.instanceFilters.status} onClick={page.resetInstanceFilters} />
|
||||
<AdminSearchBox
|
||||
label="实例"
|
||||
placeholder="搜索实例或模板编码"
|
||||
value={page.instanceFilters.keyword}
|
||||
onChange={page.changeInstanceKeyword}
|
||||
/>
|
||||
<AdminFilterSelect
|
||||
label="状态"
|
||||
options={statusOptions}
|
||||
value={page.instanceFilters.status}
|
||||
onChange={page.changeInstanceStatus}
|
||||
/>
|
||||
<AdminFilterResetButton
|
||||
disabled={!page.instanceFilters.keyword && !page.instanceFilters.status}
|
||||
onClick={page.resetInstanceFilters}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
@ -197,7 +242,16 @@ function InstancePanel({ page }) {
|
||||
context={{ regionLabelMap: page.regionLabelMap }}
|
||||
items={items}
|
||||
minWidth="1260px"
|
||||
pagination={total > 0 ? { page: page.instancePage, pageSize: page.instances.data.pageSize || 50, total, onPageChange: page.setInstancePage } : undefined}
|
||||
pagination={
|
||||
total > 0
|
||||
? {
|
||||
page: page.instancePage,
|
||||
pageSize: page.instances.data.pageSize || 50,
|
||||
total,
|
||||
onPageChange: page.setInstancePage,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
rowKey={(item) => item.instanceId}
|
||||
/>
|
||||
</AdminListBody>
|
||||
@ -219,11 +273,13 @@ function TemplateIdentity({ item }) {
|
||||
function TemplateRuntimeSummary({ item }) {
|
||||
const rule = item.ruleJson || {};
|
||||
const host = rule.host || {};
|
||||
const agency = rule.agency || {};
|
||||
const tasks = rule.tasks || {};
|
||||
return (
|
||||
<div className={styles.stack}>
|
||||
<span>{formatNumber(rule.points_per_usd)} POINT/USD</span>
|
||||
<span className={styles.meta}>主播 {formatNumber(host.point_ratio_percent)}% 入 POINT</span>
|
||||
<span className={styles.meta}>Agency {formatNumber(agency.point_ratio_percent || 0)}% 分成</span>
|
||||
<span className={styles.meta}>提现手续费 {formatNumber(host.withdraw_fee_bps)} bps</span>
|
||||
<span className={styles.meta}>任务资产 {tasks.reward_asset_type || "-"}</span>
|
||||
</div>
|
||||
@ -237,7 +293,9 @@ function TeamPolicySummary({ item }) {
|
||||
const coinSellerLevels = Array.isArray(rule.coin_seller?.levels) ? rule.coin_seller.levels : [];
|
||||
return (
|
||||
<div className={styles.stack}>
|
||||
<span>Agent {agentLevels.length} 档 · BD {bdLevels.length} 档</span>
|
||||
<span>
|
||||
Agent {agentLevels.length} 档 · BD {bdLevels.length} 档
|
||||
</span>
|
||||
<span className={styles.meta}>币商 {coinSellerLevels.length} 档</span>
|
||||
<span className={styles.meta}>{rule.allow_self_brushing ? "允许自刷" : "不允许自刷"}</span>
|
||||
</div>
|
||||
@ -272,7 +330,9 @@ function InstanceScope({ item, regionLabelMap = {} }) {
|
||||
<div className={styles.stack}>
|
||||
<span>{regionScopeLabel(item.regionScope, regionLabelMap)}</span>
|
||||
<span className={styles.meta}>开始 {formatMillis(item.effectiveFromMs)}</span>
|
||||
<span className={styles.meta}>结束 {item.effectiveToMs ? formatMillis(item.effectiveToMs) : "长期有效"}</span>
|
||||
<span className={styles.meta}>
|
||||
结束 {item.effectiveToMs ? formatMillis(item.effectiveToMs) : "长期有效"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -281,7 +341,9 @@ function PublishSummary({ item }) {
|
||||
return (
|
||||
<div className={styles.stack}>
|
||||
<span>{publishStatusLabel(item.publishStatus)}</span>
|
||||
<span className={styles.meta}>{item.lastPublishedAtMs ? formatMillis(item.lastPublishedAtMs) : "未发布"}</span>
|
||||
<span className={styles.meta}>
|
||||
{item.lastPublishedAtMs ? formatMillis(item.lastPublishedAtMs) : "未发布"}
|
||||
</span>
|
||||
{item.publishError ? <span className={styles.errorText}>{item.publishError}</span> : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -58,6 +58,7 @@ describe("policy rule form schema", () => {
|
||||
const form = validRuleForm();
|
||||
|
||||
expect(form).toMatchObject({
|
||||
agency: { pointRatioPercent: "0" },
|
||||
allowSelfBrushing: true,
|
||||
googleCoinPerUsd: "70000",
|
||||
host: {
|
||||
@ -80,7 +81,12 @@ describe("policy rule form schema", () => {
|
||||
levels: [
|
||||
{ coin_per_usd: 92000, name: "Beginner", threshold_usd: 100, withdraw_order_enabled: false },
|
||||
{ coin_per_usd: 96000, name: "Standard", threshold_usd: 500, withdraw_order_enabled: false },
|
||||
{ coin_per_usd: 100000, name: "Senior", single_recharge_threshold_usd: 1000, withdraw_order_enabled: true },
|
||||
{
|
||||
coin_per_usd: 100000,
|
||||
name: "Senior",
|
||||
single_recharge_threshold_usd: 1000,
|
||||
withdraw_order_enabled: true,
|
||||
},
|
||||
],
|
||||
seller_point_reward_percent: 5,
|
||||
seller_point_settle_percent: 95,
|
||||
@ -89,7 +95,10 @@ describe("policy rule form schema", () => {
|
||||
host: { withdraw_fee_bps: 500 },
|
||||
points_per_usd: 100000,
|
||||
});
|
||||
expect(JSON.parse(ruleJsonFromForm(form))).toMatchObject({ google_coin_per_usd: 70000, points_per_usd: 100000 });
|
||||
expect(JSON.parse(ruleJsonFromForm(form))).toMatchObject({
|
||||
google_coin_per_usd: 70000,
|
||||
points_per_usd: 100000,
|
||||
});
|
||||
});
|
||||
|
||||
test("preserves unknown JSON fields while visual fields override known policy keys", () => {
|
||||
@ -106,7 +115,10 @@ describe("policy rule form schema", () => {
|
||||
agent: {
|
||||
...baseAgent,
|
||||
future_agent: "preserve-agent",
|
||||
levels: [{ ...baseAgentLevels[0], future_agent_level: "preserve-agent-level" }, ...baseAgentLevels.slice(1)],
|
||||
levels: [
|
||||
{ ...baseAgentLevels[0], future_agent_level: "preserve-agent-level" },
|
||||
...baseAgentLevels.slice(1),
|
||||
],
|
||||
},
|
||||
bd: {
|
||||
...baseBD,
|
||||
@ -116,7 +128,10 @@ describe("policy rule form schema", () => {
|
||||
coin_seller: {
|
||||
...baseCoinSeller,
|
||||
future_coin_seller: "preserve-coin-seller",
|
||||
levels: [{ ...baseCoinSellerLevels[0], future_coin_seller_level: "preserve-coin-seller-level" }, ...baseCoinSellerLevels.slice(1)],
|
||||
levels: [
|
||||
{ ...baseCoinSellerLevels[0], future_coin_seller_level: "preserve-coin-seller-level" },
|
||||
...baseCoinSellerLevels.slice(1),
|
||||
],
|
||||
},
|
||||
game_invite: { ...record(base.game_invite), future_game_invite: "preserve-game-invite" },
|
||||
host: { ...record(base.host), future_host: "preserve-host" },
|
||||
@ -127,6 +142,7 @@ describe("policy rule form schema", () => {
|
||||
form.allowSelfBrushing = false;
|
||||
form.googleCoinPerUsd = "71000";
|
||||
form.host.pointRatioPercent = "72.5";
|
||||
form.agency.pointRatioPercent = "20";
|
||||
form.agent.levels[0].ratioPercent = "6";
|
||||
form.bd.levels[0].commissionPercent = "1.75";
|
||||
form.coinSeller.levels[0].coinPerUsd = "93000";
|
||||
@ -149,11 +165,21 @@ describe("policy rule form schema", () => {
|
||||
},
|
||||
game_invite: { enabled: false, future_game_invite: "preserve-game-invite" },
|
||||
host: { future_host: "preserve-host", point_ratio_percent: 72.5 },
|
||||
agency: { point_ratio_percent: 20 },
|
||||
tasks: { future_tasks: "preserve-tasks", reward_asset_type: "COIN" },
|
||||
});
|
||||
expect(recordArray(record(rule.agent).levels)[0]).toMatchObject({ future_agent_level: "preserve-agent-level", ratio_percent: 6 });
|
||||
expect(recordArray(record(rule.bd).levels)[0]).toMatchObject({ commission_percent: 1.75, future_bd_level: "preserve-bd-level" });
|
||||
expect(recordArray(record(rule.coin_seller).levels)[0]).toMatchObject({ coin_per_usd: 93000, future_coin_seller_level: "preserve-coin-seller-level" });
|
||||
expect(recordArray(record(rule.agent).levels)[0]).toMatchObject({
|
||||
future_agent_level: "preserve-agent-level",
|
||||
ratio_percent: 6,
|
||||
});
|
||||
expect(recordArray(record(rule.bd).levels)[0]).toMatchObject({
|
||||
commission_percent: 1.75,
|
||||
future_bd_level: "preserve-bd-level",
|
||||
});
|
||||
expect(recordArray(record(rule.coin_seller).levels)[0]).toMatchObject({
|
||||
coin_per_usd: 93000,
|
||||
future_coin_seller_level: "preserve-coin-seller-level",
|
||||
});
|
||||
});
|
||||
|
||||
test("does not reattach removed level unknown fields by array index", () => {
|
||||
@ -168,11 +194,17 @@ describe("policy rule form schema", () => {
|
||||
...base,
|
||||
agent: {
|
||||
...baseAgent,
|
||||
levels: [{ ...baseAgentLevels[0], marker: "deleted-agent-level" }, { ...baseAgentLevels[1], marker: "kept-agent-level" }],
|
||||
levels: [
|
||||
{ ...baseAgentLevels[0], marker: "deleted-agent-level" },
|
||||
{ ...baseAgentLevels[1], marker: "kept-agent-level" },
|
||||
],
|
||||
},
|
||||
bd: {
|
||||
...baseBD,
|
||||
levels: [{ ...baseBDLevels[0], marker: "deleted-bd-level" }, { ...baseBDLevels[1], marker: "kept-bd-level" }],
|
||||
levels: [
|
||||
{ ...baseBDLevels[0], marker: "deleted-bd-level" },
|
||||
{ ...baseBDLevels[1], marker: "kept-bd-level" },
|
||||
],
|
||||
},
|
||||
coin_seller: {
|
||||
...baseCoinSeller,
|
||||
@ -190,13 +222,18 @@ describe("policy rule form schema", () => {
|
||||
const rule = ruleObjectFromForm(form);
|
||||
expect(recordArray(record(rule.agent).levels)[0]).toMatchObject({ level: 2, marker: "kept-agent-level" });
|
||||
expect(recordArray(record(rule.bd).levels)[0]).toMatchObject({ level: 2, marker: "kept-bd-level" });
|
||||
expect(recordArray(record(rule.coin_seller).levels)[0]).toMatchObject({ name: "Standard", marker: "kept-coin-seller-level" });
|
||||
expect(recordArray(record(rule.coin_seller).levels)[0]).toMatchObject({
|
||||
name: "Standard",
|
||||
marker: "kept-coin-seller-level",
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects non-object and invalid advanced JSON", () => {
|
||||
const nonObjectJson = validRuleForm();
|
||||
nonObjectJson.rawJson = "[]";
|
||||
expect(invalidRuleMessages(nonObjectJson).some((message) => message.includes("规则 JSON 必须是对象"))).toBe(true);
|
||||
expect(invalidRuleMessages(nonObjectJson).some((message) => message.includes("规则 JSON 必须是对象"))).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
const invalidJson = validRuleForm();
|
||||
invalidJson.rawJson = "{bad json";
|
||||
@ -255,6 +292,11 @@ describe("policy rule form schema", () => {
|
||||
const invalidFeeBps = validRuleForm();
|
||||
invalidFeeBps.host.withdrawFeeBps = "10001";
|
||||
expectInvalidRule(invalidFeeBps, "提现手续费 bps 必须在 0-10000 之间");
|
||||
|
||||
const invalidCombinedRatio = validRuleForm();
|
||||
invalidCombinedRatio.host.pointRatioPercent = "81";
|
||||
invalidCombinedRatio.agency.pointRatioPercent = "20";
|
||||
expectInvalidRule(invalidCombinedRatio, "主播收益比例和 Agency 分成比例合计不能超过 100");
|
||||
});
|
||||
});
|
||||
|
||||
@ -293,10 +335,18 @@ describe("policy instance payload", () => {
|
||||
test("rejects missing region id and invalid effective time boundaries", () => {
|
||||
const startMs = Date.UTC(2026, 0, 1, 0, 0, 0, 0);
|
||||
|
||||
expect(() => policyInstancePayloadFromForm({ ...baseInstanceForm(), regionScopeMode: "region_id" })).toThrow("请选择适用区域");
|
||||
expect(() => policyInstancePayloadFromForm({ ...baseInstanceForm(), effectiveFrom: startMs, effectiveTo: startMs })).toThrow("生效时间范围不正确");
|
||||
expect(() => policyInstancePayloadFromForm({ ...baseInstanceForm(), effectiveFrom: startMs, effectiveTo: startMs - 1 })).toThrow("生效时间范围不正确");
|
||||
expect(() => policyInstancePayloadFromForm({ ...baseInstanceForm(), effectiveFrom: "-1", effectiveTo: "" })).toThrow("生效时间范围不正确");
|
||||
expect(() => policyInstancePayloadFromForm({ ...baseInstanceForm(), regionScopeMode: "region_id" })).toThrow(
|
||||
"请选择适用区域",
|
||||
);
|
||||
expect(() =>
|
||||
policyInstancePayloadFromForm({ ...baseInstanceForm(), effectiveFrom: startMs, effectiveTo: startMs }),
|
||||
).toThrow("生效时间范围不正确");
|
||||
expect(() =>
|
||||
policyInstancePayloadFromForm({ ...baseInstanceForm(), effectiveFrom: startMs, effectiveTo: startMs - 1 }),
|
||||
).toThrow("生效时间范围不正确");
|
||||
expect(() =>
|
||||
policyInstancePayloadFromForm({ ...baseInstanceForm(), effectiveFrom: "-1", effectiveTo: "" }),
|
||||
).toThrow("生效时间范围不正确");
|
||||
});
|
||||
|
||||
test("truncates decimal millisecond inputs after schema validation accepts the range", () => {
|
||||
|
||||
@ -53,6 +53,9 @@ export interface PolicyRuleForm {
|
||||
supportRatioPercent: NumericInput;
|
||||
};
|
||||
allowSelfBrushing: boolean;
|
||||
agency: {
|
||||
pointRatioPercent: NumericInput;
|
||||
};
|
||||
bd: {
|
||||
base: string;
|
||||
levels: BDLevelForm[];
|
||||
@ -182,6 +185,9 @@ export const policyRuleFormSchema = z
|
||||
pointRatioPercent: z.union([z.string(), z.number()]),
|
||||
withdrawFeeBps: z.union([z.string(), z.number()]),
|
||||
}),
|
||||
agency: z.object({
|
||||
pointRatioPercent: z.union([z.string(), z.number()]),
|
||||
}),
|
||||
manager: z.object({
|
||||
coinSellerRechargeCommissionPercent: z.union([z.string(), z.number()]),
|
||||
coinSellerRechargeThresholdUsd: z.union([z.string(), z.number()]),
|
||||
@ -202,29 +208,145 @@ export const policyRuleFormSchema = z
|
||||
}
|
||||
requirePositiveInteger(context, ["pointsPerUsd"], value.pointsPerUsd, "POINT/USD 必须大于 0");
|
||||
requirePositiveInteger(context, ["googleCoinPerUsd"], value.googleCoinPerUsd, "Google 金币单价必须大于 0");
|
||||
requirePercent(context, ["host", "pointRatioPercent"], value.host.pointRatioPercent, "主播收益比例必须在 0-100 之间", false);
|
||||
requireNonNegativeInteger(context, ["host", "minimumWithdrawPoints"], value.host.minimumWithdrawPoints, "最低提现积分不能小于 0");
|
||||
requireIntegerRange(context, ["host", "withdrawFeeBps"], value.host.withdrawFeeBps, 0, 10000, "提现手续费 bps 必须在 0-10000 之间");
|
||||
requirePercent(context, ["agent", "supportRatioPercent"], value.agent.supportRatioPercent, "Agent 扶持比例必须在 0-100 之间", false);
|
||||
requireNonNegativeInteger(context, ["agent", "supportDays"], value.agent.supportDays, "Agent 扶持天数不能小于 0");
|
||||
requirePositiveInteger(context, ["agent", "qualifiedHostMin"], value.agent.qualifiedHostMin, "Agent 合格主播数必须大于 0");
|
||||
requireNonNegativeInteger(context, ["agent", "differential", "maxDepth"], value.agent.differential.maxDepth, "Agent 差值深度不能小于 0");
|
||||
validateAgentLevels(context, value.agent.levels);
|
||||
requirePositiveInteger(context, ["bd", "qualifiedAgencyMin"], value.bd.qualifiedAgencyMin, "BD 合格 Agency 数必须大于 0");
|
||||
requirePositiveInteger(context, ["bd", "qualifiedHostPerAgencyMin"], value.bd.qualifiedHostPerAgencyMin, "BD 每 Agency 合格主播数必须大于 0");
|
||||
validateBDLevels(context, value.bd.levels);
|
||||
requirePositiveNumber(context, ["manager", "coinSellerRechargeThresholdUsd"], value.manager.coinSellerRechargeThresholdUsd, "经理币商充值门槛必须大于 0");
|
||||
requirePercent(context, ["manager", "coinSellerRechargeCommissionPercent"], value.manager.coinSellerRechargeCommissionPercent, "经理币商提成比例必须在 0-100 之间", true);
|
||||
requirePercent(context, ["manager", "hostPointCommissionPercent"], value.manager.hostPointCommissionPercent, "经理主播提成比例必须在 0-100 之间", true);
|
||||
requirePositiveInteger(context, ["coinSeller", "orderTimeoutSeconds"], value.coinSeller.orderTimeoutSeconds, "币商订单超时秒数必须大于 0");
|
||||
requirePercent(context, ["coinSeller", "sellerPointSettlePercent"], value.coinSeller.sellerPointSettlePercent, "币商结算比例必须在 0-100 之间", true);
|
||||
requirePercent(context, ["coinSeller", "sellerPointRewardPercent"], value.coinSeller.sellerPointRewardPercent, "币商奖励比例必须在 0-100 之间", true);
|
||||
if (numberValue(value.coinSeller.sellerPointSettlePercent) + numberValue(value.coinSeller.sellerPointRewardPercent) > 100) {
|
||||
context.addIssue({ code: "custom", message: "币商结算比例和奖励比例合计不能超过 100", path: ["coinSeller", "sellerPointRewardPercent"] });
|
||||
requirePercent(
|
||||
context,
|
||||
["host", "pointRatioPercent"],
|
||||
value.host.pointRatioPercent,
|
||||
"主播收益比例必须在 0-100 之间",
|
||||
false,
|
||||
);
|
||||
requirePercent(
|
||||
context,
|
||||
["agency", "pointRatioPercent"],
|
||||
value.agency.pointRatioPercent,
|
||||
"Agency 分成比例必须在 0-100 之间",
|
||||
true,
|
||||
);
|
||||
if (numberValue(value.host.pointRatioPercent) + numberValue(value.agency.pointRatioPercent) > 100) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "主播收益比例和 Agency 分成比例合计不能超过 100",
|
||||
path: ["agency", "pointRatioPercent"],
|
||||
});
|
||||
}
|
||||
requirePercent(context, ["coinSeller", "successRateCloseThresholdPercent"], value.coinSeller.successRateCloseThresholdPercent, "成功率关闭门槛必须在 0-100 之间", true);
|
||||
requireNonNegativeInteger(
|
||||
context,
|
||||
["host", "minimumWithdrawPoints"],
|
||||
value.host.minimumWithdrawPoints,
|
||||
"最低提现积分不能小于 0",
|
||||
);
|
||||
requireIntegerRange(
|
||||
context,
|
||||
["host", "withdrawFeeBps"],
|
||||
value.host.withdrawFeeBps,
|
||||
0,
|
||||
10000,
|
||||
"提现手续费 bps 必须在 0-10000 之间",
|
||||
);
|
||||
requirePercent(
|
||||
context,
|
||||
["agent", "supportRatioPercent"],
|
||||
value.agent.supportRatioPercent,
|
||||
"Agent 扶持比例必须在 0-100 之间",
|
||||
false,
|
||||
);
|
||||
requireNonNegativeInteger(
|
||||
context,
|
||||
["agent", "supportDays"],
|
||||
value.agent.supportDays,
|
||||
"Agent 扶持天数不能小于 0",
|
||||
);
|
||||
requirePositiveInteger(
|
||||
context,
|
||||
["agent", "qualifiedHostMin"],
|
||||
value.agent.qualifiedHostMin,
|
||||
"Agent 合格主播数必须大于 0",
|
||||
);
|
||||
requireNonNegativeInteger(
|
||||
context,
|
||||
["agent", "differential", "maxDepth"],
|
||||
value.agent.differential.maxDepth,
|
||||
"Agent 差值深度不能小于 0",
|
||||
);
|
||||
validateAgentLevels(context, value.agent.levels);
|
||||
requirePositiveInteger(
|
||||
context,
|
||||
["bd", "qualifiedAgencyMin"],
|
||||
value.bd.qualifiedAgencyMin,
|
||||
"BD 合格 Agency 数必须大于 0",
|
||||
);
|
||||
requirePositiveInteger(
|
||||
context,
|
||||
["bd", "qualifiedHostPerAgencyMin"],
|
||||
value.bd.qualifiedHostPerAgencyMin,
|
||||
"BD 每 Agency 合格主播数必须大于 0",
|
||||
);
|
||||
validateBDLevels(context, value.bd.levels);
|
||||
requirePositiveNumber(
|
||||
context,
|
||||
["manager", "coinSellerRechargeThresholdUsd"],
|
||||
value.manager.coinSellerRechargeThresholdUsd,
|
||||
"经理币商充值门槛必须大于 0",
|
||||
);
|
||||
requirePercent(
|
||||
context,
|
||||
["manager", "coinSellerRechargeCommissionPercent"],
|
||||
value.manager.coinSellerRechargeCommissionPercent,
|
||||
"经理币商提成比例必须在 0-100 之间",
|
||||
true,
|
||||
);
|
||||
requirePercent(
|
||||
context,
|
||||
["manager", "hostPointCommissionPercent"],
|
||||
value.manager.hostPointCommissionPercent,
|
||||
"经理主播提成比例必须在 0-100 之间",
|
||||
true,
|
||||
);
|
||||
requirePositiveInteger(
|
||||
context,
|
||||
["coinSeller", "orderTimeoutSeconds"],
|
||||
value.coinSeller.orderTimeoutSeconds,
|
||||
"币商订单超时秒数必须大于 0",
|
||||
);
|
||||
requirePercent(
|
||||
context,
|
||||
["coinSeller", "sellerPointSettlePercent"],
|
||||
value.coinSeller.sellerPointSettlePercent,
|
||||
"币商结算比例必须在 0-100 之间",
|
||||
true,
|
||||
);
|
||||
requirePercent(
|
||||
context,
|
||||
["coinSeller", "sellerPointRewardPercent"],
|
||||
value.coinSeller.sellerPointRewardPercent,
|
||||
"币商奖励比例必须在 0-100 之间",
|
||||
true,
|
||||
);
|
||||
if (
|
||||
numberValue(value.coinSeller.sellerPointSettlePercent) +
|
||||
numberValue(value.coinSeller.sellerPointRewardPercent) >
|
||||
100
|
||||
) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "币商结算比例和奖励比例合计不能超过 100",
|
||||
path: ["coinSeller", "sellerPointRewardPercent"],
|
||||
});
|
||||
}
|
||||
requirePercent(
|
||||
context,
|
||||
["coinSeller", "successRateCloseThresholdPercent"],
|
||||
value.coinSeller.successRateCloseThresholdPercent,
|
||||
"成功率关闭门槛必须在 0-100 之间",
|
||||
true,
|
||||
);
|
||||
validateCoinSellerLevels(context, value.coinSeller.levels);
|
||||
requireNonNegativeInteger(context, ["tasks", "newHost7dMaxPoints"], value.tasks.newHost7dMaxPoints, "新主播 7 日任务积分不能小于 0");
|
||||
requireNonNegativeInteger(
|
||||
context,
|
||||
["tasks", "newHost7dMaxPoints"],
|
||||
value.tasks.newHost7dMaxPoints,
|
||||
"新主播 7 日任务积分不能小于 0",
|
||||
);
|
||||
});
|
||||
|
||||
export const policyTemplateFormSchema = z.object({
|
||||
@ -258,10 +380,22 @@ export const policyInstanceFormSchema = z
|
||||
}
|
||||
});
|
||||
|
||||
export function policyTemplateFormFromItem(item?: Partial<PolicyTemplateForm> & { ruleJson?: unknown; rule_json?: unknown; template_code?: string; template_version?: string } | null): PolicyTemplateForm {
|
||||
export function policyTemplateFormFromItem(
|
||||
item?:
|
||||
| (Partial<PolicyTemplateForm> & {
|
||||
ruleJson?: unknown;
|
||||
rule_json?: unknown;
|
||||
template_code?: string;
|
||||
template_version?: string;
|
||||
})
|
||||
| null,
|
||||
): PolicyTemplateForm {
|
||||
const rule = ruleFormFromJson(item?.ruleJson ?? item?.rule_json ?? defaultPolicyRule());
|
||||
return {
|
||||
description: stringValue(item?.description, "新文档口径:允许自刷;BD 按 UTC 当月自然月;Huwaa 使用 POINT/COIN_SELLER_POINT。"),
|
||||
description: stringValue(
|
||||
item?.description,
|
||||
"新文档口径:允许自刷;BD 按 UTC 当月自然月;Huwaa 使用 POINT/COIN_SELLER_POINT。",
|
||||
),
|
||||
name: stringValue(item?.name, "第一套政策:Google 70000 + 币商 92000-100000"),
|
||||
rule,
|
||||
status: normalizeStatus(item?.status),
|
||||
@ -282,13 +416,24 @@ export function policyTemplatePayloadFromForm(form: PolicyTemplateForm) {
|
||||
};
|
||||
}
|
||||
|
||||
export function policyInstanceFormFromTemplate(template?: { templateCode?: string; templateVersion?: string; template_code?: string; template_version?: string } | null): PolicyInstanceForm {
|
||||
export function policyInstanceFormFromTemplate(
|
||||
template?: {
|
||||
templateCode?: string;
|
||||
templateVersion?: string;
|
||||
template_code?: string;
|
||||
template_version?: string;
|
||||
} | null,
|
||||
): PolicyInstanceForm {
|
||||
const templateCode = stringValue(template?.templateCode ?? template?.template_code, DEFAULT_POLICY_TEMPLATE_CODE);
|
||||
const templateVersion = stringValue(template?.templateVersion ?? template?.template_version, DEFAULT_POLICY_TEMPLATE_VERSION);
|
||||
const templateVersion = stringValue(
|
||||
template?.templateVersion ?? template?.template_version,
|
||||
DEFAULT_POLICY_TEMPLATE_VERSION,
|
||||
);
|
||||
return {
|
||||
effectiveFrom: "",
|
||||
effectiveTo: "",
|
||||
instanceCode: templateCode === DEFAULT_POLICY_TEMPLATE_CODE ? "huwaa_first_google70000_coin_seller_92000_100000" : "",
|
||||
instanceCode:
|
||||
templateCode === DEFAULT_POLICY_TEMPLATE_CODE ? "huwaa_first_google70000_coin_seller_92000_100000" : "",
|
||||
regionId: "",
|
||||
regionScopeMode: "all_active_regions",
|
||||
status: "active",
|
||||
@ -303,7 +448,10 @@ export function policyInstancePayloadFromForm(form: PolicyInstanceForm) {
|
||||
effective_from_ms: timeValueToMs(parsed.effectiveFrom),
|
||||
effective_to_ms: timeValueToMs(parsed.effectiveTo),
|
||||
instance_code: parsed.instanceCode.trim(),
|
||||
region_scope: parsed.regionScopeMode === "region_id" ? `region_id:${integerValue(parsed.regionId || 0)}` : "all_active_regions",
|
||||
region_scope:
|
||||
parsed.regionScopeMode === "region_id"
|
||||
? `region_id:${integerValue(parsed.regionId || 0)}`
|
||||
: "all_active_regions",
|
||||
status: parsed.status,
|
||||
template_code: parsed.templateCode.trim(),
|
||||
template_version: parsed.templateVersion.trim(),
|
||||
@ -314,6 +462,7 @@ export function ruleFormFromJson(raw: unknown): PolicyRuleForm {
|
||||
const rule = parseRuleObject(raw);
|
||||
const host = objectValue(rule.host);
|
||||
const agent = objectValue(rule.agent);
|
||||
const agency = objectValue(rule.agency);
|
||||
const differential = objectValue(agent.differential);
|
||||
const bd = objectValue(rule.bd);
|
||||
const manager = objectValue(rule.manager);
|
||||
@ -321,6 +470,9 @@ export function ruleFormFromJson(raw: unknown): PolicyRuleForm {
|
||||
const tasks = objectValue(rule.tasks);
|
||||
const gameInvite = objectValue(rule.game_invite);
|
||||
return {
|
||||
agency: {
|
||||
pointRatioPercent: inputValue(agency.point_ratio_percent, 0),
|
||||
},
|
||||
agent: {
|
||||
differential: {
|
||||
crossLevelFormula: stringValue(differential.cross_level_formula, "parent_minus_direct_child"),
|
||||
@ -384,12 +536,36 @@ export function ruleObjectFromForm(form: PolicyRuleForm): RuleObject {
|
||||
const baseAgent = objectValue(base.agent);
|
||||
const baseBD = objectValue(base.bd);
|
||||
const baseCoinSeller = objectValue(base.coin_seller);
|
||||
const baseAgentLevelExtras = extraByKey(arrayValue(baseAgent.levels), "level", ["level", "manual_review", "max_usd", "min_usd", "name", "qualified_host_min", "ratio_percent"]);
|
||||
const baseBDLevelExtras = extraByKey(arrayValue(baseBD.levels), "level", ["commission_percent", "level", "name", "salary_usd", "threshold_usd"]);
|
||||
const baseCoinSellerLevelExtras = extraByKey(arrayValue(baseCoinSeller.levels), "name", ["coin_per_usd", "name", "single_recharge_threshold_usd", "threshold_usd", "withdraw_order_enabled"]);
|
||||
const baseAgentLevelExtras = extraByKey(arrayValue(baseAgent.levels), "level", [
|
||||
"level",
|
||||
"manual_review",
|
||||
"max_usd",
|
||||
"min_usd",
|
||||
"name",
|
||||
"qualified_host_min",
|
||||
"ratio_percent",
|
||||
]);
|
||||
const baseBDLevelExtras = extraByKey(arrayValue(baseBD.levels), "level", [
|
||||
"commission_percent",
|
||||
"level",
|
||||
"name",
|
||||
"salary_usd",
|
||||
"threshold_usd",
|
||||
]);
|
||||
const baseCoinSellerLevelExtras = extraByKey(arrayValue(baseCoinSeller.levels), "name", [
|
||||
"coin_per_usd",
|
||||
"name",
|
||||
"single_recharge_threshold_usd",
|
||||
"threshold_usd",
|
||||
"withdraw_order_enabled",
|
||||
]);
|
||||
|
||||
return {
|
||||
...base,
|
||||
agency: {
|
||||
...objectValue(base.agency),
|
||||
point_ratio_percent: numberValue(parsed.agency.pointRatioPercent),
|
||||
},
|
||||
allow_self_brushing: parsed.allowSelfBrushing,
|
||||
google_coin_per_usd: integerValue(parsed.googleCoinPerUsd),
|
||||
points_per_usd: integerValue(parsed.pointsPerUsd),
|
||||
@ -408,7 +584,9 @@ export function ruleObjectFromForm(form: PolicyRuleForm): RuleObject {
|
||||
enabled: parsed.agent.differential.enabled,
|
||||
max_depth: integerValue(parsed.agent.differential.maxDepth),
|
||||
},
|
||||
levels: parsed.agent.levels.map((level) => agentLevelToRule(level, baseAgentLevelExtras.get(String(integerValue(level.level))) || {})),
|
||||
levels: parsed.agent.levels.map((level) =>
|
||||
agentLevelToRule(level, baseAgentLevelExtras.get(String(integerValue(level.level))) || {}),
|
||||
),
|
||||
period: parsed.agent.period.trim(),
|
||||
qualified_host_min: integerValue(parsed.agent.qualifiedHostMin),
|
||||
support_days: integerValue(parsed.agent.supportDays),
|
||||
@ -417,7 +595,9 @@ export function ruleObjectFromForm(form: PolicyRuleForm): RuleObject {
|
||||
bd: {
|
||||
...baseBD,
|
||||
base: parsed.bd.base.trim(),
|
||||
levels: parsed.bd.levels.map((level) => bdLevelToRule(level, baseBDLevelExtras.get(String(integerValue(level.level))) || {})),
|
||||
levels: parsed.bd.levels.map((level) =>
|
||||
bdLevelToRule(level, baseBDLevelExtras.get(String(integerValue(level.level))) || {}),
|
||||
),
|
||||
period: parsed.bd.period.trim(),
|
||||
qualified_agency_min: integerValue(parsed.bd.qualifiedAgencyMin),
|
||||
qualified_host_per_agency_min: integerValue(parsed.bd.qualifiedHostPerAgencyMin),
|
||||
@ -430,7 +610,9 @@ export function ruleObjectFromForm(form: PolicyRuleForm): RuleObject {
|
||||
},
|
||||
coin_seller: {
|
||||
...baseCoinSeller,
|
||||
levels: parsed.coinSeller.levels.map((level) => coinSellerLevelToRule(level, baseCoinSellerLevelExtras.get(String(level.name || "").trim()) || {})),
|
||||
levels: parsed.coinSeller.levels.map((level) =>
|
||||
coinSellerLevelToRule(level, baseCoinSellerLevelExtras.get(String(level.name || "").trim()) || {}),
|
||||
),
|
||||
order_timeout_seconds: integerValue(parsed.coinSeller.orderTimeoutSeconds),
|
||||
seller_point_reward_percent: numberValue(parsed.coinSeller.sellerPointRewardPercent),
|
||||
seller_point_settle_percent: numberValue(parsed.coinSeller.sellerPointSettlePercent),
|
||||
@ -452,10 +634,16 @@ export function ruleObjectFromForm(form: PolicyRuleForm): RuleObject {
|
||||
|
||||
export function defaultPolicyRule(): RuleObject {
|
||||
return {
|
||||
agency: { point_ratio_percent: 0 },
|
||||
points_per_usd: 100000,
|
||||
allow_self_brushing: true,
|
||||
google_coin_per_usd: 70000,
|
||||
host: { point_ratio_percent: 70, affects_room_heat: false, minimum_withdraw_points: 1000000, withdraw_fee_bps: 500 },
|
||||
host: {
|
||||
point_ratio_percent: 70,
|
||||
affects_room_heat: false,
|
||||
minimum_withdraw_points: 1000000,
|
||||
withdraw_fee_bps: 500,
|
||||
},
|
||||
agent: {
|
||||
period: "rolling_30d",
|
||||
support_ratio_percent: 8,
|
||||
@ -489,13 +677,22 @@ export function defaultPolicyRule(): RuleObject {
|
||||
{ level: 12, name: "超级Admin-2", threshold_usd: 40000, salary_usd: 2000, commission_percent: 3 },
|
||||
],
|
||||
},
|
||||
manager: { coin_seller_recharge_threshold_usd: 2000, coin_seller_recharge_commission_percent: 3, host_point_commission_percent: 2 },
|
||||
manager: {
|
||||
coin_seller_recharge_threshold_usd: 2000,
|
||||
coin_seller_recharge_commission_percent: 3,
|
||||
host_point_commission_percent: 2,
|
||||
},
|
||||
coin_seller: {
|
||||
withdraw_order_enabled: true,
|
||||
levels: [
|
||||
{ name: "Beginner", threshold_usd: 100, coin_per_usd: 92000, withdraw_order_enabled: false },
|
||||
{ name: "Standard", threshold_usd: 500, coin_per_usd: 96000, withdraw_order_enabled: false },
|
||||
{ name: "Senior", single_recharge_threshold_usd: 1000, coin_per_usd: 100000, withdraw_order_enabled: true },
|
||||
{
|
||||
name: "Senior",
|
||||
single_recharge_threshold_usd: 1000,
|
||||
coin_per_usd: 100000,
|
||||
withdraw_order_enabled: true,
|
||||
},
|
||||
],
|
||||
order_timeout_seconds: 7200,
|
||||
seller_point_settle_percent: 95,
|
||||
@ -510,7 +707,15 @@ export function defaultPolicyRule(): RuleObject {
|
||||
function agentLevelFromRule(raw: unknown): AgentLevelForm {
|
||||
const level = objectValue(raw);
|
||||
return {
|
||||
extra: omitKeys(level, ["level", "manual_review", "max_usd", "min_usd", "name", "qualified_host_min", "ratio_percent"]),
|
||||
extra: omitKeys(level, [
|
||||
"level",
|
||||
"manual_review",
|
||||
"max_usd",
|
||||
"min_usd",
|
||||
"name",
|
||||
"qualified_host_min",
|
||||
"ratio_percent",
|
||||
]),
|
||||
level: inputValue(level.level, ""),
|
||||
manualReview: booleanValue(level.manual_review, false),
|
||||
maxUsd: inputValue(level.max_usd, 0),
|
||||
@ -537,7 +742,13 @@ function coinSellerLevelFromRule(raw: unknown): CoinSellerLevelForm {
|
||||
const level = objectValue(raw);
|
||||
return {
|
||||
coinPerUsd: inputValue(level.coin_per_usd, 0),
|
||||
extra: omitKeys(level, ["coin_per_usd", "name", "single_recharge_threshold_usd", "threshold_usd", "withdraw_order_enabled"]),
|
||||
extra: omitKeys(level, [
|
||||
"coin_per_usd",
|
||||
"name",
|
||||
"single_recharge_threshold_usd",
|
||||
"threshold_usd",
|
||||
"withdraw_order_enabled",
|
||||
]),
|
||||
name: stringValue(level.name, ""),
|
||||
singleRechargeThresholdUsd: inputValue(level.single_recharge_threshold_usd, ""),
|
||||
thresholdUsd: inputValue(level.threshold_usd, ""),
|
||||
@ -593,16 +804,45 @@ function validateAgentLevels(context: z.RefinementCtx, levels: AgentLevelForm[])
|
||||
levels.forEach((level, index) => {
|
||||
const levelNo = integerValue(level.level);
|
||||
if (!Number.isInteger(levelNo) || levelNo <= 0 || seen.has(levelNo)) {
|
||||
context.addIssue({ code: "custom", message: "Agent 等级必须为不重复的正整数", path: ["agent", "levels", index, "level"] });
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Agent 等级必须为不重复的正整数",
|
||||
path: ["agent", "levels", index, "level"],
|
||||
});
|
||||
}
|
||||
seen.add(levelNo);
|
||||
requireNonNegativeNumber(context, ["agent", "levels", index, "minUsd"], level.minUsd, "Agent 最小 USD 不能小于 0");
|
||||
requireNonNegativeNumber(context, ["agent", "levels", index, "maxUsd"], level.maxUsd, "Agent 最大 USD 不能小于 0");
|
||||
requireNonNegativeNumber(
|
||||
context,
|
||||
["agent", "levels", index, "minUsd"],
|
||||
level.minUsd,
|
||||
"Agent 最小 USD 不能小于 0",
|
||||
);
|
||||
requireNonNegativeNumber(
|
||||
context,
|
||||
["agent", "levels", index, "maxUsd"],
|
||||
level.maxUsd,
|
||||
"Agent 最大 USD 不能小于 0",
|
||||
);
|
||||
if (numberValue(level.maxUsd) > 0 && numberValue(level.maxUsd) <= numberValue(level.minUsd)) {
|
||||
context.addIssue({ code: "custom", message: "Agent 最大 USD 必须大于最小 USD,0 表示无上限", path: ["agent", "levels", index, "maxUsd"] });
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Agent 最大 USD 必须大于最小 USD,0 表示无上限",
|
||||
path: ["agent", "levels", index, "maxUsd"],
|
||||
});
|
||||
}
|
||||
requirePercent(context, ["agent", "levels", index, "ratioPercent"], level.ratioPercent, "Agent 提成比例必须在 0-100 之间", false);
|
||||
requireNonNegativeInteger(context, ["agent", "levels", index, "qualifiedHostMin"], level.qualifiedHostMin, "Agent 合格主播数不能小于 0");
|
||||
requirePercent(
|
||||
context,
|
||||
["agent", "levels", index, "ratioPercent"],
|
||||
level.ratioPercent,
|
||||
"Agent 提成比例必须在 0-100 之间",
|
||||
false,
|
||||
);
|
||||
requireNonNegativeInteger(
|
||||
context,
|
||||
["agent", "levels", index, "qualifiedHostMin"],
|
||||
level.qualifiedHostMin,
|
||||
"Agent 合格主播数不能小于 0",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -610,18 +850,42 @@ function validateBDLevels(context: z.RefinementCtx, levels: BDLevelForm[]) {
|
||||
const seen = new Set<number>();
|
||||
let previousThreshold = 0;
|
||||
levels
|
||||
.map((level, index) => ({ index, level, levelNo: integerValue(level.level), threshold: numberValue(level.thresholdUsd) }))
|
||||
.map((level, index) => ({
|
||||
index,
|
||||
level,
|
||||
levelNo: integerValue(level.level),
|
||||
threshold: numberValue(level.thresholdUsd),
|
||||
}))
|
||||
.sort((left, right) => left.levelNo - right.levelNo)
|
||||
.forEach(({ index, level, levelNo, threshold }) => {
|
||||
if (!Number.isInteger(levelNo) || levelNo <= 0 || seen.has(levelNo)) {
|
||||
context.addIssue({ code: "custom", message: "BD 等级必须为不重复的正整数", path: ["bd", "levels", index, "level"] });
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "BD 等级必须为不重复的正整数",
|
||||
path: ["bd", "levels", index, "level"],
|
||||
});
|
||||
}
|
||||
seen.add(levelNo);
|
||||
if (threshold <= 0 || threshold <= previousThreshold) {
|
||||
context.addIssue({ code: "custom", message: "BD 收入门槛必须逐级递增", path: ["bd", "levels", index, "thresholdUsd"] });
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "BD 收入门槛必须逐级递增",
|
||||
path: ["bd", "levels", index, "thresholdUsd"],
|
||||
});
|
||||
}
|
||||
requireNonNegativeNumber(context, ["bd", "levels", index, "salaryUsd"], level.salaryUsd, "BD 固定工资不能小于 0");
|
||||
requirePercent(context, ["bd", "levels", index, "commissionPercent"], level.commissionPercent, "BD 提成比例必须在 0-100 之间", false);
|
||||
requireNonNegativeNumber(
|
||||
context,
|
||||
["bd", "levels", index, "salaryUsd"],
|
||||
level.salaryUsd,
|
||||
"BD 固定工资不能小于 0",
|
||||
);
|
||||
requirePercent(
|
||||
context,
|
||||
["bd", "levels", index, "commissionPercent"],
|
||||
level.commissionPercent,
|
||||
"BD 提成比例必须在 0-100 之间",
|
||||
false,
|
||||
);
|
||||
previousThreshold = threshold;
|
||||
});
|
||||
}
|
||||
@ -629,17 +893,40 @@ function validateBDLevels(context: z.RefinementCtx, levels: BDLevelForm[]) {
|
||||
function validateCoinSellerLevels(context: z.RefinementCtx, levels: CoinSellerLevelForm[]) {
|
||||
levels.forEach((level, index) => {
|
||||
if (!String(level.name || "").trim()) {
|
||||
context.addIssue({ code: "custom", message: "请输入币商等级名称", path: ["coinSeller", "levels", index, "name"] });
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "请输入币商等级名称",
|
||||
path: ["coinSeller", "levels", index, "name"],
|
||||
});
|
||||
}
|
||||
requirePositiveInteger(context, ["coinSeller", "levels", index, "coinPerUsd"], level.coinPerUsd, "币商金币单价必须大于 0");
|
||||
requirePositiveInteger(
|
||||
context,
|
||||
["coinSeller", "levels", index, "coinPerUsd"],
|
||||
level.coinPerUsd,
|
||||
"币商金币单价必须大于 0",
|
||||
);
|
||||
if (!String(level.thresholdUsd || "").trim() && !String(level.singleRechargeThresholdUsd || "").trim()) {
|
||||
context.addIssue({ code: "custom", message: "币商等级必须配置累计门槛或单次充值门槛", path: ["coinSeller", "levels", index, "thresholdUsd"] });
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "币商等级必须配置累计门槛或单次充值门槛",
|
||||
path: ["coinSeller", "levels", index, "thresholdUsd"],
|
||||
});
|
||||
}
|
||||
if (String(level.thresholdUsd || "").trim()) {
|
||||
requirePositiveNumber(context, ["coinSeller", "levels", index, "thresholdUsd"], level.thresholdUsd, "币商累计门槛必须大于 0");
|
||||
requirePositiveNumber(
|
||||
context,
|
||||
["coinSeller", "levels", index, "thresholdUsd"],
|
||||
level.thresholdUsd,
|
||||
"币商累计门槛必须大于 0",
|
||||
);
|
||||
}
|
||||
if (String(level.singleRechargeThresholdUsd || "").trim()) {
|
||||
requirePositiveNumber(context, ["coinSeller", "levels", index, "singleRechargeThresholdUsd"], level.singleRechargeThresholdUsd, "币商单次充值门槛必须大于 0");
|
||||
requirePositiveNumber(
|
||||
context,
|
||||
["coinSeller", "levels", index, "singleRechargeThresholdUsd"],
|
||||
level.singleRechargeThresholdUsd,
|
||||
"币商单次充值门槛必须大于 0",
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -726,7 +1013,12 @@ function requirePositiveInteger(context: z.RefinementCtx, path: (number | string
|
||||
}
|
||||
}
|
||||
|
||||
function requireNonNegativeInteger(context: z.RefinementCtx, path: (number | string)[], value: unknown, message: string) {
|
||||
function requireNonNegativeInteger(
|
||||
context: z.RefinementCtx,
|
||||
path: (number | string)[],
|
||||
value: unknown,
|
||||
message: string,
|
||||
) {
|
||||
const number = integerValue(value);
|
||||
if (!Number.isInteger(number) || number < 0) {
|
||||
context.addIssue({ code: "custom", message, path });
|
||||
@ -740,21 +1032,39 @@ function requirePositiveNumber(context: z.RefinementCtx, path: (number | string)
|
||||
}
|
||||
}
|
||||
|
||||
function requireNonNegativeNumber(context: z.RefinementCtx, path: (number | string)[], value: unknown, message: string) {
|
||||
function requireNonNegativeNumber(
|
||||
context: z.RefinementCtx,
|
||||
path: (number | string)[],
|
||||
value: unknown,
|
||||
message: string,
|
||||
) {
|
||||
const number = numberValue(value);
|
||||
if (!Number.isFinite(number) || number < 0) {
|
||||
context.addIssue({ code: "custom", message, path });
|
||||
}
|
||||
}
|
||||
|
||||
function requirePercent(context: z.RefinementCtx, path: (number | string)[], value: unknown, message: string, allowZero: boolean) {
|
||||
function requirePercent(
|
||||
context: z.RefinementCtx,
|
||||
path: (number | string)[],
|
||||
value: unknown,
|
||||
message: string,
|
||||
allowZero: boolean,
|
||||
) {
|
||||
const number = numberValue(value);
|
||||
if (!Number.isFinite(number) || number > 100 || (allowZero ? number < 0 : number <= 0)) {
|
||||
context.addIssue({ code: "custom", message, path });
|
||||
}
|
||||
}
|
||||
|
||||
function requireIntegerRange(context: z.RefinementCtx, path: (number | string)[], value: unknown, min: number, max: number, message: string) {
|
||||
function requireIntegerRange(
|
||||
context: z.RefinementCtx,
|
||||
path: (number | string)[],
|
||||
value: unknown,
|
||||
min: number,
|
||||
max: number,
|
||||
message: string,
|
||||
) {
|
||||
const number = integerValue(value);
|
||||
if (!Number.isInteger(number) || number < min || number > max) {
|
||||
context.addIssue({ code: "custom", message, path });
|
||||
|
||||
@ -78,6 +78,7 @@
|
||||
--danger-border: rgba(220, 38, 38, 0.28);
|
||||
--danger-border-strong: rgba(220, 38, 38, 0.72);
|
||||
--danger-glow: rgba(220, 38, 38, 0.28);
|
||||
--gender-female: #ec4899;
|
||||
--info: #0284c7;
|
||||
--info-surface: rgba(2, 132, 199, 0.1);
|
||||
--info-surface-strong: rgba(2, 132, 199, 0.14);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user